diff --git a/extensions/sessions/sessions.ts b/extensions/sessions/sessions.ts index 1517e19c..a6f29589 100644 --- a/extensions/sessions/sessions.ts +++ b/extensions/sessions/sessions.ts @@ -74,19 +74,37 @@ export function formatTimestamp(date: Date): string { return `${year}-${month}-${day} ${hours}:${minutes}`; } -export function formatRelativeTime(date: Date): string { - const now = new Date(); +export function formatRelativeTime(date: Date, now: Date = new Date()): string { const diffMs = now.getTime() - date.getTime(); - const diffSec = Math.floor(diffMs / 1000); + const diffSec = Math.max(0, Math.floor(diffMs / 1000)); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); - if (diffSec < 60) return "Just now"; - if (diffMin < 60) return `${diffMin}m ago`; - if (diffHour < 24) return `${diffHour}h ago`; - if (diffDay === 1) return "Yesterday"; - return `${diffDay}d ago`; + // Within 24 hours: display clock time and relative duration (m/h ago) + // This avoids midnight flips showing "0d ago" or cross-year jumps in early January + if (diffHour < 24) { + const time = `${pad(date.getHours())}:${pad(date.getMinutes())}`; + if (diffSec < 60) return `${time} (just now)`; + if (diffMin < 60) return `${time} (${diffMin}m ago)`; + return `${time} (${diffHour}h ago)`; + } + + // Older than 24 hours: display calendar date and day/month/year distance + // If under a year, show MM-DD so year transitions in January stay readable + if (diffDay < 365) { + const dateStr = `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; + if (diffDay < 60) { + return `${dateStr} (${diffDay}d ago)`; + } + const months = Math.max(1, Math.floor(diffDay / 30)); + return `${dateStr} (${months}mo ago)`; + } + + // Cross-year older than 365 days + const years = Math.max(1, Math.floor(diffDay / 365)); + const dateStr = `${date.getFullYear()}-${pad(date.getMonth() + 1)}`; + return `${dateStr} (${years}y ago)`; } const cleanDisplayLine = (text: string) => @@ -127,6 +145,8 @@ export const buildSearchText = (session: SessionInfoLike): string => session.id, session.cwd, session.firstMessage ?? "", + formatTimestamp(session.modified), + formatRelativeTime(session.modified), ] .join(" ") .toLowerCase(); diff --git a/tests/extensions/sessions/sessions.test.ts b/tests/extensions/sessions/sessions.test.ts index 2f60f43a..04838c6b 100644 --- a/tests/extensions/sessions/sessions.test.ts +++ b/tests/extensions/sessions/sessions.test.ts @@ -4,10 +4,12 @@ import { buildSessionDescription, buildSessionLabel, buildSessionPreview, + buildSessionSearchEntries, filterSessionEntries, + formatRelativeTime, parseLimit, - selectSessionStatsWindow, type SessionInfoLike, + selectSessionStatsWindow, } from "../../../extensions/sessions/sessions.ts"; const session: SessionInfoLike = { @@ -144,3 +146,48 @@ test("bounded preview reports omitted messages and content bytes", () => { ]); assert.match(preview.subtitle, /100 messages/); }); + +test("formatRelativeTime deterministically formats relative hours and calendar dates with frozen clocks", () => { + const baseNow = new Date("2026-09-21T12:00:00"); + const tenMins = new Date("2026-09-21T11:50:00"); + const twoHours = new Date("2026-09-21T10:00:00"); + const threeDays = new Date("2026-09-18T12:00:00"); + const twelveDays = new Date("2026-09-09T12:00:00"); + + assert.equal(formatRelativeTime(tenMins, baseNow), "11:50 (10m ago)"); + assert.equal(formatRelativeTime(twoHours, baseNow), "10:00 (2h ago)"); + assert.equal(formatRelativeTime(threeDays, baseNow), "09-18 (3d ago)"); + assert.equal(formatRelativeTime(twelveDays, baseNow), "09-09 (12d ago)"); + + // Midnight boundary test case: 10 minutes ago across midnight (23:55 viewed at 00:05) + const midnightNow = new Date("2026-09-21T00:05:00"); + const midnightTenMinsAgo = new Date("2026-09-20T23:55:00"); + assert.equal( + formatRelativeTime(midnightTenMinsAgo, midnightNow), + "23:55 (10m ago)", + ); + + // Cross-year boundary test case: New Year 00:05 viewing New Year's Eve 23:55 + const janNow = new Date("2027-01-01T00:05:00"); + const decEve = new Date("2026-12-31T23:55:00"); + assert.equal(formatRelativeTime(decEve, janNow), "23:55 (10m ago)"); + + // Cross-year within 60 days + const jan3rd = new Date("2027-01-03T12:00:00"); + const dec30 = new Date("2026-12-30T12:00:00"); + assert.equal(formatRelativeTime(dec30, jan3rd), "12-30 (4d ago)"); + + // Over 1 year ago + const twoYearsLater = new Date("2028-09-21T12:00:00"); + assert.equal(formatRelativeTime(baseNow, twoYearsLater), "2026-09 (2y ago)"); +}); + +test("session search matches formatted dates and years", () => { + const sample = { + ...session, + modified: new Date("2026-09-06T10:00:00Z"), + }; + const entries = buildSessionSearchEntries([sample]); + assert.equal(filterSessionEntries(entries, "2026").length, 1); + assert.equal(filterSessionEntries(entries, "09-06").length, 1); +});