From ad054a758678317ed32ef89d867a834483bad365 Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:34:53 +0200 Subject: [PATCH] fix(web): land new accounts in onboarding and show how to connect a model (#134) Signing up refreshes the session, which briefly routes /onboarding to /sign-in without a return path; the sign-in route then sent the new account to /app. The shell's own redirect to onboarding was skipped whenever a focus refresh landed before the bootstrap response, so a fresh account could stay on an empty Today view. /onboarding now sends signed-out visitors to /sign-in?next=/onboarding and sign-in honours it, and the shell redirects an account without any bots or conversations regardless of which refresh landed first. Without a connected model nothing can reply, so the composer shows a "Connect model" action that opens model settings instead of a send error after the fact. Claude-Session: https://claude.ai/code/session_019HKhzQ65ctE5qmkiF9Apxo Co-authored-by: Claude --- apps/web/e2e/first-conversation.spec.ts | 41 +++++++++++++++++++++++++ apps/web/src/App.tsx | 8 +++-- apps/web/src/lib/auth-next.test.ts | 15 +++++++++ apps/web/src/lib/auth-next.ts | 6 ++++ apps/web/src/locales/de/messages.po | 8 +++++ apps/web/src/locales/en/messages.po | 8 +++++ apps/web/src/locales/es/messages.po | 8 +++++ apps/web/src/locales/hi/messages.po | 8 +++++ apps/web/src/locales/ko/messages.po | 8 +++++ apps/web/src/locales/pt-BR/messages.po | 8 +++++ apps/web/src/locales/ru/messages.po | 8 +++++ apps/web/src/locales/tr/messages.po | 8 +++++ apps/web/src/locales/zh-CN/messages.po | 8 +++++ apps/web/src/pages/Auth.tsx | 9 ++---- apps/web/src/pages/Shell.tsx | 12 +++++++- apps/web/src/pages/shell/composer.tsx | 17 +++++++++- 16 files changed, 168 insertions(+), 12 deletions(-) create mode 100644 apps/web/e2e/first-conversation.spec.ts create mode 100644 apps/web/src/lib/auth-next.test.ts create mode 100644 apps/web/src/lib/auth-next.ts diff --git a/apps/web/e2e/first-conversation.spec.ts b/apps/web/e2e/first-conversation.spec.ts new file mode 100644 index 0000000..3722b6f --- /dev/null +++ b/apps/web/e2e/first-conversation.spec.ts @@ -0,0 +1,41 @@ +/** + * E2E: a new account reaches onboarding and, without a model, sees how to connect one. + */ +import { expect, test } from "@playwright/test"; +import { captureScreenshot, completeOnboarding, routePatchRpc, signup } from "./helpers"; + +test("a signed-out visit to onboarding returns there after sign-in", async ({ page }) => { + const email = `return-onboarding-${Date.now()}@rakazo.test`; + await signup(page, email, "password12", "Return"); + await page.waitForURL(/\/onboarding$/, { timeout: 20_000 }); + + await page.context().clearCookies(); + await page.goto("/onboarding", { waitUntil: "domcontentloaded" }); + await page.waitForURL((url) => url.pathname === "/sign-in"); + expect(new URL(page.url()).searchParams.get("next")).toBe("/onboarding"); + + await page.getByPlaceholder("Your email address").fill(email); + await page.getByPlaceholder("Password").fill("password12"); + await page.getByRole("button", { name: "Continue with email" }).click(); + await page.waitForURL(/\/onboarding$/, { timeout: 20_000 }); + await expect(page.locator('[data-rakazo-surface="onboarding"]')).toBeVisible(); +}); + +test("without a model the composer offers to connect one", async ({ page }) => { + const testInfo = test.info(); + await signup(page, `model-needed-${Date.now()}@rakazo.test`, "password12", "Model needed"); + await completeOnboarding(page, undefined, { mainConversation: true }); + + await routePatchRpc(page, "bootstrap", (json) => ({ + ...json, + me: { ...(json.me as Record), needsModel: true }, + })); + await page.reload({ waitUntil: "domcontentloaded" }); + + const notice = page.getByTestId("composer-model-needed"); + await expect(notice).toContainText("Connect a model so your Oo can reply."); + await captureScreenshot(page, testInfo, "first-conversation-model-needed"); + + await notice.getByRole("button", { name: "Connect model" }).click(); + await expect(page.getByTestId("model-settings")).toBeVisible(); +}); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 1b14b88..7d13621 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -5,6 +5,7 @@ import { Suspense, useEffect, useLayoutEffect, useRef, useState } from "react"; import { Navigate, Route, Routes, useSearchParams } from "react-router-dom"; import { LoadingState } from "./components/ai/primitives"; import { authClient } from "./lib/auth"; +import { authReturnPath } from "./lib/auth-next"; import { lazyWithRecovery } from "./lib/lazy-recovery"; import { markAfterPaint, markOnce } from "./lib/performance"; import { @@ -46,8 +47,7 @@ export function App() { function SessionApp() { const [searchParams] = useSearchParams(); - const signInDestination = - searchParams.get("next") === "/integrations/setup" ? "/integrations/setup" : "/app"; + const signInDestination = authReturnPath(searchParams.get("next")); const session = authClient.useSession(); const gate = sessionGate(session); const [holdingUnreachable, setHoldingUnreachable] = useState(false); @@ -101,7 +101,9 @@ function SessionApp() { } /> : } + element={ + user ? : + } /> { + it("returns to onboarding and integration setup", () => { + expect(authReturnPath("/onboarding")).toBe("/onboarding"); + expect(authReturnPath("/integrations/setup")).toBe("/integrations/setup"); + }); + + it("sends anything else to the app", () => { + expect(authReturnPath(null)).toBe("/app"); + expect(authReturnPath("https://example.test")).toBe("/app"); + expect(authReturnPath("//example.test")).toBe("/app"); + }); +}); diff --git a/apps/web/src/lib/auth-next.ts b/apps/web/src/lib/auth-next.ts new file mode 100644 index 0000000..8e0847f --- /dev/null +++ b/apps/web/src/lib/auth-next.ts @@ -0,0 +1,6 @@ +/** Routes a signed-out visitor may return to after signing in; anything else lands in the app. */ +const RETURN_PATHS = new Set(["/onboarding", "/integrations/setup"]); + +export function authReturnPath(next: string | null): string { + return next && RETURN_PATHS.has(next) ? next : "/app"; +} diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index 6bea333..e54f8d7 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -1252,6 +1252,10 @@ msgstr "Passwort bestätigen" msgid "Connect" msgstr "Verbinden" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "Verbinde ein Modell, damit dein Oo antworten kann." + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "API-Schlüssel verbinden" @@ -1264,6 +1268,10 @@ msgstr "Executor verbinden" msgid "Connect MCP server “{name}”" msgstr "MCP-Server „{name}“ verbinden" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "Modell verbinden" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "OAuth verbinden" diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index 4cc014b..89f5775 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -1252,6 +1252,10 @@ msgstr "Confirm password" msgid "Connect" msgstr "Connect" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "Connect a model so your Oo can reply." + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "Connect API key" @@ -1264,6 +1268,10 @@ msgstr "Connect Executor" msgid "Connect MCP server “{name}”" msgstr "Connect MCP server “{name}”" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "Connect model" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "Connect OAuth" diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index b053216..31ebe36 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -1252,6 +1252,10 @@ msgstr "Confirmar contraseña" msgid "Connect" msgstr "Conectar" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "Conectar clave de API" @@ -1264,6 +1268,10 @@ msgstr "Conectar Executor" msgid "Connect MCP server “{name}”" msgstr "Conectar servidor MCP “{name}”" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "Conectar OAuth" diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index e29b22d..33e5203 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -1252,6 +1252,10 @@ msgstr "पासवर्ड की पुष्टि करें" msgid "Connect" msgstr "कनेक्ट करें" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "API कुंजी कनेक्ट करें" @@ -1264,6 +1268,10 @@ msgstr "Executor कनेक्ट करें" msgid "Connect MCP server “{name}”" msgstr "MCP सर्वर “{name}” कनेक्ट करें" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "OAuth कनेक्ट करें" diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index 63efe4b..1bd9164 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -1252,6 +1252,10 @@ msgstr "비밀번호 확인" msgid "Connect" msgstr "연결" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "API 키 연결" @@ -1264,6 +1268,10 @@ msgstr "Executor 연결" msgid "Connect MCP server “{name}”" msgstr "MCP 서버 ‘{name}’ 연결" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "OAuth 연결" diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index f39dadb..e96f31e 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -1252,6 +1252,10 @@ msgstr "Confirmar senha" msgid "Connect" msgstr "Conectar" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "Conecte-se com a chave API" @@ -1264,6 +1268,10 @@ msgstr "Conectar Executor" msgid "Connect MCP server “{name}”" msgstr "Conecte o servidor MCP “{name}”" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "Conectar OAuth" diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index c708afe..160e421 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -1252,6 +1252,10 @@ msgstr "Подтвердите пароль" msgid "Connect" msgstr "Подключить" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "Подключить API-ключ" @@ -1264,6 +1268,10 @@ msgstr "Подключить Executor" msgid "Connect MCP server “{name}”" msgstr "Подключите MCP-сервер «{name}»" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "Подключить OAuth" diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index a0c39f2..a2da959 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -1252,6 +1252,10 @@ msgstr "Parolayı doğrulayın" msgid "Connect" msgstr "Bağlan" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "API anahtarıyla bağlan" @@ -1264,6 +1268,10 @@ msgstr "Executor'a bağlan" msgid "Connect MCP server “{name}”" msgstr "“{name}” MCP sunucusunu bağla" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "OAuth ile bağlan" diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index f119a92..6fbc473 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -1252,6 +1252,10 @@ msgstr "确认密码" msgid "Connect" msgstr "连接" +#: src/pages/shell/composer.tsx +msgid "Connect a model so your Oo can reply." +msgstr "" + #: src/pages/ModelSettingsOverlay.tsx msgid "Connect API key" msgstr "连接 API 密钥" @@ -1264,6 +1268,10 @@ msgstr "连接 Executor" msgid "Connect MCP server “{name}”" msgstr "连接 MCP 服务器“{name}”" +#: src/pages/shell/composer.tsx +msgid "Connect model" +msgstr "" + #: src/pages/McpServersOverlay.tsx msgid "Connect OAuth" msgstr "连接 OAuth" diff --git a/apps/web/src/pages/Auth.tsx b/apps/web/src/pages/Auth.tsx index 72dc709..45216c9 100644 --- a/apps/web/src/pages/Auth.tsx +++ b/apps/web/src/pages/Auth.tsx @@ -5,6 +5,7 @@ import { Eye, EyeOff } from "lucide-react"; import { useEffect, useState } from "react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { authClient } from "../lib/auth"; +import { authReturnPath } from "../lib/auth-next"; import { clearSpaceSelection } from "../lib/rpc"; type AuthMode = "in" | "up" | "forgot"; @@ -106,13 +107,7 @@ export function AuthPage({ mode }: { mode: AuthMode }) { return; } clearSpaceSelection(); - navigate( - mode === "up" - ? "/onboarding" - : searchParams.get("next") === "/integrations/setup" - ? "/integrations/setup" - : "/app", - ); + navigate(mode === "up" ? "/onboarding" : authReturnPath(searchParams.get("next"))); } catch { setError(t`Could not reach the server`); } finally { diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index b78cecd..8a68433 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -957,7 +957,8 @@ export function ShellPage() { markOnce("rk:renderer:bots-response"); markOnce("rk:renderer:thread-response"); } - if (!applyBotLists) return; + // A focus refresh can land first and skip the list writes; a fresh account still + // belongs in onboarding, so this check never depends on applyBotLists. if ( bootstrap.bots.length === 0 && bootstrap.archivedBots.length === 0 && @@ -968,6 +969,7 @@ export function ShellPage() { navigate("/onboarding", { replace: true }); return; } + if (!applyBotLists) return; const bootstrapHomeSpaceId = bootstrap.spaces.find((space) => space.isDefault)?.id ?? bootstrap.me.spaceId; const bootstrapSpaceId = selectedSpaceId() ?? bootstrap.me.spaceId; @@ -3365,6 +3367,7 @@ export function ShellPage() { mentionTargets={composerMentionTargets} agentSkills={agentSkills} onSlashOpen={refreshAgentSkills} + onConnectModel={bootstrapMe?.needsModel ? () => openSettings("models") : undefined} onSlashAction={(action) => { if (action === "chat-settings") { setPanel(inGroup ? "group-settings" : "settings"); @@ -4124,6 +4127,13 @@ export function ShellPage() { onClose={() => { setSettingsOpen(false); setSettingsSection("general"); + // A model connected in settings unblocks the composer. + if (bootstrapMe?.needsModel) { + void rpc + .me() + .then(setBootstrapMe) + .catch(() => undefined); + } }} /> ) : null} diff --git a/apps/web/src/pages/shell/composer.tsx b/apps/web/src/pages/shell/composer.tsx index b8b465b..11ae6db 100644 --- a/apps/web/src/pages/shell/composer.tsx +++ b/apps/web/src/pages/shell/composer.tsx @@ -61,6 +61,7 @@ export const Composer = memo(function Composer({ agentSkills, onSlashOpen, onSlashAction, + onConnectModel, }: { running: boolean; disabled?: boolean; @@ -88,6 +89,8 @@ export const Composer = memo(function Composer({ agentSkills?: AgentSkillCatalogEntry[]; onSlashOpen?: () => void; onSlashAction?: (action: SlashActionId) => void; + /** Set while no model is connected: nothing can run until one is. */ + onConnectModel?: () => void; }) { const { t } = useLingui(); const [draft, setDraft] = useState(""); @@ -370,7 +373,19 @@ export const Composer = memo(function Composer({ }`} >
- {sendError || runError ? ( + {onConnectModel ? ( +
+ + Connect a model so your Oo can reply. + + +
+ ) : sendError || runError ? (