@walwal-harness/cli 7.1.41 → 7.1.42

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.
@@ -199,6 +199,23 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
199
199
  missionId: activeMission?.missionId ?? null,
200
200
  });
201
201
  }
202
+ // Authoritative "who's working now": always light the runtime's current
203
+ // agent lane while the loop is active (we only reach here when not idle),
204
+ // even if that agent has no per-lane todo/worker row yet — work often runs
205
+ // in a background agent the runtime only names via current_agent.
206
+ const currentRole = (snapshot.runtime?.currentAgent ?? "").replace(/^harness-/, "");
207
+ if (currentRole) {
208
+ const currentLane = lanes.find((l) => l.kind === "cxx" && l.role === currentRole);
209
+ if (currentLane && !next.some((s) => s.laneId === currentLane.id)) {
210
+ next.push({
211
+ ts: now,
212
+ laneId: currentLane.id,
213
+ count: 2,
214
+ hotfix: activeMission?.type === "hotfix",
215
+ missionId: activeMission?.missionId ?? null,
216
+ });
217
+ }
218
+ }
202
219
  if (next.length === 0) return prev;
203
220
  return [...prev, ...next]
204
221
  .filter((sample) => now - sample.ts < 30 * 60 * 1000)
@@ -229,12 +246,10 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
229
246
  title: string;
230
247
  content: string;
231
248
  } | null>(null);
232
- const [selectedHarnessPath, setSelectedHarnessPath] = useState<string | null>(null);
233
249
 
234
250
  // Selecting a mission or lane returns the viewer to mission-context mode.
235
251
  useEffect(() => {
236
252
  setCustomDoc(null);
237
- setSelectedHarnessPath(null);
238
253
  }, [selectedMissionId, selectedLaneId]);
239
254
 
240
255
  void lang;
@@ -245,32 +260,19 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
245
260
  (snapshot.missions.find((mission) => mission.active) ?? snapshot.missions[0])?.workers.filter((w) => w.active).length ?? 0;
246
261
  const hotfixCount = snapshot.missions.filter((m) => m.type === "hotfix").length;
247
262
  const runtimeHealth = useMemo(() => deriveRuntimeHealth(snapshot), [snapshot]);
248
- const highlightedPaths = useMemo(
249
- () => buildHighlightedPaths(selectedMission, selectedLane, snapshot.todos),
250
- [selectedMission, selectedLane, snapshot.todos]
251
- );
252
263
 
253
264
  const handleSelectWorker = (mission: MissionDoc, worker: WorkerDocEntry) => {
254
265
  setSelectedMissionId(mission.missionId);
255
266
  setSelectedLaneId(`${worker.owner}:${worker.name}`);
256
267
  };
257
268
 
258
- const handleSelectHarnessFile = (entry: HarnessFileEntry) => {
259
- setSelectedHarnessPath(entry.path);
260
- setCustomDoc({
261
- source: "Harness file",
262
- title: entry.path,
263
- content: [
264
- `# ${entry.path}`,
265
- "",
266
- `- kind: ${entry.kind}`,
267
- `- category: ${entry.category}`,
268
- `- updated: ${entry.updatedAt ?? "(unknown)"}`,
269
- `- size: ${entry.size === null ? "(directory)" : `${entry.size} bytes`}`,
270
- "",
271
- "Select a mission, CXX lane, or worker report to inspect document contents.",
272
- ].join("\n"),
273
- });
269
+ // Open a gotcha/convention in the document viewer with its FULL markdown body
270
+ // (entry.content is the real readFileSync'd file — restores full-document reading).
271
+ const handleSelectKnowledge = (
272
+ entry: GotchaEntry | ConventionEntry,
273
+ kind: KnowledgeKind
274
+ ) => {
275
+ setCustomDoc({ source: kind, title: `${entry.id}.md`, content: entry.content });
274
276
  };
275
277
 
276
278
  return (
@@ -295,23 +297,27 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
295
297
  />
296
298
 
297
299
  <main className="flex min-w-0 flex-1 flex-col gap-2 p-2">
298
- {/* Top row: Owner situational awareness */}
300
+ {/* Top row: Owner situational awareness.
301
+ PULSE (trust verdict) · ACTIVE NOW (per-CXX activity, fixes the
302
+ "only CEO" heatmap illusion) · KNOWLEDGE (gotchas/conventions, full
303
+ body) · RECENT (worker reports, full body). All viewer-openable. */}
299
304
  <section className="grid shrink-0 grid-cols-4 gap-2" style={{ height: 310 }}>
300
305
  <OperationalStatusPanel
301
306
  snapshot={snapshot}
302
307
  health={runtimeHealth}
303
308
  mission={selectedMission}
304
309
  />
305
- <StepProgressPanel
306
- mission={selectedMission}
307
- runtimeAgent={snapshot.runtime.currentAgent}
308
- todos={snapshot.todos}
310
+ <ActiveNowPanel
311
+ lanes={lanes}
312
+ snapshot={snapshot}
313
+ selectedLaneId={selectedLaneId}
314
+ onSelectLane={setSelectedLaneId}
309
315
  />
310
- <HarnessFileMapPanel
311
- files={snapshot.files}
312
- highlightedPaths={highlightedPaths}
313
- selectedPath={selectedHarnessPath}
314
- onSelect={handleSelectHarnessFile}
316
+ <KnowledgeBasePanel
317
+ gotchas={snapshot.gotchas}
318
+ conventions={snapshot.conventions}
319
+ baselineTs={selectedGoal?.ts ?? null}
320
+ onSelect={handleSelectKnowledge}
315
321
  />
316
322
  <RecentReportPanel
317
323
  missions={snapshot.missions}
@@ -1012,6 +1018,113 @@ function RuntimeMetric({ label, value }: { label: string; value: string }) {
1012
1018
  );
1013
1019
  }
1014
1020
 
1021
+ // ACTIVE NOW — a per-CXX roster so the whole company is legible at a glance,
1022
+ // independent of heatmap brightness. Each role shows LIVE (open todo / running
1023
+ // worker), RECENT (newest activity sample, relative age), or IDLE — in its own
1024
+ // CXX colour. This is the perception-level fix for "heatmap shows only CEO".
1025
+ function ActiveNowPanel({
1026
+ lanes,
1027
+ snapshot,
1028
+ selectedLaneId,
1029
+ onSelectLane,
1030
+ }: {
1031
+ lanes: AgentLane[];
1032
+ snapshot: HarnessSnapshot;
1033
+ selectedLaneId: string | null;
1034
+ onSelectLane: (laneId: string) => void;
1035
+ }) {
1036
+ const lastSampleByRole = useMemo(() => {
1037
+ const m = new Map<string, string>();
1038
+ for (const s of snapshot.activitySamples) {
1039
+ const role = s.laneId.split(":")[0];
1040
+ const prev = m.get(role);
1041
+ if (!prev || Date.parse(s.ts) > Date.parse(prev)) m.set(role, s.ts);
1042
+ }
1043
+ return m;
1044
+ }, [snapshot.activitySamples]);
1045
+
1046
+ // The runtime's current_agent is the authoritative "who is working now" signal:
1047
+ // a freshly-spawned agent often has no per-lane todo/worker row yet (work runs in
1048
+ // a background agent the runtime only names via current_agent + current_action),
1049
+ // so honour it directly instead of inferring liveness from todos/workers alone.
1050
+ const rt = snapshot.runtime;
1051
+ const currentRole = (rt.currentAgent ?? "").replace(/^harness-/, "");
1052
+ const loopActive =
1053
+ rt.agentStatus !== "completed" &&
1054
+ rt.agentStatus !== "complete" &&
1055
+ rt.conductorState !== "completed" &&
1056
+ rt.conductorState !== "blocked";
1057
+
1058
+ const rows = CXX_ROLES.map((role) => {
1059
+ const lane = lanes.find((l) => l.kind === "cxx" && l.role === role) ?? null;
1060
+ const openTodos = snapshot.todos.filter(
1061
+ (t) => t.owner === role && t.status !== "done" && t.status !== "completed"
1062
+ ).length;
1063
+ const runningWorkers = (lane?.workers ?? []).filter(
1064
+ (w) => w.active || w.status === "IN_PROGRESS"
1065
+ ).length;
1066
+ const lastTs = lastSampleByRole.get(role) ?? null;
1067
+ const isCurrent = loopActive && currentRole === role;
1068
+ const state: "live" | "recent" | "idle" =
1069
+ isCurrent || openTodos > 0 || runningWorkers > 0 ? "live" : lastTs ? "recent" : "idle";
1070
+ return { role, lane, openTodos, runningWorkers, lastTs, isCurrent, state };
1071
+ });
1072
+
1073
+ return (
1074
+ <div className="panel-shell fade-in flex h-full min-h-0 flex-col overflow-hidden p-2">
1075
+ <SectionLabel>Active Now · Who · What</SectionLabel>
1076
+ <div className="mt-2 min-h-0 flex-1 space-y-1 overflow-auto pr-1">
1077
+ {rows.map(({ role, lane, openTodos, runningWorkers, lastTs, isCurrent, state }) => {
1078
+ const selected = !!lane && selectedLaneId === lane.id;
1079
+ return (
1080
+ <button
1081
+ key={role}
1082
+ type="button"
1083
+ disabled={!lane}
1084
+ onClick={() => lane && onSelectLane(lane.id)}
1085
+ title={lane ? `Open ${role.toUpperCase()} document` : `${role.toUpperCase()} not active in this mission`}
1086
+ className={`flex w-full items-center gap-2 rounded border px-2 py-1.5 text-left transition-colors ${
1087
+ selected
1088
+ ? "border-cyan-300 bg-cyan-400/10"
1089
+ : state === "live"
1090
+ ? "border-cyan-400/40 bg-cyan-400/5"
1091
+ : "border-slate-800 hover:border-cyan-400/20 hover:bg-white/[0.03]"
1092
+ } ${lane ? "cursor-pointer" : "cursor-default opacity-60"}`}
1093
+ >
1094
+ <span
1095
+ className={`h-2.5 w-2.5 shrink-0 rounded-full ${
1096
+ state === "idle" ? "bg-slate-700" : CXX_COLOR[role]
1097
+ } ${state === "live" ? "animate-pulse" : state === "recent" ? "opacity-60" : ""}`}
1098
+ />
1099
+ <span className={`w-9 shrink-0 font-mono text-[11px] font-semibold ${CXX_TEXT[role]}`}>
1100
+ {role.toUpperCase()}
1101
+ </span>
1102
+ <span className="min-w-0 flex-1 truncate text-[10px] text-slate-300">
1103
+ {state === "live"
1104
+ ? isCurrent
1105
+ ? rt.currentAction ?? "current agent · running"
1106
+ : runningWorkers > 0
1107
+ ? `${runningWorkers} worker${runningWorkers > 1 ? "s" : ""} running`
1108
+ : `${openTodos} open todo${openTodos > 1 ? "s" : ""}`
1109
+ : state === "recent"
1110
+ ? `active ${formatRelativeTime(lastTs)}`
1111
+ : "idle"}
1112
+ </span>
1113
+ <span
1114
+ className={`shrink-0 font-mono text-[8px] uppercase tracking-wider ${
1115
+ state === "live" ? "text-cyan-300" : state === "recent" ? "text-slate-500" : "text-slate-600"
1116
+ }`}
1117
+ >
1118
+ {state}
1119
+ </span>
1120
+ </button>
1121
+ );
1122
+ })}
1123
+ </div>
1124
+ </div>
1125
+ );
1126
+ }
1127
+
1015
1128
  function StepProgressPanel({
1016
1129
  mission,
1017
1130
  runtimeAgent,
@@ -2036,7 +2149,7 @@ function WorkflowHeatmap({
2036
2149
  : selected
2037
2150
  ? "border-cyan-300"
2038
2151
  : "border-transparent"
2039
- } ${heatColor(cell.count)}`}
2152
+ } ${heatColor(cell.count, cell.laneId)}`}
2040
2153
  style={{ gridColumn: cell.x + 2, gridRow: cell.y + 2 }}
2041
2154
  title={
2042
2155
  interactive
@@ -2330,8 +2443,16 @@ function Arrow() {
2330
2443
  return <span className="text-slate-600">→</span>;
2331
2444
  }
2332
2445
 
2333
- function heatColor(count: number) {
2334
- // 3-state activity from sample.count: 0 idle / 1 standby / 2 in-progress.
2446
+ function heatColor(count: number, laneId?: string) {
2447
+ // 3-state activity from sample.count: 0 idle / 1 recent / 2 in-progress.
2448
+ // Render per-role so each lane's history shows in its own CXX colour instead of
2449
+ // collapsing into one dim emerald (which read as "only CEO is working"). The
2450
+ // base colour + "/30" variant mirror the patterns already used in LayerActivityPanel.
2451
+ const role = (laneId ?? "").split(":")[0] as CxxRole;
2452
+ if (role in CXX_COLOR) {
2453
+ if (count >= 2) return CXX_COLOR[role];
2454
+ if (count >= 1) return `${CXX_COLOR[role]}/30`;
2455
+ }
2335
2456
  if (count >= 2) return "bg-emerald-500/80";
2336
2457
  if (count >= 1) return "bg-emerald-500/30";
2337
2458
  return "bg-slate-900";
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ /* =============================================================
4
+ walwal-harness · BRICK OFFICE — App shell
5
+ Ported from design_handoff_harness_dashboard/Harness Dashboard.html
6
+ App() (lines ~395-437) + the view switch.
7
+
8
+ The mockup drove its state from a `useHarness(motion)` simulation;
9
+ here we drive it from the REAL SSE feed (useHarnessStream) mapped
10
+ through toContractState(snapshot, connected, nowMs). The shell ticks
11
+ a `nowMs` clock ~1/s so "ago"/heartbeat/stale read live.
12
+
13
+ The mockup set data-tone / data-density on <body>; in the Next app the
14
+ shell is a component, so those attributes (and the .brick scope class)
15
+ live on the shell root <div>.
16
+
17
+ brick.css is imported at app/layout level (see app/layout.tsx), matching
18
+ how Scene's CSS is loaded via globals.css.
19
+ ============================================================= */
20
+
21
+ import React, { useEffect, useState } from "react";
22
+
23
+ import type { HarnessSnapshot } from "../../lib/types";
24
+ import { useHarnessStream } from "../../hooks/useHarnessStream";
25
+ import { toContractState } from "../../lib/brick/adapter";
26
+ import type { AgentRole, DocTarget } from "../../lib/brick/contract";
27
+ import type { MetricsSample } from "../../lib/metrics/sampler";
28
+
29
+ import { Header, Sidebar, type BrickView } from "./ui";
30
+ import { GridView } from "./views-grid";
31
+ import { GraphView } from "./views-graph";
32
+ import { TimelineView } from "./views-timeline";
33
+ import { DocViewer } from "./reports";
34
+ import { BrickTweaks, useTweaks } from "./tweaks";
35
+
36
+ export interface BrickDashboardProps {
37
+ /** Server-rendered initial snapshot, hydrated then kept fresh over SSE. */
38
+ initialSnapshot: HarnessSnapshot;
39
+ }
40
+
41
+ export function BrickDashboard({
42
+ initialSnapshot,
43
+ }: BrickDashboardProps): React.JSX.Element {
44
+ const { snapshot, connectionState } = useHarnessStream(initialSnapshot);
45
+
46
+ // tweaks (agent layout / density / live motion / surface) — persisted.
47
+ const [t, setTweak] = useTweaks();
48
+
49
+ // top-level view + chrome state (mirrors the mockup App() useState block).
50
+ const [view, setView] = useState<BrickView>("grid");
51
+ const [collapsed, setCollapsed] = useState(false);
52
+ const [sel, setSel] = useState<AgentRole>("cto");
53
+ const [doc, setDoc] = useState<DocTarget | null>(null);
54
+
55
+ // ~1/s clock so "ago"/heartbeat/stale derive against a live `now`.
56
+ const [nowMs, setNowMs] = useState<number>(() => Date.now());
57
+ useEffect(() => {
58
+ const id = window.setInterval(() => setNowMs(Date.now()), 1000);
59
+ return () => window.clearInterval(id);
60
+ }, []);
61
+
62
+ // REAL telemetry poll (read-only): /api/metrics every ~5s. On any failure we
63
+ // keep the last good sample (or null), and the honest "—" placeholders stand.
64
+ const [metrics, setMetrics] = useState<MetricsSample | null>(null);
65
+ useEffect(() => {
66
+ let cancelled = false;
67
+ const poll = async (): Promise<void> => {
68
+ try {
69
+ const res = await fetch("/api/metrics", { cache: "no-store" });
70
+ if (!res.ok) return;
71
+ const sample = (await res.json()) as MetricsSample;
72
+ if (!cancelled && sample && typeof sample === "object") {
73
+ setMetrics(sample);
74
+ }
75
+ } catch {
76
+ /* network/parse error — keep last sample; never fabricate */
77
+ }
78
+ };
79
+ void poll();
80
+ const id = window.setInterval(() => void poll(), 5000);
81
+ return () => {
82
+ cancelled = true;
83
+ window.clearInterval(id);
84
+ };
85
+ }, []);
86
+
87
+ // build the single Contract state object every view renders from.
88
+ const connected = connectionState === "open";
89
+ const s = toContractState(snapshot, connected, nowMs);
90
+
91
+ // Override the adapter's honest nulls with REAL sampled telemetry where it
92
+ // exists. Anything still null stays null -> the views render "—".
93
+ if (metrics) {
94
+ s.metrics.tokensPerMin = metrics.tokensPerMin;
95
+ s.metrics.costToday = metrics.costToday;
96
+ s.metrics.cpu = metrics.cpu;
97
+ s.metrics.contextPct = metrics.contextPct;
98
+ s.metrics.costEstimated = metrics.costEstimated;
99
+ s.mtrend = metrics.mtrend ?? [];
100
+ s.ctrend = metrics.ctrend ?? [];
101
+ }
102
+
103
+ const openDoc = (target: DocTarget): void => setDoc(target);
104
+ const closeDoc = (): void => setDoc(null);
105
+
106
+ return (
107
+ <div className="brick" data-tone={t.surface} data-density={t.density}>
108
+ <div className="app">
109
+ <Header metrics={s.metrics} view={view} setView={setView} alerts={s.alerts} />
110
+ <div className="app-body">
111
+ <Sidebar
112
+ collapsed={collapsed}
113
+ onToggle={() => setCollapsed((c) => !c)}
114
+ missions={snapshot.missions}
115
+ ownerHistory={snapshot.ownerHistory}
116
+ />
117
+ <div className="app-main">
118
+ {view === "grid" && (
119
+ <div className="viewscroll">
120
+ <GridView s={s} layout={t.agentLayout} openDoc={openDoc} />
121
+ </div>
122
+ )}
123
+ {view === "graph" && (
124
+ <div className="viewscroll viewscroll-graph">
125
+ <GraphView s={s} sel={sel} setSel={setSel} openDoc={openDoc} />
126
+ </div>
127
+ )}
128
+ {view === "timeline" && (
129
+ <div className="viewscroll">
130
+ <TimelineView s={s} openDoc={openDoc} />
131
+ </div>
132
+ )}
133
+ </div>
134
+ </div>
135
+
136
+ <DocViewer
137
+ target={doc}
138
+ state={s}
139
+ onClose={closeDoc}
140
+ onRetarget={openDoc}
141
+ />
142
+
143
+ <BrickTweaks values={t} setTweak={setTweak} />
144
+ </div>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ export default BrickDashboard;
@@ -0,0 +1,291 @@
1
+ "use client";
2
+
3
+ /* =============================================================
4
+ walwal-harness · BRICK OFFICE — markdown renderer + DocViewer
5
+ Ported from design_handoff_harness_dashboard/harness/reports.jsx.
6
+
7
+ The mockup hard-coded its report bodies in a REPORTS table; here
8
+ buildReport returns REAL markdown supplied by the adapter
9
+ (agent.report / worker.report), with leading YAML frontmatter
10
+ stripped before rendering. Everything else (classNames, CSS,
11
+ markdown grammar, DocViewer layout) is preserved verbatim.
12
+
13
+ Exports: MD, DocViewer, buildReport
14
+ ============================================================= */
15
+
16
+ import React, { useEffect, type CSSProperties, type ReactNode } from "react";
17
+ import {
18
+ fmt,
19
+ statusColor,
20
+ type ContractState,
21
+ type ContractAgent,
22
+ type ContractWorker,
23
+ type DocTarget,
24
+ } from "../../lib/brick/contract";
25
+ import { stripFrontmatter } from "../../lib/brick/adapter";
26
+ import { StatusDot } from "./ui";
27
+
28
+ /* ---- tiny markdown renderer ------------------------------------------- */
29
+
30
+ /** Inline pass: splits on `code` and **bold**, returns React children. */
31
+ function inline(text: string, key: string | number): ReactNode[] {
32
+ const out: ReactNode[] = [];
33
+ let rest = text;
34
+ let i = 0;
35
+ const re = /(`[^`]+`|\*\*[^*]+\*\*)/;
36
+ while (rest.length) {
37
+ const m = rest.match(re);
38
+ if (!m || m.index === undefined) {
39
+ out.push(rest);
40
+ break;
41
+ }
42
+ if (m.index > 0) out.push(rest.slice(0, m.index));
43
+ const tok = m[0];
44
+ if (tok.startsWith("`")) {
45
+ out.push(<code key={key + "-" + i}>{tok.slice(1, -1)}</code>);
46
+ } else {
47
+ out.push(<b key={key + "-" + i}>{tok.slice(2, -2)}</b>);
48
+ }
49
+ rest = rest.slice(m.index + tok.length);
50
+ i++;
51
+ }
52
+ return out;
53
+ }
54
+
55
+ interface ListProps {
56
+ ordered: boolean;
57
+ items: string[];
58
+ }
59
+
60
+ function List({ ordered, items }: ListProps): React.JSX.Element {
61
+ const Tag = ordered ? "ol" : "ul";
62
+ return (
63
+ <Tag>
64
+ {items.map((it, i) => (
65
+ <li key={i}>{inline(it, "li" + i)}</li>
66
+ ))}
67
+ </Tag>
68
+ );
69
+ }
70
+
71
+ export interface MDProps {
72
+ src: string;
73
+ }
74
+
75
+ /**
76
+ * Block-level markdown renderer. Supports #/##/###, -/1. lists, `code`,
77
+ * **bold**, > blockquote and --- rules (same grammar as the mockup).
78
+ */
79
+ export function MD({ src }: MDProps): React.JSX.Element {
80
+ const lines = src.split("\n");
81
+ const blocks: ReactNode[] = [];
82
+ // accumulate list items so consecutive bullets/numbers merge into one list
83
+ let listOrdered: boolean | null = null;
84
+ let listItems: string[] = [];
85
+ let listKey = 0;
86
+ const flush = () => {
87
+ if (listOrdered !== null) {
88
+ blocks.push(
89
+ <List key={"list-" + listKey} ordered={listOrdered} items={listItems} />
90
+ );
91
+ listOrdered = null;
92
+ listItems = [];
93
+ }
94
+ };
95
+
96
+ lines.forEach((ln, idx) => {
97
+ const t = ln.trimEnd();
98
+ if (!t.trim()) {
99
+ flush();
100
+ return;
101
+ }
102
+ if (t.startsWith("### ")) {
103
+ flush();
104
+ blocks.push(<h4 key={idx}>{inline(t.slice(4), idx)}</h4>);
105
+ return;
106
+ }
107
+ if (t.startsWith("## ")) {
108
+ flush();
109
+ blocks.push(<h3 key={idx}>{inline(t.slice(3), idx)}</h3>);
110
+ return;
111
+ }
112
+ if (t.startsWith("# ")) {
113
+ flush();
114
+ blocks.push(<h2 key={idx}>{inline(t.slice(2), idx)}</h2>);
115
+ return;
116
+ }
117
+ if (t === "---") {
118
+ flush();
119
+ blocks.push(<hr key={idx} />);
120
+ return;
121
+ }
122
+ if (t.startsWith("> ")) {
123
+ flush();
124
+ blocks.push(<blockquote key={idx}>{inline(t.slice(2), idx)}</blockquote>);
125
+ return;
126
+ }
127
+ const oli = t.match(/^(\d+)\.\s+(.*)/);
128
+ const uli = t.match(/^[-*]\s+(.*)/);
129
+ if (oli || uli) {
130
+ const ordered = !!oli;
131
+ if (listOrdered === null || listOrdered !== ordered) {
132
+ flush();
133
+ listOrdered = ordered;
134
+ listKey = idx;
135
+ }
136
+ listItems.push(oli ? oli[2] : uli![1]);
137
+ return;
138
+ }
139
+ flush();
140
+ blocks.push(<p key={idx}>{inline(t, idx)}</p>);
141
+ });
142
+ flush();
143
+ return <div className="md">{blocks}</div>;
144
+ }
145
+
146
+ /* ---- report assembly (REAL markdown, not the mockup table) ------------ */
147
+
148
+ export interface BuiltReport {
149
+ /** Title shown in the meta block (agent work string / worker name). */
150
+ title: string;
151
+ /** The owning agent of this document. */
152
+ owner: ContractAgent;
153
+ /** Short document-kind label. */
154
+ kind: string;
155
+ /** Status string for the StatusDot + color. */
156
+ status: string;
157
+ /** Markdown body, frontmatter already stripped. */
158
+ body: string;
159
+ }
160
+
161
+ const NO_SOURCE = "_no report source — this agent has not produced a document yet._";
162
+ const NO_WORKER_SOURCE = "_no report source — this worker has not produced a brief yet._";
163
+
164
+ /**
165
+ * Build the document to render for a DocTarget, pulling REAL markdown from
166
+ * the adapter-provided report strings. Agent reports are stripped of leading
167
+ * YAML frontmatter at render time (per spec); worker reports are already
168
+ * stripped by the adapter but stripping again is harmless and keeps the
169
+ * render path uniform.
170
+ */
171
+ export function buildReport(target: DocTarget, state: ContractState): BuiltReport {
172
+ const agent =
173
+ state.agents.find((a) => a.id === target.agent) ?? state.agents[0];
174
+
175
+ if (target.type === "worker") {
176
+ const worker = target.worker;
177
+ const body = stripFrontmatter(worker.report) || NO_WORKER_SOURCE;
178
+ return {
179
+ title: worker.name,
180
+ owner: agent,
181
+ kind: "Worker Brief",
182
+ status: worker.status,
183
+ body,
184
+ };
185
+ }
186
+
187
+ const body = stripFrontmatter(agent.report) || NO_SOURCE;
188
+ return {
189
+ title: agent.work,
190
+ owner: agent,
191
+ kind: "CXX Document",
192
+ status: agent.status,
193
+ body,
194
+ };
195
+ }
196
+
197
+ /* ---- DocViewer drawer -------------------------------------------------- */
198
+
199
+ export interface DocViewerProps {
200
+ /** The document to show, or null to render nothing. */
201
+ target: DocTarget | null;
202
+ /** The live contract state. */
203
+ state: ContractState;
204
+ /** Close the drawer. */
205
+ onClose: () => void;
206
+ /** Re-point the drawer at another document (e.g. a spawned worker). */
207
+ onRetarget: (target: DocTarget) => void;
208
+ }
209
+
210
+ /** Right-hand document drawer with the agent meta block + markdown body. */
211
+ export function DocViewer({
212
+ target,
213
+ state,
214
+ onClose,
215
+ onRetarget,
216
+ }: DocViewerProps): React.JSX.Element | null {
217
+ useEffect(() => {
218
+ const h = (e: KeyboardEvent) => {
219
+ if (e.key === "Escape") onClose();
220
+ };
221
+ window.addEventListener("keydown", h);
222
+ return () => window.removeEventListener("keydown", h);
223
+ }, [onClose]);
224
+
225
+ if (!target) return null;
226
+
227
+ const rep = buildReport(target, state);
228
+ const a = rep.owner;
229
+ const mine = state.workers.filter((w) => w.agent === a.id);
230
+ const hueVar = { "--hue": a.hue } as CSSProperties;
231
+
232
+ return (
233
+ <div className="docroot">
234
+ <div className="docback" onClick={onClose} />
235
+ <aside className="docview" style={hueVar}>
236
+ <div className="docview-bar">
237
+ <span className="docview-crumb">
238
+ DOCUMENT VIEWER <span className="dv-sep">·</span>
239
+ <span style={{ color: a.hue }}>{a.name}</span>
240
+ {target.type === "worker" && (
241
+ <>
242
+ <span className="dv-sep">·</span> {target.worker.name}
243
+ </>
244
+ )}
245
+ </span>
246
+ <button className="docview-x" onClick={onClose}>
247
+ esc ✕
248
+ </button>
249
+ </div>
250
+ <div className="docview-scroll">
251
+ <div className="docview-meta">
252
+ <span
253
+ className="glyph glyph-lg"
254
+ style={{ color: a.hue, borderColor: a.hue + "66" }}
255
+ >
256
+ {a.name}
257
+ </span>
258
+ <div className="dvm-id">
259
+ <div className="dvm-kind">{rep.kind}</div>
260
+ <div className="dvm-status" style={{ color: statusColor(rep.status) }}>
261
+ <StatusDot status={rep.status} size={7} />
262
+ {rep.status}
263
+ </div>
264
+ </div>
265
+ <div className="dvm-time">{fmt.time(new Date(state.now))}</div>
266
+ </div>
267
+
268
+ {target.type === "agent" && mine.length > 0 && (
269
+ <div className="dvm-workers">
270
+ <span className="dvm-wlabel">spawned workers</span>
271
+ {mine.map((w: ContractWorker) => (
272
+ <button
273
+ key={w.id}
274
+ className="dvm-wchip"
275
+ onClick={() =>
276
+ onRetarget({ type: "worker", agent: a.id, worker: w })
277
+ }
278
+ >
279
+ <StatusDot status={w.status} size={5} pulse={false} />
280
+ {w.name} <em>{w.progress == null ? "—" : Math.round(w.progress) + "%"}</em>
281
+ </button>
282
+ ))}
283
+ </div>
284
+ )}
285
+
286
+ <MD src={rep.body} />
287
+ </div>
288
+ </aside>
289
+ </div>
290
+ );
291
+ }