fix(web): sync browser tab title with session (#1034)

* test: reproduce issue #712

* fix: sync browser title with session (closes #712)
This commit is contained in:
SSU-WEI HUANG
2026-07-16 12:30:21 +08:00
committed by GitHub
parent 553b3492f1
commit 53406b2e97
6 changed files with 109 additions and 28 deletions
+1 -14
View File
@@ -13,20 +13,7 @@ import { getSessionModelLabel } from '@/lib/sessionModelLabel'
import { useTranslation } from '@/lib/use-translation'
import { AgentFlavorIcon } from '@/components/AgentFlavorIcon'
import { isFastServiceTier } from '@/components/AssistantChat/codexFastMode'
function getSessionTitle(session: Session): string {
if (session.metadata?.name) {
return session.metadata.name
}
if (session.metadata?.summary?.text) {
return session.metadata.summary.text
}
if (session.metadata?.path) {
const parts = session.metadata.path.split('/').filter(Boolean)
return parts.length > 0 ? parts[parts.length - 1] : session.id.slice(0, 8)
}
return session.id.slice(0, 8)
}
import { getSessionTitle } from '@/lib/sessionTitle'
function FilesIcon(props: { className?: string }) {
return (
+2 -13
View File
@@ -22,6 +22,7 @@ import { formatRelativeTime } from '@/lib/relativeTime'
import { formatScheduledTooltipDetail } from '@/lib/scheduledTime'
import { getCodexImportedAt, subscribeCodexImportedSessions } from '@/lib/codexImportedSessions'
import { formatReopenError } from '@/lib/reopenError'
import { getSessionTitle } from '@/lib/sessionTitle'
import type { Machine } from '@/types/api'
import { getMachinePlatform, presentMachineHealth } from '@/lib/machineHealth'
import { MachineGroupHeader } from '@/components/MachineGroupHeader'
@@ -439,19 +440,7 @@ function ChevronIcon(props: { className?: string; collapsed?: boolean }) {
)
}
export function getSessionTitle(session: SessionSummary): string {
if (session.metadata?.name) {
return session.metadata.name
}
if (session.metadata?.summary?.text) {
return session.metadata.summary.text
}
if (session.metadata?.path) {
const parts = session.metadata.path.split('/').filter(Boolean)
return parts.length > 0 ? parts[parts.length - 1] : session.id.slice(0, 8)
}
return session.id.slice(0, 8)
}
export { getSessionTitle } from '@/lib/sessionTitle'
function getTodoProgress(session: SessionSummary): { completed: number; total: number } | null {
if (!session.todoProgress) return null
@@ -0,0 +1,64 @@
import { renderHook } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import type { Session } from '@/types/api'
import { useSessionBrowserTitle } from './useSessionBrowserTitle'
function makeSession(metadata: Session['metadata']): Session {
return {
id: '1234567890abcdef',
active: true,
thinking: false,
activeAt: 0,
updatedAt: 0,
metadata,
} as Session
}
describe('useSessionBrowserTitle', () => {
it('tracks session title updates and restores the app title on unmount', () => {
document.title = 'HAPI'
const initialSession = makeSession({
path: '/work/hapi',
host: 'localhost',
summary: { text: 'Initial summary', updatedAt: 1 },
})
const { rerender, unmount } = renderHook(
({ session }) => useSessionBrowserTitle(session),
{ initialProps: { session: initialSession } },
)
expect(document.title).toBe('Initial summary - HAPI')
rerender({
session: makeSession({
...initialSession.metadata!,
name: 'Renamed session',
}),
})
expect(document.title).toBe('Renamed session - HAPI')
unmount()
expect(document.title).toBe('HAPI')
})
it('uses the app title while loading and the shared session fallbacks when titles are missing', () => {
document.title = 'Stale session - HAPI'
const { rerender } = renderHook(
({ session }: { session: Session | null }) => useSessionBrowserTitle(session),
{ initialProps: { session: null as Session | null } },
)
expect(document.title).toBe('HAPI')
rerender({
session: makeSession({ path: '/work/hapi', host: 'localhost' }),
})
expect(document.title).toBe('hapi - HAPI')
rerender({ session: makeSession(null) })
expect(document.title).toBe('12345678 - HAPI')
})
})
+17
View File
@@ -0,0 +1,17 @@
import { useEffect } from 'react'
import type { Session } from '@/types/api'
import { getSessionTitle } from '@/lib/sessionTitle'
const APP_TITLE = 'HAPI'
export function useSessionBrowserTitle(session: Session | null): void {
const sessionTitle = session ? getSessionTitle(session) : null
useEffect(() => {
document.title = sessionTitle ? `${sessionTitle} - ${APP_TITLE}` : APP_TITLE
return () => {
document.title = APP_TITLE
}
}, [sessionTitle])
}
+22
View File
@@ -0,0 +1,22 @@
type SessionTitleSource = {
id: string
metadata?: {
name?: string
summary?: { text: string }
path?: string
} | null
}
export function getSessionTitle(session: SessionTitleSource): string {
if (session.metadata?.name) {
return session.metadata.name
}
if (session.metadata?.summary?.text) {
return session.metadata.summary.text
}
if (session.metadata?.path) {
const parts = session.metadata.path.split('/').filter(Boolean)
return parts.length > 0 ? parts[parts.length - 1] : session.id.slice(0, 8)
}
return session.id.slice(0, 8)
}
+3 -1
View File
@@ -41,6 +41,7 @@ import { fetchLatestMessages, seedMessageWindowFromSession } from '@/lib/message
import { clearDraftsAfterSend } from '@/lib/clearDraftsAfterSend'
import { inactiveSessionCanResume } from '@/lib/sessionResume'
import { markSessionSeen } from '@/lib/sessionLastSeen'
import { useSessionBrowserTitle } from '@/hooks/useSessionBrowserTitle'
import { clearCodexImportedSession, markCodexSessionsImported } from '@/lib/codexImportedSessions'
import type { Machine, CodexDuplicateSessionGroup, CodexLocalSessionSummary } from '@/types/api'
import FilesPage from '@/routes/sessions/files'
@@ -949,7 +950,8 @@ function SessionDetailRoute() {
const pathname = useLocation({ select: location => location.pathname })
const { sessionId } = useParams({ from: '/sessions/$sessionId' })
const navigate = useNavigate()
const { notFound: sessionNotFound } = useSession(api, sessionId)
const { session, notFound: sessionNotFound } = useSession(api, sessionId)
useSessionBrowserTitle(session)
const basePath = `/sessions/${sessionId}`
const isChat = pathname === basePath || pathname === `${basePath}/`