diff --git a/clients/web/src/lib/oauthResume.test.ts b/clients/web/src/lib/oauthResume.test.ts index 34ccdad5..bd73611d 100644 --- a/clients/web/src/lib/oauthResume.test.ts +++ b/clients/web/src/lib/oauthResume.test.ts @@ -123,6 +123,40 @@ describe("oauthResume", () => { expect(setToolsUi).toHaveBeenCalledWith(toolsUi); }); + it("drops a pre-#2001 selectedToolName from a legacy snapshot, keeping the rest", () => { + // A redirect begun on a build that keyed tool selection by name, restored + // on one that keys it by row identity (the app upgraded mid-redirect). + // The name can't be mapped to a row key here — the tools list is fetched + // after reconnect — so the selection is dropped, not carried through as a + // stray field, and everything else survives. + const setToolsUi = vi.fn(); + restoreTabUiFromSnapshot( + { + Tools: { + formValues: { city: "NYC" }, + search: "get", + runAsTask: false, + selectedToolName: "get_temp", + }, + }, + { + setToolsUi, + setPromptsUi: vi.fn(), + setResourcesUi: vi.fn(), + setAppsUi: vi.fn(), + setTasksUi: vi.fn(), + setLogsUi: vi.fn(), + setProtocolUi: vi.fn(), + setNetworkUi: vi.fn(), + }, + ); + expect(setToolsUi).toHaveBeenCalledWith({ + formValues: { city: "NYC" }, + search: "get", + runAsTask: false, + }); + }); + it("falls back to legacy pending server key", () => { storage.set(OAUTH_PENDING_SERVER_KEY, "legacy-srv"); const snapshot = readOAuthResumeSnapshot(); diff --git a/clients/web/src/lib/oauthResume.ts b/clients/web/src/lib/oauthResume.ts index 30a05837..4684ed71 100644 --- a/clients/web/src/lib/oauthResume.ts +++ b/clients/web/src/lib/oauthResume.ts @@ -96,6 +96,23 @@ export function buildTabUiSnapshot( }; } +/** + * A snapshot written before #2001 carries `selectedToolName` (a tool's name) + * where {@link ToolsUiState} now expects `selectedToolKey` (its `index:name` + * row identity). A snapshot only lives for the length of one OAuth redirect, + * so this matters exactly when the app is upgraded mid-redirect — and the name + * cannot be mapped to a row key here, since the tools list is fetched after + * reconnect, long after restore. So the stale selection is dropped rather than + * carried as a stray field that would be re-serialized on the next redirect; + * the rest of the tab state (search text, form values) is restored intact and + * the screen opens on its "select a tool" placeholder. + */ +function normalizeToolsUi(value: unknown): ToolsUiState { + const ui = { ...((value as ToolsUiState | undefined) ?? EMPTY_TOOLS_UI) }; + delete (ui as ToolsUiState & { selectedToolName?: string }).selectedToolName; + return ui; +} + export function restoreTabUiFromSnapshot( tabUi: Partial> | undefined, setters: TabUiSetters, @@ -110,9 +127,7 @@ export function restoreTabUiFromSnapshot( const value = tabUi[tabId]; switch (tabId) { case "Tools": - setters.setToolsUi( - (value as ToolsUiState | undefined) ?? EMPTY_TOOLS_UI, - ); + setters.setToolsUi(normalizeToolsUi(value)); break; case "Prompts": setters.setPromptsUi(