mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
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:
@@ -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 (
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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
@@ -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}/`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user