mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): zero-pad message and session timestamps (#1112)
This commit is contained in:
@@ -167,8 +167,13 @@ describe('formatResetTime', () => {
|
|||||||
describe('formatMessageTimestamp', () => {
|
describe('formatMessageTimestamp', () => {
|
||||||
it('formats today without requiring a date prefix', () => {
|
it('formats today without requiring a date prefix', () => {
|
||||||
const now = new Date(2026, 4, 22, 14, 30)
|
const now = new Date(2026, 4, 22, 14, 30)
|
||||||
const result = formatMessageTimestamp(new Date(2026, 4, 22, 9, 5), now)
|
const date = new Date(2026, 4, 22, 9, 5)
|
||||||
expect(result).toBeTruthy()
|
const result = formatMessageTimestamp(date, now)
|
||||||
|
expect(result).toBe(date.toLocaleTimeString(undefined, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hourCycle: 'h23'
|
||||||
|
}))
|
||||||
expect(result).not.toContain('2026')
|
expect(result).not.toContain('2026')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -32,15 +32,15 @@ export function formatMessageTimestamp(date: Date, now: Date = new Date()): stri
|
|||||||
&& date.getDate() === now.getDate()
|
&& date.getDate() === now.getDate()
|
||||||
|
|
||||||
if (sameDay) {
|
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()
|
const sameYear = date.getFullYear() === now.getFullYear()
|
||||||
if (sameYear) {
|
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 {
|
export function formatMessageTimestampTitle(date: Date): string {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useAssistantState } from '@assistant-ui/react'
|
import { useAssistantState } from '@assistant-ui/react'
|
||||||
import { formatMessageTimestamp, formatMessageTimestampTitle } from '@/chat/presentation'
|
import { formatMessageTimestamp, formatMessageTimestampTitle } from '@/chat/presentation'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type MessageTimestampProps = {
|
type MessageTimestampProps = {
|
||||||
className?: string
|
className?: string
|
||||||
@@ -12,7 +13,7 @@ export function MessageTimestamp(props: MessageTimestampProps) {
|
|||||||
<time
|
<time
|
||||||
dateTime={createdAt.toISOString()}
|
dateTime={createdAt.toISOString()}
|
||||||
title={formatMessageTimestampTitle(createdAt)}
|
title={formatMessageTimestampTitle(createdAt)}
|
||||||
className={props.className}
|
className={cn('tabular-nums', props.className)}
|
||||||
>
|
>
|
||||||
{formatMessageTimestamp(createdAt)}
|
{formatMessageTimestamp(createdAt)}
|
||||||
</time>
|
</time>
|
||||||
|
|||||||
@@ -730,7 +730,10 @@ function SessionListSearch(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCodexImportedRelativeTime(value: number, t: (key: string, params?: Record<string, string | number>) => string): string | null {
|
function formatCodexImportedRelativeTime(
|
||||||
|
value: number,
|
||||||
|
t: (key: string, params?: Record<string, string | number>) => string
|
||||||
|
): string | null {
|
||||||
const ms = value < 1_000_000_000_000 ? value * 1000 : value
|
const ms = value < 1_000_000_000_000 ? value * 1000 : value
|
||||||
if (!Number.isFinite(ms)) return null
|
if (!Number.isFinite(ms)) return null
|
||||||
const delta = Date.now() - ms
|
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 })
|
if (hours < 24) return t('session.time.importedFromCodex.hoursAgo', { n: hours })
|
||||||
const days = Math.floor(hours / 24)
|
const days = Math.floor(hours / 24)
|
||||||
if (days < 7) return t('session.time.importedFromCodex.daysAgo', { n: days })
|
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 | number>) => string): string | null {
|
function getSessionTimeLabel(
|
||||||
|
session: SessionSummary,
|
||||||
|
t: (key: string, params?: Record<string, string | number>) => string
|
||||||
|
): string | null {
|
||||||
const codexSessionId = session.metadata?.agentSessionId
|
const codexSessionId = session.metadata?.agentSessionId
|
||||||
const importedAt = session.metadata?.flavor === 'codex'
|
const importedAt = session.metadata?.flavor === 'codex'
|
||||||
? getCodexImportedAt(codexSessionId)
|
? getCodexImportedAt(codexSessionId)
|
||||||
@@ -904,7 +910,7 @@ function SessionItem(props: {
|
|||||||
{t('session.item.pending')} {s.pendingRequestsCount}
|
{t('session.item.pending')} {s.pendingRequestsCount}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="text-[var(--app-hint)]">
|
<span className="tabular-nums text-[var(--app-hint)]">
|
||||||
{getSessionTimeLabel(s, t)}
|
{getSessionTimeLabel(s, t)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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.
|
* 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.
|
* 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 })
|
if (hours < 24) return t('session.time.hoursAgo', { n: hours })
|
||||||
const days = Math.floor(hours / 24)
|
const days = Math.floor(hours / 24)
|
||||||
if (days < 7) return t('session.time.daysAgo', { n: days })
|
if (days < 7) return t('session.time.daysAgo', { n: days })
|
||||||
return new Date(ms).toLocaleDateString()
|
return formatSessionListDate(new Date(ms))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user