hermes-agent/apps/desktop/src/lib/time.ts
Brooklyn Nicholson e0325cf769 feat(desktop): Capabilities foundation — shared utils, master-detail, editors, primitives
The reusable base the Capabilities rework sits on:
- lib/{text,time,format,json-format}: consolidate ~30 hand-rolled string/date/
  number/JSON helpers behind one set of tested utilities.
- master-detail scaffold (MasterDetail / ListColumn / DetailColumn / DetailPane /
  CapRow / ListStrip / ToolChip / ICON_BUTTON) + row-hover; tabs-as-data
  PageSearchShell; EmptyState + ErrorBanner; a shared LogTail terminal surface.
- framed CodeEditor + JsonDocumentEditor, wired into every in-app markdown/JSON
  edit surface (profile SOUL.md, memory nodes, right-click sidebar profile).
- shared React Query cache helper (writeCache) + per-profile-switch/ debounce hooks.
2026-07-03 05:08:13 -05:00

74 lines
2.7 KiB
TypeScript

// Canonical time/date formatting. Shared `Intl` instances (created once, not
// per-render) + relative-time helpers. Every surface that shows a timestamp or
// an age pulls from here so the rendered strings stay consistent app-wide.
export const SECOND = 1000
export const MINUTE = 60_000
export const HOUR = 3_600_000
export const DAY = 86_400_000
// ── Absolute date/time formatters ──────────────────────────────────────────
// `hh:mm` clock (thread today/yesterday lines).
export const fmtClock = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' })
// Compact "day + clock", no year/seconds (artifacts, thread fallback, cron runs).
export const fmtDayTime = new Intl.DateTimeFormat(undefined, {
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
month: 'short'
})
// Medium date + short time (command center session detail).
export const fmtDateTime = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' })
// Date only, "5 Jun 2026" (starmap tooltip).
export const fmtDate = new Intl.DateTimeFormat(undefined, { day: 'numeric', month: 'short', year: 'numeric' })
// ── Relative time ──────────────────────────────────────────────────────────
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', style: 'short' })
// Localized bidirectional "in 5 min" / "2 hr ago" — coarsest sensible unit so a
// daily job reads "in 14 hr", not "in 840 min".
export function relativeTime(targetMs: number, nowMs = Date.now()): string {
const diff = targetMs - nowMs
const abs = Math.abs(diff)
const sign = diff < 0 ? -1 : 1
if (abs < MINUTE) {
return rtf.format(sign * Math.round(abs / SECOND), 'second')
}
if (abs < HOUR) {
return rtf.format(sign * Math.round(abs / MINUTE), 'minute')
}
if (abs < DAY) {
return rtf.format(sign * Math.round(abs / HOUR), 'hour')
}
return rtf.format(sign * Math.round(abs / DAY), 'day')
}
export type ElapsedUnit = 'day' | 'hour' | 'minute' | 'second'
// Coarsest elapsed bucket for a (clamped-nonnegative) duration, floored. The
// caller owns rendering — compact "5m", "5m ago", etc. — so no format is baked
// in here.
export function coarseElapsed(deltaMs: number): { unit: ElapsedUnit; value: number } {
const ms = Math.max(0, deltaMs)
if (ms >= DAY) {
return { unit: 'day', value: Math.floor(ms / DAY) }
}
if (ms >= HOUR) {
return { unit: 'hour', value: Math.floor(ms / HOUR) }
}
if (ms >= MINUTE) {
return { unit: 'minute', value: Math.floor(ms / MINUTE) }
}
return { unit: 'second', value: Math.floor(ms / SECOND) }
}