/** Planner-launched workflow runs: which are alive, which need someone, and how the last ones ended. */ import { useEffect, useState } from "react"; import { CheckIcon, ChevronIcon, CloseIcon, WarningIcon, WrenchIcon } from "@chopin/icons"; import plannerResume from "../assets/icons/planner-resume.svg"; import plannerStop from "../assets/icons/planner-stop.svg"; import type { Chat as Wire } from "@chopin/protocol"; const SHOWN_STAGES = 6; const SHOWN_RUNS = 3; const STATUS: Record = { running: "running", waiting: "waiting on Decisions", paused: "paused", finished: "finished", blocked: "blocked", failed: "failed", stopped: "stopped", }; const STAGE: Record = { pending: "pending", running: "running", awaiting_input: "waiting on Decisions", paused: "paused", blocked: "blocked", completed: "done", failed: "failed", skipped: "skipped", }; /** Waiting runs first, then running, paused, and ended; newest first within each. */ const GROUP: Record = { waiting: 0, running: 1, paused: 2, finished: 3, blocked: 3, failed: 3, stopped: 3, }; /** "45s", "6m", "1h 4m", "3d 4h". */ export function elapsed(seconds: number): string { let s = Math.max(0, Math.floor(seconds)); if (s < 60) return `${s}s`; let m = Math.floor(s / 60); if (m < 60) return `${m}m`; let h = Math.floor(m / 60); if (h < 24) return `${h}h ${m % 60}m`; return `${Math.floor(h / 24)}d ${h % 24}h`; } export function orderRuns(runs: readonly Wire.Run[]): Wire.Run[] { return [...runs].sort((a, b) => GROUP[a.status] - GROUP[b.status] || b.started - a.started); } /** The run whose stages show without a click: the first one waiting on people, else the newest live one. */ export function defaultOpen(ordered: readonly Wire.Run[]): string | undefined { return ordered.find(run => run.status === "waiting")?.id ?? ordered.find(run => run.status === "running")?.id; } /** The rows shown before "more": at least the first few, and never fewer than every waiting run. */ export function visibleRuns(ordered: readonly Wire.Run[], showAll: boolean): Wire.Run[] { if (showAll) return [...ordered]; let waiting = ordered.filter(run => run.status === "waiting").length; return ordered.slice(0, Math.max(SHOWN_RUNS, waiting)); } function useNow(live: boolean): number { let [now, setNow] = useState(() => Date.now() / 1000); useEffect(() => { if (!live) return; let timer = setInterval(() => setNow(Date.now() / 1000), 5_000); return () => clearInterval(timer); }, [live]); return now; } /** A dot that pulses while work is live; it stays still under reduced motion. */ function RunPulse() { return