diff --git a/packages/cli/src/commands/history.ts b/packages/cli/src/commands/history.ts index e0e0c24c7af..64991bdacc8 100644 --- a/packages/cli/src/commands/history.ts +++ b/packages/cli/src/commands/history.ts @@ -26,6 +26,7 @@ import { type Turn, type UndoMode, } from "../utils/historyOwner.js"; +import { ago } from "../utils/historyAgo.js"; import { AmbiguousPreviewServerError } from "../utils/studioSelectionClient.js"; import { withMeta } from "../utils/updateCheck.js"; @@ -103,20 +104,12 @@ function since(entries: HistoryListItem[], ref: string, who: HistoryWho): Histor return entries.slice(entries.indexOf(entryOf(entries, ref)) + 1); } -function ago(at: number): string { - const seconds = Math.max(0, Math.round((historyDeps.now() - at) / 1000)); - if (seconds < 60) return `${seconds}s ago`; - if (seconds < 3600) return `${Math.round(seconds / 60)}m ago`; - if (seconds < 86_400) return `${Math.round(seconds / 3600)}h ago`; - return `${Math.round(seconds / 86_400)}d ago`; -} - function line(entry: HistoryListItem | HistoryEntry): string { const paths = entry.files.map((file) => file.path); const files = paths.slice(0, 2).join(", ") + (paths.length > 2 ? ` +${paths.length - 2}` : ""); const flags = "undone" in entry ? `${entry.undone ? " [undone]" : ""}${entry.pinned ? " [pinned]" : ""}` : ""; - return `${short(entry.id)} ${ago(entry.endedAt)} ${entry.who.name} ${entry.label} (${files})${flags}`; + return `${short(entry.id)} ${ago(entry.endedAt, historyDeps.now())} ${entry.who.name} ${entry.label} (${files})${flags}`; } /** The public entry shape: files as paths, no hashes. */ diff --git a/packages/cli/src/utils/historyAgo.test.ts b/packages/cli/src/utils/historyAgo.test.ts new file mode 100644 index 00000000000..9daf5bb842b --- /dev/null +++ b/packages/cli/src/utils/historyAgo.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { ago } from "./historyAgo.js"; + +describe("ago", () => { + const at = 1_000_000_000_000; + const after = (seconds: number) => ago(at, at + seconds * 1000); + + it("counts whole seconds, minutes, hours and days", () => { + expect(after(0)).toBe("0s ago"); + expect(after(59)).toBe("59s ago"); + expect(after(3600)).toBe("1h ago"); + expect(after(86_400)).toBe("1d ago"); + expect(after(3 * 86_400)).toBe("3d ago"); + }); + + it("never prints a unit at the size of the next one", () => { + expect(after(3570)).toBe("59m ago"); + expect(after(86_000)).toBe("23h ago"); + }); + + it("treats a future timestamp as now", () => { + expect(ago(at + 5000, at)).toBe("0s ago"); + }); +}); diff --git a/packages/cli/src/utils/historyAgo.ts b/packages/cli/src/utils/historyAgo.ts new file mode 100644 index 00000000000..edf4e0f938a --- /dev/null +++ b/packages/cli/src/utils/historyAgo.ts @@ -0,0 +1,8 @@ +// Floor each larger unit: rounding would print "60m ago" at 59.5 minutes and "24h ago" at 23.5 hours. +export function ago(at: number, now: number): string { + const seconds = Math.max(0, Math.round((now - at) / 1000)); + if (seconds < 60) return `${seconds}s ago`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86_400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86_400)}d ago`; +}