Skip to content

Commit 8eea076

Browse files
authored
feat(acp): show relative last-activity time on chat history rows (#668)
* feat(acp): show relative last-activity time on chat history rows Replace the bare message-count number on ACP chat-history rows with a compact relative-time string (e.g. '3h', '2d', '5w') derived from the lastActivityAt timestamp each row already carries. Both chat-history surfaces — the sidebar ChatHistoryTab and the per-project ProjectChatList — already sorted newest-first; only the display changed. Reuses the existing git-time formatter by adding a number-based core (formatRelativeTimeFromMs) that the ISO-string formatRelativeTime now delegates to, preserving the git panel's API and tests exactly. * docs(acp): add docstrings to ChatHistoryEntryRow and ProjectChatRow Clears CodeRabbit's docstring-coverage pre-merge warning (50% → 100% on the touched functions) by adding concise JSDoc to the two previously undocumented row components.
1 parent 5a55da9 commit 8eea076

4 files changed

Lines changed: 65 additions & 15 deletions

File tree

src/renderer/components/ProjectChatList.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ContextMenuTrigger
1313
} from '@/components/ui/context-menu'
1414
import { clipboardApi, openerApi } from '@/lib/api'
15+
import { formatRelativeTimeFromMs } from '@/lib/git-time'
1516
import { openTerminalAtCwd } from '@/lib/terminal-spawn'
1617
import { cn } from '@/lib/utils'
1718
import { useAcpStore, useAgentIcon, useAgentTemplateId } from '@/stores/acp-store'
@@ -288,7 +289,10 @@ interface ProjectChatRowProps {
288289
onCopyPath: (cwd: string) => void
289290
onDelete: (entry: ProjectChatEntry) => void
290291
}
291-
292+
/**
293+
* A single per-project chat row: title + relative last-activity time, with a
294+
* context menu (open terminal / file explorer / copy path / delete).
295+
*/
292296
function ProjectChatRow({
293297
entry,
294298
onOpen,
@@ -315,7 +319,9 @@ function ProjectChatRow({
315319
>
316320
<ChatRowIcon agentId={entry.agentId} agentConfigId={entry.agentConfigId} />
317321
<span className="truncate flex-1 text-sidebar-foreground">{entry.title}</span>
318-
<span className="text-3xs text-muted-foreground">{entry.messageCount}</span>
322+
<span className="text-3xs text-muted-foreground">
323+
{formatRelativeTimeFromMs(entry.lastActivityAt)}
324+
</span>
319325
</button>
320326
<button
321327
type="button"

src/renderer/components/chat/ChatHistoryEntryRow.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Trash2 } from 'lucide-react'
2+
import { formatRelativeTimeFromMs } from '@/lib/git-time'
23
import { cn } from '@/lib/utils'
34
import { useAgentIcon, useAgentTemplateId } from '@/stores/acp-store'
45
import { AgentGlyph } from './AgentGlyph'
@@ -37,7 +38,10 @@ interface ChatHistoryEntryRowProps {
3738
onOpen: (entry: ChatHistorySidebarEntry) => void
3839
onDelete: (id: string) => void
3940
}
40-
41+
/**
42+
* A single chat-history row for the sidebar `ChatHistoryTab`: agent icon, title,
43+
* and a compact relative last-activity time (replacing the old message count).
44+
*/
4145
export function ChatHistoryEntryRow({
4246
entry,
4347
onOpen,
@@ -71,7 +75,9 @@ export function ChatHistoryEntryRow({
7175
<span className="text-3xs text-muted-foreground/70 shrink-0">{entry.agentName}</span>
7276
) : null
7377
) : (
74-
<span className="text-3xs text-muted-foreground">{entry.messageCount}</span>
78+
<span className="text-3xs text-muted-foreground">
79+
{formatRelativeTimeFromMs(entry.lastActivityAt)}
80+
</span>
7581
)}
7682
</button>
7783
{!entry.discovered && (

src/renderer/lib/git-time.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest'
2-
import { formatRelativeTime } from './git-time'
2+
import { formatRelativeTime, formatRelativeTimeFromMs } from './git-time'
33

44
describe('formatRelativeTime', () => {
55
const now = Date.parse('2026-05-30T12:00:00Z')
@@ -31,3 +31,29 @@ describe('formatRelativeTime', () => {
3131
expect(result).not.toMatch(/^\d+[mhdw]$/)
3232
})
3333
})
34+
35+
describe('formatRelativeTimeFromMs', () => {
36+
const now = Date.parse('2026-05-30T12:00:00Z')
37+
38+
it('returns empty string for non-finite input (NaN/Infinity)', () => {
39+
expect(formatRelativeTimeFromMs(Number.NaN, now)).toBe('')
40+
expect(formatRelativeTimeFromMs(Number.POSITIVE_INFINITY, now)).toBe('')
41+
})
42+
43+
it('matches formatRelativeTime for the same instant', () => {
44+
const iso = '2026-05-30T09:00:00Z'
45+
expect(formatRelativeTimeFromMs(Date.parse(iso), now)).toBe(formatRelativeTime(iso, now))
46+
})
47+
48+
it("formats sub-minute as 'now' and clamps future timestamps to 'now'", () => {
49+
expect(formatRelativeTimeFromMs(now - 30_000, now)).toBe('now')
50+
expect(formatRelativeTimeFromMs(now + 60_000, now)).toBe('now')
51+
})
52+
53+
it('formats minutes, hours, days, and weeks', () => {
54+
expect(formatRelativeTimeFromMs(now - 5 * 60_000, now)).toBe('5m')
55+
expect(formatRelativeTimeFromMs(now - 3 * 3_600_000, now)).toBe('3h')
56+
expect(formatRelativeTimeFromMs(now - 2 * 86_400_000, now)).toBe('2d')
57+
expect(formatRelativeTimeFromMs(now - 14 * 86_400_000, now)).toBe('2w')
58+
})
59+
})

src/renderer/lib/git-time.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
/**
2-
* Format an ISO 8601 timestamp as a compact relative time for commit rows,
3-
* e.g. "now", "5m", "3h", "2d", "5w". Falls back to a short localized date for
4-
* anything older than ~8 weeks. Invalid input yields an empty string so the UI
5-
* can render nothing rather than "Invalid Date".
2+
* Format an epoch-millisecond timestamp as a compact relative time, e.g.
3+
* "now", "5m", "3h", "2d", "5w". Falls back to a short localized date for
4+
* anything older than ~8 weeks. A non-finite timestamp yields an empty string
5+
* so the UI can render nothing rather than "Invalid Date".
66
*
7-
* `now` is injectable for deterministic tests.
7+
* `now` is injectable for deterministic tests. This is the number-based core
8+
* shared by commit rows (ISO via `formatRelativeTime`) and chat-history rows
9+
* (epoch-ms `lastActivityAt`).
810
*/
9-
export function formatRelativeTime(iso: string, now: number = Date.now()): string {
10-
const then = Date.parse(iso)
11-
if (Number.isNaN(then)) return ''
11+
export function formatRelativeTimeFromMs(ms: number, now: number = Date.now()): string {
12+
if (!Number.isFinite(ms)) return ''
1213

13-
const diffMs = now - then
14+
const diffMs = now - ms
1415
// Future timestamps (clock skew) clamp to "now" rather than negative values.
1516
const seconds = Math.max(0, Math.floor(diffMs / 1000))
1617

@@ -24,9 +25,20 @@ export function formatRelativeTime(iso: string, now: number = Date.now()): strin
2425
const weeks = Math.floor(days / 7)
2526
if (weeks < 8) return `${weeks}w`
2627

27-
return new Date(then).toLocaleDateString(undefined, {
28+
return new Date(ms).toLocaleDateString(undefined, {
2829
year: 'numeric',
2930
month: 'short',
3031
day: 'numeric'
3132
})
3233
}
34+
35+
/**
36+
* Format an ISO 8601 timestamp as a compact relative time for commit rows.
37+
* Delegates to `formatRelativeTimeFromMs` after parsing; invalid input yields
38+
* an empty string. `now` is injectable for deterministic tests.
39+
*/
40+
export function formatRelativeTime(iso: string, now: number = Date.now()): string {
41+
const then = Date.parse(iso)
42+
if (Number.isNaN(then)) return ''
43+
return formatRelativeTimeFromMs(then, now)
44+
}

0 commit comments

Comments
 (0)