From 04bbd222a782ac5cb86e0b4ab9340dd22f65a016 Mon Sep 17 00:00:00 2001 From: OpeOginni Date: Thu, 30 Jul 2026 10:54:10 +0200 Subject: [PATCH] fix(app): repair malformed persisted tabs --- packages/app/src/context/tab-migration.ts | 23 +++++++++++++++++++++++ packages/app/src/context/tabs.test.ts | 23 +++++++++++++++++++++++ packages/app/src/context/tabs.tsx | 9 ++------- 3 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 packages/app/src/context/tab-migration.ts diff --git a/packages/app/src/context/tab-migration.ts b/packages/app/src/context/tab-migration.ts new file mode 100644 index 000000000000..04c1cb6804ab --- /dev/null +++ b/packages/app/src/context/tab-migration.ts @@ -0,0 +1,23 @@ +import type { ServerConnection } from "./server" +import type { Tab } from "./tabs" + +export function migrateTabs(value: unknown, fallback: ServerConnection.Key): Tab[] { + if (!Array.isArray(value)) return [] + return value.flatMap((tab) => { + if (!tab || typeof tab !== "object") return [] + if ("server" in tab && typeof tab.server !== "string") return [] + const server = ("server" in tab ? tab.server : fallback) as ServerConnection.Key + if (tab.type === "session" && typeof tab.sessionId === "string") { + return [{ type: tab.type, server, sessionId: tab.sessionId }] + } + if ( + tab.type === "draft" && + typeof tab.draftID === "string" && + typeof tab.directory === "string" && + (tab.worktree === undefined || typeof tab.worktree === "string") + ) { + return [{ type: tab.type, server, draftID: tab.draftID, directory: tab.directory, worktree: tab.worktree }] + } + return [] + }) +} diff --git a/packages/app/src/context/tabs.test.ts b/packages/app/src/context/tabs.test.ts index b56ea2dfcf93..17371efd80ce 100644 --- a/packages/app/src/context/tabs.test.ts +++ b/packages/app/src/context/tabs.test.ts @@ -3,6 +3,7 @@ import { createRoot, getOwner, onCleanup } from "solid-js" import { createTabMemory } from "./tab-memory" import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" import type { SessionTab, Tab } from "./tabs" +import { migrateTabs } from "./tab-migration" import type { ServerConnection } from "./server" const server = "local\nhttp://localhost:4096" as ServerConnection.Key @@ -11,6 +12,28 @@ function sessionTab(sessionId: string): SessionTab { return { type: "session", server, sessionId } } +describe("tab migration", () => { + test("drops null and malformed persisted tabs", () => { + expect( + migrateTabs( + [null, sessionTab("a"), { type: "session", server }, { type: "unknown", server }, "invalid"], + server, + ), + ).toEqual([sessionTab("a")]) + }) + + test("adds the fallback server to valid legacy tabs", () => { + expect(migrateTabs([{ type: "session", sessionId: "a", dirBase64: "legacy" }], server)).toEqual([ + sessionTab("a"), + ]) + }) + + test("replaces invalid top-level persisted data", () => { + expect(migrateTabs(null, server)).toEqual([]) + expect(migrateTabs({}, server)).toEqual([]) + }) +}) + describe("tab memory", () => { test("keeps state until its tab is removed", () => { createRoot((dispose) => { diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index 75191d54cf4a..e4d50ac16118 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -12,6 +12,7 @@ import { sessionHref } from "@/utils/session-route" import { createTabMemory } from "./tab-memory" import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" import { createDraftPromptSession, type PromptModel } from "./prompt-state" +import { migrateTabs } from "./tab-migration" export type SessionTab = { type: "session" @@ -59,13 +60,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const [store, setStore, _, ready] = persisted( { ...Persist.window("tabs"), - migrate: (value: unknown) => { - if (!Array.isArray(value)) return value - return value.map((tab) => { - if (!tab || typeof tab !== "object" || "server" in tab) return tab - return { ...tab, server: fallback } - }) - }, + migrate: (value: unknown) => migrateTabs(value, fallback), }, createStore([]), )