From 636ef5dcc21ec2658f98a33b67445386ea0378b2 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Wed, 2 Sep 2026 10:07:04 +0200 Subject: [PATCH] fix(quick-pane): recover catalog after bridge restarts Treat missing stale runtimes as already closed and defer thinking reconciliation until the refreshed model catalog is ready. --- ui/src/quick-pane/QuickPane.test.tsx | 46 ++++++++++++++++++++++++++++ ui/src/quick-pane/QuickPane.tsx | 6 +++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/ui/src/quick-pane/QuickPane.test.tsx b/ui/src/quick-pane/QuickPane.test.tsx index a24f563..0f45ac6 100644 --- a/ui/src/quick-pane/QuickPane.test.tsx +++ b/ui/src/quick-pane/QuickPane.test.tsx @@ -459,6 +459,52 @@ describe("QuickPane", () => { ); }); + it("recovers a stale Quick runtime and keeps persisted thinking after bridge restart", async () => { + const thinkingModels: Model[] = [ + { + provider: "provider", + id: "quick/model", + reasoning: true, + thinkingLevelMap: { high: "high" }, + }, + ]; + const persistedSettings = { + ...settings, + defaultThinkingLevel: "high", + }; + const tracker = bridgeMock([thinkingModels, thinkingModels], undefined, { + ...uiState, + quickPane: persistedSettings, + }); + const invoke = mocks.invoke.getMockImplementation(); + mocks.invoke.mockImplementation( + (command: string, payload?: { runtimeId?: string }) => { + if ( + command === "close_quick_runtime" && + payload?.runtimeId === "runtime-1" + ) + return Promise.reject( + new Error("unknown session runtime: stale-runtime-id"), + ); + return invoke?.(command, payload); + }, + ); + render(); + await preparedTrigger(); + const settingsListener = mocks.listen.mock.calls.find( + ([event]) => event === "quick-pane-settings-saved", + )?.[1] as ((event: { payload: QuickPaneSettings }) => void) | undefined; + expect(settingsListener).toBeTypeOf("function"); + + await act(async () => settingsListener?.({ payload: persistedSettings })); + + await waitFor(() => expect(tracker.runtimeCount()).toBe(2)); + expect(screen.queryByText(/unknown session runtime/)).toBeNull(); + expect(screen.getByRole("combobox", { name: "Thinking" })).toHaveValue( + "high", + ); + }); + it("derives thinking choices from selected model metadata and falls back to Off", async () => { const thinkingModels: Model[] = [ { diff --git a/ui/src/quick-pane/QuickPane.tsx b/ui/src/quick-pane/QuickPane.tsx index a59572d..7422928 100644 --- a/ui/src/quick-pane/QuickPane.tsx +++ b/ui/src/quick-pane/QuickPane.tsx @@ -341,10 +341,11 @@ export default function QuickPane() { !!extension; useEffect(() => { + if (catalogStatus !== "ready") return; setThinkingLevel((current) => reconcileThinkingLevel(selectedCatalogModel, current), ); - }, [selectedCatalogModel]); + }, [catalogStatus, selectedCatalogModel]); const recordInputSelection = (input: HTMLTextAreaElement) => { if ( @@ -468,6 +469,9 @@ export default function QuickPane() { cleanupQueueRef.current.add(runtimeId); const cleanup = invoke("close_quick_runtime", { runtimeId }) + .catch((reason) => { + if (!String(reason).includes("unknown session runtime:")) throw reason; + }) .then(() => { cleanupQueueRef.current.delete(runtimeId); })