From 1b61c98eb4704c15cb5a1412135a671ed0f49ee7 Mon Sep 17 00:00:00 2001 From: Artiom Neganov Date: Mon, 8 Jun 2026 23:46:47 +0300 Subject: [PATCH] feat(desktop): make window title reflect currently selected project and session --- packages/app/src/pages/home.tsx | 3 +++ packages/app/src/pages/layout.tsx | 15 +++++++++++++++ packages/ui/package.json | 1 + packages/ui/src/utils/first.ts | 10 ++++++++++ packages/ui/src/v2/components/avatar-v2.tsx | 12 +----------- .../ui/src/v2/components/project-avatar-v2.tsx | 12 +----------- 6 files changed, 31 insertions(+), 22 deletions(-) create mode 100644 packages/ui/src/utils/first.ts diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index eed3b6bde045..d44523e1da7a 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -231,6 +231,9 @@ function HomeDesign() { if (conn) setSelection({ server: ServerConnection.key(conn) }) }) + createEffect(() => { + document.title = "OpenCode" + }) createEffect(() => { const pending = pendingHomeNavigation if (!pending || pending.server !== server.key) return diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 199db2fda66c..2bb2d5a68909 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -25,6 +25,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" +import { first } from "@opencode-ai/ui/utils/first" import { getFilename } from "@opencode-ai/core/util/path" import { Session, type Message } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" @@ -638,6 +639,12 @@ export default function Layout(props: ParentProps) { return result }) + const currentSession = createMemo(() => { + const id = params.id + if (!id) return + return currentSessions().find((s) => s.id === id) + }) + type PrefetchQueue = { inflight: Set pending: string[] @@ -886,6 +893,14 @@ export default function Layout(props: ParentProps) { warm(sessions, index) }) + createEffect(() => { + const project = currentProject() + const session = currentSession() + const projectName = project ? displayName(project) : getFilename(currentDir() ?? "") + const letter = first(projectName).toUpperCase() + const title = session?.title + document.title = title ? `${letter}|${title} - ${projectName}` : `${letter}|${projectName}` + }) function navigateSessionByOffset(offset: number) { const sessions = currentSessions() diff --git a/packages/ui/package.json b/packages/ui/package.json index ce934925aba3..e42743580a9e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -23,6 +23,7 @@ "./icons/app": "./src/components/app-icons/types.ts", "./fonts/*": "./src/assets/fonts/*", "./audio/*": "./src/assets/audio/*", + "./utils/*": "./src/utils/*.ts", "./v2/*.css": "./src/v2/components/*.css", "./v2/*": "./src/v2/components/*.tsx", "./v2/styles/*": "./src/v2/styles/*" diff --git a/packages/ui/src/utils/first.ts b/packages/ui/src/utils/first.ts new file mode 100644 index 000000000000..1600e28c0c24 --- /dev/null +++ b/packages/ui/src/utils/first.ts @@ -0,0 +1,10 @@ +const segmenter = + typeof Intl !== "undefined" && "Segmenter" in Intl + ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) + : undefined + +export function first(value: string) { + if (!value) return "" + if (!segmenter) return Array.from(value)[0] ?? "" + return segmenter.segment(value)[Symbol.iterator]().next().value?.segment ?? Array.from(value)[0] ?? "" +} diff --git a/packages/ui/src/v2/components/avatar-v2.tsx b/packages/ui/src/v2/components/avatar-v2.tsx index 37b223772f2c..8b7786c325c8 100644 --- a/packages/ui/src/v2/components/avatar-v2.tsx +++ b/packages/ui/src/v2/components/avatar-v2.tsx @@ -1,17 +1,7 @@ +import { first } from "../../utils/first" import { type ComponentProps, splitProps, Show } from "solid-js" import "./avatar-v2.css" -const segmenter = - typeof Intl !== "undefined" && "Segmenter" in Intl - ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) - : undefined - -function first(value: string) { - if (!value) return "" - if (!segmenter) return Array.from(value)[0] ?? "" - return segmenter.segment(value)[Symbol.iterator]().next().value?.segment ?? Array.from(value)[0] ?? "" -} - export interface AvatarProps extends ComponentProps<"div"> { fallback: string src?: string diff --git a/packages/ui/src/v2/components/project-avatar-v2.tsx b/packages/ui/src/v2/components/project-avatar-v2.tsx index ff5cfa525f28..99c69fdbc3f9 100644 --- a/packages/ui/src/v2/components/project-avatar-v2.tsx +++ b/packages/ui/src/v2/components/project-avatar-v2.tsx @@ -1,17 +1,7 @@ +import { first } from "../../utils/first" import { type ComponentProps, splitProps, Show } from "solid-js" import "./project-avatar-v2.css" -const segmenter = - typeof Intl !== "undefined" && "Segmenter" in Intl - ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) - : undefined - -function first(value: string) { - if (!value) return "" - if (!segmenter) return Array.from(value)[0] ?? "" - return segmenter.segment(value)[Symbol.iterator]().next().value?.segment ?? Array.from(value)[0] ?? "" -} - export const PROJECT_AVATAR_VARIANTS = [ "orange", "yellow",