fix(app): handle deleted session sync cache in V2 titlebar (#29328)

This commit is contained in:
Eric Guo 2026-05-26 21:23:17 +08:00 committed by GitHub
commit 1a8fd0e1dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 165 additions and 47 deletions

View file

@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test"
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "./titlebar-session-events"
describe("titlebar session events", () => {
test("reads valid removed session tab details", () => {
expect(
readSessionTabsRemovedDetail(
new CustomEvent(SESSION_TABS_REMOVED_EVENT, {
detail: { directory: "/tmp/project", sessionIDs: ["ses_1", "ses_2", 1] },
}),
),
).toEqual({
directory: "/tmp/project",
sessionIDs: ["ses_1", "ses_2"],
})
})
test("ignores invalid removed session tab details", () => {
expect(readSessionTabsRemovedDetail(new Event(SESSION_TABS_REMOVED_EVENT))).toBeUndefined()
expect(
readSessionTabsRemovedDetail(
new CustomEvent(SESSION_TABS_REMOVED_EVENT, {
detail: { directory: "/tmp/project", sessionIDs: [] },
}),
),
).toBeUndefined()
})
})

View file

@ -0,0 +1,29 @@
export const SESSION_TABS_REMOVED_EVENT = "opencode:session-tabs-removed"
export type SessionTabsRemovedDetail = {
directory: string
sessionIDs: string[]
}
export function notifySessionTabsRemoved(input: SessionTabsRemovedDetail) {
window.dispatchEvent(new CustomEvent(SESSION_TABS_REMOVED_EVENT, { detail: input }))
}
export function readSessionTabsRemovedDetail(event: Event): SessionTabsRemovedDetail | undefined {
if (!(event instanceof CustomEvent)) return undefined
const detail: unknown = event.detail
if (!detail || typeof detail !== "object") return undefined
if (!("directory" in detail)) return undefined
if (!("sessionIDs" in detail)) return undefined
if (typeof detail.directory !== "string") return undefined
if (!Array.isArray(detail.sessionIDs)) return undefined
const sessionIDs = detail.sessionIDs.filter((id): id is string => typeof id === "string")
if (sessionIDs.length === 0) return undefined
return {
directory: detail.directory,
sessionIDs,
}
}

View file

@ -24,6 +24,11 @@ import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx"
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
import { makeEventListener } from "@solid-primitives/event-listener"
import { StatusPopoverV2 } from "@/components/status-popover"
import {
readSessionTabsRemovedDetail,
SESSION_TABS_REMOVED_EVENT,
type SessionTabsRemovedDetail,
} from "@/components/titlebar-session-events"
type TauriDesktopWindow = {
startDragging?: () => Promise<void>
@ -276,7 +281,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
)
},
removeTab: (href: string) => {
startTransition(() => {
void startTransition(() => {
setStore(
produce((tabs) => {
const index = tabs.findIndex((t) => t.href === href)
@ -289,11 +294,45 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
)
})
},
removeSessions: (input: SessionTabsRemovedDetail) => {
void startTransition(() => {
setStore(
produce((tabs) => {
const sessionIDs = new Set(input.sessionIDs)
const currentHref = params.dir && params.id ? makeSessionHref(params.dir, params.id) : undefined
const currentIndex = currentHref ? tabs.findIndex((tab) => tab.href === currentHref) : -1
const removedCurrent =
currentIndex !== -1 &&
tabs[currentIndex]?.dir === input.directory &&
sessionIDs.has(tabs[currentIndex]?.sessionId ?? "")
for (let i = tabs.length - 1; i >= 0; i--) {
const tab = tabs[i]
if (!tab) continue
if (tab.dir !== input.directory) continue
if (!sessionIDs.has(tab.sessionId)) continue
tabs.splice(i, 1)
}
if (!removedCurrent) return
const nextTab = tabs[currentIndex] ?? tabs[tabs.length - 1]
if (nextTab) navigate(nextTab.href)
else navigate("/")
}),
)
})
},
}
return [store, actions]
})
makeEventListener(window, SESSION_TABS_REMOVED_EVENT, (event) => {
const detail = readSessionTabsRemovedDetail(event)
if (!detail) return
tabsStoreActions.removeSessions(detail)
})
createEffect(() => {
const params = useParams()
if (!(params.dir && params.id)) return