fix(tui): keep remotely closed tabs closed (#44715)

This commit is contained in:
Kit Langton
2026-08-24 13:06:02 -04:00
committed by GitHub
parent 3e82b1a9fd
commit f4cb9d06c8
2 changed files with 53 additions and 21 deletions
+25 -21
View File
@@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
import { useKeyboard, useRenderer } from "@opentui/solid"
import { isDeepEqual } from "remeda"
import { createSimpleContext } from "./helper"
@@ -156,26 +156,30 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
}
}
createEffect(() => {
if (!enabled()) return
if (route.data.type !== "session" || route.data.sessionID === "dummy") return
const sessionID = root(route.data.sessionID)
cancelledTabs.delete(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) return
update((draft) => {
if (cancelledTabs.has(sessionID)) return
draft.tabs = openSessionTab(draft.tabs, {
sessionID,
title: title(sessionID, draft.tabs.find((tab) => tab.sessionID === sessionID)?.title, fallback),
})
})
})
// Shared storage updates must not re-admit a tab unless this client changes route or scope.
createEffect(
on(
[
() => (enabled() && route.data.type === "session" ? route.data.sessionID : undefined),
() => config.tabs.scope,
],
([routed]) => {
if (!routed || routed === "dummy") return
const sessionID = root(routed)
cancelledTabs.delete(sessionID)
history = recordSessionTabHistory(history, sessionID)
if (state().tabs.some((tab) => tab.sessionID === sessionID)) return
const fallback = newTab() ? NEW_SESSION_TAB_TITLE : undefined
update((draft) => {
if (cancelledTabs.has(sessionID)) return
draft.tabs = openSessionTab(draft.tabs, {
sessionID,
title: title(sessionID, draft.tabs.find((tab) => tab.sessionID === sessionID)?.title, fallback),
})
})
},
),
)
// Viewed state is server-global, so acknowledgement runs even with tabs disabled: other
// clients rely on this client reporting what its user has seen.
@@ -505,6 +505,34 @@ 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 () => {
await using temporary = await tmpdir()
const clients: Awaited<ReturnType<typeof renderSessionTabs>>[] = []
try {
const first = await renderSessionTabs("shared", { state: temporary.path })
clients.push(first)
const second = await renderSessionTabs("shared", { state: temporary.path })
clients.push(second)
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 Promise.all([first.flush(), second.flush()])
const stored = await Bun.file(path.join(temporary.path, "test", "tui", "tabs.json")).json()
expect(stored.cwd[directory].tabs).toEqual([])
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 {
await Promise.allSettled(clients.map((client) => client.destroy()))
}
})
test("user prompt admissions pulse an already-busy background tab", async () => {
const setup = await renderSessionTabs("background")
const admitted = (sessionID: string, inboxID: string): OpenCodeEvent => ({