From 7a780d89046ee9618561b983e193bdb9123b374e Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 4 Aug 2026 06:05:22 +0200 Subject: [PATCH] fix: block creation when workspace summaries fail --- frontend/src/shell/SteerInput.test.tsx | 38 ++++++++++++++++++++++++++ frontend/src/shell/SteerInput.tsx | 14 +++++++--- frontend/src/workspaces/drafts.ts | 11 +++++++- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/frontend/src/shell/SteerInput.test.tsx b/frontend/src/shell/SteerInput.test.tsx index 608c6571..76d474bd 100644 --- a/frontend/src/shell/SteerInput.test.tsx +++ b/frontend/src/shell/SteerInput.test.tsx @@ -379,6 +379,44 @@ test("initial submit waits for delayed workspace summaries before allowing a con })); }); +test("failed workspace summaries block creation and report a safe error", async () => { + let body: unknown; + let failure: string | undefined; + let summaryRequestFailed = false; + localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({ + workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium", + })); + server.use( + http.get("http://localhost:8787/settings", () => HttpResponse.json({ workspace: "psd-clinical" })), + http.get("http://localhost:8787/workspaces", () => { + summaryRequestFailed = true; + return new HttpResponse(null, { status: 503 }); + }), + http.get("http://localhost:8787/models", () => HttpResponse.json({ models: [ + { provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true }, + ] })), + http.post("http://localhost:8787/sessions", async ({ request }) => { + body = await request.json(); + return HttpResponse.json({ id: "s1" }); + }), + ); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render( + + + { failure = message; }} /> + , + ); + + await waitFor(() => expect(summaryRequestFailed).toBe(true)); + await waitFor(() => expect(client.getQueryState(["workspaces"])?.status).toBe("error")); + await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q"); + await userEvent.click(screen.getByRole("button", { name: /send/i })); + + await waitFor(() => expect(failure).toBe("Could not load workspace registry. Please retry.")); + expect(body).toBeUndefined(); +}); + test("submit follows a rapid workspace switch instead of waiting for an abandoned policy", async () => { let body: unknown; let releaseC!: () => void; diff --git a/frontend/src/shell/SteerInput.tsx b/frontend/src/shell/SteerInput.tsx index 91f95936..7b08dbba 100644 --- a/frontend/src/shell/SteerInput.tsx +++ b/frontend/src/shell/SteerInput.tsx @@ -8,7 +8,9 @@ import { getSettings } from "../api/settings"; import { getWorkspace, listWorkspaces } from "../api/workspaces"; import { listModels } from "../api/models"; import { useSessionStore } from "../store/sessionStore"; -import { workspacePolicyGate, workspacePreferences, type WorkspacePreference } from "../workspaces/drafts"; +import { + WorkspaceSelectionError, workspacePolicyGate, workspacePreferences, type WorkspacePreference, +} from "../workspaces/drafts"; const THINKING_LEVELS = ["low", "medium", "high"] as const; @@ -94,7 +96,9 @@ export function SteerInput({ const payload = error instanceof ApiError ? (error.payload as { code?: string; error?: string } | undefined) : undefined; - const alert = payload?.code === "dwh_unreachable" ? payload.error : undefined; + const alert = payload?.code === "dwh_unreachable" + ? payload.error + : error instanceof WorkspaceSelectionError ? error.message : undefined; onSessionCreateFailed?.(alert); } finally { setBusy(false); @@ -156,7 +160,7 @@ export function SteerInput({ */ export function ComposerFooter() { const { data: settings } = useQuery({ queryKey: ["settings"], queryFn: getSettings }); - const { data: workspaces = [], isLoading: workspacesLoading } = useQuery({ + const { data: workspaces = [], isLoading: workspacesLoading, isError: workspaceSummariesError } = useQuery({ queryKey: ["workspaces"], queryFn: listWorkspaces, }); const { data: modelsData } = useQuery({ queryKey: ["models"], queryFn: listModels }); @@ -196,12 +200,14 @@ export function ComposerFooter() { workspacePolicyGate.clear(); } else if (workspacesLoading) { workspacePolicyGate.beginSummary(workspace); + } else if (workspaceSummariesError) { + workspacePolicyGate.rejectSummary(workspace); } else if (selectedWorkspace?.revision) { workspacePolicyGate.select(workspace); } else { workspacePolicyGate.allowLegacy(workspace); } - }, [selectedWorkspace, workspace, workspacesLoading]); + }, [selectedWorkspace, workspace, workspaceSummariesError, workspacesLoading]); useEffect(() => { if (!allowedModels?.length) return; diff --git a/frontend/src/workspaces/drafts.ts b/frontend/src/workspaces/drafts.ts index afac013f..7007eb71 100644 --- a/frontend/src/workspaces/drafts.ts +++ b/frontend/src/workspaces/drafts.ts @@ -9,6 +9,8 @@ export interface WorkspaceDraft { updatedAt: string; } +export class WorkspaceSelectionError extends Error {} + type PolicySelection = { workspaceId: string; generation: number; @@ -65,6 +67,13 @@ export const workspacePolicyGate = { } }, + rejectSummary(workspaceId: string): void { + if (!selection || selection.workspaceId !== workspaceId || selection.state !== "summary") return; + selection.state = "error"; + selection.error = new WorkspaceSelectionError("Could not load workspace registry. Please retry."); + selection.settle(); + }, + clear(): void { selection?.supersede(); selection = undefined; @@ -79,7 +88,7 @@ export const workspacePolicyGate = { reject(workspaceId: string): void { if (!selection || selection.workspaceId !== workspaceId || selection.state !== "pending") return; selection.state = "error"; - selection.error = new Error("Selected workspace policy could not be loaded."); + selection.error = new WorkspaceSelectionError("Could not load selected workspace policy. Please retry."); selection.settle(); },