diff --git a/packages/app/src/components/titlebar-tabs.ts b/packages/app/src/components/titlebar-tabs.ts new file mode 100644 index 000000000000..396b764eb5bb --- /dev/null +++ b/packages/app/src/components/titlebar-tabs.ts @@ -0,0 +1,19 @@ +import { mapArray } from "solid-js" +import type { Accessor } from "solid-js" + +export type TitlebarTab = { dir: string; sessionId: string; href: string } + +export function createTitlebarTabsEnriched( + tabsStore: TitlebarTab[], + sessionForTab: (tab: TitlebarTab) => Accessor, +) { + const base = mapArray( + () => tabsStore, + (tab) => { + const info = sessionForTab(tab) + return { ...tab, info, title: () => info()?.title } + }, + ) + + return () => base().filter((tab) => tab.info()) +} diff --git a/packages/app/src/components/titlebar.test.ts b/packages/app/src/components/titlebar.test.ts new file mode 100644 index 000000000000..8b431c465a64 --- /dev/null +++ b/packages/app/src/components/titlebar.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "bun:test" + +describe("titlebar tabs", () => { + test("updates enriched tab title when the session title changes", () => { + const result = Bun.spawnSync({ + cmd: [ + process.execPath, + "--conditions=browser", + "--preload", + "./happydom.ts", + "-e", + ` + import { createMemo, createRoot } from "solid-js" + import { createStore } from "solid-js/store" + import { createTitlebarTabsEnriched } from "./src/components/titlebar-tabs.ts" + + createRoot((dispose) => { + const [tabs] = createStore([ + { dir: "/tmp/project", sessionId: "ses_1", href: "/tmp/project/session/ses_1" }, + ]) + const [sessions, setSessions] = createStore([{ id: "ses_1", title: "Session" }]) + const tabsEnriched = createTitlebarTabsEnriched(tabs, (tab) => () => + sessions.find((session) => session.id === tab.sessionId), + ) + const titleFor = (tab) => (typeof tab.title === "function" ? tab.title() : tab.title) + const titles = createMemo(() => tabsEnriched().map(titleFor)) + + if (JSON.stringify(titles()) !== JSON.stringify(["Session"])) { + throw new Error("expected initial title") + } + setSessions(0, "title", "Generated Title") + if (JSON.stringify(titles()) !== JSON.stringify(["Generated Title"])) { + throw new Error("expected generated title") + } + + dispose() + }) + `, + ], + cwd: new URL("../..", import.meta.url).pathname, + stderr: "pipe", + stdout: "pipe", + }) + + expect(result.exitCode, result.stderr.toString() || result.stdout.toString()).toBe(0) + }) +}) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 511375c1be0f..3ad7365a796d 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, For, mapArray, Match, Show, startTransition, Switch, untrack } from "solid-js" +import { createEffect, createMemo, For, Match, Show, startTransition, Switch, untrack } from "solid-js" import { createStore, produce } from "solid-js/store" import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -30,6 +30,7 @@ import { SESSION_TABS_REMOVED_EVENT, type SessionTabsRemovedDetail, } from "@/components/titlebar-session-events" +import { createTitlebarTabsEnriched, type TitlebarTab } from "./titlebar-tabs" type TauriDesktopWindow = { startDragging?: () => Promise @@ -255,10 +256,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { return `/${base64Encode(project.worktree)}/session` } - type Tab = { dir: string; sessionId: string; href: string } - const [tabsStore, tabsStoreActions] = iife(() => { - const [store, setStore] = createStore( + const [store, setStore] = createStore( iife(() => { if (!params.dir || !params.id) return [] return [ @@ -272,7 +271,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { ) const actions = { - addTab: (tab: Tab) => { + addTab: (tab: TitlebarTab) => { setStore( produce((tabs) => { if (tabs.some((t) => t.href === tab.href)) return @@ -448,17 +447,9 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { return commands }) - const tabsEnriched = iife(() => { - const base = mapArray( - () => tabsStore, - (tab) => { - const sync = serverSync.createDirSyncContext(tab.dir) - const session = sync.session.get(tab.sessionId) - return session ? { ...tab, info: session } : null - }, - ) - - return () => base().flatMap((s) => (s ? [s] : [])) + const tabsEnriched = createTitlebarTabsEnriched(tabsStore, (tab) => { + const sync = serverSync.createDirSyncContext(tab.dir) + return () => sync.session.get(tab.sessionId) }) return ( @@ -486,21 +477,25 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
- {(tab, i) => ( - <> - {i() !== 0 && ( -
- )} - tabsStoreActions.removeTab(tab.href)} - /> - - )} + {(tab, i) => { + const info = tab.info() + if (!info) return null + return ( + <> + {i() !== 0 && ( +
+ )} + tabsStoreActions.removeTab(tab.href)} + /> + + ) + }}