@clemrig/flow-ui 0.3.0 → 0.4.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/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/static/dashboard.js +32 -100
- package/dist/static/detail.js +424 -0
- package/dist/static/style.css +296 -17
- package/dist/static/timeline.js +209 -14
- 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 its
|
|
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.
|
|
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/server.js
CHANGED
|
@@ -33,6 +33,7 @@ const pageFiles = {
|
|
|
33
33
|
"/dashboard.js": ["dashboard.js", "text/javascript; charset=utf-8"],
|
|
34
34
|
"/timeline.js": ["timeline.js", "text/javascript; charset=utf-8"],
|
|
35
35
|
"/follow.js": ["follow.js", "text/javascript; charset=utf-8"],
|
|
36
|
+
"/detail.js": ["detail.js", "text/javascript; charset=utf-8"],
|
|
36
37
|
};
|
|
37
38
|
async function loadPage() {
|
|
38
39
|
const page = new Map();
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EACL,YAAY,GAIb,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,QAAQ,EACR,OAAO,EACP,aAAa,EACb,aAAa,EAEb,oBAAoB,EACpB,YAAY,EACZ,eAAe,GAChB,MAAM,eAAe,CAAC;AAavB,gEAAgE;AAChE,KAAK,UAAU,IAAI,CAAC,OAAyB;IAC3C,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,IAAI,EAAE,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;aACpC,OAAO,EAAE;aACT,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;KAC1E,CAAC;AACJ,CAAC;AAED,kGAAkG;AAClG,KAAK,UAAU,MAAM,CAAC,OAAyB,EAAE,EAAU,EAAE,KAAa;IACxE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACxD,OAAO;QACL,GAAG,EAAE,YAAY,CAAC,KAAK,CAAC;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,EAAE;QAC9B,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,IAAI;QAC1B,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;QAC3B,IAAI,EAAE,MAAM,CAAC,MAAM;QACnB,4EAA4E;QAC5E,WAAW,EAAE,oBAAoB,CAAC,KAAK,EAAE,MAAM,CAAC;QAChD,MAAM,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC;KACtC,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,SAAS,GAAG;IAChB,GAAG,EAAE,CAAC,YAAY,EAAE,0BAA0B,CAAC;IAC/C,YAAY,EAAE,CAAC,WAAW,EAAE,yBAAyB,CAAC;IACtD,eAAe,EAAE,CAAC,cAAc,EAAE,gCAAgC,CAAC;IACnE,cAAc,EAAE,CAAC,aAAa,EAAE,gCAAgC,CAAC;IACjE,YAAY,EAAE,CAAC,WAAW,EAAE,gCAAgC,CAAC;IAC7D,YAAY,EAAE,CAAC,WAAW,EAAE,gCAAgC,CAAC;CACrD,CAAC;AAEX,KAAK,UAAU,QAAQ;IACrB,MAAM,IAAI,GAAG,IAAI,GAAG,EAA0C,CAAC;IAC/D,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC;QAC1D,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE;YACb,IAAI,EAAE,MAAM,QAAQ,CAClB,IAAI,GAAG,CAAC,YAAY,IAAI,EAAE,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAC5C,MAAM,CACP;YACD,IAAI;SACL,CAAC,CAAC;IACL,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,IAAI,CACX,QAAwB,EACxB,MAAc,EACd,IAAY,EACZ,IAAI,GAAG,2BAA2B;IAElC,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE;QACzB,cAAc,EAAE,IAAI;QACpB,eAAe,EAAE,UAAU;QAC3B,wBAAwB,EAAE,SAAS;QACnC,yBAAyB,EACvB,0IAA0I;KAC7I,CAAC,CAAC;IACH,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAaD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAyB;IAEzB,MAAM,IAAI,GAAG,MAAM,QAAQ,EAAE,CAAC;IAC9B,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE;QACtC,qEAAqE;QACrE,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC;QACtC,IACE,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,aAAa,IAAI,EAAE;YAC5C,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,aAAa,IAAI,EAAE;YAE5C,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;QAC1C,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK;YAC1B,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,oBAAoB,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW;YAC9B,OAAO,IAAI,CACT,QAAQ,EACR,GAAG,EACH,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC,EACnC,kBAAkB,CACnB,CAAC;QACJ,MAAM,EAAE,GAAG,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC5D,IAAI,EAAE,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;QAC9D,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC;QACnD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;QACpE,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,EAAE,kBAAkB,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,iEAAiE;YACjE,OAAO,KAAK,YAAY,eAAe;gBACrC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,WAAW,CAAC;gBAClC,CAAC,CAAC,IAAI,CACF,QAAQ,EACR,GAAG,EACH,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CAAC;QACR,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;IACvE,CAAC,CAAC;AACJ,CAAC;AAED,wFAAwF;AACxF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,OAAyB,EACzB,MAAM,GAAG,CAAC;IAEV,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QAChD,IAAI,GAAQ,CAAC;QACb,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC;YACP,kDAAkD;YAClD,OAAO,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;QAC5C,CAAC;QACD,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CACtD,IAAI,CACF,QAAQ,EACR,GAAG,EACH,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CACvD,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,OAAO,EAAiB,CAAC;IACjD,OAAO;QACL,GAAG,EAAE,oBAAoB,IAAI,GAAG;QAChC,MAAM;QACN,KAAK,EAAE,GAAG,EAAE,CACV,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACpC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAC/B,CAAC,CAAC;KACL,CAAC;AACJ,CAAC"}
|
package/dist/static/dashboard.js
CHANGED
|
@@ -3,35 +3,13 @@
|
|
|
3
3
|
* The dashboard: the run list every 2 seconds, and the selected run as
|
|
4
4
|
* `follow.js` follows it. Text only, never HTML.
|
|
5
5
|
*/
|
|
6
|
+
import { detailSections } from "./detail.js";
|
|
6
7
|
import { follower } from "./follow.js";
|
|
7
|
-
import { cost, duration
|
|
8
|
+
import { cost, duration } from "./timeline.js";
|
|
8
9
|
|
|
9
10
|
/** @typedef {import("@clemrig/flow").RunEvent} RunEvent */
|
|
10
|
-
/** @typedef {import("./
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @typedef {object} Summary
|
|
14
|
-
* @property {string} id
|
|
15
|
-
* @property {string} [workflow]
|
|
16
|
-
* @property {string} [status]
|
|
17
|
-
* @property {string} [stage]
|
|
18
|
-
* @property {number | null} [durationMs]
|
|
19
|
-
* @property {number | null} [costUsd]
|
|
20
|
-
* @property {boolean} [partial]
|
|
21
|
-
* @property {string} [updatedAt]
|
|
22
|
-
* @property {string} [error] Why the run cannot be read.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @typedef {object} Detail
|
|
27
|
-
* @property {Required<Omit<Summary, "error">>} run
|
|
28
|
-
* @property {Record<string, string>} runtimes
|
|
29
|
-
* @property {string | null} error
|
|
30
|
-
* @property {RunEvent[]} events
|
|
31
|
-
* @property {number} next
|
|
32
|
-
* @property {import("@clemrig/flow").Correction[]} corrections
|
|
33
|
-
* @property {string[]} result
|
|
34
|
-
*/
|
|
11
|
+
/** @typedef {import("./detail.js").Summary} Summary */
|
|
12
|
+
/** @typedef {import("./detail.js").Detail} Detail */
|
|
35
13
|
|
|
36
14
|
const LIST_EVERY_MS = 2000;
|
|
37
15
|
const DETAIL_EVERY_MS = 1000;
|
|
@@ -70,11 +48,6 @@ function time(iso) {
|
|
|
70
48
|
});
|
|
71
49
|
}
|
|
72
50
|
|
|
73
|
-
/** @param {import("@clemrig/flow").TokenCounts} tokens */
|
|
74
|
-
function tokenLine(tokens) {
|
|
75
|
-
return `${tokens.input} input · ${tokens.output} output · ${tokens.cacheRead} cache read · ${tokens.cacheWrite} cache write tokens`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
51
|
/**
|
|
79
52
|
* @param {string} path
|
|
80
53
|
* @returns {Promise<any>}
|
|
@@ -127,82 +100,41 @@ function showRuns(runs) {
|
|
|
127
100
|
);
|
|
128
101
|
}
|
|
129
102
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
const item = el("li", undefined, "block");
|
|
133
|
-
const head = el("p");
|
|
134
|
-
head.append(
|
|
135
|
-
`${block.kind} ${block.stage} · `,
|
|
136
|
-
el("span", block.status, block.status),
|
|
137
|
-
block.durationMs === null ? "" : ` · ${duration(block.durationMs)}`,
|
|
138
|
-
);
|
|
139
|
-
item.append(head);
|
|
140
|
-
const line = (/** @type {string} */ text, className = "line") =>
|
|
141
|
-
item.append(el("p", text, className));
|
|
142
|
-
const { call } = block;
|
|
143
|
-
if (call?.model)
|
|
144
|
-
line(`${call.runtime} ${call.model} · session ${call.session}`);
|
|
145
|
-
if (call?.settings)
|
|
146
|
-
for (const setting of settingsLines(call.settings)) line(setting);
|
|
147
|
-
for (const tool of block.tools)
|
|
148
|
-
line(
|
|
149
|
-
tool.status === "running"
|
|
150
|
-
? `${tool.name} …`
|
|
151
|
-
: `${tool.name}${tool.status === "failed" ? " failed" : ""} ${((tool.durationMs ?? 0) / 1000).toFixed(1)} s`,
|
|
152
|
-
);
|
|
153
|
-
if (call?.usage)
|
|
154
|
-
line(
|
|
155
|
-
`${call.scope === "session" ? "Session total" : "Call"}: ${tokenLine(call.usage)} · ${cost(call.usage.costUsd ?? null, false)}`,
|
|
156
|
-
);
|
|
157
|
-
else if (block.tokens)
|
|
158
|
-
line(
|
|
159
|
-
`So far: ${tokenLine(block.tokens)}${block.provisional ? " (provisional)" : ""}`,
|
|
160
|
-
);
|
|
161
|
-
for (const warning of block.warnings) line(warning, "line warning");
|
|
162
|
-
for (const entry of block.journal) line(`${time(entry.at)} ${entry.message}`);
|
|
163
|
-
if (block.decision) {
|
|
164
|
-
const { verdict, reason, destination } = block.decision;
|
|
165
|
-
const retry = block.retry
|
|
166
|
-
? ` [${block.retry.key} ${block.retry.count}/${block.retry.max}]`
|
|
167
|
-
: "";
|
|
168
|
-
line(`${verdict} — ${reason} → ${destination ?? "end"}${retry}`);
|
|
169
|
-
}
|
|
170
|
-
if (block.detail) line(block.detail, "line failed");
|
|
171
|
-
return item;
|
|
172
|
-
}
|
|
103
|
+
/** The detail on screen, to render it again when the operator changes the view. */
|
|
104
|
+
let shown = /** @type {[Detail, RunEvent[]] | null} */ (null);
|
|
173
105
|
|
|
174
106
|
/**
|
|
107
|
+
* Renders the selected run's detail, keeping where the operator scrolled and
|
|
108
|
+
* what has the focus, as the page renders it again each time the run changes.
|
|
175
109
|
* @param {Detail} detail
|
|
176
110
|
* @param {RunEvent[]} events Every event received for the run.
|
|
177
111
|
*/
|
|
178
112
|
function showDetail(detail, events) {
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
113
|
+
const container = byId("detail");
|
|
114
|
+
const same = shown?.[0].run.id === detail.run.id;
|
|
115
|
+
shown = [detail, events];
|
|
116
|
+
/** @type {Map<string, number>} */
|
|
117
|
+
const scrolled = new Map();
|
|
118
|
+
if (same)
|
|
119
|
+
for (const node of container.querySelectorAll("[data-scroll]"))
|
|
120
|
+
if (node instanceof HTMLElement && node.dataset.scroll)
|
|
121
|
+
scrolled.set(node.dataset.scroll, node.scrollTop);
|
|
122
|
+
const active = document.activeElement;
|
|
123
|
+
const focused =
|
|
124
|
+
same && active instanceof HTMLElement && container.contains(active)
|
|
125
|
+
? active.dataset.key
|
|
126
|
+
: undefined;
|
|
127
|
+
container.replaceChildren(
|
|
128
|
+
...detailSections(detail, events, () => shown && showDetail(...shown)),
|
|
185
129
|
);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const sections = [summary, stages];
|
|
195
|
-
if (detail.corrections.length) {
|
|
196
|
-
const corrections = el("section");
|
|
197
|
-
corrections.append(el("h2", "Corrections"));
|
|
198
|
-
for (const { key, count, max } of detail.corrections)
|
|
199
|
-
corrections.append(el("p", `${key} ${count}/${max ?? "?"}`, "line"));
|
|
200
|
-
sections.push(corrections);
|
|
201
|
-
}
|
|
202
|
-
const result = el("section");
|
|
203
|
-
result.append(el("h2", "Result"), el("pre", detail.result.join("\n")));
|
|
204
|
-
sections.push(result);
|
|
205
|
-
byId("detail").replaceChildren(...sections);
|
|
130
|
+
for (const node of container.querySelectorAll("[data-scroll]"))
|
|
131
|
+
if (node instanceof HTMLElement && node.dataset.scroll)
|
|
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 });
|
|
206
138
|
}
|
|
207
139
|
|
|
208
140
|
/** @param {string} message */
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* A run's detail: one card with its facts and launches, each launch's stages as
|
|
4
|
+
* a strip sized by duration; then the list of its stages beside the selected
|
|
5
|
+
* one. Text only, never HTML.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
brief,
|
|
9
|
+
cost,
|
|
10
|
+
dollars,
|
|
11
|
+
duration,
|
|
12
|
+
launches,
|
|
13
|
+
settingRows,
|
|
14
|
+
shownStage,
|
|
15
|
+
spent,
|
|
16
|
+
stateOf,
|
|
17
|
+
summary,
|
|
18
|
+
timeline,
|
|
19
|
+
tokenLine,
|
|
20
|
+
toolUse,
|
|
21
|
+
} from "./timeline.js";
|
|
22
|
+
|
|
23
|
+
/** @typedef {import("@clemrig/flow").RunEvent} RunEvent */
|
|
24
|
+
/** @typedef {import("./timeline.js").Block} Block */
|
|
25
|
+
/** @typedef {import("./timeline.js").Launch} Launch */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} Summary
|
|
29
|
+
* @property {string} id
|
|
30
|
+
* @property {string} [workflow]
|
|
31
|
+
* @property {string} [status]
|
|
32
|
+
* @property {string} [stage]
|
|
33
|
+
* @property {number | null} [durationMs]
|
|
34
|
+
* @property {number | null} [costUsd]
|
|
35
|
+
* @property {boolean} [partial]
|
|
36
|
+
* @property {string} [updatedAt]
|
|
37
|
+
* @property {string} [error] Why the run cannot be read.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} Detail
|
|
42
|
+
* @property {Required<Omit<Summary, "error">>} run
|
|
43
|
+
* @property {Record<string, string>} runtimes
|
|
44
|
+
* @property {string | null} error
|
|
45
|
+
* @property {RunEvent[]} events
|
|
46
|
+
* @property {number} next
|
|
47
|
+
* @property {import("@clemrig/flow").Correction[]} corrections
|
|
48
|
+
* @property {string[]} result
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/** Tool calls a stage shows until the operator asks for all of them. */
|
|
52
|
+
const LATEST_TOOLS = 5;
|
|
53
|
+
|
|
54
|
+
const icons = /** @type {const} */ ({
|
|
55
|
+
passed: "✓",
|
|
56
|
+
correction: "↻",
|
|
57
|
+
failed: "✕",
|
|
58
|
+
running: "●",
|
|
59
|
+
completed: "✓",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 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
|
+
*/
|
|
66
|
+
const view = { run: "", selected: null, chosen: false, allTools: new Set() };
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} tag
|
|
70
|
+
* @param {string | (string | Node)[]} [content]
|
|
71
|
+
* @param {string} [className]
|
|
72
|
+
* @returns {HTMLElement}
|
|
73
|
+
*/
|
|
74
|
+
function el(tag, content, className) {
|
|
75
|
+
const node = document.createElement(tag);
|
|
76
|
+
if (typeof content === "string") node.textContent = content;
|
|
77
|
+
else if (content) node.append(...content);
|
|
78
|
+
if (className) node.className = className;
|
|
79
|
+
return node;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A state as data, so that it never meets the page's status classes.
|
|
84
|
+
* @param {HTMLElement} node
|
|
85
|
+
* @param {string} state
|
|
86
|
+
* @returns {HTMLElement}
|
|
87
|
+
*/
|
|
88
|
+
function stated(node, state) {
|
|
89
|
+
node.dataset.state = state;
|
|
90
|
+
return node;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const timeFormat = /** @type {const} */ ({
|
|
94
|
+
hour: "2-digit",
|
|
95
|
+
minute: "2-digit",
|
|
96
|
+
second: "2-digit",
|
|
97
|
+
hourCycle: "h23",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** @param {string} iso A clock time, without its date. */
|
|
101
|
+
const hour = (iso) => new Date(iso).toLocaleTimeString(undefined, timeFormat);
|
|
102
|
+
|
|
103
|
+
/** @param {string} iso A day, unless it is today. */
|
|
104
|
+
function day(iso) {
|
|
105
|
+
const date = new Date(iso);
|
|
106
|
+
return new Date().toDateString() === date.toDateString()
|
|
107
|
+
? "today"
|
|
108
|
+
: date.toLocaleDateString(undefined, { day: "numeric", month: "short" });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @param {string | null} iso A clock time, with its day unless it is today. */
|
|
112
|
+
function clock(iso) {
|
|
113
|
+
if (!iso) return "—";
|
|
114
|
+
const today = day(iso) === "today";
|
|
115
|
+
return today ? hour(iso) : `${day(iso)} ${hour(iso)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The run's card: its status and facts, then each launch with its strip of
|
|
120
|
+
* stages sized by duration, why a failed one stopped, and the pause before the
|
|
121
|
+
* next.
|
|
122
|
+
* @param {Detail} detail
|
|
123
|
+
* @param {Launch[]} found
|
|
124
|
+
* @param {(index: number) => void} select
|
|
125
|
+
* @param {number} now
|
|
126
|
+
*/
|
|
127
|
+
function overview(detail, found, select, now) {
|
|
128
|
+
const { run } = detail;
|
|
129
|
+
const section = el("section", undefined, "overview");
|
|
130
|
+
const title = el("div", undefined, "overview-title");
|
|
131
|
+
title.append(
|
|
132
|
+
el("h3", `${run.workflow} · ${run.id}`),
|
|
133
|
+
stated(el("span", run.status, "pill"), run.status),
|
|
134
|
+
);
|
|
135
|
+
const facts = el("dl", undefined, "facts");
|
|
136
|
+
const fact = (/** @type {string} */ name, /** @type {string} */ value) =>
|
|
137
|
+
facts.append(el("div", [el("dt", name), el("dd", value)]));
|
|
138
|
+
fact("Started", clock(found[0]?.startedAt ?? null));
|
|
139
|
+
if (run.status !== "running")
|
|
140
|
+
fact("Ended", clock(found.at(-1)?.endedAt ?? null));
|
|
141
|
+
fact("Active time", duration(run.durationMs));
|
|
142
|
+
fact("Cost", dollars(run.costUsd, run.partial));
|
|
143
|
+
section.append(title, facts);
|
|
144
|
+
// A failure no stage recorded, such as a journal that could not be written.
|
|
145
|
+
if (detail.error && !found.at(-1)?.cause)
|
|
146
|
+
section.append(el("pre", detail.error, "callout bad"));
|
|
147
|
+
found.forEach((launch, position) => {
|
|
148
|
+
const end = launch.endedAt ? Date.parse(launch.endedAt) : now;
|
|
149
|
+
const length = end - Date.parse(launch.startedAt);
|
|
150
|
+
const head = el("div", undefined, "launch-head");
|
|
151
|
+
head.append(
|
|
152
|
+
el("span", [
|
|
153
|
+
el("strong", `#${launch.number}`),
|
|
154
|
+
` ${position ? "resumed " : ""}${clock(launch.startedAt)} → ${launch.endedAt ? clock(launch.endedAt) : "now"}`,
|
|
155
|
+
el("span", ` · ${brief(length)}`, "muted"),
|
|
156
|
+
]),
|
|
157
|
+
stated(
|
|
158
|
+
el("span", `${icons[launch.status]} ${launch.status}`, "state"),
|
|
159
|
+
launch.status,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
const track = el("div", undefined, "track");
|
|
163
|
+
for (const { block, index } of launch.stages) {
|
|
164
|
+
const segment = stated(
|
|
165
|
+
el("button", undefined, "segment"),
|
|
166
|
+
stateOf(block),
|
|
167
|
+
);
|
|
168
|
+
segment.dataset.kind = block.kind;
|
|
169
|
+
segment.dataset.key = `segment-${index}`;
|
|
170
|
+
segment.title = `${block.stage} · ${block.kind} · ${clock(block.startedAt)} · ${brief(spent(block, now))}`;
|
|
171
|
+
if (view.selected === index) segment.setAttribute("aria-current", "true");
|
|
172
|
+
// A width relative to the launch, set through CSSOM: the page allows no inline style.
|
|
173
|
+
segment.style.flexGrow = String(Math.max(spent(block, now), 1));
|
|
174
|
+
if (spent(block, now) > length / 12) segment.textContent = block.stage;
|
|
175
|
+
segment.addEventListener("click", () => select(index));
|
|
176
|
+
track.append(segment);
|
|
177
|
+
}
|
|
178
|
+
const item = el("div", [head, track], "launch");
|
|
179
|
+
if (launch.status === "failed" && launch.cause)
|
|
180
|
+
item.append(el("p", launch.cause, "launch-cause"));
|
|
181
|
+
section.append(item);
|
|
182
|
+
const next = found[position + 1];
|
|
183
|
+
if (next && launch.endedAt)
|
|
184
|
+
section.append(
|
|
185
|
+
el(
|
|
186
|
+
"p",
|
|
187
|
+
`paused ${brief(Date.parse(next.startedAt) - Date.parse(launch.endedAt))}`,
|
|
188
|
+
"pause",
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
return section;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The run's stages, grouped by launch and day: state, name, kind and duration.
|
|
197
|
+
* @param {Launch[]} found
|
|
198
|
+
* @param {(index: number) => void} select
|
|
199
|
+
* @param {number} now
|
|
200
|
+
*/
|
|
201
|
+
function stageList(found, select, now) {
|
|
202
|
+
const list = el("div", undefined, "stage-list");
|
|
203
|
+
list.dataset.scroll = "stages";
|
|
204
|
+
const count = found.reduce((sum, launch) => sum + launch.stages.length, 0);
|
|
205
|
+
list.append(el("h2", `Stages (${count})`));
|
|
206
|
+
for (const launch of found) {
|
|
207
|
+
list.append(
|
|
208
|
+
el(
|
|
209
|
+
"p",
|
|
210
|
+
`${found.length > 1 ? `Launch #${launch.number} · ` : ""}${day(launch.startedAt)}`,
|
|
211
|
+
"group",
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
const rows = el("ol", undefined, "stages");
|
|
215
|
+
for (const { block, index } of launch.stages) {
|
|
216
|
+
const state = stateOf(block);
|
|
217
|
+
const row = stated(el("button", undefined, "stage"), state);
|
|
218
|
+
row.dataset.kind = block.kind;
|
|
219
|
+
row.dataset.key = `stage-${index}`;
|
|
220
|
+
row.title = summary(block);
|
|
221
|
+
if (view.selected === index) row.setAttribute("aria-current", "true");
|
|
222
|
+
row.append(
|
|
223
|
+
el("span", icons[state], "icon"),
|
|
224
|
+
el("span", block.stage, "name"),
|
|
225
|
+
el("span", block.kind, "kind"),
|
|
226
|
+
el("span", brief(spent(block, now)), "took"),
|
|
227
|
+
);
|
|
228
|
+
row.addEventListener("click", () => select(index));
|
|
229
|
+
rows.append(el("li", [row]));
|
|
230
|
+
}
|
|
231
|
+
list.append(rows);
|
|
232
|
+
}
|
|
233
|
+
return list;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* What a stage execution did: a sheet of its facts, why it failed or was sent
|
|
238
|
+
* back, the tools it used, then its journal and its tool calls.
|
|
239
|
+
* @param {Block} block
|
|
240
|
+
* @param {number} index
|
|
241
|
+
* @param {() => void} rerender
|
|
242
|
+
* @param {number} now
|
|
243
|
+
*/
|
|
244
|
+
function stagePanel(block, index, rerender, now) {
|
|
245
|
+
const state = stateOf(block);
|
|
246
|
+
const panel = el("div", undefined, "stage-panel");
|
|
247
|
+
panel.append(
|
|
248
|
+
el(
|
|
249
|
+
"div",
|
|
250
|
+
[
|
|
251
|
+
stated(el("span", state, "pill"), state),
|
|
252
|
+
el("h3", block.stage),
|
|
253
|
+
el("span", block.kind, "kind tag"),
|
|
254
|
+
],
|
|
255
|
+
"panel-title",
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
const sheet = el("dl", undefined, "sheet");
|
|
259
|
+
const fact = (
|
|
260
|
+
/** @type {string} */ name,
|
|
261
|
+
/** @type {string} */ value,
|
|
262
|
+
/** @type {string} */ className = "",
|
|
263
|
+
) => sheet.append(el("dt", name), el("dd", value, className));
|
|
264
|
+
fact("Started", clock(block.startedAt));
|
|
265
|
+
fact(
|
|
266
|
+
block.durationMs === null ? "Running for" : "Duration",
|
|
267
|
+
brief(spent(block, now)),
|
|
268
|
+
);
|
|
269
|
+
if (block.decision) {
|
|
270
|
+
const { verdict, destination } = block.decision;
|
|
271
|
+
const retry = block.retry
|
|
272
|
+
? ` (${block.retry.key} ${block.retry.count}/${block.retry.max})`
|
|
273
|
+
: "";
|
|
274
|
+
fact(
|
|
275
|
+
"Outcome",
|
|
276
|
+
verdict === "correction"
|
|
277
|
+
? `↻ back to ${destination}${retry}`
|
|
278
|
+
: `accepted → ${destination ?? "end"}`,
|
|
279
|
+
verdict === "correction" ? "warn" : "",
|
|
280
|
+
);
|
|
281
|
+
} else fact("Outcome", block.status, block.status === "failed" ? "bad" : "");
|
|
282
|
+
const { call } = block;
|
|
283
|
+
if (call?.model) {
|
|
284
|
+
fact("Agent", `${call.runtime} · ${call.model}`);
|
|
285
|
+
if (call.session) fact("Session", call.session);
|
|
286
|
+
if (call.settings)
|
|
287
|
+
for (const { label, value } of settingRows(call.settings))
|
|
288
|
+
fact(label, value);
|
|
289
|
+
}
|
|
290
|
+
if (call?.usage) {
|
|
291
|
+
fact(
|
|
292
|
+
call.scope === "session" ? "Tokens (session)" : "Tokens",
|
|
293
|
+
tokenLine(call.usage, false),
|
|
294
|
+
);
|
|
295
|
+
fact("Cost", cost(call.usage.costUsd ?? null, false));
|
|
296
|
+
} else if (block.tokens)
|
|
297
|
+
fact("Tokens so far", tokenLine(block.tokens, block.provisional));
|
|
298
|
+
panel.append(sheet);
|
|
299
|
+
if (block.detail) panel.append(el("pre", block.detail, "callout bad"));
|
|
300
|
+
else if (block.decision?.verdict === "correction")
|
|
301
|
+
panel.append(el("pre", block.decision.reason, "callout warn"));
|
|
302
|
+
for (const warning of block.warnings)
|
|
303
|
+
panel.append(el("p", warning, "callout warn"));
|
|
304
|
+
if (block.tools.length) {
|
|
305
|
+
const chips = el("div", undefined, "chips");
|
|
306
|
+
for (const { name, count, durationMs, failed } of toolUse(block.tools))
|
|
307
|
+
chips.append(
|
|
308
|
+
el(
|
|
309
|
+
"span",
|
|
310
|
+
`${name} ×${count} · ${brief(durationMs)}${failed ? ` · ${failed} failed` : ""}`,
|
|
311
|
+
failed ? "chip warn" : "chip",
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
panel.append(el("h4", `Tools used (${block.tools.length})`), chips);
|
|
315
|
+
}
|
|
316
|
+
if (block.journal.length) {
|
|
317
|
+
const journal = el("ol", undefined, "log");
|
|
318
|
+
journal.dataset.scroll = `journal-${index}`;
|
|
319
|
+
for (const entry of block.journal)
|
|
320
|
+
journal.append(
|
|
321
|
+
el("li", [el("span", hour(entry.at), "at"), el("span", entry.message)]),
|
|
322
|
+
);
|
|
323
|
+
panel.append(el("h4", "Journal"), journal);
|
|
324
|
+
}
|
|
325
|
+
if (block.tools.length) {
|
|
326
|
+
const all = view.allTools.has(index);
|
|
327
|
+
const shown = all ? block.tools : block.tools.slice(-LATEST_TOOLS);
|
|
328
|
+
const head = el(
|
|
329
|
+
"div",
|
|
330
|
+
[
|
|
331
|
+
el(
|
|
332
|
+
"h4",
|
|
333
|
+
all
|
|
334
|
+
? `Tool calls (${block.tools.length})`
|
|
335
|
+
: `Tool calls · latest ${shown.length} of ${block.tools.length}`,
|
|
336
|
+
),
|
|
337
|
+
],
|
|
338
|
+
"log-head",
|
|
339
|
+
);
|
|
340
|
+
if (block.tools.length > LATEST_TOOLS) {
|
|
341
|
+
const toggle = el("button", all ? "Latest only" : "Show all", "link");
|
|
342
|
+
toggle.dataset.key = `tools-${index}`;
|
|
343
|
+
toggle.addEventListener("click", () => {
|
|
344
|
+
if (all) view.allTools.delete(index);
|
|
345
|
+
else view.allTools.add(index);
|
|
346
|
+
rerender();
|
|
347
|
+
});
|
|
348
|
+
head.append(toggle);
|
|
349
|
+
}
|
|
350
|
+
const calls = el("ol", undefined, "log");
|
|
351
|
+
calls.dataset.scroll = `tools-${index}`;
|
|
352
|
+
for (const tool of shown)
|
|
353
|
+
calls.append(
|
|
354
|
+
el(
|
|
355
|
+
"li",
|
|
356
|
+
[
|
|
357
|
+
el(
|
|
358
|
+
"span",
|
|
359
|
+
tool.status === "running"
|
|
360
|
+
? "…"
|
|
361
|
+
: `${((tool.durationMs ?? 0) / 1000).toFixed(1)} s`,
|
|
362
|
+
"at",
|
|
363
|
+
),
|
|
364
|
+
el(
|
|
365
|
+
"span",
|
|
366
|
+
`${tool.name}${tool.status === "failed" ? " — failed" : ""}`,
|
|
367
|
+
),
|
|
368
|
+
],
|
|
369
|
+
tool.status === "failed" ? "bad" : "",
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
panel.append(head, calls);
|
|
373
|
+
}
|
|
374
|
+
return panel;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The detail's sections. The selected stage follows the running or failed one
|
|
379
|
+
* until the operator chooses one, and stays chosen as the run goes on.
|
|
380
|
+
* @param {Detail} detail
|
|
381
|
+
* @param {RunEvent[]} events Every event received for the run.
|
|
382
|
+
* @param {() => void} rerender Renders the detail again, as the operator changes the view.
|
|
383
|
+
* @returns {HTMLElement[]}
|
|
384
|
+
*/
|
|
385
|
+
export function detailSections(detail, events, rerender) {
|
|
386
|
+
const now = Date.now();
|
|
387
|
+
const blocks = timeline(events);
|
|
388
|
+
const found = launches(blocks);
|
|
389
|
+
if (view.run !== detail.run.id) {
|
|
390
|
+
view.run = detail.run.id;
|
|
391
|
+
view.chosen = false;
|
|
392
|
+
view.allTools.clear();
|
|
393
|
+
}
|
|
394
|
+
if (!view.chosen || view.selected === null || !blocks[view.selected])
|
|
395
|
+
view.selected = shownStage(blocks);
|
|
396
|
+
const select = (/** @type {number} */ index) => {
|
|
397
|
+
view.selected = index;
|
|
398
|
+
view.chosen = true;
|
|
399
|
+
rerender();
|
|
400
|
+
};
|
|
401
|
+
const sections = [overview(detail, found, select, now)];
|
|
402
|
+
const stages = el("section", undefined, "stages-section");
|
|
403
|
+
const chosen = view.selected === null ? undefined : blocks[view.selected];
|
|
404
|
+
stages.append(
|
|
405
|
+
stageList(found, select, now),
|
|
406
|
+
chosen && view.selected !== null
|
|
407
|
+
? stagePanel(chosen, view.selected, rerender, now)
|
|
408
|
+
: el("p", "No stage yet.", "empty"),
|
|
409
|
+
);
|
|
410
|
+
sections.push(stages);
|
|
411
|
+
if (detail.corrections.length) {
|
|
412
|
+
const corrections = el("section");
|
|
413
|
+
corrections.append(el("h2", "Corrections"));
|
|
414
|
+
for (const { key, count, max } of detail.corrections)
|
|
415
|
+
corrections.append(el("p", `${key} ${count}/${max ?? "?"}`, "mono"));
|
|
416
|
+
sections.push(corrections);
|
|
417
|
+
}
|
|
418
|
+
const result = el("section");
|
|
419
|
+
result.append(el("h2", "Result"), el("pre", detail.result.join("\n")));
|
|
420
|
+
for (const runtime of Object.values(detail.runtimes))
|
|
421
|
+
result.append(el("p", `Runtime: ${runtime}`, "runtime"));
|
|
422
|
+
sections.push(result);
|
|
423
|
+
return sections;
|
|
424
|
+
}
|
package/dist/static/style.css
CHANGED
|
@@ -121,46 +121,325 @@ h2 {
|
|
|
121
121
|
text-transform: uppercase;
|
|
122
122
|
}
|
|
123
123
|
h3 {
|
|
124
|
-
margin: 0
|
|
124
|
+
margin: 0;
|
|
125
125
|
font-size: 14px;
|
|
126
126
|
overflow-wrap: anywhere;
|
|
127
127
|
}
|
|
128
|
+
h4 {
|
|
129
|
+
margin: 14px 0 6px;
|
|
130
|
+
color: var(--muted);
|
|
131
|
+
font-size: 11px;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
letter-spacing: 0.04em;
|
|
134
|
+
text-transform: uppercase;
|
|
135
|
+
}
|
|
128
136
|
ol {
|
|
129
137
|
margin: 0;
|
|
130
138
|
padding: 0;
|
|
131
139
|
list-style: none;
|
|
132
140
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
pre,
|
|
142
|
+
.mono,
|
|
143
|
+
.log {
|
|
144
|
+
font:
|
|
145
|
+
12px / 1.45 ui-monospace,
|
|
146
|
+
SFMono-Regular,
|
|
147
|
+
Menlo,
|
|
148
|
+
monospace;
|
|
149
|
+
overflow-wrap: anywhere;
|
|
136
150
|
}
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
pre {
|
|
152
|
+
margin: 0;
|
|
153
|
+
white-space: pre-wrap;
|
|
139
154
|
}
|
|
140
|
-
.
|
|
155
|
+
.empty {
|
|
141
156
|
margin: 0;
|
|
157
|
+
color: var(--muted);
|
|
142
158
|
}
|
|
143
|
-
.
|
|
144
|
-
|
|
159
|
+
.muted {
|
|
160
|
+
color: var(--muted);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* States, carried as data so that they never meet the list's status classes. */
|
|
164
|
+
[data-state="passed"],
|
|
165
|
+
[data-state="completed"] {
|
|
166
|
+
--state: var(--ok);
|
|
167
|
+
}
|
|
168
|
+
[data-state="correction"] {
|
|
169
|
+
--state: var(--run);
|
|
170
|
+
}
|
|
171
|
+
[data-state="failed"] {
|
|
172
|
+
--state: var(--bad);
|
|
173
|
+
}
|
|
174
|
+
[data-state="running"] {
|
|
175
|
+
--state: var(--accent);
|
|
176
|
+
}
|
|
177
|
+
.pill {
|
|
178
|
+
padding: 1px 8px;
|
|
179
|
+
border-radius: 999px;
|
|
180
|
+
background: color-mix(in srgb, var(--state) 16%, transparent);
|
|
181
|
+
color: var(--state);
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
font-weight: 600;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* The run's card: facts on one line, then each launch with its strip. */
|
|
187
|
+
.overview-title {
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: space-between;
|
|
191
|
+
gap: 10px;
|
|
192
|
+
}
|
|
193
|
+
.facts {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-wrap: wrap;
|
|
196
|
+
gap: 6px 28px;
|
|
197
|
+
margin: 10px 0 14px;
|
|
198
|
+
}
|
|
199
|
+
.facts dt {
|
|
200
|
+
color: var(--muted);
|
|
201
|
+
font-size: 12px;
|
|
202
|
+
}
|
|
203
|
+
.facts dd {
|
|
204
|
+
margin: 0;
|
|
205
|
+
font-size: 15px;
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
font-variant-numeric: tabular-nums;
|
|
208
|
+
}
|
|
209
|
+
.launch {
|
|
210
|
+
padding-top: 12px;
|
|
211
|
+
border-top: 1px solid var(--line);
|
|
212
|
+
}
|
|
213
|
+
.launch-head {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: baseline;
|
|
216
|
+
justify-content: space-between;
|
|
217
|
+
gap: 12px;
|
|
218
|
+
margin-bottom: 6px;
|
|
219
|
+
font-size: 13px;
|
|
220
|
+
font-variant-numeric: tabular-nums;
|
|
221
|
+
}
|
|
222
|
+
.launch-head .state {
|
|
223
|
+
color: var(--state);
|
|
224
|
+
font-size: 12px;
|
|
225
|
+
font-weight: 600;
|
|
226
|
+
white-space: nowrap;
|
|
227
|
+
}
|
|
228
|
+
.launch-cause {
|
|
229
|
+
margin: 6px 0 0;
|
|
230
|
+
color: var(--bad);
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
}
|
|
233
|
+
.pause {
|
|
234
|
+
margin: 10px 0;
|
|
235
|
+
color: var(--muted);
|
|
236
|
+
font-size: 12px;
|
|
237
|
+
}
|
|
238
|
+
.track {
|
|
239
|
+
display: flex;
|
|
240
|
+
gap: 2px;
|
|
241
|
+
height: 22px;
|
|
242
|
+
}
|
|
243
|
+
.segment {
|
|
244
|
+
min-width: 4px;
|
|
245
|
+
flex-basis: 0;
|
|
246
|
+
padding: 0 6px;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
border: 0;
|
|
249
|
+
border-radius: 3px;
|
|
250
|
+
background: var(--state);
|
|
251
|
+
color: var(--panel);
|
|
252
|
+
font:
|
|
253
|
+
600 11px / 22px ui-sans-serif,
|
|
254
|
+
system-ui,
|
|
255
|
+
sans-serif;
|
|
256
|
+
text-overflow: ellipsis;
|
|
257
|
+
white-space: nowrap;
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
}
|
|
260
|
+
.segment[data-state="passed"]:not([data-kind="AGENT"]) {
|
|
261
|
+
background: color-mix(in srgb, var(--ok) 65%, var(--panel));
|
|
262
|
+
}
|
|
263
|
+
.segment[aria-current="true"] {
|
|
264
|
+
outline: 2px solid var(--fg);
|
|
265
|
+
outline-offset: 1px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* The stages: the list beside the selected stage's panel. */
|
|
269
|
+
.stages-section {
|
|
270
|
+
display: grid;
|
|
271
|
+
grid-template-columns: minmax(0, 1fr);
|
|
272
|
+
gap: 16px;
|
|
273
|
+
}
|
|
274
|
+
@media (min-width: 1300px) {
|
|
275
|
+
.stages-section {
|
|
276
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
|
|
277
|
+
align-items: start;
|
|
278
|
+
}
|
|
279
|
+
.stage-list {
|
|
280
|
+
max-height: 75vh;
|
|
281
|
+
overflow: auto;
|
|
282
|
+
}
|
|
283
|
+
.stage-panel {
|
|
284
|
+
position: sticky;
|
|
285
|
+
top: 12px;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
.group {
|
|
289
|
+
margin: 10px 0 4px;
|
|
290
|
+
color: var(--muted);
|
|
291
|
+
font-size: 12px;
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
}
|
|
294
|
+
.stages > li {
|
|
295
|
+
border-bottom: 1px solid var(--line);
|
|
296
|
+
}
|
|
297
|
+
.stages > li:last-child {
|
|
298
|
+
border-bottom: 0;
|
|
299
|
+
}
|
|
300
|
+
.stage {
|
|
301
|
+
display: grid;
|
|
302
|
+
grid-template-columns: 18px minmax(0, 1fr) 56px 84px;
|
|
303
|
+
gap: 8px;
|
|
304
|
+
align-items: center;
|
|
305
|
+
width: 100%;
|
|
306
|
+
padding: 4px;
|
|
307
|
+
border: 0;
|
|
308
|
+
background: none;
|
|
309
|
+
color: inherit;
|
|
310
|
+
font: inherit;
|
|
311
|
+
text-align: left;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
}
|
|
314
|
+
.stage:hover,
|
|
315
|
+
.stage[aria-current="true"] {
|
|
316
|
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
|
317
|
+
}
|
|
318
|
+
.stage .icon {
|
|
319
|
+
color: var(--state);
|
|
320
|
+
font-weight: 700;
|
|
321
|
+
text-align: center;
|
|
322
|
+
}
|
|
323
|
+
.stage .name {
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
font-weight: 600;
|
|
326
|
+
text-overflow: ellipsis;
|
|
327
|
+
white-space: nowrap;
|
|
328
|
+
}
|
|
329
|
+
.stage[data-kind="GATE"][data-state="passed"] {
|
|
330
|
+
font-size: 13px;
|
|
331
|
+
}
|
|
332
|
+
.stage[data-kind="GATE"][data-state="passed"] .name,
|
|
333
|
+
.stage[data-kind="GATE"][data-state="passed"] .icon {
|
|
334
|
+
color: var(--muted);
|
|
335
|
+
font-weight: 400;
|
|
336
|
+
}
|
|
337
|
+
.kind {
|
|
338
|
+
color: var(--muted);
|
|
339
|
+
font-size: 11px;
|
|
340
|
+
letter-spacing: 0.04em;
|
|
341
|
+
}
|
|
342
|
+
.kind.tag {
|
|
343
|
+
padding: 1px 6px;
|
|
344
|
+
border: 1px solid var(--line);
|
|
345
|
+
border-radius: 4px;
|
|
346
|
+
}
|
|
347
|
+
.took {
|
|
145
348
|
font:
|
|
146
349
|
12px / 1.45 ui-monospace,
|
|
147
350
|
SFMono-Regular,
|
|
148
351
|
Menlo,
|
|
149
352
|
monospace;
|
|
150
|
-
|
|
353
|
+
text-align: right;
|
|
354
|
+
white-space: nowrap;
|
|
355
|
+
}
|
|
356
|
+
.panel-title {
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
gap: 8px;
|
|
360
|
+
}
|
|
361
|
+
.sheet {
|
|
362
|
+
display: grid;
|
|
363
|
+
grid-template-columns: max-content minmax(0, 1fr);
|
|
364
|
+
gap: 5px 14px;
|
|
365
|
+
margin: 12px 0 0;
|
|
366
|
+
font-size: 13px;
|
|
151
367
|
}
|
|
152
|
-
.
|
|
153
|
-
margin: 2px 0 0 16px;
|
|
368
|
+
.sheet dt {
|
|
154
369
|
color: var(--muted);
|
|
155
370
|
}
|
|
156
|
-
.
|
|
371
|
+
.sheet dd {
|
|
372
|
+
margin: 0;
|
|
373
|
+
overflow-wrap: anywhere;
|
|
374
|
+
font-variant-numeric: tabular-nums;
|
|
375
|
+
}
|
|
376
|
+
.warn {
|
|
157
377
|
color: var(--run);
|
|
158
378
|
}
|
|
159
|
-
|
|
160
|
-
|
|
379
|
+
.bad {
|
|
380
|
+
color: var(--bad);
|
|
381
|
+
}
|
|
382
|
+
.callout {
|
|
383
|
+
margin: 12px 0 0;
|
|
384
|
+
padding: 8px 10px;
|
|
385
|
+
border-left: 3px solid currentColor;
|
|
386
|
+
border-radius: 4px;
|
|
387
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
388
|
+
font-size: 12px;
|
|
161
389
|
white-space: pre-wrap;
|
|
162
390
|
}
|
|
163
|
-
.
|
|
164
|
-
|
|
391
|
+
.chips {
|
|
392
|
+
display: flex;
|
|
393
|
+
flex-wrap: wrap;
|
|
394
|
+
gap: 4px;
|
|
395
|
+
}
|
|
396
|
+
.chip {
|
|
397
|
+
padding: 1px 7px;
|
|
398
|
+
border: 1px solid var(--line);
|
|
399
|
+
border-radius: 999px;
|
|
400
|
+
font-size: 12px;
|
|
401
|
+
font-variant-numeric: tabular-nums;
|
|
402
|
+
}
|
|
403
|
+
.chip.warn {
|
|
404
|
+
border-color: color-mix(in srgb, var(--run) 50%, transparent);
|
|
405
|
+
}
|
|
406
|
+
.log {
|
|
407
|
+
max-height: 260px;
|
|
408
|
+
padding: 6px 8px;
|
|
409
|
+
overflow: auto;
|
|
410
|
+
border: 1px solid var(--line);
|
|
411
|
+
border-radius: 6px;
|
|
412
|
+
}
|
|
413
|
+
.log li {
|
|
414
|
+
display: grid;
|
|
415
|
+
grid-template-columns: 70px minmax(0, 1fr);
|
|
416
|
+
gap: 8px;
|
|
417
|
+
padding: 1px 0;
|
|
418
|
+
}
|
|
419
|
+
.log .at {
|
|
165
420
|
color: var(--muted);
|
|
166
421
|
}
|
|
422
|
+
.log-head {
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: baseline;
|
|
425
|
+
justify-content: space-between;
|
|
426
|
+
}
|
|
427
|
+
button.link {
|
|
428
|
+
padding: 0;
|
|
429
|
+
border: 0;
|
|
430
|
+
background: none;
|
|
431
|
+
color: var(--accent);
|
|
432
|
+
font: inherit;
|
|
433
|
+
font-size: 12px;
|
|
434
|
+
cursor: pointer;
|
|
435
|
+
}
|
|
436
|
+
.runtime {
|
|
437
|
+
margin: 10px 0 0;
|
|
438
|
+
color: var(--muted);
|
|
439
|
+
font-size: 12px;
|
|
440
|
+
overflow-wrap: anywhere;
|
|
441
|
+
}
|
|
442
|
+
:is(.stage, .segment, button.link):focus-visible {
|
|
443
|
+
outline: 2px solid var(--accent);
|
|
444
|
+
outline-offset: 1px;
|
|
445
|
+
}
|
package/dist/static/timeline.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* The dashboard's pure logic: a run's events grouped into one block per stage
|
|
4
|
-
* execution,
|
|
5
|
-
* is to the browser.
|
|
4
|
+
* execution, those blocks grouped into launches, and the formats of durations,
|
|
5
|
+
* costs, tokens and agent settings. Served as is to the browser.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/** @typedef {import("@clemrig/flow").RunEvent} RunEvent */
|
|
@@ -171,6 +171,47 @@ export function duration(ms) {
|
|
|
171
171
|
return `${hours ? `${hours} h ` : ""}${minutes} min ${seconds % 60} s`;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* A duration in its two largest units: `1 h 07 min`, `7 min 02 s`, `12 s`,
|
|
176
|
+
* `< 1 s`.
|
|
177
|
+
* @param {number | null} ms
|
|
178
|
+
* @returns {string}
|
|
179
|
+
*/
|
|
180
|
+
export function brief(ms) {
|
|
181
|
+
if (ms === null) return "—";
|
|
182
|
+
const seconds = Math.floor(ms / 1000);
|
|
183
|
+
if (seconds < 1) return "< 1 s";
|
|
184
|
+
const hours = Math.floor(seconds / 3600);
|
|
185
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
186
|
+
const pad = (/** @type {number} */ n) => String(n).padStart(2, "0");
|
|
187
|
+
if (hours) return `${hours} h ${pad(minutes)} min`;
|
|
188
|
+
if (minutes) return `${minutes} min ${pad(seconds % 60)} s`;
|
|
189
|
+
return `${seconds} s`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A token count: exact below ten thousand, then in thousands or millions.
|
|
194
|
+
* @param {number} count
|
|
195
|
+
* @returns {string}
|
|
196
|
+
*/
|
|
197
|
+
export function tokenCount(count) {
|
|
198
|
+
if (count >= 1e6) return `${(count / 1e6).toFixed(1)} M`;
|
|
199
|
+
if (count >= 1e4) return `${Math.round(count / 1e3)} k`;
|
|
200
|
+
return count.toLocaleString("en-US");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A call's tokens: read without the cache, written, read from and written to
|
|
205
|
+
* the cache; provisional figures, which the call's end replaces, say so.
|
|
206
|
+
* @param {TokenCounts} tokens
|
|
207
|
+
* @param {boolean} provisional
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
export function tokenLine(tokens, provisional) {
|
|
211
|
+
const { input, output, cacheRead, cacheWrite } = tokens;
|
|
212
|
+
return `${tokenCount(input)} in · ${tokenCount(output)} out · ${tokenCount(cacheRead)} cache read · ${tokenCount(cacheWrite)} cache write${provisional ? " (provisional)" : ""}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
174
215
|
/** Setting names that are not plain words. */
|
|
175
216
|
const settingLabels = /** @type {Record<string, string>} */ ({
|
|
176
217
|
mcpServers: "MCP servers",
|
|
@@ -189,13 +230,13 @@ function settingLabel(key) {
|
|
|
189
230
|
}
|
|
190
231
|
|
|
191
232
|
/**
|
|
192
|
-
* An agent call's settings as its runtime recorded them, whatever the runtime
|
|
193
|
-
* its thinking,
|
|
194
|
-
* tools even when empty, the others only with items.
|
|
233
|
+
* An agent call's settings as its runtime recorded them, whatever the runtime,
|
|
234
|
+
* as labelled rows: its thinking, other single values and timeout in one, then
|
|
235
|
+
* one per list, its tools even when empty, the others only with items.
|
|
195
236
|
* @param {AgentSettings} settings
|
|
196
|
-
* @returns {string[]}
|
|
237
|
+
* @returns {{ label: string; value: string }[]}
|
|
197
238
|
*/
|
|
198
|
-
export function
|
|
239
|
+
export function settingRows(settings) {
|
|
199
240
|
const entries = Object.entries(settings);
|
|
200
241
|
const values = entries.flatMap(([key, value]) =>
|
|
201
242
|
key === "thinking" ||
|
|
@@ -207,17 +248,23 @@ export function settingsLines(settings) {
|
|
|
207
248
|
);
|
|
208
249
|
const lists = entries.flatMap(([key, value]) => {
|
|
209
250
|
if (!Array.isArray(value) || (!value.length && key !== "tools")) return [];
|
|
210
|
-
const label = settingLabel(key);
|
|
251
|
+
const label = key === "tools" ? "allowed tools" : settingLabel(key);
|
|
211
252
|
return [
|
|
212
|
-
|
|
253
|
+
{
|
|
254
|
+
label: `${label[0]?.toUpperCase()}${label.slice(1)}`,
|
|
255
|
+
value: value.join(", ") || "none",
|
|
256
|
+
},
|
|
213
257
|
];
|
|
214
258
|
});
|
|
215
259
|
return [
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
260
|
+
{
|
|
261
|
+
label: "Settings",
|
|
262
|
+
value: [
|
|
263
|
+
`thinking ${settings.thinking}`,
|
|
264
|
+
...values,
|
|
265
|
+
`timeout ${brief(settings.timeoutMs)}`,
|
|
266
|
+
].join(" · "),
|
|
267
|
+
},
|
|
221
268
|
...lists,
|
|
222
269
|
];
|
|
223
270
|
}
|
|
@@ -232,3 +279,151 @@ export function cost(usd, partial) {
|
|
|
232
279
|
const value = usd === null ? "—" : `${usd.toFixed(4)} USD`;
|
|
233
280
|
return partial ? `${value} (partial)` : value;
|
|
234
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* An estimated cost as the Kanban shows it: in dollars, `≥` while some call
|
|
285
|
+
* announced none, a dash while none announced any.
|
|
286
|
+
* @param {number | null} usd
|
|
287
|
+
* @param {boolean} partial
|
|
288
|
+
* @returns {string}
|
|
289
|
+
*/
|
|
290
|
+
export function dollars(usd, partial) {
|
|
291
|
+
if (usd === null) return "—";
|
|
292
|
+
return `${partial ? "≥ " : ""}$${usd.toFixed(2)}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @typedef {"passed" | "correction" | "failed" | "running"} State */
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* How a stage execution went: running, failed, sent back for a correction, or
|
|
299
|
+
* passed (the run's last stage included).
|
|
300
|
+
* @param {Block} block
|
|
301
|
+
* @returns {State}
|
|
302
|
+
*/
|
|
303
|
+
export function stateOf(block) {
|
|
304
|
+
if (block.status === "running" || block.status === "failed")
|
|
305
|
+
return block.status;
|
|
306
|
+
return block.decision?.verdict === "correction" ? "correction" : "passed";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* A stage execution's time so far: its duration once ended.
|
|
311
|
+
* @param {Block} block
|
|
312
|
+
* @param {number} now
|
|
313
|
+
* @returns {number}
|
|
314
|
+
*/
|
|
315
|
+
export function spent(block, now) {
|
|
316
|
+
return block.durationMs ?? Math.max(0, now - Date.parse(block.startedAt));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @typedef {object} Launch
|
|
321
|
+
* @property {number} number From 1: the run's first execution, then each resumption.
|
|
322
|
+
* @property {string} startedAt
|
|
323
|
+
* @property {string | null} endedAt Null while it runs.
|
|
324
|
+
* @property {"running" | "failed" | "completed"} status
|
|
325
|
+
* @property {string} [cause] Why a failed launch stopped.
|
|
326
|
+
* @property {{ block: Block; index: number }[]} stages Its stage executions, with their index in the run.
|
|
327
|
+
*/
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The stage execution to show until the operator chooses one: the last, which
|
|
331
|
+
* is the one running or the one where the run stopped, never an earlier
|
|
332
|
+
* launch's failure once the run was resumed.
|
|
333
|
+
* @param {Block[]} blocks
|
|
334
|
+
* @returns {number | null}
|
|
335
|
+
*/
|
|
336
|
+
export function shownStage(blocks) {
|
|
337
|
+
return blocks.length ? blocks.length - 1 : null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The run's launches: its first execution, then one per resumption. A failed
|
|
342
|
+
* stage ends a launch, since it ends the run, and only a failed run is
|
|
343
|
+
* resumed: the next stage starts the following launch. A failure reported
|
|
344
|
+
* once the run completed, when its completed state could not be saved,
|
|
345
|
+
* ends the same launch.
|
|
346
|
+
* @param {Block[]} blocks
|
|
347
|
+
* @returns {Launch[]}
|
|
348
|
+
*/
|
|
349
|
+
export function launches(blocks) {
|
|
350
|
+
/** @type {Launch[]} */
|
|
351
|
+
const found = [];
|
|
352
|
+
blocks.forEach((block, index) => {
|
|
353
|
+
let current = found.at(-1);
|
|
354
|
+
if (!current || current.status === "failed") {
|
|
355
|
+
current = {
|
|
356
|
+
number: found.length + 1,
|
|
357
|
+
startedAt: block.startedAt,
|
|
358
|
+
endedAt: null,
|
|
359
|
+
status: "running",
|
|
360
|
+
stages: [],
|
|
361
|
+
};
|
|
362
|
+
found.push(current);
|
|
363
|
+
}
|
|
364
|
+
current.stages.push({ block, index });
|
|
365
|
+
if (block.status === "failed" || block.status === "completed") {
|
|
366
|
+
current.status = block.status;
|
|
367
|
+
current.endedAt =
|
|
368
|
+
block.durationMs === null
|
|
369
|
+
? block.startedAt
|
|
370
|
+
: new Date(
|
|
371
|
+
Date.parse(block.startedAt) + block.durationMs,
|
|
372
|
+
).toISOString();
|
|
373
|
+
if (block.detail) current.cause = block.detail;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
return found;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* What a stage execution did, in one line: its failure, its agent's model,
|
|
381
|
+
* tools and cost, its decision, or its last journal message.
|
|
382
|
+
* @param {Block} block
|
|
383
|
+
* @returns {string}
|
|
384
|
+
*/
|
|
385
|
+
export function summary(block) {
|
|
386
|
+
if (block.status === "failed" && block.detail) return block.detail;
|
|
387
|
+
if (block.call?.model) {
|
|
388
|
+
const tools = `${block.tools.length} tool${block.tools.length === 1 ? "" : "s"}`;
|
|
389
|
+
const usd = block.call.usage?.costUsd;
|
|
390
|
+
return [
|
|
391
|
+
block.call.model,
|
|
392
|
+
tools,
|
|
393
|
+
...(usd === undefined ? [] : [dollars(usd, false)]),
|
|
394
|
+
].join(" · ");
|
|
395
|
+
}
|
|
396
|
+
if (block.decision) {
|
|
397
|
+
const { verdict, reason, destination } = block.decision;
|
|
398
|
+
return verdict === "correction"
|
|
399
|
+
? `back to ${destination}: ${reason.split("\n")[0]}`
|
|
400
|
+
: `accepted → ${destination ?? "end"}`;
|
|
401
|
+
}
|
|
402
|
+
return block.journal.at(-1)?.message ?? "";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The tools a stage execution used, by name, the most used first: how many
|
|
407
|
+
* calls, how long in all and how many failed.
|
|
408
|
+
* @param {Tool[]} tools
|
|
409
|
+
* @returns {{ name: string; count: number; durationMs: number; failed: number }[]}
|
|
410
|
+
*/
|
|
411
|
+
export function toolUse(tools) {
|
|
412
|
+
/** @type {Map<string, { name: string; count: number; durationMs: number; failed: number }>} */
|
|
413
|
+
const byName = new Map();
|
|
414
|
+
for (const tool of tools) {
|
|
415
|
+
const entry = byName.get(tool.name) ?? {
|
|
416
|
+
name: tool.name,
|
|
417
|
+
count: 0,
|
|
418
|
+
durationMs: 0,
|
|
419
|
+
failed: 0,
|
|
420
|
+
};
|
|
421
|
+
entry.count++;
|
|
422
|
+
entry.durationMs += tool.durationMs ?? 0;
|
|
423
|
+
if (tool.status === "failed") entry.failed++;
|
|
424
|
+
byName.set(tool.name, entry);
|
|
425
|
+
}
|
|
426
|
+
return [...byName.values()].sort(
|
|
427
|
+
(a, b) => b.count - a.count || a.name.localeCompare(b.name),
|
|
428
|
+
);
|
|
429
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clemrig/flow-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.5.
|
|
37
|
+
"@clemrig/flow": "0.5.1"
|
|
38
38
|
}
|
|
39
39
|
}
|