Skip to content

Commit f669c46

Browse files
feat(app): show the open session's title in the browser tab (#59131)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
1 parent 5b41e1f commit f669c46

5 files changed

Lines changed: 42 additions & 3 deletions

File tree

‎packages/app/e2e/regression/tabs.spec.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,21 @@ test("inactive session attention dots share the close control without marking th
121121
await expectPath(page, sessionHref(a.id))
122122
})
123123

124+
test("the browser title names the open session and resets after leaving it", async ({ page }) => {
125+
await mockWorkspace(page, { name: "Tabs", sessions: [a, b] })
126+
await page.goto(sessionHref(a.id))
127+
await expect(page).toHaveTitle(`${a.title} · OpenCode`)
128+
129+
const tabB = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(b.id)}"])`)
130+
await tabB.locator("[data-titlebar-tab-link]").click()
131+
await expectPath(page, sessionHref(b.id))
132+
await expect(page).toHaveTitle(`${b.title} · OpenCode`)
133+
134+
await page.getByRole("button", { name: "Home", exact: true }).click()
135+
await expectPath(page, "/")
136+
await expect(page).toHaveTitle("OpenCode")
137+
})
138+
124139
test("a tab does not reopen its title editor while a rename is saving", async ({ page }) => {
125140
await mockWorkspace(page, { name: "Tabs", sessions: [a, b] })
126141
await page.goto(sessionHref(a.id))

‎packages/app/index.html‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
name="viewport"
77
content="width=device-width, initial-scale=1, interactive-widget=resizes-content, viewport-fit=cover"
88
/>
9-
<title>OpenCode</title>
9+
<!-- data-sm lets the MetaProvider replace this pre-load title with the app's <Title> tags. -->
10+
<title data-sm>OpenCode</title>
1011
<link rel="icon" type="image/x-icon" href="%OPENCODE_FAVICON%" />
1112
<link rel="apple-touch-icon" sizes="180x180" href="%OPENCODE_APPLE_TOUCH_ICON%" />
1213
<link rel="manifest" href="/site.webmanifest" crossorigin="use-credentials" />

‎packages/app/src/app.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DialogProvider } from "@opencode/ui/context/dialog"
33
import { FileComponentProvider } from "@opencode/ui/context/file"
44
import { Font } from "@opencode/ui/font"
55
import { ThemeProvider } from "@opencode/ui/theme/context"
6-
import { MetaProvider } from "@solidjs/meta"
6+
import { MetaProvider, Title } from "@solidjs/meta"
77
import { type BaseRouterProps, Router } from "@solidjs/router"
88
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
99
import { type Component, createRenderEffect, ErrorBoundary, type JSX, type ParentProps } from "solid-js"
@@ -54,6 +54,7 @@ export function AppBaseProviders(
5454
) {
5555
return (
5656
<MetaProvider>
57+
<Title>OpenCode</Title>
5758
<Font />
5859
<ThemeProvider
5960
onThemeApplied={(_, mode, scheme) => {

‎packages/app/src/session/route.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { ErrorBoundary, createEffect, createMemo, Show, type ParentProps } from "solid-js"
2+
import { Title } from "@solidjs/meta"
23
import { useParams } from "@solidjs/router"
34
import { createStore } from "solid-js/store"
45
import { makeEventListener } from "@solid-primitives/event-listener"
56
import { DataProvider } from "@opencode/session-ui/context"
67
import { SessionUserMessage } from "@opencode/session-ui/message"
78
import { TextShimmer } from "@opencode/ui/text-shimmer"
9+
import { isFallbackTitle } from "@opencode/util/session-title-fallback"
810
import { CommentsProvider } from "@/composer/comments"
911
import { readPromptPresentation } from "@/composer/comment-note"
1012
import { FileProvider } from "@/workspaces/files/model"
@@ -39,6 +41,7 @@ export function TargetSessionRouteContent() {
3941
return (
4042
<>
4143
<MarkSessionViewed sessionID={() => params.id} />
44+
<SessionDocumentTitle sessionID={() => params.id} />
4245
<ModelsProvider directory={directory()}>
4346
<TargetSessionSettingsCommand />
4447
<SessionRouteErrorBoundary sessionID={params.id} serverKey={requireServerKey(params.serverKey)}>
@@ -207,3 +210,21 @@ function MarkSessionViewed(props: { sessionID: () => string | undefined }) {
207210

208211
return null
209212
}
213+
214+
// Browser tabs and desktop windows are otherwise indistinguishable, so name them after the open session.
215+
function SessionDocumentTitle(props: { sessionID: () => string }) {
216+
const data = useData()
217+
218+
const title = createMemo(() => {
219+
const value = data.session.get(props.sessionID())?.title
220+
221+
return isFallbackTitle(value) ? undefined : value
222+
})
223+
224+
// Keyed: <Title> reads its text inside a meta effect, which would hit a stale Show accessor once the session is deleted.
225+
return (
226+
<Show when={title()} keyed>
227+
{(value) => <Title>{`${value} · OpenCode`}</Title>}
228+
</Show>
229+
)
230+
}

‎packages/desktop/src/renderer/index.html‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
<head>
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
6-
<title>OpenCode</title>
6+
<!-- data-sm lets the MetaProvider replace this pre-load title with the app's <Title> tags. -->
7+
<title data-sm>OpenCode</title>
78
<link rel="icon" type="image/png" href="./favicon-96x96-v3.png" sizes="96x96" />
89
<link rel="icon" type="image/svg+xml" href="./favicon-v3.svg" />
910
<link rel="shortcut icon" href="./favicon-v3.ico" />

0 commit comments

Comments
 (0)