@agenit/cli 2.1.0 → 2.1.2
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/CHANGELOG.md +149 -0
- package/cli.js +63177 -40802
- package/config/flow.toml +64 -3
- package/config/recipes.toml +60 -0
- package/control/api.mjs +32 -3
- package/control/app.mjs +5 -0
- package/control/panels/Debug.mjs +681 -0
- package/control/shell/LeftNav.mjs +1 -0
- package/control/style.css +138 -0
- package/package.json +1 -1
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
// Debug Mode panel — LangSmith-style trace tree.
|
|
2
|
+
//
|
|
3
|
+
// Three columns: trace list (left), tree of selected trace (middle),
|
|
4
|
+
// run detail (right). All reads come from /api/debug/state on mount
|
|
5
|
+
// + per-trace `/api/debug/traces/<id>` on selection. Pending gates
|
|
6
|
+
// surface in a banner at the top with Release/Skip/Abort buttons.
|
|
7
|
+
//
|
|
8
|
+
// When debug mode is OFF the panel shows a single "Turn on" call-to-
|
|
9
|
+
// action that POSTs `/api/debug/config` `{enabled: true}`.
|
|
10
|
+
import { h } from "preact";
|
|
11
|
+
import { useEffect, useMemo, useState } from "preact/hooks";
|
|
12
|
+
import htm from "htm";
|
|
13
|
+
|
|
14
|
+
const html = htm.bind(h);
|
|
15
|
+
|
|
16
|
+
const POLL_MS = 1500;
|
|
17
|
+
|
|
18
|
+
function tokenFromUrl() {
|
|
19
|
+
try {
|
|
20
|
+
const u = new URL(window.location.href);
|
|
21
|
+
return u.searchParams.get("token");
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function authHeaders() {
|
|
28
|
+
const t = tokenFromUrl();
|
|
29
|
+
return t ? { "x-control-token": t } : {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getJson(path) {
|
|
33
|
+
const res = await fetch(path, {
|
|
34
|
+
headers: { Accept: "application/json", ...authHeaders() },
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const body = await res.text().catch(() => "");
|
|
38
|
+
throw new Error(`${res.status} ${res.statusText} ${body}`);
|
|
39
|
+
}
|
|
40
|
+
return await res.json();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function postJson(path, body) {
|
|
44
|
+
const res = await fetch(path, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
Accept: "application/json",
|
|
49
|
+
...authHeaders(),
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify(body ?? {}),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const text = await res.text().catch(() => "");
|
|
55
|
+
throw new Error(`${res.status} ${res.statusText} ${text}`);
|
|
56
|
+
}
|
|
57
|
+
return await res.json();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function statusGlyph(status) {
|
|
61
|
+
switch (status) {
|
|
62
|
+
case "ok":
|
|
63
|
+
return "✓";
|
|
64
|
+
case "error":
|
|
65
|
+
return "⨯";
|
|
66
|
+
case "running":
|
|
67
|
+
return "●";
|
|
68
|
+
case "aborted":
|
|
69
|
+
return "⏹";
|
|
70
|
+
case "skipped":
|
|
71
|
+
return "⊝";
|
|
72
|
+
default:
|
|
73
|
+
return "○";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function statusClass(status) {
|
|
78
|
+
return `debug-status debug-status-${status || "unknown"}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatDuration(ms) {
|
|
82
|
+
if (ms === null || ms === undefined) return "—";
|
|
83
|
+
if (ms < 1000) return `${ms}ms`;
|
|
84
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function flattenTree(runs) {
|
|
88
|
+
const byParent = new Map();
|
|
89
|
+
for (const r of runs) {
|
|
90
|
+
const arr = byParent.get(r.parentRunId);
|
|
91
|
+
if (arr) arr.push(r);
|
|
92
|
+
else byParent.set(r.parentRunId, [r]);
|
|
93
|
+
}
|
|
94
|
+
const out = [];
|
|
95
|
+
const walk = (parentId, depth) => {
|
|
96
|
+
const kids = byParent.get(parentId) ?? [];
|
|
97
|
+
for (const r of kids) {
|
|
98
|
+
out.push({ run: r, depth });
|
|
99
|
+
walk(r.id, depth + 1);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
walk(null, 0);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function OffState({ onEnable, busy }) {
|
|
107
|
+
return html`
|
|
108
|
+
<div class="panel">
|
|
109
|
+
<h2 class="panel-title">Debug</h2>
|
|
110
|
+
<p class="placeholder">
|
|
111
|
+
Debug mode is <strong>OFF</strong>. Turn it on to start capturing
|
|
112
|
+
a tree of every chat turn, LLM call, tool dispatch, and agent
|
|
113
|
+
run. You can also flip it on from the REPL with
|
|
114
|
+
<code>/debug on</code> or <kbd>Ctrl+D</kbd>.
|
|
115
|
+
</p>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
class="primary-button"
|
|
119
|
+
onClick=${onEnable}
|
|
120
|
+
disabled=${busy}
|
|
121
|
+
>
|
|
122
|
+
${busy ? "enabling…" : "Turn on debug mode"}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function PendingGatesBanner({ gates, onRelease, onSkip, onAbort }) {
|
|
129
|
+
if (gates.length === 0) return null;
|
|
130
|
+
return html`
|
|
131
|
+
<div class="debug-gates-banner" role="alert">
|
|
132
|
+
<div class="debug-gates-title">
|
|
133
|
+
${gates.length} pending gate${gates.length === 1 ? "" : "s"} —
|
|
134
|
+
awaiting your decision
|
|
135
|
+
</div>
|
|
136
|
+
<ul class="debug-gates-list">
|
|
137
|
+
${gates.map(
|
|
138
|
+
(g) => html`
|
|
139
|
+
<li key=${g.runId}>
|
|
140
|
+
<div class="debug-gate-row">
|
|
141
|
+
<span class="debug-gate-kind">${g.kind}</span>
|
|
142
|
+
<span class="debug-gate-desc">${g.description}</span>
|
|
143
|
+
<div class="debug-gate-actions">
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick=${() => onRelease(g.runId)}
|
|
147
|
+
>
|
|
148
|
+
release
|
|
149
|
+
</button>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick=${() => onSkip(g.runId)}
|
|
153
|
+
>
|
|
154
|
+
skip
|
|
155
|
+
</button>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
class="danger"
|
|
159
|
+
onClick=${() => onAbort(g.runId)}
|
|
160
|
+
>
|
|
161
|
+
abort
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</li>
|
|
166
|
+
`,
|
|
167
|
+
)}
|
|
168
|
+
</ul>
|
|
169
|
+
</div>
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function TraceList({ traces, selectedTraceId, onSelect }) {
|
|
174
|
+
if (traces.length === 0) {
|
|
175
|
+
return html`<div class="debug-empty">(no traces yet)</div>`;
|
|
176
|
+
}
|
|
177
|
+
return html`
|
|
178
|
+
<ul class="debug-trace-list">
|
|
179
|
+
${traces.map((t) => {
|
|
180
|
+
const active = t.traceId === selectedTraceId;
|
|
181
|
+
return html`
|
|
182
|
+
<li key=${t.traceId}>
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
class=${"debug-trace-row " + (active ? "active" : "")}
|
|
186
|
+
onClick=${() => onSelect(t.traceId)}
|
|
187
|
+
>
|
|
188
|
+
<span class=${statusClass(t.status)}>${statusGlyph(t.status)}</span>
|
|
189
|
+
<span class="debug-trace-name">${t.name}</span>
|
|
190
|
+
<span class="debug-trace-meta">
|
|
191
|
+
${formatDuration(t.durationMs)}
|
|
192
|
+
${t.totalTokens ? html` · ${t.totalTokens} tok` : null}
|
|
193
|
+
${t.totalCostUsd
|
|
194
|
+
? html` · $${t.totalCostUsd.toFixed(4)}`
|
|
195
|
+
: null}
|
|
196
|
+
</span>
|
|
197
|
+
</button>
|
|
198
|
+
</li>
|
|
199
|
+
`;
|
|
200
|
+
})}
|
|
201
|
+
</ul>
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function TraceTree({ runs, selectedRunId, onSelect }) {
|
|
206
|
+
const flat = useMemo(() => flattenTree(runs), [runs]);
|
|
207
|
+
if (flat.length === 0) {
|
|
208
|
+
return html`<div class="debug-empty">(empty trace)</div>`;
|
|
209
|
+
}
|
|
210
|
+
return html`
|
|
211
|
+
<ul class="debug-tree">
|
|
212
|
+
${flat.map(({ run, depth }) => {
|
|
213
|
+
const active = run.id === selectedRunId;
|
|
214
|
+
return html`
|
|
215
|
+
<li key=${run.id} style=${`padding-left: ${depth * 16}px`}>
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
class=${"debug-tree-row " + (active ? "active" : "")}
|
|
219
|
+
onClick=${() => onSelect(run.id)}
|
|
220
|
+
>
|
|
221
|
+
<span class=${statusClass(run.status)}>${statusGlyph(run.status)}</span>
|
|
222
|
+
<span class="debug-kind">[${run.kind}]</span>
|
|
223
|
+
<span class="debug-name">${run.name}</span>
|
|
224
|
+
<span class="debug-meta">
|
|
225
|
+
${formatDuration(run.durationMs)}
|
|
226
|
+
${run.tokens?.total ? html` · ${run.tokens.total}tk` : null}
|
|
227
|
+
${run.costUsd ? html` · $${run.costUsd.toFixed(4)}` : null}
|
|
228
|
+
</span>
|
|
229
|
+
</button>
|
|
230
|
+
</li>
|
|
231
|
+
`;
|
|
232
|
+
})}
|
|
233
|
+
</ul>
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function RunDetail({ run, onAddTag, onReplay }) {
|
|
238
|
+
const [tagInput, setTagInput] = useState("");
|
|
239
|
+
const [replayArgs, setReplayArgs] = useState("");
|
|
240
|
+
const [replayBusy, setReplayBusy] = useState(false);
|
|
241
|
+
const [replayResult, setReplayResult] = useState(null);
|
|
242
|
+
if (!run) {
|
|
243
|
+
return html`<div class="debug-empty">select a run to see its inputs / outputs</div>`;
|
|
244
|
+
}
|
|
245
|
+
const submitTag = (ev) => {
|
|
246
|
+
ev.preventDefault();
|
|
247
|
+
const v = tagInput.trim();
|
|
248
|
+
if (!v) return;
|
|
249
|
+
onAddTag(run.id, v);
|
|
250
|
+
setTagInput("");
|
|
251
|
+
};
|
|
252
|
+
const isTool = run.kind === "tool";
|
|
253
|
+
const originalArgs = (run.inputs && run.inputs.args) || {};
|
|
254
|
+
const initialReplayBody = useMemo(
|
|
255
|
+
() => (isTool ? JSON.stringify(originalArgs, null, 2) : ""),
|
|
256
|
+
[isTool, JSON.stringify(originalArgs)],
|
|
257
|
+
);
|
|
258
|
+
// Initialise replayArgs from initialReplayBody when the selected run
|
|
259
|
+
// changes — preact useMemo above gives us a stable string per run.
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
setReplayArgs(initialReplayBody);
|
|
262
|
+
setReplayResult(null);
|
|
263
|
+
}, [run.id]);
|
|
264
|
+
|
|
265
|
+
const doReplay = async () => {
|
|
266
|
+
let parsed = null;
|
|
267
|
+
try {
|
|
268
|
+
parsed = JSON.parse(replayArgs || "{}");
|
|
269
|
+
} catch (e) {
|
|
270
|
+
setReplayResult({ ok: false, output: `invalid JSON: ${e.message}` });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
setReplayBusy(true);
|
|
274
|
+
try {
|
|
275
|
+
const res = await onReplay(run.id, parsed);
|
|
276
|
+
setReplayResult({
|
|
277
|
+
ok: !res.isError,
|
|
278
|
+
output: res.output,
|
|
279
|
+
runId: res.runId,
|
|
280
|
+
});
|
|
281
|
+
} catch (e) {
|
|
282
|
+
setReplayResult({ ok: false, output: e.message });
|
|
283
|
+
} finally {
|
|
284
|
+
setReplayBusy(false);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
return html`
|
|
288
|
+
<div class="debug-detail">
|
|
289
|
+
<div class="debug-detail-header">
|
|
290
|
+
<span class=${statusClass(run.status)}>${statusGlyph(run.status)}</span>
|
|
291
|
+
<span class="debug-kind">[${run.kind}]</span>
|
|
292
|
+
<span class="debug-name">${run.name}</span>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="debug-detail-meta">
|
|
295
|
+
<span>id: ${run.id.slice(0, 8)}</span>
|
|
296
|
+
<span>${formatDuration(run.durationMs)}</span>
|
|
297
|
+
${run.tokens?.total
|
|
298
|
+
? html`<span>${run.tokens.total} tok</span>`
|
|
299
|
+
: null}
|
|
300
|
+
${run.costUsd
|
|
301
|
+
? html`<span>$${run.costUsd.toFixed(4)}</span>`
|
|
302
|
+
: null}
|
|
303
|
+
${run.model ? html`<span>model: ${run.model}</span>` : null}
|
|
304
|
+
${run.tier ? html`<span>tier: ${run.tier}</span>` : null}
|
|
305
|
+
</div>
|
|
306
|
+
${run.error
|
|
307
|
+
? html`<div class="debug-error">error: ${run.error}</div>`
|
|
308
|
+
: null}
|
|
309
|
+
|
|
310
|
+
<details open class="debug-section">
|
|
311
|
+
<summary>INPUTS</summary>
|
|
312
|
+
<pre class="debug-json">${JSON.stringify(run.inputs, null, 2)}</pre>
|
|
313
|
+
</details>
|
|
314
|
+
|
|
315
|
+
<details open class="debug-section">
|
|
316
|
+
<summary>OUTPUTS</summary>
|
|
317
|
+
<pre class="debug-json">${JSON.stringify(run.outputs, null, 2)}</pre>
|
|
318
|
+
</details>
|
|
319
|
+
|
|
320
|
+
${run.events && run.events.length > 0
|
|
321
|
+
? html`
|
|
322
|
+
<details class="debug-section">
|
|
323
|
+
<summary>EVENTS (${run.events.length})</summary>
|
|
324
|
+
<pre class="debug-json">
|
|
325
|
+
${run.events.map((e) => JSON.stringify(e)).join("\n")}</pre>
|
|
326
|
+
</details>
|
|
327
|
+
`
|
|
328
|
+
: null}
|
|
329
|
+
|
|
330
|
+
<div class="debug-tags-row">
|
|
331
|
+
<span class="debug-tags-label">tags:</span>
|
|
332
|
+
${run.tags && run.tags.length > 0
|
|
333
|
+
? run.tags.map(
|
|
334
|
+
(t) => html`<span key=${t} class="debug-tag">${t}</span>`,
|
|
335
|
+
)
|
|
336
|
+
: html`<span class="debug-empty-inline">(none)</span>`}
|
|
337
|
+
<form onSubmit=${submitTag} class="debug-tag-form">
|
|
338
|
+
<input
|
|
339
|
+
type="text"
|
|
340
|
+
value=${tagInput}
|
|
341
|
+
placeholder="add tag…"
|
|
342
|
+
onInput=${(e) => setTagInput(e.currentTarget.value)}
|
|
343
|
+
/>
|
|
344
|
+
<button type="submit">add</button>
|
|
345
|
+
</form>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
${isTool
|
|
349
|
+
? html`
|
|
350
|
+
<details class="debug-section debug-replay">
|
|
351
|
+
<summary>REPLAY — re-fire this tool with the same or edited args</summary>
|
|
352
|
+
<div class="debug-replay-body">
|
|
353
|
+
<textarea
|
|
354
|
+
class="debug-replay-input"
|
|
355
|
+
rows="6"
|
|
356
|
+
value=${replayArgs}
|
|
357
|
+
onInput=${(e) => setReplayArgs(e.currentTarget.value)}
|
|
358
|
+
spellcheck="false"
|
|
359
|
+
/>
|
|
360
|
+
<div class="debug-replay-actions">
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
class="primary-button"
|
|
364
|
+
onClick=${doReplay}
|
|
365
|
+
disabled=${replayBusy}
|
|
366
|
+
>
|
|
367
|
+
${replayBusy ? "running…" : "Try call again"}
|
|
368
|
+
</button>
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
onClick=${() => setReplayArgs(initialReplayBody)}
|
|
372
|
+
disabled=${replayBusy}
|
|
373
|
+
>
|
|
374
|
+
reset
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
${replayResult
|
|
378
|
+
? html`
|
|
379
|
+
<div class=${"debug-replay-result " + (replayResult.ok ? "ok" : "error")}>
|
|
380
|
+
${replayResult.runId
|
|
381
|
+
? html`<div>new run: <code>${replayResult.runId.slice(0, 8)}</code></div>`
|
|
382
|
+
: null}
|
|
383
|
+
<pre>${replayResult.output}</pre>
|
|
384
|
+
</div>
|
|
385
|
+
`
|
|
386
|
+
: null}
|
|
387
|
+
</div>
|
|
388
|
+
</details>
|
|
389
|
+
`
|
|
390
|
+
: null}
|
|
391
|
+
</div>
|
|
392
|
+
`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function ConfigDrawer({ mode, onToggle, onSetKind, onSetFlag }) {
|
|
396
|
+
if (!mode) return null;
|
|
397
|
+
const pause = new Set(mode.pauseBeforeKinds);
|
|
398
|
+
return html`
|
|
399
|
+
<details class="debug-config">
|
|
400
|
+
<summary>Config</summary>
|
|
401
|
+
<div class="debug-config-body">
|
|
402
|
+
<label>
|
|
403
|
+
<input
|
|
404
|
+
type="checkbox"
|
|
405
|
+
checked=${mode.enabled}
|
|
406
|
+
onChange=${(e) => onToggle(e.currentTarget.checked)}
|
|
407
|
+
/>
|
|
408
|
+
enabled
|
|
409
|
+
</label>
|
|
410
|
+
<label>
|
|
411
|
+
<input
|
|
412
|
+
type="checkbox"
|
|
413
|
+
checked=${mode.persistDeltas}
|
|
414
|
+
onChange=${(e) =>
|
|
415
|
+
onSetFlag("persistDeltas", e.currentTarget.checked)}
|
|
416
|
+
/>
|
|
417
|
+
persistDeltas
|
|
418
|
+
</label>
|
|
419
|
+
<label>
|
|
420
|
+
<input
|
|
421
|
+
type="checkbox"
|
|
422
|
+
checked=${mode.persistThinking}
|
|
423
|
+
onChange=${(e) =>
|
|
424
|
+
onSetFlag("persistThinking", e.currentTarget.checked)}
|
|
425
|
+
/>
|
|
426
|
+
persistThinking
|
|
427
|
+
</label>
|
|
428
|
+
<label>
|
|
429
|
+
<input
|
|
430
|
+
type="checkbox"
|
|
431
|
+
checked=${mode.noRedaction}
|
|
432
|
+
onChange=${(e) =>
|
|
433
|
+
onSetFlag("noRedaction", e.currentTarget.checked)}
|
|
434
|
+
/>
|
|
435
|
+
noRedaction (⚠ secrets persisted verbatim)
|
|
436
|
+
</label>
|
|
437
|
+
<div class="debug-config-kinds">
|
|
438
|
+
<span>pause-before:</span>
|
|
439
|
+
${["llm", "tool", "agent"].map(
|
|
440
|
+
(k) => html`
|
|
441
|
+
<label key=${k}>
|
|
442
|
+
<input
|
|
443
|
+
type="checkbox"
|
|
444
|
+
checked=${pause.has(k)}
|
|
445
|
+
onChange=${(e) => onSetKind(k, e.currentTarget.checked)}
|
|
446
|
+
/>
|
|
447
|
+
${k}
|
|
448
|
+
</label>
|
|
449
|
+
`,
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
</details>
|
|
454
|
+
`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function Debug() {
|
|
458
|
+
const [mode, setMode] = useState(null);
|
|
459
|
+
const [traces, setTraces] = useState([]);
|
|
460
|
+
const [gates, setGates] = useState([]);
|
|
461
|
+
const [selectedTraceId, setSelectedTraceId] = useState(null);
|
|
462
|
+
const [traceRuns, setTraceRuns] = useState([]);
|
|
463
|
+
const [selectedRunId, setSelectedRunId] = useState(null);
|
|
464
|
+
const [selectedRun, setSelectedRun] = useState(null);
|
|
465
|
+
const [busy, setBusy] = useState(false);
|
|
466
|
+
const [error, setError] = useState(null);
|
|
467
|
+
|
|
468
|
+
const refreshState = async () => {
|
|
469
|
+
try {
|
|
470
|
+
const data = await getJson("/api/debug/state");
|
|
471
|
+
setMode(data.mode);
|
|
472
|
+
setTraces(data.recentTraces || []);
|
|
473
|
+
setGates(data.pendingGates || []);
|
|
474
|
+
setError(null);
|
|
475
|
+
if (
|
|
476
|
+
data.recentTraces.length > 0 &&
|
|
477
|
+
!data.recentTraces.find((t) => t.traceId === selectedTraceId)
|
|
478
|
+
) {
|
|
479
|
+
setSelectedTraceId(data.recentTraces[0].traceId);
|
|
480
|
+
}
|
|
481
|
+
} catch (e) {
|
|
482
|
+
setError(e.message);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
useEffect(() => {
|
|
487
|
+
refreshState();
|
|
488
|
+
const id = setInterval(refreshState, POLL_MS);
|
|
489
|
+
return () => clearInterval(id);
|
|
490
|
+
}, []);
|
|
491
|
+
|
|
492
|
+
// Load the selected trace's tree.
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
if (!selectedTraceId) {
|
|
495
|
+
setTraceRuns([]);
|
|
496
|
+
setSelectedRunId(null);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
getJson(`/api/debug/traces/${encodeURIComponent(selectedTraceId)}`)
|
|
500
|
+
.then((d) => {
|
|
501
|
+
setTraceRuns(d.runs || []);
|
|
502
|
+
if (d.runs.length > 0) setSelectedRunId(d.runs[0].id);
|
|
503
|
+
else setSelectedRunId(null);
|
|
504
|
+
})
|
|
505
|
+
.catch((e) => setError(e.message));
|
|
506
|
+
}, [selectedTraceId]);
|
|
507
|
+
|
|
508
|
+
// Load full run detail when selection changes.
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
if (!selectedRunId) {
|
|
511
|
+
setSelectedRun(null);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
getJson(`/api/debug/runs/${encodeURIComponent(selectedRunId)}`)
|
|
515
|
+
.then((d) => setSelectedRun(d.run))
|
|
516
|
+
.catch((e) => setError(e.message));
|
|
517
|
+
}, [selectedRunId]);
|
|
518
|
+
|
|
519
|
+
const turnOn = async () => {
|
|
520
|
+
setBusy(true);
|
|
521
|
+
try {
|
|
522
|
+
await postJson("/api/debug/config", { enabled: true });
|
|
523
|
+
await refreshState();
|
|
524
|
+
} catch (e) {
|
|
525
|
+
setError(e.message);
|
|
526
|
+
} finally {
|
|
527
|
+
setBusy(false);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const onToggleEnabled = async (on) => {
|
|
532
|
+
try {
|
|
533
|
+
await postJson("/api/debug/config", { enabled: on });
|
|
534
|
+
await refreshState();
|
|
535
|
+
} catch (e) {
|
|
536
|
+
setError(e.message);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const onSetFlag = async (field, value) => {
|
|
541
|
+
try {
|
|
542
|
+
await postJson("/api/debug/config", { [field]: value });
|
|
543
|
+
await refreshState();
|
|
544
|
+
} catch (e) {
|
|
545
|
+
setError(e.message);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const onSetKind = async (kind, on) => {
|
|
550
|
+
const cur = new Set(mode?.pauseBeforeKinds ?? []);
|
|
551
|
+
if (on) cur.add(kind);
|
|
552
|
+
else cur.delete(kind);
|
|
553
|
+
try {
|
|
554
|
+
await postJson("/api/debug/config", {
|
|
555
|
+
pauseBeforeKinds: Array.from(cur),
|
|
556
|
+
});
|
|
557
|
+
await refreshState();
|
|
558
|
+
} catch (e) {
|
|
559
|
+
setError(e.message);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const onGateAction = async (action, runId) => {
|
|
564
|
+
try {
|
|
565
|
+
await postJson(`/api/debug/gate/${encodeURIComponent(runId)}`, {
|
|
566
|
+
action,
|
|
567
|
+
});
|
|
568
|
+
await refreshState();
|
|
569
|
+
} catch (e) {
|
|
570
|
+
setError(e.message);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const onReplay = async (runId, argsOverride) => {
|
|
575
|
+
const res = await postJson(`/api/debug/replay/${encodeURIComponent(runId)}`, {
|
|
576
|
+
args: argsOverride,
|
|
577
|
+
});
|
|
578
|
+
// Refresh trace tree so the new sibling run appears.
|
|
579
|
+
if (selectedTraceId) {
|
|
580
|
+
const d = await getJson(
|
|
581
|
+
`/api/debug/traces/${encodeURIComponent(selectedTraceId)}`,
|
|
582
|
+
);
|
|
583
|
+
setTraceRuns(d.runs || []);
|
|
584
|
+
}
|
|
585
|
+
return res;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const onAddTag = async (runId, tag) => {
|
|
589
|
+
const merged = Array.from(new Set([...(selectedRun?.tags ?? []), tag]));
|
|
590
|
+
try {
|
|
591
|
+
await postJson(
|
|
592
|
+
`/api/debug/runs/${encodeURIComponent(runId)}/tags`,
|
|
593
|
+
{ tags: merged },
|
|
594
|
+
);
|
|
595
|
+
const data = await getJson(
|
|
596
|
+
`/api/debug/runs/${encodeURIComponent(runId)}`,
|
|
597
|
+
);
|
|
598
|
+
setSelectedRun(data.run);
|
|
599
|
+
} catch (e) {
|
|
600
|
+
setError(e.message);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
if (!mode) {
|
|
605
|
+
return html`
|
|
606
|
+
<div class="panel">
|
|
607
|
+
<h2 class="panel-title">Debug</h2>
|
|
608
|
+
<p class="placeholder">
|
|
609
|
+
${error ? `error: ${error}` : "loading…"}
|
|
610
|
+
</p>
|
|
611
|
+
</div>
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (!mode.enabled) {
|
|
616
|
+
return html`<${OffState} onEnable=${turnOn} busy=${busy} />`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return html`
|
|
620
|
+
<div class="panel debug-panel">
|
|
621
|
+
<header class="debug-header">
|
|
622
|
+
<h2 class="panel-title">Debug</h2>
|
|
623
|
+
<div class="debug-header-meta">
|
|
624
|
+
<span>${traces.length} trace${traces.length === 1 ? "" : "s"}</span>
|
|
625
|
+
${gates.length > 0
|
|
626
|
+
? html`<span class="debug-header-pending">
|
|
627
|
+
· ${gates.length} pending gate${gates.length === 1 ? "" : "s"}
|
|
628
|
+
</span>`
|
|
629
|
+
: null}
|
|
630
|
+
${mode.pauseBeforeKinds.length > 0
|
|
631
|
+
? html`<span>· pause-before: ${mode.pauseBeforeKinds.join(", ")}</span>`
|
|
632
|
+
: null}
|
|
633
|
+
</div>
|
|
634
|
+
<${ConfigDrawer}
|
|
635
|
+
mode=${mode}
|
|
636
|
+
onToggle=${onToggleEnabled}
|
|
637
|
+
onSetKind=${onSetKind}
|
|
638
|
+
onSetFlag=${onSetFlag}
|
|
639
|
+
/>
|
|
640
|
+
</header>
|
|
641
|
+
|
|
642
|
+
${error ? html`<div class="debug-error">error: ${error}</div>` : null}
|
|
643
|
+
|
|
644
|
+
<${PendingGatesBanner}
|
|
645
|
+
gates=${gates}
|
|
646
|
+
onRelease=${(id) => onGateAction("release", id)}
|
|
647
|
+
onSkip=${(id) => onGateAction("skip", id)}
|
|
648
|
+
onAbort=${(id) => onGateAction("abort", id)}
|
|
649
|
+
/>
|
|
650
|
+
|
|
651
|
+
<div class="debug-3pane">
|
|
652
|
+
<aside class="debug-traces">
|
|
653
|
+
<div class="debug-pane-title">Traces</div>
|
|
654
|
+
<${TraceList}
|
|
655
|
+
traces=${traces}
|
|
656
|
+
selectedTraceId=${selectedTraceId}
|
|
657
|
+
onSelect=${setSelectedTraceId}
|
|
658
|
+
/>
|
|
659
|
+
</aside>
|
|
660
|
+
|
|
661
|
+
<section class="debug-tree-pane">
|
|
662
|
+
<div class="debug-pane-title">Tree</div>
|
|
663
|
+
<${TraceTree}
|
|
664
|
+
runs=${traceRuns}
|
|
665
|
+
selectedRunId=${selectedRunId}
|
|
666
|
+
onSelect=${setSelectedRunId}
|
|
667
|
+
/>
|
|
668
|
+
</section>
|
|
669
|
+
|
|
670
|
+
<section class="debug-detail-pane">
|
|
671
|
+
<div class="debug-pane-title">Detail</div>
|
|
672
|
+
<${RunDetail}
|
|
673
|
+
run=${selectedRun}
|
|
674
|
+
onAddTag=${onAddTag}
|
|
675
|
+
onReplay=${onReplay}
|
|
676
|
+
/>
|
|
677
|
+
</section>
|
|
678
|
+
</div>
|
|
679
|
+
</div>
|
|
680
|
+
`;
|
|
681
|
+
}
|