@clemrig/flow-ui 0.4.1 → 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 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
 
@@ -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
- .querySelector(`[data-key="${CSS.escape(focused)}"]`)
136
- ?.closest("button")
137
- ?.focus({ preventScroll: true });
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 */
@@ -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 = { run: "", selected: null, chosen: false, allTools: new Set() };
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
- verdict === "correction"
277
- ? `↻ back to ${destination}${retry}`
278
- : `accepted → ${destination ?? "end"}`,
279
- verdict === "correction" ? "warn" : "",
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);
@@ -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;
@@ -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.4.1",
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.6.0"
37
+ "@clemrig/flow": "0.7.0"
38
38
  }
39
39
  }