diff --git a/packages/tui/src/context/session-tabs.tsx b/packages/tui/src/context/session-tabs.tsx index 3c8b56af0bb0..5902521d0a0d 100644 --- a/packages/tui/src/context/session-tabs.tsx +++ b/packages/tui/src/context/session-tabs.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" import { isDeepEqual } from "remeda" import { createSimpleContext } from "./helper" import { useClient } from "./client" @@ -126,22 +126,40 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp }) } + createEffect( + on( + [ + () => (enabled() && route.data.type === "session" ? route.data.sessionID : undefined), + () => config.tabs?.scope, + () => paths.cwd, + ], + ([routed]) => { + if (!routed || routed === "dummy") return + const sessionID = root(routed) + history = recordSessionTabHistory(history, sessionID) + const fallback = newTab() ? NEW_SESSION_TAB_TITLE : undefined + update((draft) => { + draft.tabs = openSessionTab(draft.tabs, { + sessionID, + title: title(sessionID, draft.tabs.find((tab) => tab.sessionID === sessionID)?.title, fallback), + }) + delete draft.unread[sessionID] + }) + }, + ), + ) + createEffect(() => { - if (!enabled()) return - if (route.data.type !== "session" || route.data.sessionID === "dummy") return + if (!enabled() || route.data.type !== "session" || route.data.sessionID === "dummy") return const sessionID = root(route.data.sessionID) - history = recordSessionTabHistory(history, sessionID) - const fallback = newTab() ? NEW_SESSION_TAB_TITLE : undefined - const tabs = openSessionTab(state().tabs, { - sessionID, - title: title(sessionID, state().tabs.find((tab) => tab.sessionID === sessionID)?.title, fallback), - }) - if (tabs === state().tabs && !state().unread[sessionID]) return + const tab = state().tabs.find((tab) => tab.sessionID === sessionID) + if (!tab) return + const nextTitle = title(sessionID, tab.title) + if ((!nextTitle || nextTitle === tab.title) && !state().unread[sessionID]) return update((draft) => { - draft.tabs = openSessionTab(draft.tabs, { - sessionID, - title: title(sessionID, draft.tabs.find((tab) => tab.sessionID === sessionID)?.title, fallback), - }) + const tab = draft.tabs.find((tab) => tab.sessionID === sessionID) + if (!tab) return + draft.tabs = openSessionTab(draft.tabs, { sessionID, title: title(sessionID, tab.title) }) delete draft.unread[sessionID] }) }) diff --git a/packages/tui/test/context/session-tabs.test.tsx b/packages/tui/test/context/session-tabs.test.tsx index 330cb1443ebb..8d7442ae0571 100644 --- a/packages/tui/test/context/session-tabs.test.tsx +++ b/packages/tui/test/context/session-tabs.test.tsx @@ -170,6 +170,31 @@ test("concurrent TUIs do not alternate shared tab titles from divergent session } }) +test("closing a tab is not undone by another TUI viewing the same session", async () => { + const state = stateDir("opencode-session-tabs-shared-close-") + const first = await renderSessionTabs("shared", { state }) + const second = await renderSessionTabs("shared", { state }) + + try { + await wait(() => first.tabs.tabs().some((tab) => tab.sessionID === "shared")) + await wait(() => second.tabs.tabs().some((tab) => tab.sessionID === "shared")) + first.tabs.close() + await wait(() => first.route.data.type === "home") + await wait(() => !second.tabs.tabs().some((tab) => tab.sessionID === "shared")) + await Bun.sleep(50) + + expect(first.tabs.tabs().some((tab) => tab.sessionID === "shared")).toBe(false) + + second.route.navigate({ type: "home" }) + await wait(() => second.route.data.type === "home") + second.route.navigate({ type: "session", sessionID: "shared" }) + await wait(() => first.tabs.tabs().some((tab) => tab.sessionID === "shared")) + } finally { + first.destroy() + second.destroy() + } +}) + test("user prompt admissions pulse an already-busy background tab", async () => { const setup = await renderSessionTabs("background") const admitted = (sessionID: string, inputID: string): OpenCodeEvent => ({