fix: remove redundant topbar runtime status

Keep model selection and switching feedback in the composer. Report automatic runtime status failures through notifications and move the send button accessible description into the composer.

Release note: Remove the duplicate top-right model/runtime badge while retaining switching feedback and error notifications.
This commit is contained in:
mesalogo
2026-09-19 20:24:00 +08:00
parent ec08109a09
commit df89d96803
5 changed files with 40 additions and 106 deletions
+1
View File
@@ -35,6 +35,7 @@
- 加载、空内容、无结果、失败、只读和权限不足是不同状态,必须分别呈现。
- 可撤销操作优先提供撤销,不可撤销操作必须在执行前说明影响范围。
- 不依赖颜色单独表达状态或风险。
- 当前模型或 Runtime 及切换进度集中显示在会话输入区,顶栏不重复显示可用性标识。切换失败、自动状态检查失败或返回不可用时,通过应用通知说明原因;发送按钮保留关联的无障碍状态说明。
### 2.5 默认支持浅色与深色主题
+19 -23
View File
@@ -2109,22 +2109,8 @@ describe("App", () => {
await screen.findByRole("heading", {
name: "今天想一起完成什么?",
});
const runtimeStatus =
document.querySelector<HTMLElement>(".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(<App />);
const connectingStatus =
document.querySelector<HTMLElement>(".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 () => {
+20 -24
View File
@@ -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 {
</>
)}
<div className="topbar__actions">
<span
aria-describedby={runtimeDetailId}
className={`runtime-status runtime-status--${runtimeState}`}
title={runtimeDetail}
>
<span className="runtime-status__dot" />
<span className="runtime-status__label">
{runtime?.label ?? t("runtime.detecting")}
</span>
{runtime?.capability === "image-generation" && (
<span className="runtime-capability-badge">
{t("runtime.imageGeneration")}
</span>
)}
</span>
<span className="sr-only" id={runtimeDetailId}>
{runtimeDetail}
</span>
<button
aria-controls="assistant-sidebar"
aria-expanded={assistantSidebarOpen}
@@ -10829,6 +10822,9 @@ function App(): React.JSX.Element {
/>
</button>
)}
<span className="sr-only" id={runtimeDetailId}>
{runtimeDetail}
</span>
<button
aria-describedby={
runtimeState !== "ready"
@@ -84,18 +84,6 @@ describe('WorkspacePrimitives', () => {
cleanup()
})
it('uses semantic colors for compact Runtime status dots', () => {
expect(stylesheet).toMatch(
/\.runtime-status--ready \.runtime-status__dot\s*\{[^}]*background:\s*var\(--success\);/u
)
expect(stylesheet).toMatch(
/\.runtime-status--connecting \.runtime-status__dot\s*\{[^}]*background:\s*var\(--warning\);/u
)
expect(stylesheet).toMatch(
/\.runtime-status--unavailable \.runtime-status__dot\s*\{[^}]*background:\s*var\(--text-muted\);/u
)
})
it('wraps long model names, prompts, and errors inside shared message status cards', () => {
expect(stylesheet).toMatch(
/\.message-retrieval-status\s*\{[^}]*overflow-wrap:\s*anywhere;/u
-47
View File
@@ -5419,51 +5419,6 @@ button > svg {
gap: 3px;
}
.runtime-status {
display: flex;
min-width: 0;
max-width: clamp(108px, 18vw, 220px);
align-items: center;
padding: 6px 10px;
border: 1px solid var(--border-default);
border-radius: 8px;
margin-right: 8px;
background: var(--surface-raised);
color: var(--text-secondary);
font-size: var(--font-caption);
font-weight: 600;
gap: 6px;
}
.runtime-status__dot {
flex: 0 0 auto;
}
.runtime-status__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.runtime-status__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-muted);
}
.runtime-status--ready .runtime-status__dot {
background: var(--success);
}
.runtime-status--connecting .runtime-status__dot {
background: var(--warning);
}
.runtime-status--unavailable .runtime-status__dot {
background: var(--text-muted);
}
.chat-scroll-region {
position: relative;
min-height: 0;
@@ -16234,7 +16189,6 @@ details.settings-section > :not(summary) + :not(summary) {
.user-card,
.icon-button,
.assistant-sidebar__row,
.runtime-status,
.welcome__description,
.tool-activity,
.context-chip,
@@ -16822,7 +16776,6 @@ details.settings-section > :not(summary) + :not(summary) {
:root :is(
.task-status-dot,
.runtime-status__dot,
.heartbeat-center__pulse-dot,
.heartbeat-center__timeline-dot
) {