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 <noreply@anthropic.com>
This commit is contained in:
parent
f7b0cfb341
commit
ad054a7586
41
apps/web/e2e/first-conversation.spec.ts
Normal file
41
apps/web/e2e/first-conversation.spec.ts
Normal file
@ -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<string, unknown>), 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();
|
||||||
|
});
|
||||||
@ -5,6 +5,7 @@ import { Suspense, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|||||||
import { Navigate, Route, Routes, useSearchParams } from "react-router-dom";
|
import { Navigate, Route, Routes, useSearchParams } from "react-router-dom";
|
||||||
import { LoadingState } from "./components/ai/primitives";
|
import { LoadingState } from "./components/ai/primitives";
|
||||||
import { authClient } from "./lib/auth";
|
import { authClient } from "./lib/auth";
|
||||||
|
import { authReturnPath } from "./lib/auth-next";
|
||||||
import { lazyWithRecovery } from "./lib/lazy-recovery";
|
import { lazyWithRecovery } from "./lib/lazy-recovery";
|
||||||
import { markAfterPaint, markOnce } from "./lib/performance";
|
import { markAfterPaint, markOnce } from "./lib/performance";
|
||||||
import {
|
import {
|
||||||
@ -46,8 +47,7 @@ export function App() {
|
|||||||
|
|
||||||
function SessionApp() {
|
function SessionApp() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const signInDestination =
|
const signInDestination = authReturnPath(searchParams.get("next"));
|
||||||
searchParams.get("next") === "/integrations/setup" ? "/integrations/setup" : "/app";
|
|
||||||
const session = authClient.useSession();
|
const session = authClient.useSession();
|
||||||
const gate = sessionGate(session);
|
const gate = sessionGate(session);
|
||||||
const [holdingUnreachable, setHoldingUnreachable] = useState(false);
|
const [holdingUnreachable, setHoldingUnreachable] = useState(false);
|
||||||
@ -101,7 +101,9 @@ function SessionApp() {
|
|||||||
<Route path="/reset-password" element={<PasswordResetPage />} />
|
<Route path="/reset-password" element={<PasswordResetPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="/onboarding"
|
path="/onboarding"
|
||||||
element={user ? <OnboardingPage /> : <Navigate to="/sign-in" replace />}
|
element={
|
||||||
|
user ? <OnboardingPage /> : <Navigate to="/sign-in?next=/onboarding" replace />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/mcp/oauth/callback"
|
path="/mcp/oauth/callback"
|
||||||
|
|||||||
15
apps/web/src/lib/auth-next.test.ts
Normal file
15
apps/web/src/lib/auth-next.test.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { authReturnPath } from "./auth-next";
|
||||||
|
|
||||||
|
describe("authReturnPath", () => {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
6
apps/web/src/lib/auth-next.ts
Normal file
6
apps/web/src/lib/auth-next.ts
Normal file
@ -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";
|
||||||
|
}
|
||||||
@ -1252,6 +1252,10 @@ msgstr "Passwort bestätigen"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "Verbinden"
|
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
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "API-Schlüssel verbinden"
|
msgstr "API-Schlüssel verbinden"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "Executor verbinden"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "MCP-Server „{name}“ verbinden"
|
msgstr "MCP-Server „{name}“ verbinden"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr "Modell verbinden"
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "OAuth verbinden"
|
msgstr "OAuth verbinden"
|
||||||
|
|||||||
@ -1252,6 +1252,10 @@ msgstr "Confirm password"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "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
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "Connect API key"
|
msgstr "Connect API key"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "Connect Executor"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "Connect MCP server “{name}”"
|
msgstr "Connect MCP server “{name}”"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr "Connect model"
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "Connect OAuth"
|
msgstr "Connect OAuth"
|
||||||
|
|||||||
@ -1252,6 +1252,10 @@ msgstr "Confirmar contraseña"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "Conectar"
|
msgstr "Conectar"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect a model so your Oo can reply."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "Conectar clave de API"
|
msgstr "Conectar clave de API"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "Conectar Executor"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "Conectar servidor MCP “{name}”"
|
msgstr "Conectar servidor MCP “{name}”"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "Conectar OAuth"
|
msgstr "Conectar OAuth"
|
||||||
|
|||||||
@ -1252,6 +1252,10 @@ msgstr "पासवर्ड की पुष्टि करें"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "कनेक्ट करें"
|
msgstr "कनेक्ट करें"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect a model so your Oo can reply."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "API कुंजी कनेक्ट करें"
|
msgstr "API कुंजी कनेक्ट करें"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "Executor कनेक्ट करें"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "MCP सर्वर “{name}” कनेक्ट करें"
|
msgstr "MCP सर्वर “{name}” कनेक्ट करें"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "OAuth कनेक्ट करें"
|
msgstr "OAuth कनेक्ट करें"
|
||||||
|
|||||||
@ -1252,6 +1252,10 @@ msgstr "비밀번호 확인"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "연결"
|
msgstr "연결"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect a model so your Oo can reply."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "API 키 연결"
|
msgstr "API 키 연결"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "Executor 연결"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "MCP 서버 ‘{name}’ 연결"
|
msgstr "MCP 서버 ‘{name}’ 연결"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "OAuth 연결"
|
msgstr "OAuth 연결"
|
||||||
|
|||||||
@ -1252,6 +1252,10 @@ msgstr "Confirmar senha"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "Conectar"
|
msgstr "Conectar"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect a model so your Oo can reply."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "Conecte-se com a chave API"
|
msgstr "Conecte-se com a chave API"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "Conectar Executor"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "Conecte o servidor MCP “{name}”"
|
msgstr "Conecte o servidor MCP “{name}”"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "Conectar OAuth"
|
msgstr "Conectar OAuth"
|
||||||
|
|||||||
@ -1252,6 +1252,10 @@ msgstr "Подтвердите пароль"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "Подключить"
|
msgstr "Подключить"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect a model so your Oo can reply."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "Подключить API-ключ"
|
msgstr "Подключить API-ключ"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "Подключить Executor"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "Подключите MCP-сервер «{name}»"
|
msgstr "Подключите MCP-сервер «{name}»"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "Подключить OAuth"
|
msgstr "Подключить OAuth"
|
||||||
|
|||||||
@ -1252,6 +1252,10 @@ msgstr "Parolayı doğrulayın"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "Bağlan"
|
msgstr "Bağlan"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect a model so your Oo can reply."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "API anahtarıyla bağlan"
|
msgstr "API anahtarıyla bağlan"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "Executor'a bağlan"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "“{name}” MCP sunucusunu bağla"
|
msgstr "“{name}” MCP sunucusunu bağla"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "OAuth ile bağlan"
|
msgstr "OAuth ile bağlan"
|
||||||
|
|||||||
@ -1252,6 +1252,10 @@ msgstr "确认密码"
|
|||||||
msgid "Connect"
|
msgid "Connect"
|
||||||
msgstr "连接"
|
msgstr "连接"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect a model so your Oo can reply."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Connect API key"
|
msgid "Connect API key"
|
||||||
msgstr "连接 API 密钥"
|
msgstr "连接 API 密钥"
|
||||||
@ -1264,6 +1268,10 @@ msgstr "连接 Executor"
|
|||||||
msgid "Connect MCP server “{name}”"
|
msgid "Connect MCP server “{name}”"
|
||||||
msgstr "连接 MCP 服务器“{name}”"
|
msgstr "连接 MCP 服务器“{name}”"
|
||||||
|
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
|
msgid "Connect model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/McpServersOverlay.tsx
|
#: src/pages/McpServersOverlay.tsx
|
||||||
msgid "Connect OAuth"
|
msgid "Connect OAuth"
|
||||||
msgstr "连接 OAuth"
|
msgstr "连接 OAuth"
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Eye, EyeOff } from "lucide-react";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { authClient } from "../lib/auth";
|
import { authClient } from "../lib/auth";
|
||||||
|
import { authReturnPath } from "../lib/auth-next";
|
||||||
import { clearSpaceSelection } from "../lib/rpc";
|
import { clearSpaceSelection } from "../lib/rpc";
|
||||||
|
|
||||||
type AuthMode = "in" | "up" | "forgot";
|
type AuthMode = "in" | "up" | "forgot";
|
||||||
@ -106,13 +107,7 @@ export function AuthPage({ mode }: { mode: AuthMode }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearSpaceSelection();
|
clearSpaceSelection();
|
||||||
navigate(
|
navigate(mode === "up" ? "/onboarding" : authReturnPath(searchParams.get("next")));
|
||||||
mode === "up"
|
|
||||||
? "/onboarding"
|
|
||||||
: searchParams.get("next") === "/integrations/setup"
|
|
||||||
? "/integrations/setup"
|
|
||||||
: "/app",
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
setError(t`Could not reach the server`);
|
setError(t`Could not reach the server`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -957,7 +957,8 @@ export function ShellPage() {
|
|||||||
markOnce("rk:renderer:bots-response");
|
markOnce("rk:renderer:bots-response");
|
||||||
markOnce("rk:renderer:thread-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 (
|
if (
|
||||||
bootstrap.bots.length === 0 &&
|
bootstrap.bots.length === 0 &&
|
||||||
bootstrap.archivedBots.length === 0 &&
|
bootstrap.archivedBots.length === 0 &&
|
||||||
@ -968,6 +969,7 @@ export function ShellPage() {
|
|||||||
navigate("/onboarding", { replace: true });
|
navigate("/onboarding", { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!applyBotLists) return;
|
||||||
const bootstrapHomeSpaceId =
|
const bootstrapHomeSpaceId =
|
||||||
bootstrap.spaces.find((space) => space.isDefault)?.id ?? bootstrap.me.spaceId;
|
bootstrap.spaces.find((space) => space.isDefault)?.id ?? bootstrap.me.spaceId;
|
||||||
const bootstrapSpaceId = selectedSpaceId() ?? bootstrap.me.spaceId;
|
const bootstrapSpaceId = selectedSpaceId() ?? bootstrap.me.spaceId;
|
||||||
@ -3365,6 +3367,7 @@ export function ShellPage() {
|
|||||||
mentionTargets={composerMentionTargets}
|
mentionTargets={composerMentionTargets}
|
||||||
agentSkills={agentSkills}
|
agentSkills={agentSkills}
|
||||||
onSlashOpen={refreshAgentSkills}
|
onSlashOpen={refreshAgentSkills}
|
||||||
|
onConnectModel={bootstrapMe?.needsModel ? () => openSettings("models") : undefined}
|
||||||
onSlashAction={(action) => {
|
onSlashAction={(action) => {
|
||||||
if (action === "chat-settings") {
|
if (action === "chat-settings") {
|
||||||
setPanel(inGroup ? "group-settings" : "settings");
|
setPanel(inGroup ? "group-settings" : "settings");
|
||||||
@ -4124,6 +4127,13 @@ export function ShellPage() {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setSettingsOpen(false);
|
setSettingsOpen(false);
|
||||||
setSettingsSection("general");
|
setSettingsSection("general");
|
||||||
|
// A model connected in settings unblocks the composer.
|
||||||
|
if (bootstrapMe?.needsModel) {
|
||||||
|
void rpc
|
||||||
|
.me()
|
||||||
|
.then(setBootstrapMe)
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -61,6 +61,7 @@ export const Composer = memo(function Composer({
|
|||||||
agentSkills,
|
agentSkills,
|
||||||
onSlashOpen,
|
onSlashOpen,
|
||||||
onSlashAction,
|
onSlashAction,
|
||||||
|
onConnectModel,
|
||||||
}: {
|
}: {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -88,6 +89,8 @@ export const Composer = memo(function Composer({
|
|||||||
agentSkills?: AgentSkillCatalogEntry[];
|
agentSkills?: AgentSkillCatalogEntry[];
|
||||||
onSlashOpen?: () => void;
|
onSlashOpen?: () => void;
|
||||||
onSlashAction?: (action: SlashActionId) => void;
|
onSlashAction?: (action: SlashActionId) => void;
|
||||||
|
/** Set while no model is connected: nothing can run until one is. */
|
||||||
|
onConnectModel?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState("");
|
||||||
@ -370,7 +373,19 @@ export const Composer = memo(function Composer({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mx-auto w-full max-w-4xl">
|
<div className="mx-auto w-full max-w-4xl">
|
||||||
{sendError || runError ? (
|
{onConnectModel ? (
|
||||||
|
<div
|
||||||
|
data-testid="composer-model-needed"
|
||||||
|
className="mb-3 flex items-center gap-3 rounded-[14px] border border-border bg-muted px-4 py-2 text-ui-body-sm text-foreground"
|
||||||
|
>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<Trans>Connect a model so your Oo can reply.</Trans>
|
||||||
|
</span>
|
||||||
|
<Button size="sm" onClick={onConnectModel}>
|
||||||
|
<Trans>Connect model</Trans>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : sendError || runError ? (
|
||||||
<div
|
<div
|
||||||
ref={runErrorRef}
|
ref={runErrorRef}
|
||||||
role="alert"
|
role="alert"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user