@clemrig/flow-ui 0.4.0 → 0.5.0
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/README.md +1 -1
- package/dist/static/dashboard.js +6 -5
- package/dist/static/detail.js +71 -6
- package/dist/static/style.css +15 -0
- package/dist/static/timeline.js +14 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install -g @clemrig/flow-ui
|
|
|
11
11
|
flow-ui [RUNS_DIR] [--port PORT]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`flow-ui` serves the runs kept in `RUNS_DIR` (default: `.flow/runs`, where `@clemrig/flow` keeps them; a `RUNS_DIR` given must be an existing directory) on `127.0.0.1`, on `PORT` or a port the system chooses, prints its URL and stops on Ctrl-C. The page lists the runs with their workflow, status, current stage, duration, estimated cost and last update, and for a run a card with its start, end, active time and cost, then each launch (the first one, then each resumption) with its status, the cause of a failure and a strip of its stages sized by duration; the list of its stages beside the selected one's panel (start, duration, outcome, the agent call's model, settings, tools and tokens, the tools used, then the journal and the tool calls); its correction counters, its result and its runtimes. It follows a running run as it goes, and follows the selected run again when it is resumed. A result shows the run's validated data as JSON.
|
|
14
|
+
`flow-ui` serves the runs kept in `RUNS_DIR` (default: `.flow/runs`, where `@clemrig/flow` keeps them; a `RUNS_DIR` given must be an existing directory) on `127.0.0.1`, on `PORT` or a port the system chooses, prints its URL and stops on Ctrl-C. The page lists the runs with their workflow, status, current stage, duration, estimated cost and last update, and for a run a card with its start, end, active time and cost, then each launch (the first one, then each resumption) with its status, the cause of a failure and a strip of its stages sized by duration; the list of its stages beside the selected one's panel (start, duration, outcome, the agent call's model, settings, tools and tokens, the tools used, then a folded Output section with its agent's answer and the correction it sent back or could not send, the journal and the tool calls); its correction counters, its result and its runtimes. It follows a running run as it goes, and follows the selected run again when it is resumed. A result shows the run's validated data as JSON.
|
|
15
15
|
|
|
16
16
|
To watch a run on another machine, forward the same port through SSH: `ssh -L 4321:127.0.0.1:4321 machine`, run `flow-ui --port 4321` there, and open `http://localhost:4321/`.
|
|
17
17
|
|
package/dist/static/dashboard.js
CHANGED
|
@@ -130,11 +130,12 @@ function showDetail(detail, events) {
|
|
|
130
130
|
for (const node of container.querySelectorAll("[data-scroll]"))
|
|
131
131
|
if (node instanceof HTMLElement && node.dataset.scroll)
|
|
132
132
|
node.scrollTop = scrolled.get(node.dataset.scroll) ?? 0;
|
|
133
|
-
if (focused)
|
|
134
|
-
container
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
if (focused) {
|
|
134
|
+
const again = container.querySelector(
|
|
135
|
+
`[data-key="${CSS.escape(focused)}"]`,
|
|
136
|
+
);
|
|
137
|
+
if (again instanceof HTMLElement) again.focus({ preventScroll: true });
|
|
138
|
+
}
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
/** @param {string} message */
|
package/dist/static/detail.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
dollars,
|
|
11
11
|
duration,
|
|
12
12
|
launches,
|
|
13
|
+
outputText,
|
|
13
14
|
settingRows,
|
|
14
15
|
shownStage,
|
|
15
16
|
spent,
|
|
@@ -61,9 +62,15 @@ const icons = /** @type {const} */ ({
|
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* What the page keeps from one rendering of a run to the next, as it follows it.
|
|
64
|
-
* @type {{ run: string; selected: number | null; chosen: boolean; allTools: Set<number> }}
|
|
65
|
+
* @type {{ run: string; selected: number | null; chosen: boolean; allTools: Set<number>; outputs: Set<number> }}
|
|
65
66
|
*/
|
|
66
|
-
const view = {
|
|
67
|
+
const view = {
|
|
68
|
+
run: "",
|
|
69
|
+
selected: null,
|
|
70
|
+
chosen: false,
|
|
71
|
+
allTools: new Set(),
|
|
72
|
+
outputs: new Set(),
|
|
73
|
+
};
|
|
67
74
|
|
|
68
75
|
/**
|
|
69
76
|
* @param {string} tag
|
|
@@ -271,12 +278,16 @@ function stagePanel(block, index, rerender, now) {
|
|
|
271
278
|
const retry = block.retry
|
|
272
279
|
? ` (${block.retry.key} ${block.retry.count}/${block.retry.max})`
|
|
273
280
|
: "";
|
|
281
|
+
// A correction past its maximum is not taken: the run stopped there.
|
|
282
|
+
const refused = verdict === "correction" && pastMaximum(block);
|
|
274
283
|
fact(
|
|
275
284
|
"Outcome",
|
|
276
|
-
|
|
277
|
-
?
|
|
278
|
-
:
|
|
279
|
-
|
|
285
|
+
refused
|
|
286
|
+
? `✕ not sent back to ${destination}${retry}`
|
|
287
|
+
: verdict === "correction"
|
|
288
|
+
? `↻ back to ${destination}${retry}`
|
|
289
|
+
: `accepted → ${destination ?? "end"}`,
|
|
290
|
+
refused ? "bad" : verdict === "correction" ? "warn" : "",
|
|
280
291
|
);
|
|
281
292
|
} else fact("Outcome", block.status, block.status === "failed" ? "bad" : "");
|
|
282
293
|
const { call } = block;
|
|
@@ -301,6 +312,8 @@ function stagePanel(block, index, rerender, now) {
|
|
|
301
312
|
panel.append(el("pre", block.decision.reason, "callout warn"));
|
|
302
313
|
for (const warning of block.warnings)
|
|
303
314
|
panel.append(el("p", warning, "callout warn"));
|
|
315
|
+
const output = outputSection(block, index);
|
|
316
|
+
if (output) panel.append(output);
|
|
304
317
|
if (block.tools.length) {
|
|
305
318
|
const chips = el("div", undefined, "chips");
|
|
306
319
|
for (const { name, count, durationMs, failed } of toolUse(block.tools))
|
|
@@ -374,6 +387,57 @@ function stagePanel(block, index, rerender, now) {
|
|
|
374
387
|
return panel;
|
|
375
388
|
}
|
|
376
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Whether the stage's correction went past its maximum.
|
|
392
|
+
* @param {Block} block
|
|
393
|
+
*/
|
|
394
|
+
function pastMaximum(block) {
|
|
395
|
+
return block.retry !== undefined && block.retry.count > block.retry.max;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* What the stage produced, folded until the operator opens it: its agent's
|
|
400
|
+
* answer, and the correction it sent back or could not send.
|
|
401
|
+
* @param {Block} block
|
|
402
|
+
* @param {number} index
|
|
403
|
+
* @returns {HTMLElement | undefined}
|
|
404
|
+
*/
|
|
405
|
+
function outputSection(block, index) {
|
|
406
|
+
/** @type {[string, unknown][]} */
|
|
407
|
+
const parts = [];
|
|
408
|
+
if (Object.hasOwn(block, "output")) parts.push(["Answer", block.output]);
|
|
409
|
+
if (block.feedback !== undefined)
|
|
410
|
+
parts.push([
|
|
411
|
+
pastMaximum(block)
|
|
412
|
+
? "Correction not sent, past its maximum"
|
|
413
|
+
: "Correction sent",
|
|
414
|
+
block.feedback,
|
|
415
|
+
]);
|
|
416
|
+
if (parts.length === 0) return undefined;
|
|
417
|
+
const head = el("summary", "Output");
|
|
418
|
+
head.dataset.key = `output-${index}`;
|
|
419
|
+
const section = el(
|
|
420
|
+
"details",
|
|
421
|
+
[
|
|
422
|
+
head,
|
|
423
|
+
...parts.flatMap(([title, value], part) => {
|
|
424
|
+
const text = el("pre", outputText(value));
|
|
425
|
+
text.dataset.scroll = `output-${index}-${part}`;
|
|
426
|
+
return [el("h4", title), text];
|
|
427
|
+
}),
|
|
428
|
+
],
|
|
429
|
+
"output",
|
|
430
|
+
);
|
|
431
|
+
// Opened by the operator, it stays open, and focused, as the page follows the run.
|
|
432
|
+
/** @type {HTMLDetailsElement} */ (section).open = view.outputs.has(index);
|
|
433
|
+
section.addEventListener("toggle", () => {
|
|
434
|
+
if (/** @type {HTMLDetailsElement} */ (section).open)
|
|
435
|
+
view.outputs.add(index);
|
|
436
|
+
else view.outputs.delete(index);
|
|
437
|
+
});
|
|
438
|
+
return section;
|
|
439
|
+
}
|
|
440
|
+
|
|
377
441
|
/**
|
|
378
442
|
* The detail's sections. The selected stage follows the running or failed one
|
|
379
443
|
* until the operator chooses one, and stays chosen as the run goes on.
|
|
@@ -390,6 +454,7 @@ export function detailSections(detail, events, rerender) {
|
|
|
390
454
|
view.run = detail.run.id;
|
|
391
455
|
view.chosen = false;
|
|
392
456
|
view.allTools.clear();
|
|
457
|
+
view.outputs.clear();
|
|
393
458
|
}
|
|
394
459
|
if (!view.chosen || view.selected === null || !blocks[view.selected])
|
|
395
460
|
view.selected = shownStage(blocks);
|
package/dist/static/style.css
CHANGED
|
@@ -419,6 +419,21 @@ pre {
|
|
|
419
419
|
.log .at {
|
|
420
420
|
color: var(--muted);
|
|
421
421
|
}
|
|
422
|
+
details.output {
|
|
423
|
+
margin: 12px 0 0;
|
|
424
|
+
}
|
|
425
|
+
details.output summary {
|
|
426
|
+
cursor: pointer;
|
|
427
|
+
font-size: 13px;
|
|
428
|
+
font-weight: 600;
|
|
429
|
+
}
|
|
430
|
+
details.output pre {
|
|
431
|
+
max-height: 360px;
|
|
432
|
+
padding: 6px 8px;
|
|
433
|
+
overflow: auto;
|
|
434
|
+
border: 1px solid var(--line);
|
|
435
|
+
border-radius: 6px;
|
|
436
|
+
}
|
|
422
437
|
.log-head {
|
|
423
438
|
display: flex;
|
|
424
439
|
align-items: baseline;
|
package/dist/static/timeline.js
CHANGED
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
* @property {{ verdict: string; reason: string; destination: string | null }} [decision]
|
|
41
41
|
* @property {{ key: string; count: number; max: number }} [retry]
|
|
42
42
|
* @property {string} [detail]
|
|
43
|
+
* @property {unknown} [output] What the stage's agent answered.
|
|
44
|
+
* @property {unknown} [feedback] The correction the stage sent back, or could not send past its maximum.
|
|
43
45
|
* @property {Call} [call]
|
|
44
46
|
* @property {Tool[]} tools
|
|
45
47
|
* @property {TokenCounts | null} tokens The call's responses so far.
|
|
@@ -152,6 +154,8 @@ export function timeline(events) {
|
|
|
152
154
|
if (event.decision) target.decision = event.decision;
|
|
153
155
|
if (event.retry) target.retry = event.retry;
|
|
154
156
|
if (event.detail) target.detail = event.detail;
|
|
157
|
+
if (Object.hasOwn(event, "output")) target.output = event.output;
|
|
158
|
+
if (event.feedback !== undefined) target.feedback = event.feedback;
|
|
155
159
|
} else if (event.type === "log")
|
|
156
160
|
target.journal.push({ at: event.at, message: event.message });
|
|
157
161
|
else apply(target, event.activity);
|
|
@@ -159,6 +163,16 @@ export function timeline(events) {
|
|
|
159
163
|
return blocks;
|
|
160
164
|
}
|
|
161
165
|
|
|
166
|
+
/**
|
|
167
|
+
* A stage's answer or correction as text: a string as it is, anything else as
|
|
168
|
+
* indented JSON.
|
|
169
|
+
* @param {unknown} value
|
|
170
|
+
* @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
export function outputText(value) {
|
|
173
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
174
|
+
}
|
|
175
|
+
|
|
162
176
|
/**
|
|
163
177
|
* @param {number | null} ms
|
|
164
178
|
* @returns {string}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clemrig/flow-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A local read-only dashboard of the runs of @clemrig/flow workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"prepack": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@clemrig/flow": "0.
|
|
37
|
+
"@clemrig/flow": "0.7.0"
|
|
38
38
|
}
|
|
39
39
|
}
|