@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,305 @@
1
+ "use client";
2
+
3
+ /* =============================================================
4
+ walwal-harness · VIEW 2 — ORCHESTRATION (node-edge graph)
5
+ Ported from design_handoff_harness_dashboard/harness/views-graph.jsx.
6
+ Exports: GraphView, useSize
7
+
8
+ Renders the CEO -> exec -> worker node/edge graph (animated active
9
+ edges, ResizeObserver sizing) plus a detail panel for the selected
10
+ node. Every value comes from the real-data Contract (ContractState);
11
+ where the contract is honest about missing data (null heartbeat /
12
+ progress / event time) we render an em-dash, never a fake number.
13
+ ============================================================= */
14
+
15
+ import React, { useState, useRef, useLayoutEffect } from "react";
16
+ import {
17
+ fmt,
18
+ statusColor,
19
+ type AgentRole,
20
+ type AgentStatus,
21
+ type ContractState,
22
+ type ContractAgent,
23
+ type ContractWorker,
24
+ type DocTarget,
25
+ } from "../../lib/brick/contract";
26
+ import { StatusDot, Label } from "./ui";
27
+
28
+ /* ---- sizing hook ------------------------------------------------------ */
29
+
30
+ export interface Size {
31
+ w: number;
32
+ h: number;
33
+ }
34
+
35
+ export function useSize(ref: React.RefObject<HTMLElement | null>): Size {
36
+ const [size, setSize] = useState<Size>({ w: 1000, h: 600 });
37
+ useLayoutEffect(() => {
38
+ if (!ref.current) return;
39
+ const ro = new ResizeObserver((ents) => {
40
+ const r = ents[0].contentRect;
41
+ setSize({ w: r.width, h: r.height });
42
+ });
43
+ ro.observe(ref.current);
44
+ return () => ro.disconnect();
45
+ }, [ref]);
46
+ return size;
47
+ }
48
+
49
+ /* ---- helpers ---------------------------------------------------------- */
50
+
51
+ interface Point {
52
+ x: number;
53
+ y: number;
54
+ }
55
+
56
+ /** Inline `--hue` custom property (kept as the mockup's per-node convention). */
57
+ type HueStyle = React.CSSProperties & { "--hue"?: string };
58
+
59
+ const isActive = (st: AgentStatus): boolean =>
60
+ st === "typing" || st === "talking" || st === "active" || st === "live";
61
+
62
+ const edge = (a: Point, b: Point): string =>
63
+ `M${a.x} ${a.y} C ${(a.x + b.x) / 2} ${a.y}, ${(a.x + b.x) / 2} ${b.y}, ${b.x} ${b.y}`;
64
+
65
+ /** Honest progress width: null -> 0 (no fabricated bar). */
66
+ const progWidth = (p: number | null): number => (p == null ? 0 : Math.round(p));
67
+ /** Honest progress label: null -> em-dash. */
68
+ const progLabel = (p: number | null): string => (p == null ? "—" : Math.round(p) + "%");
69
+
70
+ /* ---- props ------------------------------------------------------------ */
71
+
72
+ export interface GraphViewProps {
73
+ s: ContractState;
74
+ /** Selected node id (controlled). Falls back to an internal default. */
75
+ sel?: AgentRole;
76
+ setSel?: (id: AgentRole) => void;
77
+ /** Open a report document for an agent or worker. */
78
+ openDoc?: (target: DocTarget) => void;
79
+ }
80
+
81
+ /* ---- GraphView -------------------------------------------------------- */
82
+
83
+ export function GraphView({ s, sel: selProp, setSel: setSelProp, openDoc }: GraphViewProps) {
84
+ const wrapRef = useRef<HTMLDivElement | null>(null);
85
+ const { w: W, h: H } = useSize(wrapRef);
86
+ const [selLocal, setSelLocal] = useState<AgentRole>("cto");
87
+ const sel = selProp ?? selLocal;
88
+ const setSel = setSelProp ?? setSelLocal;
89
+
90
+ const pad = 56;
91
+ const order: AgentRole[] = ["ceo", "coo", "cto", "cqo", "cdo", "ops"];
92
+ const execs: ContractAgent[] = order
93
+ .map((id) => s.agents.find((a) => a.id === id))
94
+ .filter((a): a is ContractAgent => Boolean(a));
95
+
96
+ // workers grouped by exec order
97
+ const workers: ContractWorker[] = [];
98
+ execs.forEach((e) =>
99
+ s.workers.filter((w) => w.agent === e.id).forEach((w) => workers.push(w))
100
+ );
101
+ const N = Math.max(workers.length, 1);
102
+
103
+ const xCEO = pad + 24;
104
+ const xExec = Math.max(260, W * 0.4);
105
+ const xWork = Math.max(520, W * 0.8);
106
+
107
+ const wpos: Record<string, Point> = {};
108
+ workers.forEach((w, i) => {
109
+ wpos[w.id] = { x: xWork, y: pad + ((i + 0.5) * (H - 2 * pad)) / N };
110
+ });
111
+ const epos: Record<string, Point> = {};
112
+ execs.forEach((e, i) => {
113
+ const mine = workers.filter((w) => w.agent === e.id);
114
+ const y = mine.length
115
+ ? mine.reduce((acc, w) => acc + wpos[w.id].y, 0) / mine.length
116
+ : pad + ((i + 0.5) * (H - 2 * pad)) / execs.length;
117
+ epos[e.id] = { x: xExec, y };
118
+ });
119
+ const ceoPos: Point = { x: xCEO, y: H / 2 };
120
+
121
+ const selAgent: ContractAgent = s.agents.find((a) => a.id === sel) ?? execs[0];
122
+ const selWorkers = s.workers.filter((w) => w.agent === sel);
123
+ const selEvents = s.events.filter((e) => e.agent === sel).slice(-10).reverse();
124
+
125
+ return (
126
+ <div className="graphview">
127
+ <div className="graph-wrap" ref={wrapRef}>
128
+ <svg className="graph-edges" width={W} height={H}>
129
+ {execs.map((e) => {
130
+ const active = isActive(e.status);
131
+ return (
132
+ <path
133
+ key={"ce" + e.id}
134
+ d={edge(ceoPos, epos[e.id])}
135
+ className={"gedge" + (active ? " is-active" : "")}
136
+ style={{ stroke: e.hue, "--hue": e.hue } as HueStyle}
137
+ />
138
+ );
139
+ })}
140
+ {workers.map((w) => {
141
+ const e = s.agents.find((a) => a.id === w.agent);
142
+ const active = isActive(w.status);
143
+ return (
144
+ <path
145
+ key={"ew" + w.id}
146
+ d={edge(epos[w.agent], wpos[w.id])}
147
+ className={"gedge gedge-w" + (active ? " is-active" : "")}
148
+ style={{ stroke: e?.hue, "--hue": e?.hue } as HueStyle}
149
+ />
150
+ );
151
+ })}
152
+ </svg>
153
+
154
+ {/* CEO root */}
155
+ <div
156
+ className="gnode gnode-root"
157
+ style={{ left: ceoPos.x, top: ceoPos.y, "--hue": "#4cc2ff" } as HueStyle}
158
+ onClick={() => setSel("ceo")}
159
+ >
160
+ <span className="gnode-ring" />
161
+ <span className="glyph" style={{ color: "#4cc2ff", borderColor: "#4cc2ff66" }}>
162
+ CEO
163
+ </span>
164
+ <span className="gnode-cap">root loop</span>
165
+ </div>
166
+
167
+ {/* exec nodes */}
168
+ {execs
169
+ .filter((e) => e.id !== "ceo")
170
+ .map((e) => {
171
+ const active = isActive(e.status);
172
+ return (
173
+ <div
174
+ key={e.id}
175
+ className={
176
+ "gnode gnode-exec" +
177
+ (sel === e.id ? " is-sel" : "") +
178
+ (active ? " is-alive" : "")
179
+ }
180
+ style={{ left: epos[e.id].x, top: epos[e.id].y, "--hue": e.hue } as HueStyle}
181
+ onClick={() => setSel(e.id)}
182
+ >
183
+ {active && <span className="gnode-ring" />}
184
+ <span className="glyph" style={{ color: e.hue, borderColor: e.hue + "66" }}>
185
+ {e.name}
186
+ </span>
187
+ <span className="gnode-meta">
188
+ <StatusDot status={e.status} size={6} />
189
+ {e.workers}w
190
+ </span>
191
+ </div>
192
+ );
193
+ })}
194
+
195
+ {/* worker nodes */}
196
+ {workers.map((w) => {
197
+ const e = s.agents.find((a) => a.id === w.agent);
198
+ const hue = e?.hue;
199
+ const fresh = w.started != null && s.now - w.started < 4000;
200
+ return (
201
+ <div
202
+ key={w.id}
203
+ className={
204
+ "gnode gnode-worker" +
205
+ (sel === w.agent ? " is-dim-on" : "") +
206
+ (fresh ? " is-fresh" : "")
207
+ }
208
+ style={{ left: wpos[w.id].x, top: wpos[w.id].y, "--hue": hue } as HueStyle}
209
+ onClick={() => setSel(w.agent)}
210
+ title={w.name}
211
+ >
212
+ <span className="gw-dot" style={{ background: hue }} />
213
+ <span className="gw-name">{w.name}</span>
214
+ <span className="gw-prog">
215
+ <span style={{ width: progWidth(w.progress) + "%", background: hue }} />
216
+ </span>
217
+ </div>
218
+ );
219
+ })}
220
+
221
+ <div className="graph-legend">
222
+ <span>CEO → exec → worker · animated edge = active dispatch</span>
223
+ </div>
224
+ </div>
225
+
226
+ <aside className="graph-detail">
227
+ <Label right={selAgent.loop}>NODE · {selAgent.name}</Label>
228
+ <div className="gd-head" style={{ "--hue": selAgent.hue } as HueStyle}>
229
+ <span
230
+ className="glyph glyph-lg"
231
+ style={{ color: selAgent.hue, borderColor: selAgent.hue + "66" }}
232
+ >
233
+ {selAgent.name}
234
+ </span>
235
+ <div>
236
+ <div className="gd-role">{selAgent.role}</div>
237
+ <div className="gd-status" style={{ color: statusColor(selAgent.status) }}>
238
+ <StatusDot status={selAgent.status} size={7} />
239
+ {selAgent.status} · loop {selAgent.loop}
240
+ </div>
241
+ </div>
242
+ <button
243
+ className="gd-read"
244
+ onClick={() => openDoc && openDoc({ type: "agent", agent: selAgent.id })}
245
+ >
246
+ read report ↗
247
+ </button>
248
+ </div>
249
+ <div className="gd-work">{selAgent.work}</div>
250
+ <div className="gd-stats">
251
+ <div>
252
+ <b>{selWorkers.length}</b>
253
+ <span>workers</span>
254
+ </div>
255
+ <div>
256
+ <b>{selAgent.todos}</b>
257
+ <span>open todos</span>
258
+ </div>
259
+ <div>
260
+ <b>{selAgent.heartbeat == null ? "—" : fmt.ago(s.now - selAgent.heartbeat)}</b>
261
+ <span>heartbeat</span>
262
+ </div>
263
+ </div>
264
+ <Label>SPAWNED WORKERS</Label>
265
+ <div className="gd-workers">
266
+ {selWorkers.length === 0 && <div className="gd-empty">no active workers</div>}
267
+ {selWorkers.map((w) => (
268
+ <button
269
+ key={w.id}
270
+ className="gd-worker"
271
+ onClick={() => openDoc && openDoc({ type: "worker", agent: selAgent.id, worker: w })}
272
+ >
273
+ <StatusDot status={w.status} size={6} />
274
+ <span className="gd-wname">{w.name}</span>
275
+ <span className="gd-wprog">
276
+ <span className="prog-bar">
277
+ <span
278
+ className="prog-fill"
279
+ style={{ width: progWidth(w.progress) + "%", background: selAgent.hue }}
280
+ />
281
+ </span>
282
+ {progLabel(w.progress)}
283
+ </span>
284
+ </button>
285
+ ))}
286
+ </div>
287
+ <Label>RECENT · {selAgent.name}</Label>
288
+ <div className="gd-events">
289
+ {selEvents.map((e) => (
290
+ <button
291
+ key={e.id}
292
+ className="gd-ev"
293
+ onClick={() => openDoc && openDoc({ type: "agent", agent: selAgent.id })}
294
+ >
295
+ <span className="sl-time">{e.at == null ? "—" : fmt.time(new Date(e.at))}</span>
296
+ <span className="sl-msg">
297
+ {e.verb} <span className="sl-obj">`{e.obj}`</span>
298
+ </span>
299
+ </button>
300
+ ))}
301
+ </div>
302
+ </aside>
303
+ </div>
304
+ );
305
+ }