From 4fee1bcf3c0970de55169b2879dea635ffcb386a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 00:16:03 +0000 Subject: [PATCH] fix(tui): preserve rollback-compatible tab state (#44277) --- packages/tui/src/context/session-tabs.tsx | 9 +++++---- packages/tui/test/context/session-tabs.test.tsx | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/tui/src/context/session-tabs.tsx b/packages/tui/src/context/session-tabs.tsx index b1183ca1d33..9dd11c67ed0 100644 --- a/packages/tui/src/context/session-tabs.tsx +++ b/packages/tui/src/context/session-tabs.tsx @@ -29,8 +29,8 @@ import { type TabsState = { tabs: SessionTab[] - // Read only long enough to remove the former client-owned state from persisted tab files. - unread?: Record + // Kept empty for rollback compatibility with clients that still read this field. + unread: Record } type PersistedState = { @@ -43,7 +43,7 @@ type ScrollAnchor = { screenY: number } -const empty = (): TabsState => ({ tabs: [] }) +const empty = (): TabsState => ({ tabs: [], unread: {} }) // Deliberately after connect settles: the visible session's mount syncs win the first slots. const TAB_PREFETCH_DELAY = 300 @@ -128,6 +128,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp const sessionID = root(tab.sessionID) return openSessionTab(tabs, { sessionID, title: title(sessionID, tab.title) }) }, []), + unread: {}, }) const current = () => (route.data.type === "session" ? root(route.data.sessionID) : undefined) const newTab = createMemo((open = false) => { @@ -219,7 +220,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp update((draft) => { const next = normalize(draft) draft.tabs = next.tabs - delete draft.unread + draft.unread = next.unread }) }) diff --git a/packages/tui/test/context/session-tabs.test.tsx b/packages/tui/test/context/session-tabs.test.tsx index f9c1fdb5f22..63fe46f027c 100644 --- a/packages/tui/test/context/session-tabs.test.tsx +++ b/packages/tui/test/context/session-tabs.test.tsx @@ -265,10 +265,10 @@ test("stores session tabs for the current working directory by default", async ( const file = path.join(setup.state, "test", "tui", "tabs.json") await wait(() => Bun.file(file).size > 0) const stored = await Bun.file(file).json() - expect(stored.global).toEqual({ tabs: [] }) + expect(stored.global).toEqual({ tabs: [], unread: {} }) expect(Object.keys(stored.cwd)).toEqual([directory]) expect(stored.cwd[directory].tabs.map((tab: { sessionID: string }) => tab.sessionID)).toEqual(["first"]) - expect(stored.cwd[directory]).not.toHaveProperty("unread") + expect(stored.cwd[directory].unread).toEqual({}) } finally { await setup.destroy() } @@ -336,14 +336,14 @@ test("acknowledges viewed sessions even when tabs are disabled", async () => { } }) -test("purges legacy persisted unread records", async () => { +test("empties legacy persisted unread records for rollback compatibility", async () => { const setup = await renderSessionTabs("first", { persisted: ["first"] }) try { const file = path.join(setup.state, "test", "tui", "tabs.json") - // Normalize rewrites the active scope; the legacy record must not survive it. + // Normalize rewrites the active scope; legacy values must not survive, but older clients require the field. await wait(async () => { const stored = await Bun.file(file).json() - return !("unread" in stored.cwd[directory]) + return Object.keys(stored.cwd[directory].unread).length === 0 }) } finally { await setup.destroy()