1import type { RuntimeSubagent } from "@t3tools/client-runtime/state/subagentRuntime";
2import { isOrchestrationV2WorkActive } from "@t3tools/contracts";
3import { deriveSubagentElapsedMs } from "@t3tools/shared/orchestrationTiming";
4import { useEffect, useRef } from "react";
5
6function formatElapsedSeconds(totalSeconds: number): string {
7 const seconds = Math.max(0, Math.floor(totalSeconds));
8 const minutes = Math.floor(seconds / 60);
9 if (minutes === 0) {
10 return `${seconds}s`;
11 }
12 const hours = Math.floor(minutes / 60);
13 if (hours === 0) {
14 return `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
15 }
16 return `${hours}h ${String(minutes % 60).padStart(2, "0")}m`;
17}
18
19/** Short form for tight rows: "45s", "12m", "1.5h", "14h". Rounds down. */
20export function formatCompactElapsedSeconds(totalSeconds: number): string {
21 const seconds = Math.max(0, Math.floor(totalSeconds));
22 if (seconds < 60) return `${seconds}s`;
23 const minutes = Math.floor(seconds / 60);
24 if (minutes < 60) return `${minutes}m`;
25 if (minutes >= 600) return `${Math.floor(minutes / 60)}h`;
26 return `${Math.floor(minutes / 6) / 10}h`;
27}
28
29/**
30 * Elapsed time for the current activation. Live agents self-tick via DOM
31 * writes (zero React commits per tick); settled agents freeze at completedAt.
32 * `compact` drops the smaller unit for narrow rows such as Lineage.
33 */
34export function AgentElapsed({
35 agent,
36 compact = false,
37}: {
38 agent: Pick<RuntimeSubagent, "status" | "startedAt" | "completedAt">;
39 compact?: boolean;
40}) {
41 const format = compact ? formatCompactElapsedSeconds : formatElapsedSeconds;
42 const textRef = useRef<HTMLSpanElement>(null);
43 const live = isOrchestrationV2WorkActive(agent.status);
44 const startedAt = agent.startedAt;
45 const completedAt = agent.completedAt;
46
47 useEffect(() => {
48 if (!startedAt) {
49 return;
50 }
51 const update = () => {
52 if (textRef.current) {
53 const elapsedMs = deriveSubagentElapsedMs(
54 { status: agent.status, startedAt, completedAt },
55 Date.now(),
56 );
57 const text = elapsedMs === null ? "" : format(elapsedMs / 1000);
58 if (textRef.current.textContent !== text) textRef.current.textContent = text;
59 }
60 };
61 update();
62 if (!live) return;
63 const id = setInterval(update, 1000);
64 return () => clearInterval(id);
65 }, [live, startedAt, completedAt, agent.status, format]);
66
67 const elapsedMs = deriveSubagentElapsedMs(agent, 0);
68 if (elapsedMs === null) {
69 return null;
70 }
71 return (
72 <span ref={textRef} className="tabular-nums">
73 {format(elapsedMs / 1000)}
74 </span>
75 );
76}