fix: display relative time in intel status output (#2132)

* fix: display relative time instead of UTC in intel status output

The `updated_at` timestamps in `gsd-tools intel status` were displayed
as raw ISO/UTC strings, making them appear to show the wrong time in
non-UTC timezones. Replace with fuzzy relative times ("5 minutes ago",
"1 day ago") which are timezone-agnostic and more useful for freshness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add regression tests for timeAgo utility

Covers boundary values (seconds/minutes/hours/days/months/years),
singular vs plural formatting, and future-date edge case.

Addresses review feedback on #2132.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Brauchli
2026-04-12 13:54:17 -06:00
committed by GitHub
parent b896db6f91
commit 72af8cd0f7
3 changed files with 137 additions and 1 deletions

View File

@@ -1045,7 +1045,15 @@ async function runCommand(command, args, cwd, raw, defaultValue) {
core.output(intel.intelQuery(term, planningDir), raw);
} else if (subcommand === 'status') {
const planningDir = path.join(cwd, '.planning');
core.output(intel.intelStatus(planningDir), raw);
const status = intel.intelStatus(planningDir);
if (!raw && status.files) {
for (const file of Object.values(status.files)) {
if (file.updated_at) {
file.updated_at = core.timeAgo(new Date(file.updated_at));
}
}
}
core.output(status, raw);
} else if (subcommand === 'diff') {
const planningDir = path.join(cwd, '.planning');
core.output(intel.intelDiff(planningDir), raw);

View File

@@ -1560,6 +1560,32 @@ function atomicWriteFileSync(filePath, content, encoding = 'utf-8') {
}
}
/**
* Format a Date as a fuzzy relative time string (e.g. "5 minutes ago").
* @param {Date} date
* @returns {string}
*/
function timeAgo(date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 5) return 'just now';
if (seconds < 60) return `${seconds} seconds ago`;
const minutes = Math.floor(seconds / 60);
if (minutes === 1) return '1 minute ago';
if (minutes < 60) return `${minutes} minutes ago`;
const hours = Math.floor(minutes / 60);
if (hours === 1) return '1 hour ago';
if (hours < 24) return `${hours} hours ago`;
const days = Math.floor(hours / 24);
if (days === 1) return '1 day ago';
if (days < 30) return `${days} days ago`;
const months = Math.floor(days / 30);
if (months === 1) return '1 month ago';
if (months < 12) return `${months} months ago`;
const years = Math.floor(days / 365);
if (years === 1) return '1 year ago';
return `${years} years ago`;
}
module.exports = {
output,
error,
@@ -1607,4 +1633,5 @@ module.exports = {
getAgentsDir,
checkAgentsInstalled,
atomicWriteFileSync,
timeAgo,
};

View File

@@ -31,6 +31,7 @@ const {
findProjectRoot,
detectSubRepos,
planningDir,
timeAgo,
} = require('../get-shit-done/bin/lib/core.cjs');
// ─── loadConfig ────────────────────────────────────────────────────────────────
@@ -1750,3 +1751,103 @@ describe('planningDir', () => {
);
});
});
// ─── timeAgo ──────────────────────────────────────────────────────────────────
describe('timeAgo', () => {
const now = () => Date.now();
const dateAt = (msAgo) => new Date(now() - msAgo);
// ─── seconds boundary ───
test('returns "just now" for dates under 5 seconds old', () => {
assert.strictEqual(timeAgo(dateAt(0)), 'just now');
assert.strictEqual(timeAgo(dateAt(4_000)), 'just now');
});
test('returns "N seconds ago" between 5 and 59 seconds', () => {
assert.strictEqual(timeAgo(dateAt(5_000)), '5 seconds ago');
assert.strictEqual(timeAgo(dateAt(30_000)), '30 seconds ago');
assert.strictEqual(timeAgo(dateAt(59_000)), '59 seconds ago');
});
// ─── minutes boundary ───
test('transitions to minutes at 60 seconds', () => {
assert.strictEqual(timeAgo(dateAt(60_000)), '1 minute ago');
});
test('uses singular "1 minute ago" for exactly one minute', () => {
assert.strictEqual(timeAgo(dateAt(60_000)), '1 minute ago');
assert.strictEqual(timeAgo(dateAt(119_000)), '1 minute ago');
});
test('uses plural "N minutes ago" for 2-59 minutes', () => {
assert.strictEqual(timeAgo(dateAt(120_000)), '2 minutes ago');
assert.strictEqual(timeAgo(dateAt(5 * 60_000)), '5 minutes ago');
assert.strictEqual(timeAgo(dateAt(59 * 60_000)), '59 minutes ago');
});
// ─── hours boundary ───
test('transitions to hours at 60 minutes', () => {
assert.strictEqual(timeAgo(dateAt(60 * 60_000)), '1 hour ago');
});
test('uses singular "1 hour ago" for exactly one hour', () => {
assert.strictEqual(timeAgo(dateAt(60 * 60_000)), '1 hour ago');
assert.strictEqual(timeAgo(dateAt(119 * 60_000)), '1 hour ago');
});
test('uses plural "N hours ago" for 2-23 hours', () => {
assert.strictEqual(timeAgo(dateAt(2 * 3600_000)), '2 hours ago');
assert.strictEqual(timeAgo(dateAt(23 * 3600_000)), '23 hours ago');
});
// ─── days boundary ───
test('transitions to days at 24 hours', () => {
assert.strictEqual(timeAgo(dateAt(24 * 3600_000)), '1 day ago');
});
test('uses singular "1 day ago" for exactly one day', () => {
assert.strictEqual(timeAgo(dateAt(24 * 3600_000)), '1 day ago');
});
test('uses plural "N days ago" for 2-29 days', () => {
assert.strictEqual(timeAgo(dateAt(2 * 86400_000)), '2 days ago');
assert.strictEqual(timeAgo(dateAt(29 * 86400_000)), '29 days ago');
});
// ─── months boundary ───
test('transitions to months at 30 days', () => {
assert.strictEqual(timeAgo(dateAt(30 * 86400_000)), '1 month ago');
});
test('uses singular "1 month ago" for exactly one month (30 days)', () => {
assert.strictEqual(timeAgo(dateAt(30 * 86400_000)), '1 month ago');
assert.strictEqual(timeAgo(dateAt(59 * 86400_000)), '1 month ago');
});
test('uses plural "N months ago" for 2-11 months', () => {
assert.strictEqual(timeAgo(dateAt(60 * 86400_000)), '2 months ago');
assert.strictEqual(timeAgo(dateAt(180 * 86400_000)), '6 months ago');
});
// ─── years boundary ───
test('transitions to years at 365 days', () => {
assert.strictEqual(timeAgo(dateAt(365 * 86400_000)), '1 year ago');
});
test('uses singular "1 year ago" for exactly one year', () => {
assert.strictEqual(timeAgo(dateAt(365 * 86400_000)), '1 year ago');
});
test('uses plural "N years ago" for 2+ years', () => {
assert.strictEqual(timeAgo(dateAt(2 * 365 * 86400_000)), '2 years ago');
assert.strictEqual(timeAgo(dateAt(10 * 365 * 86400_000)), '10 years ago');
});
// ─── edge cases ───
test('handles future dates as "just now" (negative elapsed)', () => {
// A date 5 seconds in the future has negative elapsed time, which floors to a negative
// number of seconds and hits the "under 5 seconds" branch.
assert.strictEqual(timeAgo(new Date(Date.now() + 5_000)), 'just now');
});
});