From 62ac4e7b0aa416cf61a8b0d4db22c372da698a22 Mon Sep 17 00:00:00 2001 From: weishu Date: Wed, 20 May 2026 20:24:46 +0800 Subject: [PATCH] Show relative time in resume picker --- cli/src/ui/ink/ResumeSessionPicker.tsx | 4 +++- .../ui/ink/resumeSessionPickerState.test.ts | 12 ++++++++++++ cli/src/ui/ink/resumeSessionPickerState.ts | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/cli/src/ui/ink/ResumeSessionPicker.tsx b/cli/src/ui/ink/ResumeSessionPicker.tsx index 3be19759..7b795f42 100644 --- a/cli/src/ui/ink/ResumeSessionPicker.tsx +++ b/cli/src/ui/ink/ResumeSessionPicker.tsx @@ -3,6 +3,7 @@ import { Box, Text, useInput, useStdout } from 'ink' import type { ResumableSession } from '@hapi/protocol' import { filterResumeSessions, + formatResumeSessionRelativeTime, getResumeSessionName, getResumeSessionState, normalizeScrollOffset, @@ -41,7 +42,8 @@ function truncateText(value: string, maxLength: number): string { function formatSessionLine(session: ResumableSession, width: number): string { const state = getResumeSessionState(session) - const prefix = `${session.flavor.padEnd(8)} ${state.padEnd(8)} ` + const time = formatResumeSessionRelativeTime(session.updatedAt).padStart(10) + const prefix = `${time} ${session.flavor.padEnd(8)} ${state.padEnd(8)} ` const directoryBudget = Math.max(16, Math.floor(width * 0.35)) const nameBudget = Math.max(12, width - prefix.length - directoryBudget - 2) const name = truncateText(getResumeSessionName(session), nameBudget) diff --git a/cli/src/ui/ink/resumeSessionPickerState.test.ts b/cli/src/ui/ink/resumeSessionPickerState.test.ts index 4ca2ab9c..e943179e 100644 --- a/cli/src/ui/ink/resumeSessionPickerState.test.ts +++ b/cli/src/ui/ink/resumeSessionPickerState.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import type { ResumableSession } from '@hapi/protocol' import { filterResumeSessions, + formatResumeSessionRelativeTime, reducePickerState, type PickerState } from './resumeSessionPickerState' @@ -22,6 +23,17 @@ function session(overrides: Partial): ResumableSession { } describe('resumeSessionPickerState', () => { + it('formats updatedAt as relative time', () => { + const now = 1_700_000_000_000 + + expect(formatResumeSessionRelativeTime(now - 10_000, now)).toBe('now') + expect(formatResumeSessionRelativeTime(now - 5 * 60_000, now)).toBe('5m ago') + expect(formatResumeSessionRelativeTime(now - 3 * 60 * 60_000, now)).toBe('3h ago') + expect(formatResumeSessionRelativeTime(now - 2 * 24 * 60 * 60_000, now)).toBe('2d ago') + expect(formatResumeSessionRelativeTime(Math.floor((now - 5 * 60_000) / 1000), now)).toBe('5m ago') + expect(formatResumeSessionRelativeTime(NaN, now)).toBe('unknown') + }) + it('filters sessions by searchable fields case-insensitively', () => { const sessions = [ session({ diff --git a/cli/src/ui/ink/resumeSessionPickerState.ts b/cli/src/ui/ink/resumeSessionPickerState.ts index 935e0ae2..1ebde16f 100644 --- a/cli/src/ui/ink/resumeSessionPickerState.ts +++ b/cli/src/ui/ink/resumeSessionPickerState.ts @@ -25,6 +25,25 @@ export function getResumeSessionState(session: ResumableSession): string { return session.controlledByUser ? 'local' : 'remote' } +export function formatResumeSessionRelativeTime(value: number, now: number = Date.now()): string { + const ms = value < 1_000_000_000_000 ? value * 1000 : value + if (!Number.isFinite(ms)) return 'unknown' + + const delta = Math.max(0, now - ms) + if (delta < 60_000) return 'now' + + const minutes = Math.floor(delta / 60_000) + if (minutes < 60) return `${minutes}m ago` + + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + + const days = Math.floor(hours / 24) + if (days < 7) return `${days}d ago` + + return new Date(ms).toLocaleDateString() +} + export function filterResumeSessions( sessions: ResumableSession[], query: string