diff --git a/web/src/chat/presentation.test.ts b/web/src/chat/presentation.test.ts index e444b5f5..efa45320 100644 --- a/web/src/chat/presentation.test.ts +++ b/web/src/chat/presentation.test.ts @@ -167,8 +167,13 @@ describe('formatResetTime', () => { describe('formatMessageTimestamp', () => { it('formats today without requiring a date prefix', () => { const now = new Date(2026, 4, 22, 14, 30) - const result = formatMessageTimestamp(new Date(2026, 4, 22, 9, 5), now) - expect(result).toBeTruthy() + const date = new Date(2026, 4, 22, 9, 5) + const result = formatMessageTimestamp(date, now) + expect(result).toBe(date.toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + hourCycle: 'h23' + })) expect(result).not.toContain('2026') }) diff --git a/web/src/chat/presentation.ts b/web/src/chat/presentation.ts index 28a750ce..823c0595 100644 --- a/web/src/chat/presentation.ts +++ b/web/src/chat/presentation.ts @@ -32,15 +32,15 @@ export function formatMessageTimestamp(date: Date, now: Date = new Date()): stri && date.getDate() === now.getDate() if (sameDay) { - return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) + return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }) } const sameYear = date.getFullYear() === now.getFullYear() if (sameYear) { - return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) + return date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }) } - return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) + return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }) } export function formatMessageTimestampTitle(date: Date): string { diff --git a/web/src/components/AssistantChat/messages/MessageTimestamp.tsx b/web/src/components/AssistantChat/messages/MessageTimestamp.tsx index 650bd51e..fdc245ef 100644 --- a/web/src/components/AssistantChat/messages/MessageTimestamp.tsx +++ b/web/src/components/AssistantChat/messages/MessageTimestamp.tsx @@ -1,5 +1,6 @@ import { useAssistantState } from '@assistant-ui/react' import { formatMessageTimestamp, formatMessageTimestampTitle } from '@/chat/presentation' +import { cn } from '@/lib/utils' type MessageTimestampProps = { className?: string @@ -12,7 +13,7 @@ export function MessageTimestamp(props: MessageTimestampProps) { diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index 70e3d9c7..66f233d7 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -730,7 +730,10 @@ function SessionListSearch(props: { ) } -function formatCodexImportedRelativeTime(value: number, t: (key: string, params?: Record) => string): string | null { +function formatCodexImportedRelativeTime( + value: number, + t: (key: string, params?: Record) => string +): string | null { const ms = value < 1_000_000_000_000 ? value * 1000 : value if (!Number.isFinite(ms)) return null const delta = Date.now() - ms @@ -741,10 +744,13 @@ function formatCodexImportedRelativeTime(value: number, t: (key: string, params? if (hours < 24) return t('session.time.importedFromCodex.hoursAgo', { n: hours }) const days = Math.floor(hours / 24) if (days < 7) return t('session.time.importedFromCodex.daysAgo', { n: days }) - return new Date(ms).toLocaleDateString() + return formatRelativeTime(value, t) } -function getSessionTimeLabel(session: SessionSummary, t: (key: string, params?: Record) => string): string | null { +function getSessionTimeLabel( + session: SessionSummary, + t: (key: string, params?: Record) => string +): string | null { const codexSessionId = session.metadata?.agentSessionId const importedAt = session.metadata?.flavor === 'codex' ? getCodexImportedAt(codexSessionId) @@ -904,7 +910,7 @@ function SessionItem(props: { {t('session.item.pending')} {s.pendingRequestsCount} ) : null} - + {getSessionTimeLabel(s, t)} diff --git a/web/src/lib/relativeTime.test.ts b/web/src/lib/relativeTime.test.ts new file mode 100644 index 00000000..24f49e4e --- /dev/null +++ b/web/src/lib/relativeTime.test.ts @@ -0,0 +1,23 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { formatRelativeTime, formatSessionListDate } from '@/lib/relativeTime' + +const t = (key: string) => key + +afterEach(() => { + vi.useRealTimers() +}) + +describe('formatSessionListDate', () => { + it('zero-pads months and days', () => { + expect(formatSessionListDate(new Date(2026, 6, 7))).toBe('2026/07/07') + }) +}) + +describe('formatRelativeTime', () => { + it('uses the padded date after the relative-time window', () => { + vi.useFakeTimers() + vi.setSystemTime(new Date(2026, 6, 21, 12, 0)) + + expect(formatRelativeTime(new Date(2026, 6, 7, 9, 0).getTime(), t)).toBe('2026/07/07') + }) +}) diff --git a/web/src/lib/relativeTime.ts b/web/src/lib/relativeTime.ts index 31abbb13..ac014256 100644 --- a/web/src/lib/relativeTime.ts +++ b/web/src/lib/relativeTime.ts @@ -1,3 +1,10 @@ +export function formatSessionListDate(date: Date): string { + const year = String(date.getFullYear()) + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}/${month}/${day}` +} + /** * Formats an epoch ms / s value as a localised "Nm ago" / "Nh ago" / date label. * Accepts both ms and seconds; values smaller than 1e12 are treated as seconds. @@ -18,5 +25,5 @@ export function formatRelativeTime( if (hours < 24) return t('session.time.hoursAgo', { n: hours }) const days = Math.floor(hours / 24) if (days < 7) return t('session.time.daysAgo', { n: days }) - return new Date(ms).toLocaleDateString() + return formatSessionListDate(new Date(ms)) }