@clemrig/flow-ui 0.1.2 → 0.3.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 (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.
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, 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
 
@@ -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":"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"}
@@ -212,11 +212,15 @@ function updated(message) {
212
212
 
213
213
  async function refreshRuns() {
214
214
  try {
215
- const { title, label, runs } = await load("/api/runs");
215
+ /** @type {{ title: string; label: string; runs: Summary[] }} */
216
+ const { title, label, runs } = await load("api/runs");
216
217
  byId("title").textContent = title;
217
218
  document.title = title;
218
219
  byId("label").textContent = label;
219
220
  showRuns(runs);
221
+ // A resumed run goes on under the same ID, running or already ended again.
222
+ const listed = runs.find((run) => run.id === selected);
223
+ if (listed?.updatedAt) follow.listed(selected, listed.updatedAt);
220
224
  updated(`Updated ${new Date().toLocaleTimeString()}`);
221
225
  } catch (error) {
222
226
  unavailable(error);
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * Follows the selected run: asks only for the events not received yet, one
4
4
  * request after the answer to the previous one, drops an answer for a selection
5
- * that has changed since, and stops once the run has ended or does not exist.
5
+ * that has changed since, and stops once the run has ended or does not exist,
6
+ * until the run list shows it updated since, as a resumed run is.
6
7
  * Requests and timers are given, so that this logic is tested without a browser.
7
8
  */
8
9
 
@@ -10,7 +11,7 @@
10
11
 
11
12
  /**
12
13
  * @typedef {object} Answer
13
- * @property {{ status: string }} run
14
+ * @property {{ status: string; updatedAt?: string }} run
14
15
  * @property {RunEvent[]} events Events from the index the request asked for.
15
16
  * @property {number} next The index to ask for next.
16
17
  */
@@ -26,7 +27,15 @@
26
27
  */
27
28
  export function follower({ load, schedule, cancel, show, fail }) {
28
29
  /** One selection: a new object each time, so that an old answer never matches. */
29
- let current = { id: "", events: /** @type {RunEvent[]} */ ([]), next: 0 };
30
+ let current = {
31
+ id: "",
32
+ events: /** @type {RunEvent[]} */ ([]),
33
+ next: 0,
34
+ /** Whether following stopped, on the run's end or absence. */
35
+ stopped: false,
36
+ /** The run's last update, as the last answer showed it. */
37
+ updatedAt: /** @type {string | undefined} */ (undefined),
38
+ };
30
39
  /** @type {Timer | undefined} */
31
40
  let timer;
32
41
 
@@ -37,7 +46,7 @@ export function follower({ load, schedule, cancel, show, fail }) {
37
46
  let answer;
38
47
  try {
39
48
  answer = await load(
40
- `/api/runs/${encodeURIComponent(target.id)}?after=${target.next}`,
49
+ `api/runs/${encodeURIComponent(target.id)}?after=${target.next}`,
41
50
  );
42
51
  } catch (error) {
43
52
  if (target !== current) return;
@@ -45,14 +54,17 @@ export function follower({ load, schedule, cancel, show, fail }) {
45
54
  // A run that does not exist will not appear later.
46
55
  const status = /** @type {{ status?: number }} */ (error).status;
47
56
  if (status !== 404) timer = schedule(() => refresh(target));
57
+ else target.stopped = true;
48
58
  return;
49
59
  }
50
60
  if (target !== current) return;
51
61
  target.events.push(...answer.events);
52
62
  target.next = answer.next;
63
+ target.updatedAt = answer.run.updatedAt;
53
64
  show(answer, target.events);
54
65
  if (answer.run.status === "running")
55
66
  timer = schedule(() => refresh(target));
67
+ else target.stopped = true;
56
68
  }
57
69
 
58
70
  return {
@@ -60,7 +72,31 @@ export function follower({ load, schedule, cancel, show, fail }) {
60
72
  select(id) {
61
73
  if (id === current.id) return;
62
74
  cancel(timer);
63
- current = { id, events: [], next: 0 };
75
+ current = {
76
+ id,
77
+ events: [],
78
+ next: 0,
79
+ stopped: false,
80
+ updatedAt: undefined,
81
+ };
82
+ refresh(current);
83
+ },
84
+ /**
85
+ * The run list shows `id` last updated at `updatedAt`: following starts
86
+ * again, from the events already received, if it had stopped on an earlier
87
+ * state of the selected run, as when it was resumed since, even if the
88
+ * resumed run already ended again.
89
+ * @param {string} id
90
+ * @param {string} updatedAt
91
+ */
92
+ listed(id, updatedAt) {
93
+ if (
94
+ id !== current.id ||
95
+ !current.stopped ||
96
+ updatedAt === current.updatedAt
97
+ )
98
+ return;
99
+ current.stopped = false;
64
100
  refresh(current);
65
101
  },
66
102
  };
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clemrig/flow-ui",
3
- "version": "0.1.2",
3
+ "version": "0.3.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.3.0"
37
+ "@clemrig/flow": "0.5.0"
38
38
  }
39
39
  }