@clemrig/flow-ui 0.1.1 → 0.2.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 its runtimes, a timeline with one block per stage (agent tools and tokens, journal, decision), its correction counters and its result. It follows a running run as it goes. 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 its runtimes, a timeline with one block per stage (the agent call's model and settings, its tools and tokens, journal, decision), its correction counters and its result. It follows a running run as it goes. 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
 
@@ -32,6 +32,8 @@ console.log(dashboard.url);
32
32
  // later: await dashboard.close();
33
33
  ```
34
34
 
35
+ `dashboardHandler(options)` gives the same dashboard as a request handler, for a server that serves it beside other pages: it answers `(request, response, url)`, where `url` is the request's address relative to where the dashboard is mounted (`/api/runs` for `/runs/api/runs`), and refuses, like `startDashboard`, every method but GET and any `Host` other than its own address. The page's own addresses are relative, so it works under any path that ends with `/`.
36
+
35
37
  ## License
36
38
 
37
39
  MIT
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Public API of the dashboard: serve a read-only view of runs kept by `@clemrig/flow`. */
2
- export { type DashboardOptions, startDashboard } from "./server.js";
2
+ export { type DashboardHandler, type DashboardOptions, dashboardHandler, startDashboard, } from "./server.js";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Public API of the dashboard: serve a read-only view of runs kept by `@clemrig/flow`. */
2
- export { startDashboard } from "./server.js";
2
+ export { dashboardHandler, startDashboard, } from "./server.js";
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAE3F,OAAO,EAAyB,cAAc,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAE3F,OAAO,EAGL,gBAAgB,EAChB,cAAc,GACf,MAAM,aAAa,CAAC"}
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Server } from "node:http";
1
+ import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
2
2
  import { type RunState } from "@clemrig/flow";
3
3
  export interface DashboardOptions {
4
4
  /** Where the runs are kept. */
@@ -10,6 +10,17 @@ export interface DashboardOptions {
10
10
  /** A run's result lines; by default, its validated data as JSON. */
11
11
  presentResult?(state: Readonly<RunState>): string[];
12
12
  }
13
+ /**
14
+ * Answers one request; `url` is its address relative to where the dashboard is
15
+ * served: a server that mounts it under `/runs/` passes `/api/runs` for
16
+ * `/runs/api/runs`. The page's own addresses are relative.
17
+ */
18
+ export type DashboardHandler = (request: IncomingMessage, response: ServerResponse, url: URL) => Promise<void>;
19
+ /**
20
+ * The dashboard as a request handler, for a server that serves it beside other
21
+ * pages: it refuses other hosts than its own address and every method but GET.
22
+ */
23
+ export declare function dashboardHandler(options: DashboardOptions): Promise<DashboardHandler>;
13
24
  /** A read-only view of the runs on 127.0.0.1, on `port` or one chosen by the system. */
14
25
  export declare function startDashboard(options: DashboardOptions, listen?: number): Promise<{
15
26
  url: string;
package/dist/server.js CHANGED
@@ -52,18 +52,20 @@ function send(response, status, body, type = "text/plain; charset=utf-8") {
52
52
  });
53
53
  response.end(body);
54
54
  }
55
- /** A read-only view of the runs on 127.0.0.1, on `port` or one chosen by the system. */
56
- export async function startDashboard(options, listen = 0) {
57
- let port = 0;
55
+ /**
56
+ * The dashboard as a request handler, for a server that serves it beside other
57
+ * pages: it refuses other hosts than its own address and every method but GET.
58
+ */
59
+ export async function dashboardHandler(options) {
58
60
  const page = await loadPage();
59
- const handle = async (request, response) => {
61
+ return async (request, response, url) => {
60
62
  // Another Host means another site reached this port (DNS rebinding).
63
+ const port = request.socket.localPort;
61
64
  if (request.headers.host !== `127.0.0.1:${port}` &&
62
65
  request.headers.host !== `localhost:${port}`)
63
66
  return send(response, 403, "Forbidden");
64
67
  if (request.method !== "GET")
65
68
  return send(response, 405, "Method Not Allowed");
66
- const url = new URL(request.url ?? "/", "http://127.0.0.1");
67
69
  const file = page.get(url.pathname);
68
70
  if (file)
69
71
  return send(response, 200, file.body, file.type);
@@ -87,14 +89,26 @@ export async function startDashboard(options, listen = 0) {
87
89
  }
88
90
  return send(response, 200, JSON.stringify(body), "application/json");
89
91
  };
92
+ }
93
+ /** A read-only view of the runs on 127.0.0.1, on `port` or one chosen by the system. */
94
+ export async function startDashboard(options, listen = 0) {
95
+ const handle = await dashboardHandler(options);
90
96
  const server = createServer((request, response) => {
91
- handle(request, response).catch((error) => send(response, 500, error instanceof Error ? error.message : String(error)));
97
+ let url;
98
+ try {
99
+ url = new URL(request.url ?? "/", "http://127.0.0.1");
100
+ }
101
+ catch {
102
+ // An address that is not one, such as `http://[`.
103
+ return send(response, 400, "Bad Request");
104
+ }
105
+ handle(request, response, url).catch((error) => send(response, 500, error instanceof Error ? error.message : String(error)));
92
106
  });
93
107
  await new Promise((resolve, reject) => {
94
108
  server.once("error", reject);
95
109
  server.listen(listen, "127.0.0.1", () => resolve());
96
110
  });
97
- port = server.address().port;
111
+ const { port } = server.address();
98
112
  return {
99
113
  url: `http://127.0.0.1:${port}/`,
100
114
  server,
@@ -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;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"}
@@ -4,7 +4,7 @@
4
4
  * `follow.js` follows it. Text only, never HTML.
5
5
  */
6
6
  import { follower } from "./follow.js";
7
- import { cost, duration, timeline } from "./timeline.js";
7
+ import { cost, duration, settingsLines, timeline } from "./timeline.js";
8
8
 
9
9
  /** @typedef {import("@clemrig/flow").RunEvent} RunEvent */
10
10
  /** @typedef {import("./timeline.js").Block} Block */
@@ -142,6 +142,8 @@ function showBlock(block) {
142
142
  const { call } = block;
143
143
  if (call?.model)
144
144
  line(`${call.runtime} ${call.model} · session ${call.session}`);
145
+ if (call?.settings)
146
+ for (const setting of settingsLines(call.settings)) line(setting);
145
147
  for (const tool of block.tools)
146
148
  line(
147
149
  tool.status === "running"
@@ -150,7 +152,7 @@ function showBlock(block) {
150
152
  );
151
153
  if (call?.usage)
152
154
  line(
153
- `${call.scope === "session" ? "Session total" : "Call"}: ${tokenLine(call.usage)} · ${cost(call.usage.costUsd, false)}`,
155
+ `${call.scope === "session" ? "Session total" : "Call"}: ${tokenLine(call.usage)} · ${cost(call.usage.costUsd ?? null, false)}`,
154
156
  );
155
157
  else if (block.tokens)
156
158
  line(
@@ -210,7 +212,7 @@ function updated(message) {
210
212
 
211
213
  async function refreshRuns() {
212
214
  try {
213
- const { title, label, runs } = await load("/api/runs");
215
+ const { title, label, runs } = await load("api/runs");
214
216
  byId("title").textContent = title;
215
217
  document.title = title;
216
218
  byId("label").textContent = label;
@@ -37,7 +37,7 @@ export function follower({ load, schedule, cancel, show, fail }) {
37
37
  let answer;
38
38
  try {
39
39
  answer = await load(
40
- `/api/runs/${encodeURIComponent(target.id)}?after=${target.next}`,
40
+ `api/runs/${encodeURIComponent(target.id)}?after=${target.next}`,
41
41
  );
42
42
  } catch (error) {
43
43
  if (target !== current) return;
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>runs</title>
7
- <link rel="stylesheet" href="/style.css">
8
- <script type="module" src="/dashboard.js"></script>
7
+ <link rel="stylesheet" href="style.css">
8
+ <script type="module" src="dashboard.js"></script>
9
9
  </head>
10
10
  <body>
11
11
  <header><h1 id="title">runs</h1><span id="label"></span><span id="updated"></span></header>
@@ -1,11 +1,13 @@
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, and the formats of durations and costs. Served as is to the browser.
4
+ * execution, and the formats of durations, costs and agent settings. Served as
5
+ * is to the browser.
5
6
  */
6
7
 
7
8
  /** @typedef {import("@clemrig/flow").RunEvent} RunEvent */
8
9
  /** @typedef {import("@clemrig/flow").AgentActivity} AgentActivity */
10
+ /** @typedef {import("@clemrig/flow").AgentSettings} AgentSettings */
9
11
  /** @typedef {import("@clemrig/flow").TokenCounts} TokenCounts */
10
12
  /** @typedef {import("@clemrig/flow").AgentUsage} AgentUsage */
11
13
 
@@ -14,6 +16,7 @@
14
16
  * @property {string} [runtime]
15
17
  * @property {string} [model]
16
18
  * @property {string} [session]
19
+ * @property {AgentSettings} [settings] Absent from runs recorded before calls recorded their settings.
17
20
  * @property {"running" | "completed" | "failed"} status
18
21
  * @property {AgentUsage} [usage] As the runtime announced it at the end of the call.
19
22
  * @property {"call" | "session"} [scope] Whether `usage` is the call's own or its session's total.
@@ -72,8 +75,8 @@ function apply(target, activity) {
72
75
  switch (activity.kind) {
73
76
  case "call":
74
77
  if (activity.status === "started") {
75
- const { runtime, model, session } = activity;
76
- target.call = { runtime, model, session, status: "running" };
78
+ const { runtime, model, session, settings } = activity;
79
+ target.call = { runtime, model, session, settings, status: "running" };
77
80
  } else
78
81
  target.call = {
79
82
  ...target.call,
@@ -168,10 +171,61 @@ export function duration(ms) {
168
171
  return `${hours ? `${hours} h ` : ""}${minutes} min ${seconds % 60} s`;
169
172
  }
170
173
 
174
+ /** Setting names that are not plain words. */
175
+ const settingLabels = /** @type {Record<string, string>} */ ({
176
+ mcpServers: "MCP servers",
177
+ });
178
+
179
+ /**
180
+ * A setting's name as the operator reads it: `permissionMode` → `permission mode`.
181
+ * @param {string} key
182
+ * @returns {string}
183
+ */
184
+ function settingLabel(key) {
185
+ return (
186
+ settingLabels[key] ??
187
+ key.replace(/[A-Z]/g, (letter) => ` ${letter.toLowerCase()}`)
188
+ );
189
+ }
190
+
191
+ /**
192
+ * An agent call's settings as its runtime recorded them, whatever the runtime:
193
+ * its thinking, its other single values and its timeout, then each list, its
194
+ * tools even when empty, the others only with items.
195
+ * @param {AgentSettings} settings
196
+ * @returns {string[]}
197
+ */
198
+ export function settingsLines(settings) {
199
+ const entries = Object.entries(settings);
200
+ const values = entries.flatMap(([key, value]) =>
201
+ key === "thinking" ||
202
+ key === "timeoutMs" ||
203
+ value === undefined ||
204
+ Array.isArray(value)
205
+ ? []
206
+ : [`${settingLabel(key)} ${String(value)}`],
207
+ );
208
+ const lists = entries.flatMap(([key, value]) => {
209
+ if (!Array.isArray(value) || (!value.length && key !== "tools")) return [];
210
+ const label = settingLabel(key);
211
+ return [
212
+ `${label[0]?.toUpperCase()}${label.slice(1)}: ${value.join(", ") || "none"}`,
213
+ ];
214
+ });
215
+ return [
216
+ [
217
+ `thinking ${settings.thinking}`,
218
+ ...values,
219
+ `timeout ${duration(settings.timeoutMs)}`,
220
+ ].join(" · "),
221
+ ...lists,
222
+ ];
223
+ }
224
+
171
225
  /**
172
226
  * An estimated cost as the runtimes announced it; unknown without any measure.
173
227
  * @param {number | null} usd
174
- * @param {boolean} partial Some agent call announced no usage.
228
+ * @param {boolean} partial Some agent call announced no cost.
175
229
  * @returns {string}
176
230
  */
177
231
  export function cost(usd, partial) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clemrig/flow-ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.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.2.0"
37
+ "@clemrig/flow": "0.4.0"
38
38
  }
39
39
  }