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
+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
) {