// docs/SIDEBAR.md: the session card's meta line — activity colour mapping and // the shared duration format. Both clients compute it identically; the TUI // mirrors this in crates/maestro-tui. import type { ActivityState, IsoTimestamp, Session } from "./types.js"; import { sessionBlockers } from "./types.js"; /** A state held shorter than this keeps the card to one line (contract rule 1). */ const META_LINE_MIN_HOLD_MS = 30_000; // Payloads predating #102 default `activity_since` to the Unix epoch; anything // implausibly old is treated as "never observed" rather than a huge duration. const PLAUSIBLE_EPOCH_MS = Date.UTC(2010, 0, 1); export function activityHeldMs(since: IsoTimestamp | undefined, now: number): number | null { if (!since) return null; const t = Date.parse(since); if (Number.isNaN(t) || t < PLAUSIBLE_EPOCH_MS) return null; return Math.max(0, now - t); } /** `<60s` → whole seconds, `<60m` → whole minutes, else whole hours. Floored * everywhere so the counter never shows a duration the state hasn't reached. */ export function activityDurationLabel(heldMs: number): string { const s = Math.floor(heldMs / 1000); if (s < 60) return `${s}s`; const m = Math.floor(s / 60); if (m < 60) return `${m}m`; return `${Math.floor(m / 60)}h`; } /** CSS hook for the state's colour (`styles.css` maps slugs to tokens). An * unknown variant degrades to idle so rendering never fails (Precedence). */ export function activityColorSlug(state: ActivityState | string): string { switch (state) { case "Active": return "active"; case "AwaitingInput": return "awaiting-input"; case "Error": return "error"; default: return "idle"; } } /** Line 2 renders only when at least one contract trigger holds. */ export function showsMetaLine(s: Session, now: number = Date.now()): boolean { const held = activityHeldMs(s.activity_since, now); return ( (held !== null && held >= META_LINE_MIN_HOLD_MS) || sessionBlockers(s) !== null || s.status === "Dead" || s.activity === "Error" ); }