@walwal-harness/cli 7.1.41 → 7.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,543 @@
1
+ "use client";
2
+
3
+ /* =============================================================
4
+ walwal-harness · VIEW 1 — COMMAND (health strip + agent grid + live stream)
5
+ Ported from design_handoff_harness_dashboard/harness/views-grid.jsx.
6
+
7
+ Exports: GridView, HealthStrip, LiveStream, AlertRail.
8
+ Renders entirely from the Contract (lib/brick/contract). Honest about
9
+ missing data: a null progress/heartbeat/timestamp renders "—" and the
10
+ progress bar is hidden — never a fabricated number.
11
+ ============================================================= */
12
+
13
+ import React, { useEffect, useRef } from "react";
14
+ import {
15
+ AGENT_DEFS,
16
+ fmt,
17
+ statusColor,
18
+ type AgentRole,
19
+ type ContractState,
20
+ type ContractAgent,
21
+ type ContractWorker,
22
+ type ContractTask,
23
+ type ContractEvent,
24
+ type ContractAlert,
25
+ type ContractMetrics,
26
+ type DocTarget,
27
+ } from "../../lib/brick/contract";
28
+ import { StatusDot, Panel, Label, Spark, Bars } from "./ui";
29
+ import { Cadence } from "./views-timeline";
30
+
31
+ /* ---- shared local types ---------------------------------------------- */
32
+
33
+ /** Card vs. list rendering of the agents region (Tweak `agentLayout`). */
34
+ export type AgentLayout = "card" | "list";
35
+
36
+ /** Open the document viewer for an agent or one of its workers. */
37
+ export type OpenDoc = (target: DocTarget) => void;
38
+
39
+ const STALE_MS = 120_000;
40
+
41
+ /** Lookup the static def (hue) for an agent role, tolerating "". */
42
+ function defOf(agent: AgentRole | "") {
43
+ return AGENT_DEFS.find((x) => x.id === agent);
44
+ }
45
+
46
+ /* ---- health strip ----------------------------------------------------- */
47
+
48
+ interface MetricTileProps {
49
+ label: string;
50
+ value: React.ReactNode;
51
+ sub?: string;
52
+ color: string;
53
+ /** Small qualifier shown next to the label, e.g. "est." for estimates. */
54
+ tag?: string;
55
+ /** line sparkline data; null/empty -> no sparkline */
56
+ spark?: number[] | null;
57
+ /** bar distribution data; null/empty -> no bars */
58
+ bars?: number[] | null;
59
+ }
60
+
61
+ function MetricTile({ label, value, sub, color, tag, spark, bars }: MetricTileProps) {
62
+ const hasSpark = !!spark && spark.length > 0;
63
+ const hasBars = !!bars && bars.length > 0;
64
+ return (
65
+ <div className="mtile">
66
+ <div className="mtile-top">
67
+ <span className="mtile-label">
68
+ {label}
69
+ {tag && <span className="mtile-tag">{tag}</span>}
70
+ </span>
71
+ {hasSpark && <Spark data={spark!} color={color} w={56} h={16} />}
72
+ {hasBars && <Bars data={bars!} color={color} w={56} h={16} />}
73
+ </div>
74
+ <div className="mtile-val" style={{ color }}>
75
+ {value}
76
+ </div>
77
+ {sub && <div className="mtile-sub">{sub}</div>}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ export interface HealthStripProps {
83
+ s: ContractState;
84
+ }
85
+
86
+ /**
87
+ * The 8 metric tiles. Null metrics (tokensPerMin, costToday, cpu) render "—"
88
+ * and their sparkline (mtrend/ctrend) is hidden — never fabricated.
89
+ */
90
+ export function HealthStrip({ s }: HealthStripProps) {
91
+ const m: ContractMetrics = s.metrics;
92
+ const aliveWorkers = s.workers.length;
93
+ const aliveAgents = s.agents.filter((a) => a.status !== "idle").length;
94
+ // CPU may be null (no telemetry) -> color stays green; only flips red when
95
+ // there is a real reading over 80%.
96
+ const cpuColor = m.cpu != null && m.cpu > 80 ? "#f0506b" : "#3fd17a";
97
+ const cpuValue = m.cpu != null ? m.cpu + "%" : "—";
98
+ // Context-window occupancy of the most-active session (real, from transcripts).
99
+ const ctxColor = m.contextPct != null && m.contextPct > 80 ? "#f0506b" : "#4cc2ff";
100
+ const ctxValue = m.contextPct != null ? m.contextPct + "%" : "—";
101
+ // costToday is a price-table estimate -> show a small "est." qualifier when present.
102
+ const costTag = m.costToday != null && m.costEstimated ? "est." : undefined;
103
+ return (
104
+ <div className="healthstrip">
105
+ <MetricTile
106
+ label="ALIVE AGENTS"
107
+ value={aliveAgents + "/" + s.agents.length}
108
+ sub="loops running"
109
+ color="#3fd17a"
110
+ />
111
+ <MetricTile
112
+ label="WORKERS"
113
+ value={aliveWorkers}
114
+ sub="spawned · active"
115
+ color="#4cc2ff"
116
+ bars={s.agents.map((a) => a.workers)}
117
+ />
118
+ <MetricTile
119
+ label="TOKENS / MIN"
120
+ value={fmt.k(m.tokensPerMin)}
121
+ sub="rolling"
122
+ color="#c084fc"
123
+ spark={s.mtrend}
124
+ />
125
+ <MetricTile
126
+ label="COST · TODAY"
127
+ value={fmt.money(m.costToday)}
128
+ sub="usd"
129
+ color="#f0a23b"
130
+ tag={costTag}
131
+ spark={s.ctrend}
132
+ />
133
+ <MetricTile
134
+ label="CONTEXT"
135
+ value={ctxValue}
136
+ sub="active session"
137
+ color={ctxColor}
138
+ />
139
+ <MetricTile
140
+ label="THROUGHPUT"
141
+ value={m.throughput == null ? "—" : m.throughput}
142
+ sub="events / min"
143
+ color="#4cc2ff"
144
+ />
145
+ <MetricTile label="CPU" value={cpuValue} sub="harness host" color={cpuColor} />
146
+ <MetricTile label="HOT-FIX" value={m.hotfix} sub="queued patches" color="#8b95a4" />
147
+ <MetricTile
148
+ label="EVENTS"
149
+ value={fmt.k(m.eventsTotal)}
150
+ sub="session total"
151
+ color="#8b95a4"
152
+ />
153
+ </div>
154
+ );
155
+ }
156
+
157
+ /* ---- agent card ------------------------------------------------------- */
158
+
159
+ interface AgentCardProps {
160
+ a: ContractAgent;
161
+ now: number;
162
+ workers: ContractWorker[];
163
+ openDoc: OpenDoc;
164
+ }
165
+
166
+ function AgentCard({ a, now, workers, openDoc }: AgentCardProps) {
167
+ const c = statusColor(a.status);
168
+ const alive = a.loop === "running" && a.status !== "idle";
169
+ const hasHb = a.heartbeat != null;
170
+ const age = hasHb ? now - a.heartbeat! : null;
171
+ const stale = age != null && age > STALE_MS;
172
+ const active = workers.filter((w) => w.status !== "idle");
173
+ return (
174
+ <div
175
+ className={"acard" + (alive ? " is-alive" : "")}
176
+ style={{ ["--hue" as string]: a.hue }}
177
+ role="button"
178
+ tabIndex={0}
179
+ onClick={() => openDoc({ type: "agent", agent: a.id })}
180
+ >
181
+ {alive && <span className="acard-ring" />}
182
+ <div className="acard-head">
183
+ <span className="glyph" style={{ color: a.hue, borderColor: a.hue + "55" }}>
184
+ {a.name}
185
+ </span>
186
+ <div className="acard-id">
187
+ <div className="acard-role">{a.role}</div>
188
+ <div className="acard-status" style={{ color: c }}>
189
+ <StatusDot status={a.status} size={7} />
190
+ {a.status}
191
+ {a.loop === "running" ? " · loop" : ""}
192
+ </div>
193
+ </div>
194
+ <div className="acard-hb">
195
+ <div className={"hb-mono" + (stale ? " is-stale" : "")}>
196
+ {age == null ? "—" : fmt.ago(age)}
197
+ </div>
198
+ <Bars data={a.spark} color={a.hue} w={64} h={18} />
199
+ </div>
200
+ </div>
201
+ <div className="acard-work">{a.work}</div>
202
+ {active.length > 0 && (
203
+ <div className="acard-workers">
204
+ {active.map((w) => (
205
+ <button
206
+ key={w.id}
207
+ className="wrow"
208
+ onClick={(e) => {
209
+ e.stopPropagation();
210
+ openDoc({ type: "worker", agent: a.id, worker: w });
211
+ }}
212
+ >
213
+ <StatusDot status={w.status} size={5} pulse={false} />
214
+ <span className="wrow-name">{w.name}</span>
215
+ <span className="wrow-prog">
216
+ <span className="prog-bar prog-bar-sm">
217
+ {w.progress != null && (
218
+ <span
219
+ className="prog-fill"
220
+ style={{ width: Math.round(w.progress) + "%", background: a.hue }}
221
+ />
222
+ )}
223
+ </span>
224
+ </span>
225
+ <span className="wrow-pct">
226
+ {w.progress != null ? Math.round(w.progress) + "%" : "—"}
227
+ </span>
228
+ </button>
229
+ ))}
230
+ </div>
231
+ )}
232
+ <div className="acard-foot">
233
+ <span className="chip">
234
+ <b>{a.workers}</b> workers
235
+ </span>
236
+ <span className="chip">
237
+ <b>{a.todos}</b> todos
238
+ </span>
239
+ <span className="chip chip-read">read report ↗</span>
240
+ {stale && a.loop === "running" && (
241
+ <span className="chip chip-warn">stale {age == null ? "" : fmt.ago(age)}</span>
242
+ )}
243
+ </div>
244
+ </div>
245
+ );
246
+ }
247
+
248
+ /* ---- agent row (list layout) ----------------------------------------- */
249
+
250
+ interface AgentRowProps {
251
+ a: ContractAgent;
252
+ now: number;
253
+ workers: ContractWorker[];
254
+ openDoc: OpenDoc;
255
+ }
256
+
257
+ function AgentRow({ a, now, workers, openDoc }: AgentRowProps) {
258
+ const c = statusColor(a.status);
259
+ const hasHb = a.heartbeat != null;
260
+ const age = hasHb ? now - a.heartbeat! : null;
261
+ const stale = age != null && age > STALE_MS;
262
+ const active = workers.filter((w) => w.status !== "idle");
263
+ return (
264
+ <React.Fragment>
265
+ <button
266
+ className="arow"
267
+ onClick={() => openDoc({ type: "agent", agent: a.id })}
268
+ style={{ ["--hue" as string]: a.hue }}
269
+ >
270
+ <span className="arow-name">
271
+ <span className="glyph glyph-sm" style={{ color: a.hue, borderColor: a.hue + "55" }}>
272
+ {a.name}
273
+ </span>
274
+ </span>
275
+ <span className="arow-status" style={{ color: c }}>
276
+ <StatusDot status={a.status} size={6} />
277
+ {a.status}
278
+ </span>
279
+ <span className="arow-loop">{a.loop}</span>
280
+ <span className="arow-work">{a.work}</span>
281
+ <span className="arow-num">{a.workers}</span>
282
+ <span className="arow-num">{a.todos}</span>
283
+ <span className={"arow-age" + (stale ? " is-stale" : "")}>
284
+ {age == null ? "—" : fmt.ago(age)}
285
+ </span>
286
+ <span className="arow-spark">
287
+ <Bars data={a.spark} color={a.hue} w={70} h={16} />
288
+ </span>
289
+ </button>
290
+ {active.map((w) => (
291
+ <button
292
+ key={w.id}
293
+ className="arow arow-sub"
294
+ onClick={() => openDoc({ type: "worker", agent: a.id, worker: w })}
295
+ style={{ ["--hue" as string]: a.hue }}
296
+ >
297
+ <span className="arow-name arow-subname">
298
+ <span className="swim-tree">└</span> {w.name}
299
+ </span>
300
+ <span className="arow-status" style={{ color: statusColor(w.status) }}>
301
+ <StatusDot status={w.status} size={5} pulse={false} />
302
+ {w.status}
303
+ </span>
304
+ <span className="arow-loop">worker</span>
305
+ <span className="arow-work arow-subwork">brief · {w.name}</span>
306
+ <span className="arow-num" />
307
+ <span className="arow-num" />
308
+ <span className="arow-age">
309
+ {w.started == null ? "—" : fmt.ago(now - w.started)}
310
+ </span>
311
+ <span className="arow-spark arow-subprog">
312
+ <span className="prog-bar">
313
+ {w.progress != null && (
314
+ <span
315
+ className="prog-fill"
316
+ style={{ width: Math.round(w.progress) + "%", background: a.hue }}
317
+ />
318
+ )}
319
+ </span>
320
+ </span>
321
+ </button>
322
+ ))}
323
+ </React.Fragment>
324
+ );
325
+ }
326
+
327
+ /* ---- task rail (WORK · IN FLIGHT) ------------------------------------- */
328
+
329
+ interface TaskRailProps {
330
+ s: ContractState;
331
+ openDoc: OpenDoc;
332
+ }
333
+
334
+ function TaskRail({ s, openDoc }: TaskRailProps) {
335
+ const { running, queued, done } = s.tasks;
336
+ return (
337
+ <Panel title="WORK · IN FLIGHT" right={running.length + " running"} className="taskpanel">
338
+ <div className="tasksum">
339
+ <div className="tsum">
340
+ <b style={{ color: "#3fd17a" }}>{running.length}</b>
341
+ <span>running</span>
342
+ </div>
343
+ <div className="tsum">
344
+ <b style={{ color: "#f0a23b" }}>{queued.length}</b>
345
+ <span>queued</span>
346
+ </div>
347
+ <div className="tsum">
348
+ <b style={{ color: "#8b95a4" }}>{done.length}</b>
349
+ <span>done</span>
350
+ </div>
351
+ </div>
352
+ <div className="tasklist">
353
+ {running.map((t: ContractTask) => {
354
+ const hue = defOf(t.agent)?.hue;
355
+ return (
356
+ <button
357
+ key={t.id}
358
+ className="taskitem"
359
+ onClick={() => openDoc({ type: "agent", agent: t.agent })}
360
+ >
361
+ <StatusDot status="active" hue={statusColor("active")} size={6} />
362
+ <span className="task-owner" style={{ color: hue }}>
363
+ {t.owner}
364
+ </span>
365
+ <span className="task-title">{t.title}</span>
366
+ <span className="task-prog">
367
+ <span className="prog-bar">
368
+ {t.progress != null && (
369
+ <span
370
+ className="prog-fill"
371
+ style={{ width: Math.round(t.progress) + "%", background: hue }}
372
+ />
373
+ )}
374
+ </span>
375
+ {t.progress != null ? Math.round(t.progress) + "%" : "—"}
376
+ </span>
377
+ </button>
378
+ );
379
+ })}
380
+ {queued.map((t: ContractTask) => {
381
+ const hue = defOf(t.agent)?.hue;
382
+ return (
383
+ <button
384
+ key={t.id}
385
+ className="taskitem is-queued"
386
+ onClick={() => openDoc({ type: "agent", agent: t.agent })}
387
+ >
388
+ <span className="qdot" />
389
+ <span className="task-owner" style={{ color: hue }}>
390
+ {t.owner}
391
+ </span>
392
+ <span className="task-title">{t.title}</span>
393
+ <span className="task-prog task-queuedlbl">queued</span>
394
+ </button>
395
+ );
396
+ })}
397
+ </div>
398
+ </Panel>
399
+ );
400
+ }
401
+
402
+ /* ---- live stream ------------------------------------------------------ */
403
+
404
+ export interface LiveStreamProps {
405
+ events: ContractEvent[];
406
+ max?: number;
407
+ openDoc?: OpenDoc;
408
+ }
409
+
410
+ /** Auto-scrolling activity feed. COMPLETE/complete verbs render green. */
411
+ export function LiveStream({ events, max = 40, openDoc }: LiveStreamProps) {
412
+ const ref = useRef<HTMLDivElement>(null);
413
+ const recent = events.slice(-max);
414
+ useEffect(() => {
415
+ if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
416
+ }, [events.length]);
417
+ return (
418
+ <Panel title="LIVE · STREAM" right="tail -f" className="streampanel" flush>
419
+ <div className="stream" ref={ref}>
420
+ {recent.map((e, i) => {
421
+ const a = defOf(e.agent);
422
+ const isLast = i === recent.length - 1;
423
+ const complete = e.verb === "COMPLETE" || e.verb === "complete";
424
+ return (
425
+ <button
426
+ key={e.id}
427
+ className={"streamline" + (isLast ? " is-new" : "")}
428
+ onClick={() => openDoc && e.agent && openDoc({ type: "agent", agent: e.agent })}
429
+ >
430
+ <span className="sl-time">{e.at == null ? "—" : fmt.time(new Date(e.at))}</span>
431
+ <span className="sl-agent" style={{ color: a?.hue }}>
432
+ {a?.name}
433
+ </span>
434
+ <span className="sl-worker">{e.worker}</span>
435
+ <span className={"sl-msg" + (complete ? " sl-done" : "")}>
436
+ {e.verb} <span className="sl-obj">`{e.obj}`</span>
437
+ </span>
438
+ </button>
439
+ );
440
+ })}
441
+ </div>
442
+ </Panel>
443
+ );
444
+ }
445
+
446
+ /* ---- alert rail ------------------------------------------------------- */
447
+
448
+ export interface AlertRailProps {
449
+ alerts: ContractAlert[];
450
+ now: number;
451
+ openDoc?: OpenDoc;
452
+ }
453
+
454
+ /** ATTENTION · IN-LOOP — synthesized stale/attention alerts; null when empty. */
455
+ export function AlertRail({ alerts, now, openDoc }: AlertRailProps) {
456
+ if (!alerts.length) return null;
457
+ return (
458
+ <Panel title="ATTENTION · IN-LOOP" right={alerts.length} className="alertpanel">
459
+ <div className="alertlist">
460
+ {alerts.map((al) => {
461
+ const a = defOf(al.agent);
462
+ return (
463
+ <button
464
+ key={al.id}
465
+ className={"alertitem lvl-" + al.level}
466
+ onClick={() => openDoc && openDoc({ type: "agent", agent: al.agent })}
467
+ >
468
+ <div
469
+ className="alert-bar"
470
+ style={{ background: al.level === "stale" ? "#f0a23b" : "#f0506b" }}
471
+ />
472
+ <div className="alert-body">
473
+ <div className="alert-top">
474
+ <span className="alert-agent" style={{ color: a?.hue }}>
475
+ {a?.name}
476
+ </span>
477
+ <span className="alert-lvl">{al.level}</span>
478
+ <span className="alert-age">{al.at == null ? "—" : fmt.ago(now - al.at)}</span>
479
+ </div>
480
+ <div className="alert-msg">{al.msg}</div>
481
+ </div>
482
+ </button>
483
+ );
484
+ })}
485
+ </div>
486
+ </Panel>
487
+ );
488
+ }
489
+
490
+ /* ---- the COMMAND view ------------------------------------------------- */
491
+
492
+ export interface GridViewProps {
493
+ s: ContractState;
494
+ /** Tweak: card grid vs. list table. Defaults to "card". */
495
+ layout?: AgentLayout;
496
+ openDoc: OpenDoc;
497
+ }
498
+
499
+ export function GridView({ s, layout = "card", openDoc }: GridViewProps) {
500
+ const list = layout === "list";
501
+ const wkOf = (id: AgentRole): ContractWorker[] => s.workers.filter((w) => w.agent === id);
502
+ return (
503
+ <div className="gridview">
504
+ <HealthStrip s={s} />
505
+ <Cadence cadence={s.cadence} />
506
+ <div className="gridview-body">
507
+ <div className="gridview-main">
508
+ <Label right={s.agents.filter((a) => a.loop === "running").length + " alive"}>
509
+ AGENTS · ALIVE NOW
510
+ </Label>
511
+ {list ? (
512
+ <div className="arows">
513
+ <div className="arows-head">
514
+ <span>agent</span>
515
+ <span>status</span>
516
+ <span>loop</span>
517
+ <span>current work</span>
518
+ <span>wk</span>
519
+ <span>td</span>
520
+ <span>last</span>
521
+ <span>24h</span>
522
+ </div>
523
+ {s.agents.map((a) => (
524
+ <AgentRow key={a.id} a={a} now={s.now} workers={wkOf(a.id)} openDoc={openDoc} />
525
+ ))}
526
+ </div>
527
+ ) : (
528
+ <div className="acards">
529
+ {s.agents.map((a) => (
530
+ <AgentCard key={a.id} a={a} now={s.now} workers={wkOf(a.id)} openDoc={openDoc} />
531
+ ))}
532
+ </div>
533
+ )}
534
+ <TaskRail s={s} openDoc={openDoc} />
535
+ </div>
536
+ <div className="gridview-rail">
537
+ <AlertRail alerts={s.alerts} now={s.now} openDoc={openDoc} />
538
+ <LiveStream events={s.events} openDoc={openDoc} />
539
+ </div>
540
+ </div>
541
+ </div>
542
+ );
543
+ }