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))
}