From 12ee0ab77f1c8a8c34e4b26b1c18503f5fe17c5e Mon Sep 17 00:00:00 2001 From: Rhine <2592562934@qq.com> Date: Fri, 18 Sep 2026 16:52:03 +0800 Subject: [PATCH 1/3] feat(sessions): show calendar dates with relative time and enable date search --- extensions/sessions/sessions.ts | 35 ++++++++++++++++++---- tests/extensions/sessions/sessions.test.ts | 32 +++++++++++++++++++- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/extensions/sessions/sessions.ts b/extensions/sessions/sessions.ts index 1517e19c..e6047ba4 100644 --- a/extensions/sessions/sessions.ts +++ b/extensions/sessions/sessions.ts @@ -77,16 +77,37 @@ export function formatTimestamp(date: Date): string { export function formatRelativeTime(date: Date): string { const now = new Date(); 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`; + const isToday = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate(); + + if (isToday) { + 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)`; + } + + const isCurrentYear = date.getFullYear() === now.getFullYear(); + const dateStr = isCurrentYear + ? `${pad(date.getMonth() + 1)}-${pad(date.getDate())}` + : `${date.getFullYear()}-${pad(date.getMonth() + 1)}`; + + if (diffDay < 60) { + return `${dateStr} (${diffDay}d ago)`; + } + if (diffDay < 365) { + const months = Math.max(1, Math.floor(diffDay / 30)); + return `${dateStr} (${months}mo ago)`; + } + const years = Math.max(1, Math.floor(diffDay / 365)); + return `${dateStr} (${years}y ago)`; } const cleanDisplayLine = (text: string) => @@ -127,6 +148,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..16024dd2 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,31 @@ test("bounded preview reports omitted messages and content bytes", () => { ]); assert.match(preview.subtitle, /100 messages/); }); + +test("formatRelativeTime formats today as time and past dates as calendar date with relative duration", () => { + const now = new Date(); + + // Today + const tenMinsAgo = new Date(now.getTime() - 10 * 60 * 1000); + assert.match(formatRelativeTime(tenMinsAgo), /^\d{2}:\d{2} \(10m ago\)$/); + + const twoHoursAgo = new Date(now.getTime() - 2 * 3600 * 1000); + assert.match(formatRelativeTime(twoHoursAgo), /^\d{2}:\d{2} \(2h ago\)$/); + + // Past days (same year) + const threeDaysAgo = new Date(now.getTime() - 3 * 86400 * 1000); + assert.match(formatRelativeTime(threeDaysAgo), /^\d{2}-\d{2} \(3d ago\)$/); + + const twelveDaysAgo = new Date(now.getTime() - 12 * 86400 * 1000); + assert.match(formatRelativeTime(twelveDaysAgo), /^\d{2}-\d{2} \(12d 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); +}); From 993dbac81f5d055bf56eb921416aef655e36783e Mon Sep 17 00:00:00 2001 From: Rhine <2592562934@qq.com> Date: Fri, 18 Sep 2026 17:34:59 +0800 Subject: [PATCH 2/3] ci: trigger re-run for e2e checks From 8112d6e6b872c499c9debcd185488876217c6358 Mon Sep 17 00:00:00 2001 From: Rhine <2592562934@qq.com> Date: Tue, 22 Sep 2026 09:14:43 +0800 Subject: [PATCH 3/3] fix(sessions): use deterministic frozen clocks and eliminate midnight 0d edge case (#556) --- extensions/sessions/sessions.ts | 29 +++++++-------- tests/extensions/sessions/sessions.test.ts | 43 +++++++++++++++------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/extensions/sessions/sessions.ts b/extensions/sessions/sessions.ts index e6047ba4..a6f29589 100644 --- a/extensions/sessions/sessions.ts +++ b/extensions/sessions/sessions.ts @@ -74,39 +74,36 @@ 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.max(0, Math.floor(diffMs / 1000)); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); const diffDay = Math.floor(diffHour / 24); - const isToday = - date.getFullYear() === now.getFullYear() && - date.getMonth() === now.getMonth() && - date.getDate() === now.getDate(); - - if (isToday) { + // 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)`; } - const isCurrentYear = date.getFullYear() === now.getFullYear(); - const dateStr = isCurrentYear - ? `${pad(date.getMonth() + 1)}-${pad(date.getDate())}` - : `${date.getFullYear()}-${pad(date.getMonth() + 1)}`; - - if (diffDay < 60) { - return `${dateStr} (${diffDay}d 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)`; } diff --git a/tests/extensions/sessions/sessions.test.ts b/tests/extensions/sessions/sessions.test.ts index 16024dd2..04838c6b 100644 --- a/tests/extensions/sessions/sessions.test.ts +++ b/tests/extensions/sessions/sessions.test.ts @@ -147,22 +147,39 @@ test("bounded preview reports omitted messages and content bytes", () => { assert.match(preview.subtitle, /100 messages/); }); -test("formatRelativeTime formats today as time and past dates as calendar date with relative duration", () => { - const now = new Date(); - - // Today - const tenMinsAgo = new Date(now.getTime() - 10 * 60 * 1000); - assert.match(formatRelativeTime(tenMinsAgo), /^\d{2}:\d{2} \(10m ago\)$/); +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)", + ); - const twoHoursAgo = new Date(now.getTime() - 2 * 3600 * 1000); - assert.match(formatRelativeTime(twoHoursAgo), /^\d{2}:\d{2} \(2h 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)"); - // Past days (same year) - const threeDaysAgo = new Date(now.getTime() - 3 * 86400 * 1000); - assert.match(formatRelativeTime(threeDaysAgo), /^\d{2}-\d{2} \(3d 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)"); - const twelveDaysAgo = new Date(now.getTime() - 12 * 86400 * 1000); - assert.match(formatRelativeTime(twelveDaysAgo), /^\d{2}-\d{2} \(12d 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", () => {