@warlock.js/devtools 5.32.0 → 5.34.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +9 -12
  2. package/cjs/ai-source-lia4YS30.cjs +212 -0
  3. package/cjs/ai-source-lia4YS30.cjs.map +1 -0
  4. package/cjs/index.cjs +2 -727
  5. package/cjs/{server-CUdjfOkO.cjs → server-BNlyhwiR.cjs} +60 -1
  6. package/cjs/server-BNlyhwiR.cjs.map +1 -0
  7. package/cjs/src-CiNStw0i.cjs +699 -0
  8. package/cjs/src-CiNStw0i.cjs.map +1 -0
  9. package/esm/devtools-connector.d.mts.map +1 -1
  10. package/esm/devtools-connector.mjs +6 -0
  11. package/esm/devtools-connector.mjs.map +1 -1
  12. package/esm/index.d.mts +2 -1
  13. package/esm/process-panel.mjs +16 -0
  14. package/esm/process-panel.mjs.map +1 -0
  15. package/esm/server/ai-routes.mjs +32 -0
  16. package/esm/server/ai-routes.mjs.map +1 -0
  17. package/esm/server/index.mjs +4 -0
  18. package/esm/server/index.mjs.map +1 -1
  19. package/esm/server/process-routes.mjs +11 -0
  20. package/esm/server/process-routes.mjs.map +1 -0
  21. package/esm/server/requests-routes.mjs +1 -0
  22. package/esm/server/requests-routes.mjs.map +1 -1
  23. package/esm/server/stream-route.mjs +4 -0
  24. package/esm/server/stream-route.mjs.map +1 -1
  25. package/esm/sources/ai-source.d.mts +29 -0
  26. package/esm/sources/ai-source.d.mts.map +1 -0
  27. package/esm/sources/ai-source.mjs +136 -0
  28. package/esm/sources/ai-source.mjs.map +1 -0
  29. package/esm/sources/log-source.mjs +1 -1
  30. package/esm/sources/query-source.mjs +1 -1
  31. package/esm/types.d.mts +4 -0
  32. package/esm/types.d.mts.map +1 -1
  33. package/llms-full.txt +85 -1
  34. package/llms.txt +1 -1
  35. package/package.json +9 -5
  36. package/skills/devtools-overview/SKILL.md +85 -1
  37. package/ui/app.css +13 -0
  38. package/ui/app.js +213 -16
  39. package/ui/index.html +2 -0
  40. package/cjs/index.cjs.map +0 -1
  41. package/cjs/server-CUdjfOkO.cjs.map +0 -1
package/llms-full.txt CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ---
10
10
  name: devtools-overview
11
- description: 'Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.'
11
+ description: 'Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, AI traces per HTTP request (model, tokens, cost, cache hits, tools, retries, truncation, compaction), the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.'
12
12
  ---
13
13
 
14
14
  # `@warlock.js/devtools` — overview
@@ -58,6 +58,78 @@ of it would fail to resolve there.
58
58
  Work outside a request (boot, scheduler ticks, queue jobs) goes to a separate
59
59
  background list, so it never clutters a request's timeline.
60
60
 
61
+ ## AI runs alongside HTTP requests
62
+
63
+ The **AI** tab and each request's **AI runs** section display completed traces
64
+ from optional `@warlock.js/ai-panoptic`. If it cannot be imported, the tab shows
65
+ install/restart guidance and the other panels keep working.
66
+
67
+ ```bash
68
+ warlock add devtools
69
+ npm install @warlock.js/ai @warlock.js/ai-panoptic
70
+ ```
71
+
72
+ At app boot, enable observation, then restart `warlock dev`:
73
+
74
+ ```ts
75
+ import { ai } from "@warlock.js/ai";
76
+ import "@warlock.js/ai-panoptic";
77
+ ai.config({ panoptic: { observeAll: true } });
78
+ ```
79
+
80
+ No `dashboard: true` or extra listener port is needed. Existing `attach`,
81
+ middleware and explicit collection through Panoptic's built-in collector also
82
+ feed the panel. An installed but unobserved flow produces no trace.
83
+
84
+ - `DevtoolsAiRun` and `DevtoolsAiEvent` are public types from
85
+ `@warlock.js/devtools`; `DevtoolsStreamEvent` includes the `ai` event.
86
+ - Read all runs with `GET /__warlock/api/ai`, or filter by an exact HTTP id with
87
+ `GET /__warlock/api/ai?requestId=<encoded-id>`. Responses include `available`,
88
+ `runs` and a root-usage `aggregate`; unavailable responses contain `message`.
89
+ - Inspect tools/timings, retry attempts and compaction records with
90
+ `GET /__warlock/api/ai/runs/<encoded-trace-id>` (404 when evicted). The existing
91
+ stream emits `ai` events with `{ traceId, requestId? }`.
92
+ - Model, input/output/cache-read/cache-write tokens and historical USD cost come
93
+ from the trace, never a guessed price table. Unknown cost is unpriced. Root
94
+ usage includes children already; do not sum all spans for a run total.
95
+ - Cache hits count trips reporting positive provider `cachedTokens`.
96
+ Semantic-cache hits have no explicit report flag and remain unreported; zero
97
+ tokens alone do not prove a hit. `—` means unreported rather than zero.
98
+ - `incomplete` status or normalized `AGENT_INCOMPLETE` / `AgentIncompleteError`
99
+ marks truncation. Recovered truncated trips still contribute to their count.
100
+ - Compaction metadata includes applied/skipped agent decisions and provider
101
+ compaction-block counts; summary content is omitted. Orchestrator result-only
102
+ compaction cannot be reconstructed from a report-only trace.
103
+
104
+ A same-origin browser tool can consume the public types directly:
105
+
106
+ ```ts
107
+ import type { DevtoolsAiEvent, DevtoolsAiRun, DevtoolsStreamEvent } from "@warlock.js/devtools";
108
+
109
+ const response = await fetch("/__warlock/api/ai?requestId=request-42");
110
+ const { runs }: { runs: DevtoolsAiRun[] } = await response.json();
111
+ console.log(runs.map((run) => [run.traceId, run.usage, run.cost]));
112
+
113
+ function handleStream(event: DevtoolsStreamEvent): void {
114
+ if (event.type === "ai") {
115
+ const update: DevtoolsAiEvent = event.data;
116
+ console.log(update.traceId, update.requestId);
117
+ }
118
+ }
119
+ handleStream({ type: "ai", data: { traceId: "run-42", requestId: "request-42" } });
120
+ ```
121
+
122
+ Request correlation reads core's current request id at collection time, before
123
+ exporters. Concurrent/late runs can retain their async context. No id means
124
+ background; `sessionId` is never substituted. Replayed reports outside the
125
+ request cannot recover its context automatically.
126
+
127
+ Devtools retains its own content-free in-memory store, at most `maxRequests`
128
+ traces (200 by default), independently of HTTP request retention. Clear/shutdown
129
+ empties this view, leaving application exporter/cache stores alone. Boot-time
130
+ and persisted traces are not replayed. The source only attaches in development;
131
+ AI routes keep the same socket-loopback guard as every other panel.
132
+
61
133
  ## API docs
62
134
 
63
135
  `/__warlock/docs` renders the app's OpenAPI 3.1 document with [Scalar](https://github.com/scalar/scalar); the dashboard's **API** tab shows the same page and has a **Scalar | Swagger UI** switch (remembered in the browser). `/__warlock/docs/swagger` renders the same document with [Swagger UI](https://github.com/swagger-api/swagger-ui). The document comes from `GET /__warlock/api/openapi.json`, built in-process by core's `getDevelopmentOpenApiDocument()` from the routes already registered, so it is the same generator as `warlock generate.openapi` and follows your edits with no CLI run. It covers API routes (not pages): paths, parameters, request bodies from Seal validation, responses from each handler's `responseSchema`, `401`/`422` and bearer/cookie security from `authMiddleware()`. See core's `generate-openapi` topic for what is documented and how to declare it.
@@ -99,4 +171,16 @@ why the dashboard is development-only and loopback-only.
99
171
  cascade `dataSourceRegistry.on("query")`, core `mailEvents.onCaptured()` and
100
172
  the cache `invalidated` event are public APIs that also work without devtools.
101
173
 
174
+ ## Process supervision panel
175
+
176
+ Run `npx warlock add process` to create an app-owned supervisor and
177
+ `src/app/processes/devtools.ts`, exporting `processPanel = { list: () =>
178
+ processes.list() }`. Devtools discovers this registration only in development.
179
+ The **Processes** tab refreshes `GET /__warlock/api/processes`: PIDs, workload
180
+ categories, labels, display commands, limits, enforcement, and peak memory.
181
+ It excludes ownership tokens. Without registration the list is empty. The
182
+ endpoint retains development/loopback guards and provides no stop action.
183
+ Use the `supervise-processes` skill of `@warlock.js/process` for launch,
184
+ recovery and stop APIs; configure one owner/registry per application instance.
185
+
102
186
 
package/llms.txt CHANGED
@@ -6,4 +6,4 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [devtools-overview](@warlock.js/devtools/devtools-overview/SKILL.md): Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.
9
+ - [devtools-overview](@warlock.js/devtools/devtools-overview/SKILL.md): Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, AI traces per HTTP request (model, tokens, cost, cache hits, tools, retries, truncation, compaction), the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.
package/package.json CHANGED
@@ -5,12 +5,16 @@
5
5
  "environment": "server"
6
6
  },
7
7
  "peerDependencies": {
8
- "@warlock.js/cache": "5.32.0",
9
- "@warlock.js/cascade": "5.32.0",
10
- "@warlock.js/core": "5.32.0",
11
- "@warlock.js/logger": "5.32.0"
8
+ "@warlock.js/ai-panoptic": "5.34.0",
9
+ "@warlock.js/cache": "5.34.0",
10
+ "@warlock.js/cascade": "5.34.0",
11
+ "@warlock.js/core": "5.34.0",
12
+ "@warlock.js/logger": "5.34.0"
12
13
  },
13
14
  "peerDependenciesMeta": {
15
+ "@warlock.js/ai-panoptic": {
16
+ "optional": true
17
+ },
14
18
  "@warlock.js/cache": {
15
19
  "optional": true
16
20
  },
@@ -34,7 +38,7 @@
34
38
  ],
35
39
  "author": "hassanzohdy",
36
40
  "license": "MIT",
37
- "version": "5.32.0",
41
+ "version": "5.34.0",
38
42
  "main": "./cjs/index.cjs",
39
43
  "module": "./esm/index.mjs",
40
44
  "types": "./esm/index.d.mts",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: devtools-overview
3
- description: 'Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.'
3
+ description: 'Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, AI traces per HTTP request (model, tokens, cost, cache hits, tools, retries, truncation, compaction), the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.'
4
4
  ---
5
5
 
6
6
  # `@warlock.js/devtools` — overview
@@ -50,6 +50,78 @@ of it would fail to resolve there.
50
50
  Work outside a request (boot, scheduler ticks, queue jobs) goes to a separate
51
51
  background list, so it never clutters a request's timeline.
52
52
 
53
+ ## AI runs alongside HTTP requests
54
+
55
+ The **AI** tab and each request's **AI runs** section display completed traces
56
+ from optional `@warlock.js/ai-panoptic`. If it cannot be imported, the tab shows
57
+ install/restart guidance and the other panels keep working.
58
+
59
+ ```bash
60
+ warlock add devtools
61
+ npm install @warlock.js/ai @warlock.js/ai-panoptic
62
+ ```
63
+
64
+ At app boot, enable observation, then restart `warlock dev`:
65
+
66
+ ```ts
67
+ import { ai } from "@warlock.js/ai";
68
+ import "@warlock.js/ai-panoptic";
69
+ ai.config({ panoptic: { observeAll: true } });
70
+ ```
71
+
72
+ No `dashboard: true` or extra listener port is needed. Existing `attach`,
73
+ middleware and explicit collection through Panoptic's built-in collector also
74
+ feed the panel. An installed but unobserved flow produces no trace.
75
+
76
+ - `DevtoolsAiRun` and `DevtoolsAiEvent` are public types from
77
+ `@warlock.js/devtools`; `DevtoolsStreamEvent` includes the `ai` event.
78
+ - Read all runs with `GET /__warlock/api/ai`, or filter by an exact HTTP id with
79
+ `GET /__warlock/api/ai?requestId=<encoded-id>`. Responses include `available`,
80
+ `runs` and a root-usage `aggregate`; unavailable responses contain `message`.
81
+ - Inspect tools/timings, retry attempts and compaction records with
82
+ `GET /__warlock/api/ai/runs/<encoded-trace-id>` (404 when evicted). The existing
83
+ stream emits `ai` events with `{ traceId, requestId? }`.
84
+ - Model, input/output/cache-read/cache-write tokens and historical USD cost come
85
+ from the trace, never a guessed price table. Unknown cost is unpriced. Root
86
+ usage includes children already; do not sum all spans for a run total.
87
+ - Cache hits count trips reporting positive provider `cachedTokens`.
88
+ Semantic-cache hits have no explicit report flag and remain unreported; zero
89
+ tokens alone do not prove a hit. `—` means unreported rather than zero.
90
+ - `incomplete` status or normalized `AGENT_INCOMPLETE` / `AgentIncompleteError`
91
+ marks truncation. Recovered truncated trips still contribute to their count.
92
+ - Compaction metadata includes applied/skipped agent decisions and provider
93
+ compaction-block counts; summary content is omitted. Orchestrator result-only
94
+ compaction cannot be reconstructed from a report-only trace.
95
+
96
+ A same-origin browser tool can consume the public types directly:
97
+
98
+ ```ts
99
+ import type { DevtoolsAiEvent, DevtoolsAiRun, DevtoolsStreamEvent } from "@warlock.js/devtools";
100
+
101
+ const response = await fetch("/__warlock/api/ai?requestId=request-42");
102
+ const { runs }: { runs: DevtoolsAiRun[] } = await response.json();
103
+ console.log(runs.map((run) => [run.traceId, run.usage, run.cost]));
104
+
105
+ function handleStream(event: DevtoolsStreamEvent): void {
106
+ if (event.type === "ai") {
107
+ const update: DevtoolsAiEvent = event.data;
108
+ console.log(update.traceId, update.requestId);
109
+ }
110
+ }
111
+ handleStream({ type: "ai", data: { traceId: "run-42", requestId: "request-42" } });
112
+ ```
113
+
114
+ Request correlation reads core's current request id at collection time, before
115
+ exporters. Concurrent/late runs can retain their async context. No id means
116
+ background; `sessionId` is never substituted. Replayed reports outside the
117
+ request cannot recover its context automatically.
118
+
119
+ Devtools retains its own content-free in-memory store, at most `maxRequests`
120
+ traces (200 by default), independently of HTTP request retention. Clear/shutdown
121
+ empties this view, leaving application exporter/cache stores alone. Boot-time
122
+ and persisted traces are not replayed. The source only attaches in development;
123
+ AI routes keep the same socket-loopback guard as every other panel.
124
+
53
125
  ## API docs
54
126
 
55
127
  `/__warlock/docs` renders the app's OpenAPI 3.1 document with [Scalar](https://github.com/scalar/scalar); the dashboard's **API** tab shows the same page and has a **Scalar | Swagger UI** switch (remembered in the browser). `/__warlock/docs/swagger` renders the same document with [Swagger UI](https://github.com/swagger-api/swagger-ui). The document comes from `GET /__warlock/api/openapi.json`, built in-process by core's `getDevelopmentOpenApiDocument()` from the routes already registered, so it is the same generator as `warlock generate.openapi` and follows your edits with no CLI run. It covers API routes (not pages): paths, parameters, request bodies from Seal validation, responses from each handler's `responseSchema`, `401`/`422` and bearer/cookie security from `authMiddleware()`. See core's `generate-openapi` topic for what is documented and how to declare it.
@@ -90,3 +162,15 @@ why the dashboard is development-only and loopback-only.
90
162
  - Building your own tooling on the same hooks: core `registerTracingHooks()`,
91
163
  cascade `dataSourceRegistry.on("query")`, core `mailEvents.onCaptured()` and
92
164
  the cache `invalidated` event are public APIs that also work without devtools.
165
+
166
+ ## Process supervision panel
167
+
168
+ Run `npx warlock add process` to create an app-owned supervisor and
169
+ `src/app/processes/devtools.ts`, exporting `processPanel = { list: () =>
170
+ processes.list() }`. Devtools discovers this registration only in development.
171
+ The **Processes** tab refreshes `GET /__warlock/api/processes`: PIDs, workload
172
+ categories, labels, display commands, limits, enforcement, and peak memory.
173
+ It excludes ownership tokens. Without registration the list is empty. The
174
+ endpoint retains development/loopback guards and provides no stop action.
175
+ Use the `supervise-processes` skill of `@warlock.js/process` for launch,
176
+ recovery and stop APIs; configure one owner/registry per application instance.
package/ui/app.css CHANGED
@@ -361,3 +361,16 @@ details summary {
361
361
  grid-template-columns: 120px 1fr;
362
362
  }
363
363
  }
364
+
365
+ .ai-span {
366
+ margin: 0.75rem 0 0.75rem 0.75rem;
367
+ padding-left: 0.75rem;
368
+ border-left: 2px solid currentColor;
369
+ }
370
+ .ai-span summary {
371
+ cursor: pointer;
372
+ }
373
+ .toolbar input {
374
+ flex: 1;
375
+ min-width: 12rem;
376
+ }
package/ui/app.js CHANGED
@@ -174,7 +174,7 @@ function queryRow(query, requestId) {
174
174
  return details;
175
175
  }
176
176
 
177
- function requestDetail(request) {
177
+ async function requestDetail(request) {
178
178
  const section = el("section", { class: "detail" }, [
179
179
  el("h2", {
180
180
  text: `${request.method} ${request.path} · ${value(request.status)} · ${value(request.durationMs)} ms`,
@@ -225,6 +225,7 @@ function requestDetail(request) {
225
225
  ),
226
226
  );
227
227
  else section.append(empty("No mail captured."));
228
+ section.append(el("h3", { text: "AI runs" }), await requestAiView(request.id));
228
229
  return section;
229
230
  }
230
231
 
@@ -242,11 +243,186 @@ async function requestsView() {
242
243
  );
243
244
  const detail =
244
245
  state.selectedRequest && state.details.get(state.selectedRequest)
245
- ? requestDetail(state.details.get(state.selectedRequest))
246
+ ? await requestDetail(state.details.get(state.selectedRequest))
246
247
  : el("div", { class: "panel" }, empty("Select a request to inspect it."));
247
248
  return el("div", { class: "layout" }, [list, detail]);
248
249
  }
249
250
 
251
+ function aiUsage(usage, cost) {
252
+ const cacheSemantics =
253
+ usage.inputIncludesCache === false
254
+ ? "input excludes cache tokens"
255
+ : "input includes cache tokens";
256
+ return `${value(usage.input)} input · ${value(usage.output)} output · ${value(usage.cachedTokens)} cache read · ${value(usage.cacheWriteTokens)} cache write · ${cost == null ? "unpriced" : `$${cost.toFixed(6)} USD`} (${cacheSemantics})`;
257
+ }
258
+
259
+ function aiRunSummary(run) {
260
+ return el("div", {}, [
261
+ el("div", {
262
+ class: "row-top",
263
+ text: `${run.name} · ${run.status}${run.incomplete ? " · Incomplete / truncated" : ""} · ${run.duration} ms`,
264
+ }),
265
+ el("div", { class: "meta", text: run.models.join(", ") || "Model unreported" }),
266
+ el("p", { text: aiUsage(run.usage, run.cost) }),
267
+ el("div", {
268
+ class: "meta",
269
+ text: `${run.toolCalls} tool calls · ${run.retries} retries · ${value(run.cacheHits)} provider cache-hit trips · ${run.truncatedTrips} truncated trips · ${run.compactions} applied / ${run.skippedCompactions} skipped compactions · ${run.providerCompactions} provider compactions`,
270
+ }),
271
+ el("div", {
272
+ class: "meta",
273
+ text: `${run.traceId} · ${run.requestId ? `Request ${run.requestId}` : "Background / no request id"}`,
274
+ }),
275
+ ]);
276
+ }
277
+
278
+ function aiGuidance(data) {
279
+ return empty(
280
+ data.message ||
281
+ "Install @warlock.js/ai and @warlock.js/ai-panoptic, restart warlock dev, and enable Panoptic observation.",
282
+ );
283
+ }
284
+
285
+ async function requestAiView(requestId) {
286
+ try {
287
+ const data = await api(`api/ai?requestId=${encodeURIComponent(requestId)}`);
288
+ if (!data.available) return aiGuidance(data);
289
+ const section = el("div");
290
+ if (!data.runs.length) section.append(empty("No AI runs collected for this request yet."));
291
+ for (const run of data.runs) {
292
+ section.append(
293
+ el(
294
+ "button",
295
+ {
296
+ class: "row",
297
+ type: "button",
298
+ onclick: () => {
299
+ state.selectedAiRun = run.traceId;
300
+ state.aiRequestId = requestId;
301
+ setTab("ai");
302
+ },
303
+ },
304
+ aiRunSummary(run),
305
+ ),
306
+ );
307
+ }
308
+ return section;
309
+ } catch (error) {
310
+ return errorView(error);
311
+ }
312
+ }
313
+
314
+ function aiSpan(span, rootStartedAt) {
315
+ const incomplete =
316
+ span.status === "incomplete" ||
317
+ span.error?.type === "AgentIncompleteError" ||
318
+ span.error?.type === "AGENT_INCOMPLETE";
319
+ const start = Date.parse(span.startedAt) - Date.parse(rootStartedAt);
320
+ const section = el("details", { class: "ai-span", open: "" }, [
321
+ el("summary", {
322
+ text: `${span.type} ${span.name} · ${span.status}${incomplete ? " · Incomplete / truncated" : ""} · +${start} ms · ${span.duration} ms`,
323
+ }),
324
+ el("p", { class: "meta", text: `${span.startedAt} → ${span.endedAt}` }),
325
+ el("pre", {
326
+ text: JSON.stringify(
327
+ { usage: span.usage, attributes: span.attributes, error: span.error },
328
+ null,
329
+ 2,
330
+ ),
331
+ }),
332
+ ]);
333
+ span.children.forEach((child) => section.append(aiSpan(child, rootStartedAt)));
334
+ return section;
335
+ }
336
+
337
+ async function aiView() {
338
+ const section = el("section", { class: "detail" }, el("h2", { text: "AI runs" }));
339
+ const input = el("input", {
340
+ type: "text",
341
+ placeholder: "HTTP request id (empty for all runs)",
342
+ "aria-label": "Filter AI runs by HTTP request id",
343
+ value: state.aiRequestId || "",
344
+ });
345
+ section.append(
346
+ el(
347
+ "form",
348
+ {
349
+ class: "toolbar",
350
+ onsubmit: (event) => {
351
+ event.preventDefault();
352
+ state.aiRequestId = input.value;
353
+ state.selectedAiRun = undefined;
354
+ render();
355
+ },
356
+ },
357
+ [input, el("button", { type: "submit", text: "Filter / refresh" })],
358
+ ),
359
+ );
360
+ const data = await api(
361
+ `api/ai${state.aiRequestId ? `?requestId=${encodeURIComponent(state.aiRequestId)}` : ""}`,
362
+ );
363
+ if (!data.available) {
364
+ section.append(aiGuidance(data));
365
+ return section;
366
+ }
367
+ if (!data.runs.length) {
368
+ section.append(
369
+ empty(
370
+ "No AI runs yet. Enable Panoptic observation and execute a flow; runs appear when collection completes.",
371
+ ),
372
+ );
373
+ return section;
374
+ }
375
+ const costs = data.runs.filter((run) => run.cost != null);
376
+ section.append(
377
+ el("p", {
378
+ text: `${data.aggregate.traces} runs · ${data.aggregate.usage.input} input tokens · ${data.aggregate.usage.output} output tokens · ${costs.length ? `$${costs.reduce((sum, run) => sum + run.cost, 0).toFixed(6)} USD (reported cost)` : "unpriced"}`,
379
+ }),
380
+ );
381
+ const list = el("div", { class: "panel list" });
382
+ for (const run of data.runs)
383
+ list.append(
384
+ el(
385
+ "button",
386
+ {
387
+ class: `row${run.traceId === state.selectedAiRun ? " is-selected" : ""}`,
388
+ type: "button",
389
+ onclick: () => {
390
+ state.selectedAiRun = run.traceId;
391
+ render();
392
+ },
393
+ },
394
+ aiRunSummary(run),
395
+ ),
396
+ );
397
+ const detail = el("div", { class: "panel detail" });
398
+ const selected = data.runs.find((run) => run.traceId === state.selectedAiRun);
399
+ if (selected) {
400
+ detail.append(aiRunSummary(selected));
401
+ if (selected.requestId)
402
+ detail.append(
403
+ el("button", {
404
+ type: "button",
405
+ text: "Open HTTP request",
406
+ onclick: async () => {
407
+ state.tab = "requests";
408
+ tabs.forEach((button) =>
409
+ button.classList.toggle("is-active", button.dataset.tab === "requests"),
410
+ );
411
+ await showRequest(selected.requestId);
412
+ },
413
+ }),
414
+ );
415
+ try {
416
+ const trace = await api(`api/ai/runs/${encodeURIComponent(selected.traceId)}`);
417
+ detail.append(aiSpan(trace.root, trace.startedAt));
418
+ } catch (error) {
419
+ detail.append(errorView(error));
420
+ }
421
+ } else detail.append(empty("Select a run to inspect tools, retries and compaction metadata."));
422
+ section.append(el("div", { class: "layout" }, [list, detail]));
423
+ return section;
424
+ }
425
+
250
426
  function queriesView() {
251
427
  const requests = [...state.details.values()].sort((a, b) => b.startedAt - a.startedAt);
252
428
  // This session-only tab intentionally lists queries from request details fetched so far.
@@ -535,23 +711,42 @@ function apiView() {
535
711
  return section;
536
712
  }
537
713
 
714
+ let renderVersion = 0;
715
+
716
+ async function processesView() {
717
+ const rows = await api("api/processes");
718
+ const section = el("section", { class: "card detail" }, [el("h2", { text: "Processes" })]);
719
+ section.append(el("button", { text: "Refresh", onclick: () => render() }));
720
+ if (!rows.length)
721
+ section.append(empty("No supervised processes. Configure with warlock add process."));
722
+ for (const row of rows) {
723
+ section.append(el("pre", { text: JSON.stringify(row, null, 2) }));
724
+ }
725
+ return section;
726
+ }
727
+
538
728
  async function render() {
729
+ const version = ++renderVersion;
539
730
  try {
540
731
  const view =
541
- state.tab === "requests"
542
- ? await requestsView()
543
- : state.tab === "queries"
544
- ? queriesView()
545
- : state.tab === "mail"
546
- ? await mailView()
547
- : state.tab === "logs"
548
- ? await logsView()
549
- : state.tab === "routes"
550
- ? await routesView()
551
- : apiView();
552
- app.replaceChildren(view);
732
+ state.tab === "ai"
733
+ ? await aiView()
734
+ : state.tab === "requests"
735
+ ? await requestsView()
736
+ : state.tab === "queries"
737
+ ? queriesView()
738
+ : state.tab === "mail"
739
+ ? await mailView()
740
+ : state.tab === "logs"
741
+ ? await logsView()
742
+ : state.tab === "routes"
743
+ ? await routesView()
744
+ : state.tab === "processes"
745
+ ? await processesView()
746
+ : apiView();
747
+ if (version === renderVersion) app.replaceChildren(view);
553
748
  } catch (error) {
554
- app.replaceChildren(errorView(error));
749
+ if (version === renderVersion) app.replaceChildren(errorView(error));
555
750
  }
556
751
  }
557
752
 
@@ -562,16 +757,18 @@ document.querySelector("#clear").addEventListener("click", async () => {
562
757
  state.requests = [];
563
758
  state.details.clear();
564
759
  state.selectedRequest = undefined;
760
+ state.selectedAiRun = undefined;
565
761
  render();
566
762
  } catch (error) {
567
763
  app.prepend(errorView(error));
568
764
  }
569
765
  });
570
766
  const events = new EventSource("api/stream");
571
- for (const type of ["request", "mail", "log"])
767
+ for (const type of ["request", "mail", "log", "ai"])
572
768
  events.addEventListener(type, (event) => {
573
769
  try {
574
770
  const data = JSON.parse(event.data);
771
+ if (type === "ai" && (state.tab === "ai" || state.tab === "requests")) render();
575
772
  if (type === "request") {
576
773
  state.requests = [data, ...state.requests.filter((item) => item.id !== data.id)].slice(
577
774
  0,
package/ui/index.html CHANGED
@@ -11,10 +11,12 @@
11
11
  <h1>Warlock Devtools</h1>
12
12
  <nav aria-label="Dashboard sections">
13
13
  <button class="tab is-active" data-tab="requests">Requests</button>
14
+ <button class="tab" data-tab="ai">AI</button>
14
15
  <button class="tab" data-tab="queries">Queries</button>
15
16
  <button class="tab" data-tab="mail">Mail</button>
16
17
  <button class="tab" data-tab="logs">Logs</button>
17
18
  <button class="tab" data-tab="routes">Routes</button>
19
+ <button class="tab" data-tab="processes">Processes</button>
18
20
  <button class="tab" data-tab="api">API</button>
19
21
  </nav>
20
22
  <button id="clear" class="clear" type="button">Clear</button>