Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/app/src/components/titlebar-tabs.ts
Original file line number Diff line number Diff line change
@@ -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<T extends { title: string }>(
tabsStore: TitlebarTab[],
sessionForTab: (tab: TitlebarTab) => Accessor<T | undefined>,
) {
const base = mapArray(
() => tabsStore,
(tab) => {
const info = sessionForTab(tab)
return { ...tab, info, title: () => info()?.title }
},
)

return () => base().filter((tab) => tab.info())
}
47 changes: 47 additions & 0 deletions packages/app/src/components/titlebar.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
57 changes: 26 additions & 31 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<void>
Expand Down Expand Up @@ -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<Tab[]>(
const [store, setStore] = createStore<TitlebarTab[]>(
iife(() => {
if (!params.dir || !params.id) return []
return [
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -486,21 +477,25 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
<div class="flex min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden">
<div class="flex min-w-0 flex-row items-center gap-1.5 overflow-hidden">
<For each={tabsEnriched()}>
{(tab, i) => (
<>
{i() !== 0 && (
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
)}
<TabNavItem
href={tab.href}
title={tab.info.title}
project={projectForSession(tab.info, projects(), projectByID())}
directory={tab.dir}
sessionId={tab.info.id}
onClose={() => tabsStoreActions.removeTab(tab.href)}
/>
</>
)}
{(tab, i) => {
const info = tab.info()
if (!info) return null
return (
<>
{i() !== 0 && (
<div class="w-[1.5px] h-3 shrink-0 rounded-full bg-[var(--v2-background-bg-layer-02)]" />
)}
<TabNavItem
href={tab.href}
title={tab.title() ?? ""}
project={projectForSession(info, projects(), projectByID())}
directory={tab.dir}
sessionId={info.id}
onClose={() => tabsStoreActions.removeTab(tab.href)}
/>
</>
)
}}
</For>
</div>
<Show
Expand Down
Loading