@walwal-harness/cli 7.1.40 → 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.
- package/HR-Resource/cdo/SKILL.md +9 -1
- package/HR-Resource/ceo/SKILL.md +33 -2
- package/HR-Resource/coo/SKILL.md +9 -1
- package/HR-Resource/cqo/SKILL.md +9 -1
- package/HR-Resource/cto/SKILL.md +10 -2
- package/HR-Resource/ops/SKILL.md +12 -4
- package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
- package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
- package/apps/harness-dashboard/app/api/metrics/route.ts +17 -0
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/app/brick.css +482 -0
- package/apps/harness-dashboard/app/layout.tsx +13 -0
- package/apps/harness-dashboard/app/page.tsx +2 -2
- package/apps/harness-dashboard/components/Scene.tsx +690 -36
- package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +149 -0
- package/apps/harness-dashboard/components/brick/reports.tsx +291 -0
- package/apps/harness-dashboard/components/brick/tweaks.tsx +738 -0
- package/apps/harness-dashboard/components/brick/ui.tsx +463 -0
- package/apps/harness-dashboard/components/brick/views-graph.tsx +305 -0
- package/apps/harness-dashboard/components/brick/views-grid.tsx +543 -0
- package/apps/harness-dashboard/components/brick/views-timeline.tsx +238 -0
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
- package/apps/harness-dashboard/lib/brick/adapter.ts +423 -0
- package/apps/harness-dashboard/lib/brick/contract.ts +247 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/metrics/sampler.ts +545 -0
- package/apps/harness-dashboard/lib/types.ts +22 -222
- package/apps/harness-dashboard/playwright.config.ts +26 -7
- package/assets/templates/AGENTS-ko.md.template +1 -1
- package/assets/templates/AGENTS.md.template +12 -2
- package/assets/templates/HARNESS.md +2 -0
- package/assets/templates/company-pipeline-manifest.json +5 -14
- package/assets/templates/worker-report.md.template +38 -0
- package/bin/init.js +2 -1
- package/commands/goal.md +4 -2
- package/commands/hot-fix.md +1 -1
- package/commands/submission.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +12 -0
- package/scripts/harness-agenda.sh +105 -0
- package/scripts/harness-company-block.sh +89 -0
- package/scripts/harness-company-complete.sh +17 -8
- package/scripts/harness-company-cycle.sh +54 -0
- package/scripts/harness-progress-set.sh +8 -2
- package/scripts/harness-stop.sh +76 -0
- package/scripts/harness-wake-control.sh +151 -0
- package/scripts/harness-wake.sh +5 -0
- package/scripts/harness-worker-dispatch.sh +31 -7
- package/apps/harness-dashboard/components/Drawer.tsx +0 -146
- package/apps/harness-dashboard/components/Header.tsx +0 -40
- package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
- package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
- package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
- package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
- package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
- package/apps/harness-dashboard/lib/furniture.ts +0 -165
- package/apps/harness-dashboard/lib/i18n.ts +0 -26
- package/apps/harness-dashboard/lib/iso.ts +0 -150
- package/apps/harness-dashboard/lib/markdown.tsx +0 -306
- package/apps/harness-dashboard/lib/path-planning.ts +0 -72
- package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
|
@@ -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
|
+
}
|