diff --git a/UI-DESIGN.md b/UI-DESIGN.md index d4ef9e5a..f3769edd 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -35,6 +35,7 @@ - 加载、空内容、无结果、失败、只读和权限不足是不同状态,必须分别呈现。 - 可撤销操作优先提供撤销,不可撤销操作必须在执行前说明影响范围。 - 不依赖颜色单独表达状态或风险。 +- 当前模型或 Runtime 及切换进度集中显示在会话输入区,顶栏不重复显示可用性标识。切换失败、自动状态检查失败或返回不可用时,通过应用通知说明原因;发送按钮保留关联的无障碍状态说明。 ### 2.5 默认支持浅色与深色主题 diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index a72827fb..04516f07 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -2109,22 +2109,8 @@ describe("App", () => { await screen.findByRole("heading", { name: "今天想一起完成什么?", }); - const runtimeStatus = - document.querySelector(".runtime-status"); - expect(runtimeStatus).not.toBeNull(); - expect(within(runtimeStatus!).queryByText("就绪")).not.toBeInTheDocument(); - expect(runtimeStatus).toHaveClass("runtime-status--ready"); - expect( - runtimeStatus?.querySelector(".runtime-status__dot"), - ).toBeInTheDocument(); - expect(runtimeStatus).toHaveAttribute( - "aria-describedby", - "topbar-runtime-detail", - ); - expect(screen.getByText("Ready", { selector: ".sr-only" })).toHaveAttribute( - "id", - "topbar-runtime-detail", - ); + expect(document.querySelector(".runtime-status")).not.toBeInTheDocument(); + expect(document.getElementById("topbar-runtime-detail")).toBeNull(); expect( screen.getByText( /你好,我是 GoodBuddy。你可以直接向我提问、添加本地文件/u, @@ -2137,14 +2123,24 @@ describe("App", () => { ).not.toBeInTheDocument(); }); - it("uses a compact color indicator while connecting to a Runtime", () => { + it("notifies when automatic Runtime status checks fail", async () => { + vi.mocked(api.conversations.list).mockResolvedValue([{ + id: "00000000-0000-4000-8000-000000000951", + projectId, + title: "Runtime check", + updatedAt: Date.now(), + messages: [], + runtimeSelection: { provider: "opencode" }, + }]); + vi.mocked(api.agent.getStatus).mockImplementation(async (selection) => { + if (selection) throw new Error("Runtime status check failed"); + return { id: "model", label: "sonnet-5", available: true, supportsToolExecution: true, detail: "Ready" }; + }); render(); - const connectingStatus = - document.querySelector(".runtime-status"); - expect(connectingStatus).toHaveClass("runtime-status--connecting"); - expect( - within(connectingStatus!).queryByText("连接中"), - ).not.toBeInTheDocument(); + expect(await screen.findByRole("alert")).toHaveTextContent("Runtime status check failed"); + const sendButton = screen.getByRole("button", { name: "发送" }); + expect(sendButton).toBeDisabled(); + expect(sendButton).toHaveAccessibleDescription("Runtime status check failed"); }); it("loads customized branding into the primary sidebar", async () => { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 377b4949..2560c69a 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -3952,6 +3952,16 @@ function App(): React.JSX.Element { } setRuntime(status); setRuntimeStatusKey(activeRuntimeSelectionKey); + if (!status.available) { + notify({ + tone: "error", + message: tRef.current("runtime.selectionUnavailable", { + label: status.label, + detail: status.detail, + }), + dedupeKey: "runtime-status", + }); + } if (!status.available && !runtimeSetupPromptedRef.current) { runtimeSetupPromptedRef.current = true; setView("settings"); @@ -3961,19 +3971,20 @@ function App(): React.JSX.Element { if (runtimeStatusRequestRef.current !== requestId) { return; } + const detail = reason instanceof Error + ? reason.message + : tRef.current("runtime.errors.readStatus"); setRuntime({ id: "setup", label: tRef.current("runtime.unavailable"), available: false, supportsToolExecution: false, - detail: - reason instanceof Error - ? reason.message - : tRef.current("runtime.errors.readStatus"), + detail, }); setRuntimeStatusKey(activeRuntimeSelectionKey); + notify({ tone: "error", message: detail, dedupeKey: "runtime-status" }); }); - }, [activeRuntimeSelectionKey, runtimeSettings, setView]); + }, [activeRuntimeSelectionKey, runtimeSettings, setView, notify]); const startNewConversation = useCallback( (projectId?: string, preview?: { ready: (conversation: Conversation) => void }): boolean => { @@ -8852,7 +8863,7 @@ function App(): React.JSX.Element { : runtime.available ? "ready" : "unavailable"; - const runtimeDetailId = "topbar-runtime-detail"; + const runtimeDetailId = "composer-runtime-detail"; const runtimeDetail = runtimeState === "connecting" ? t("runtime.connecting") @@ -9735,24 +9746,6 @@ function App(): React.JSX.Element { )}
- - - - {runtime?.label ?? t("runtime.detecting")} - - {runtime?.capability === "image-generation" && ( - - {t("runtime.imageGeneration")} - - )} - - - {runtimeDetail} - )} + + {runtimeDetail} +