From 75b468e34536da3a26b29c34fd4aa661d87694f5 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 14:05:50 +1100 Subject: [PATCH 1/3] fix: share relative time updates across staged --- .../features/projects/ProjectSection.svelte | 21 ++------ .../features/timeline/BranchTimeline.svelte | 33 ++++-------- .../src/lib/shared/relativeTime.svelte.ts | 50 +++++++++++++++++++ 3 files changed, 65 insertions(+), 39 deletions(-) create mode 100644 apps/staged/src/lib/shared/relativeTime.svelte.ts diff --git a/apps/staged/src/lib/features/projects/ProjectSection.svelte b/apps/staged/src/lib/features/projects/ProjectSection.svelte index f0ed5d49..5ba110a2 100644 --- a/apps/staged/src/lib/features/projects/ProjectSection.svelte +++ b/apps/staged/src/lib/features/projects/ProjectSection.svelte @@ -42,6 +42,7 @@ import { subscribeDragDrop } from '../branches/dragDrop'; import { isImageFile } from '../branches/branchCardHelpers'; import { createImage, createImageFromData, deleteImage, getImageData } from '../../api/commands'; + import { formatRelativeTime, minuteNow } from '../../shared/relativeTime.svelte'; interface Props { project: Project; @@ -347,21 +348,6 @@ let openNote = $state<{ title: string; content: string; sessionId?: string } | null>(null); let openSessionId = $state(null); - function formatRelativeTime(timestampMs: number): string { - const date = new Date(timestampMs); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return 'just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - return date.toLocaleDateString(); - } - // ── Lifecycle ────────────────────────────────────────────────────────── onMount(() => { @@ -563,6 +549,7 @@ {#if projectNotes.length > 0} + {@const nowMs = minuteNow.now()}
@@ -580,7 +567,9 @@ : isFailed ? 'Session finished — no note created' : note.title || 'Untitled note'} - secondaryMeta={isRunning || isFailed ? undefined : formatRelativeTime(note.createdAt)} + secondaryMeta={isRunning || isFailed + ? undefined + : formatRelativeTime(note.createdAt, nowMs)} deleting={deletingNoteIds.has(note.id)} isLast={index === timelineNotes.length - 1} sessionId={note.sessionId ?? undefined} diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index 8f48824a..03f8c711 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -13,6 +13,11 @@ import type { BranchTimeline as BranchTimelineData } from '../../types'; import TimelineRow from './TimelineRow.svelte'; import type { TimelineItemType, TimelineBadge } from './TimelineRow.svelte'; + import { + formatRelativeTime, + formatRelativeTimeSeconds, + minuteNow, + } from '../../shared/relativeTime.svelte'; import { collectRunningSessionIds, createLiveSessionHints, @@ -222,6 +227,7 @@ // Merge commits, notes, and reviews into a single sorted list let items = $derived.by(() => { + const nowMs = minuteNow.now(); const all: DisplayItem[] = []; const deletingCommitIds = new Set( deletingItems.filter((item) => item.type === 'commit').map((item) => item.id) @@ -260,7 +266,7 @@ secondaryMeta = liveHint ?? 'Generating commit'; } else { type = 'commit'; - secondaryMeta = formatRelativeTime(commit.timestamp); + secondaryMeta = formatRelativeTimeSeconds(commit.timestamp, nowMs); } all.push({ @@ -300,7 +306,7 @@ secondaryMeta = liveHint ?? 'Generating note'; } else { type = 'note'; - secondaryMeta = formatRelativeTimeMs(note.createdAt); + secondaryMeta = formatRelativeTime(note.createdAt, nowMs); } all.push({ @@ -357,7 +363,7 @@ meta = liveHint ?? 'Generating review'; } else { type = 'review'; - meta = formatRelativeTimeMs(review.createdAt); + meta = formatRelativeTime(review.createdAt, nowMs); } all.push({ @@ -382,7 +388,7 @@ key: `image-${image.id}`, type: 'image' as TimelineItemType, title: image.filename, - secondaryMeta: isDeleting ? 'Deleting...' : formatRelativeTimeMs(image.createdAt), + secondaryMeta: isDeleting ? 'Deleting...' : formatRelativeTime(image.createdAt, nowMs), deleting: isDeleting, timestamp: Math.floor(image.createdAt / 1000), order: 0, @@ -496,25 +502,6 @@ onDeleteImage(item.imageId); } } - - function formatRelativeTime(timestamp: number): string { - const date = new Date(timestamp * 1000); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return 'just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - return date.toLocaleDateString(); - } - - function formatRelativeTimeMs(timestamp: number): string { - return formatRelativeTime(Math.floor(timestamp / 1000)); - } {#if items.length === 0 && !onNewNote && !onNewCommit && !onNewReview && pendingDropNotes.length === 0 && pendingItems.length === 0} diff --git a/apps/staged/src/lib/shared/relativeTime.svelte.ts b/apps/staged/src/lib/shared/relativeTime.svelte.ts new file mode 100644 index 00000000..d09bb8d8 --- /dev/null +++ b/apps/staged/src/lib/shared/relativeTime.svelte.ts @@ -0,0 +1,50 @@ +class MinuteNowStore { + private value = $state(Date.now()); + private timeoutId: ReturnType | null = null; + private intervalId: ReturnType | null = null; + + constructor() { + if (typeof window !== 'undefined') { + this.start(); + } + } + + now(): number { + return this.value; + } + + private start(): void { + this.value = Date.now(); + + const msUntilNextMinute = 60000 - (this.value % 60000); + this.timeoutId = setTimeout(() => { + this.value = Date.now(); + this.intervalId = setInterval(() => { + this.value = Date.now(); + }, 60000); + }, msUntilNextMinute); + } +} + +export const minuteNow = new MinuteNowStore(); + +export function formatRelativeTime(timestampMs: number, nowMs = minuteNow.now()): string { + const date = new Date(timestampMs); + const diffMs = nowMs - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 2) return 'just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(); +} + +export function formatRelativeTimeSeconds( + timestampSeconds: number, + nowMs = minuteNow.now() +): string { + return formatRelativeTime(timestampSeconds * 1000, nowMs); +} From de981406b5ba4141693014da517c1a709ee47cd1 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 14:46:11 +1100 Subject: [PATCH 2/3] fix: restore one-minute just-now threshold --- apps/staged/src/lib/shared/relativeTime.svelte.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/staged/src/lib/shared/relativeTime.svelte.ts b/apps/staged/src/lib/shared/relativeTime.svelte.ts index d09bb8d8..736b2b64 100644 --- a/apps/staged/src/lib/shared/relativeTime.svelte.ts +++ b/apps/staged/src/lib/shared/relativeTime.svelte.ts @@ -35,7 +35,7 @@ export function formatRelativeTime(timestampMs: number, nowMs = minuteNow.now()) const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); - if (diffMins < 2) return 'just now'; + if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; From c2934742536fb26c13f1f5352543cb79a88f1dfa Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Thu, 2 Apr 2026 14:51:16 +1100 Subject: [PATCH 3/3] fix: remove unused relative time timer fields --- apps/staged/src/lib/shared/relativeTime.svelte.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/staged/src/lib/shared/relativeTime.svelte.ts b/apps/staged/src/lib/shared/relativeTime.svelte.ts index 736b2b64..56923340 100644 --- a/apps/staged/src/lib/shared/relativeTime.svelte.ts +++ b/apps/staged/src/lib/shared/relativeTime.svelte.ts @@ -1,7 +1,5 @@ class MinuteNowStore { private value = $state(Date.now()); - private timeoutId: ReturnType | null = null; - private intervalId: ReturnType | null = null; constructor() { if (typeof window !== 'undefined') { @@ -17,9 +15,9 @@ class MinuteNowStore { this.value = Date.now(); const msUntilNextMinute = 60000 - (this.value % 60000); - this.timeoutId = setTimeout(() => { + setTimeout(() => { this.value = Date.now(); - this.intervalId = setInterval(() => { + setInterval(() => { this.value = Date.now(); }, 60000); }, msUntilNextMinute);