diff --git a/apps/desktop/src/components/assistant-ui/thread/status.test.tsx b/apps/desktop/src/components/assistant-ui/thread/status.test.tsx index b4920e1ec483..f6edaee0d7e3 100644 --- a/apps/desktop/src/components/assistant-ui/thread/status.test.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/status.test.tsx @@ -36,7 +36,7 @@ describe('ResponseLoadingIndicator timer', () => { const sessionA = renderIndicator() act(() => vi.advanceTimersByTime(5_000)) - expect(screen.getByText('5s')).toBeTruthy() + expect(screen.getAllByText((_, node) => node?.textContent === '5s').length).toBeGreaterThan(0) sessionA.unmount() $activeSessionId.set('session-b') @@ -44,13 +44,13 @@ describe('ResponseLoadingIndicator timer', () => { const sessionB = renderIndicator() act(() => vi.advanceTimersByTime(3_000)) - expect(screen.getByText('3s')).toBeTruthy() + expect(screen.getAllByText((_, node) => node?.textContent === '3s').length).toBeGreaterThan(0) sessionB.unmount() $activeSessionId.set('session-a') $turnStartedAt.set(new Date('2026-01-01T00:00:00.000Z').getTime()) renderIndicator() - expect(screen.getByText('8s')).toBeTruthy() + expect(screen.getAllByText((_, node) => node?.textContent === '8s').length).toBeGreaterThan(0) }) }) diff --git a/apps/desktop/src/components/chat/activity-timer-text.tsx b/apps/desktop/src/components/chat/activity-timer-text.tsx index aa439eb247d5..d9ca1c292e29 100644 --- a/apps/desktop/src/components/chat/activity-timer-text.tsx +++ b/apps/desktop/src/components/chat/activity-timer-text.tsx @@ -1,6 +1,7 @@ import { cn } from '@/lib/utils' import { formatElapsed } from './activity-timer' +import { StableText } from './stable-text' interface ActivityTimerTextProps { seconds: number @@ -9,16 +10,16 @@ interface ActivityTimerTextProps { export function ActivityTimerText({ seconds, className }: ActivityTimerTextProps) { return ( - {formatElapsed(seconds)} - + ) } diff --git a/apps/desktop/src/components/chat/stable-text.tsx b/apps/desktop/src/components/chat/stable-text.tsx new file mode 100644 index 000000000000..1e7e03815cd6 --- /dev/null +++ b/apps/desktop/src/components/chat/stable-text.tsx @@ -0,0 +1,23 @@ +import { cn } from '@/lib/utils' + +interface StableTextProps { + children: string + className?: string +} + +/** + * Renders text as a row of 1ch-wide cells so individual characters can't + * shift the layout as they change (e.g. digits in a ticking timer). + * Works with any proportional font — no need for font-mono. + */ +export function StableText({ children, className }: StableTextProps) { + return ( + + {children.split('').map((char, i) => ( + + {char} + + ))} + + ) +} diff --git a/apps/desktop/src/lib/statusbar.ts b/apps/desktop/src/lib/statusbar.tsx similarity index 92% rename from apps/desktop/src/lib/statusbar.ts rename to apps/desktop/src/lib/statusbar.tsx index b06f9cc0d60e..0c0d22599c58 100644 --- a/apps/desktop/src/lib/statusbar.ts +++ b/apps/desktop/src/lib/statusbar.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' +import { StableText } from '@/components/chat/stable-text' import { compactNumber } from '@/lib/format' import type { UsageStats } from '@/types/hermes' @@ -72,5 +73,9 @@ export function LiveDuration({ since }: { since: number | null | undefined }) { return () => window.clearInterval(timer) }, [since]) - return since ? formatDuration(now - since) : null + if (!since) { + return null + } + + return {formatDuration(now - since)} }