omkit 0.8.12 → 0.8.14
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/core/ActionRun.d.ts +2 -0
- package/dist/core/ActionRun.js +2 -0
- package/dist/core/ExecutionTree.js +33 -0
- package/dist/core/action.js +6 -2
- package/dist/core/step.js +14 -2
- package/dist/core/types.d.ts +2 -0
- package/dist/output/writers/ReportWriter.js +73 -18
- package/dist/output/writers/report/report.css +30 -0
- package/dist/output/writers/report/report.js +294 -39
- package/dist/output/writers/views.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,7 +185,7 @@ flakyBackgroundJob().result.catch((e) => console.warn("job failed, carrying on",
|
|
|
185
185
|
|
|
186
186
|
Every action's output, events, asserts, snapshots, and lifecycle land on **one timeline** — streamed live to the terminal (curated milestones) and written to a per-run folder:
|
|
187
187
|
|
|
188
|
-
- `result.json` — the run's tree, per-node status, verdict, and curated artifacts. Each node also carries what its definition declared about itself: the `.describe()` summary, the `.mcp()` exposure, and the `.args()` schema as JSON Schema.
|
|
188
|
+
- `result.json` — the run's tree, per-node status, verdict, and curated artifacts. Each node also carries what its definition declared about itself: the `.describe()` summary, the `.mcp()` exposure, and the `.args()` schema as JSON Schema. Each node also records what it returned (`value`, or a `valueNote` when it was too large or not JSON) and `calledAt`, the line that launched it, so a built-in step (a `prompt`, a `command`) points back at your code rather than omkit's, and a `step(...)` is defined at the line where you wrote it.
|
|
189
189
|
- `raw.jsonl` — every entry, machine-readable.
|
|
190
190
|
- `main.log` + one `.log` per action — the human-readable timelines.
|
|
191
191
|
- `events.log` / `asserts.log` / `snapshots.log` / `artifacts.log` — cross-cutting rollups.
|
package/dist/core/ActionRun.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
47
47
|
readonly tags: string[];
|
|
48
48
|
/** What the definition said about itself (`.describe()`, `.mcp()`, `.args()`); for the record. */
|
|
49
49
|
meta: ActionMeta | undefined;
|
|
50
|
+
/** `file:line` of the call that launched this node (absent for the root). */
|
|
51
|
+
calledAt: string | undefined;
|
|
50
52
|
/** The captured args this node ran with (for the log header); set at run start. */
|
|
51
53
|
args: readonly unknown[];
|
|
52
54
|
readonly children: ActionRun[];
|
package/dist/core/ActionRun.js
CHANGED
|
@@ -20,6 +20,8 @@ export class ActionRun {
|
|
|
20
20
|
tags = [];
|
|
21
21
|
/** What the definition said about itself (`.describe()`, `.mcp()`, `.args()`); for the record. */
|
|
22
22
|
meta;
|
|
23
|
+
/** `file:line` of the call that launched this node (absent for the root). */
|
|
24
|
+
calledAt;
|
|
23
25
|
/** The captured args this node ran with (for the log header); set at run start. */
|
|
24
26
|
args = [];
|
|
25
27
|
children = [];
|
|
@@ -139,6 +139,7 @@ export class ExecutionTree {
|
|
|
139
139
|
bubble: (message) => this.append(parent, "child", id, message),
|
|
140
140
|
});
|
|
141
141
|
node.meta = spec.meta;
|
|
142
|
+
node.calledAt = spec.calledAt;
|
|
142
143
|
parent.children.push(node);
|
|
143
144
|
this.registry.push(node);
|
|
144
145
|
// Launch reference — the parent's pointer to the child's own (absolute) log, plus where
|
|
@@ -378,6 +379,7 @@ export class ExecutionTree {
|
|
|
378
379
|
uuid: node.uuid,
|
|
379
380
|
name: node.name,
|
|
380
381
|
definedAt: node.definedAt,
|
|
382
|
+
...(node.calledAt === undefined ? {} : { calledAt: node.calledAt }),
|
|
381
383
|
path: node.path,
|
|
382
384
|
parentId: node.parentId,
|
|
383
385
|
tags: [...node.tags],
|
|
@@ -389,6 +391,7 @@ export class ExecutionTree {
|
|
|
389
391
|
duration: node.duration,
|
|
390
392
|
logFile: this.logFile(node.path),
|
|
391
393
|
...this.metaView(node),
|
|
394
|
+
...valueView(node.value),
|
|
392
395
|
children: node.children.map((child) => this.nodeView(child)),
|
|
393
396
|
};
|
|
394
397
|
}
|
|
@@ -504,6 +507,36 @@ const messageOf = (error) => error instanceof Error ? (error.stack ?? error.mess
|
|
|
504
507
|
* body may hand back anything — a capability handle, a cyclic object — and the run must
|
|
505
508
|
* still write its record; a value that does not survive JSON is dropped, not fatal.
|
|
506
509
|
*/
|
|
510
|
+
/** A node's return value is recorded up to this many characters of JSON. */
|
|
511
|
+
const VALUE_CAP = 512 * 1024;
|
|
512
|
+
/**
|
|
513
|
+
* What a node returned, for the record: the value itself when JSON can carry it and it fits,
|
|
514
|
+
* otherwise a one-line note saying what it was — so "returned a Page" is still on the record.
|
|
515
|
+
*/
|
|
516
|
+
function valueView(value) {
|
|
517
|
+
if (value === undefined)
|
|
518
|
+
return {};
|
|
519
|
+
const kind = value === null
|
|
520
|
+
? "null"
|
|
521
|
+
: typeof value === "object"
|
|
522
|
+
? (value.constructor?.name ?? "object")
|
|
523
|
+
: typeof value;
|
|
524
|
+
let text;
|
|
525
|
+
try {
|
|
526
|
+
text = JSON.stringify(value);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
text = undefined;
|
|
530
|
+
}
|
|
531
|
+
if (text === undefined)
|
|
532
|
+
return { valueNote: `${kind} (not JSON)` };
|
|
533
|
+
if (text.length > VALUE_CAP) {
|
|
534
|
+
return {
|
|
535
|
+
valueNote: `${kind}, ${Math.round(text.length / 1024)} KB of JSON — too large to record`,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
return { value: JSON.parse(text) };
|
|
539
|
+
}
|
|
507
540
|
function serialisable(value) {
|
|
508
541
|
if (value === undefined)
|
|
509
542
|
return undefined;
|
package/dist/core/action.js
CHANGED
|
@@ -55,7 +55,9 @@ class Builder {
|
|
|
55
55
|
};
|
|
56
56
|
const exec = body;
|
|
57
57
|
const create = (...args) => {
|
|
58
|
-
|
|
58
|
+
// where this launch was written — for a battery, the user's call rather than its definition
|
|
59
|
+
const calledAt = callerSite();
|
|
60
|
+
const spec = { name, args, tags: [], body: exec, definedAt, calledAt, meta };
|
|
59
61
|
return ExecutionTree.require().launch(spec);
|
|
60
62
|
};
|
|
61
63
|
return Object.assign(create, {
|
|
@@ -121,7 +123,9 @@ class ArgsBuilder {
|
|
|
121
123
|
};
|
|
122
124
|
const exec = body;
|
|
123
125
|
const create = (...args) => {
|
|
124
|
-
|
|
126
|
+
// where this launch was written — for a battery, the user's call rather than its definition
|
|
127
|
+
const calledAt = callerSite();
|
|
128
|
+
const spec = { name, args, tags: [], body: exec, definedAt, calledAt, meta };
|
|
125
129
|
return ExecutionTree.require().launch(spec);
|
|
126
130
|
};
|
|
127
131
|
return Object.assign(create, {
|
package/dist/core/step.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { callerSite } from "../foundation/callsite.js";
|
|
2
|
+
import { ExecutionTree } from "./ExecutionTree.js";
|
|
2
3
|
/**
|
|
3
4
|
* An inline anonymous action: creates a child node under the ambient current
|
|
4
5
|
* action and runs `fn` as that node — own id/path/log, tags, error boundary — with
|
|
@@ -7,6 +8,17 @@ import { action } from "./action.js";
|
|
|
7
8
|
* units use `action(...)`.
|
|
8
9
|
*/
|
|
9
10
|
export function step(name, fn) {
|
|
11
|
+
// An inline action is defined where it is written — the `step(...)` call — so that is its
|
|
12
|
+
// site, not this file (which is what routing through `action(name).run(fn)` would record).
|
|
13
|
+
const site = callerSite();
|
|
14
|
+
const spec = {
|
|
15
|
+
name,
|
|
16
|
+
args: [],
|
|
17
|
+
tags: [],
|
|
18
|
+
body: fn,
|
|
19
|
+
definedAt: site,
|
|
20
|
+
calledAt: site,
|
|
21
|
+
};
|
|
10
22
|
// `.result` already resolves the value and rejects on failure/cancel — exactly step's contract.
|
|
11
|
-
return
|
|
23
|
+
return ExecutionTree.require().launch(spec).result;
|
|
12
24
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -241,6 +241,8 @@ export interface LaunchSpec {
|
|
|
241
241
|
readonly definedAt: string | undefined;
|
|
242
242
|
/** What the definition declared about itself — carried onto the node for the run record. */
|
|
243
243
|
readonly meta?: ActionMeta;
|
|
244
|
+
/** `file:line` of the call that launched this instance; carried onto the node for the record. */
|
|
245
|
+
readonly calledAt?: string;
|
|
244
246
|
}
|
|
245
247
|
/**
|
|
246
248
|
* A definition's self-description, as it travels onto each node it launches: what
|
|
@@ -41,6 +41,7 @@ export async function writeReport(file, input) {
|
|
|
41
41
|
projectRoot: root,
|
|
42
42
|
sources: definitionSources(input.run.root, root),
|
|
43
43
|
code: await definitionCode(input.run.root),
|
|
44
|
+
library: librarySites(input.run.root),
|
|
44
45
|
generatedAt: Date.now(),
|
|
45
46
|
run: input.run,
|
|
46
47
|
entries: input.entries,
|
|
@@ -223,15 +224,81 @@ async function projectRoot(folder) {
|
|
|
223
224
|
function definitionSources(root, projectDir) {
|
|
224
225
|
const out = {};
|
|
225
226
|
const visit = (n) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
for (const site of [n.definedAt, n.calledAt]) {
|
|
228
|
+
if (!site || site in out)
|
|
229
|
+
continue;
|
|
230
|
+
const file = site.replace(/:\d+(:\d+)?$/, "");
|
|
231
|
+
out[site] = path.relative(projectDir, file).split(path.sep).join("/") || file;
|
|
229
232
|
}
|
|
230
233
|
n.children.forEach(visit);
|
|
231
234
|
};
|
|
232
235
|
visit(root);
|
|
233
236
|
return out;
|
|
234
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* The deepest statement containing `pos`. For an action's `.run(…)` line that is the whole
|
|
240
|
+
* `export const x = action(…)…` statement (nothing nests the chain); for a call site inside a
|
|
241
|
+
* body — `await step("x", async () => …)`, `await command(…)` — it is that one statement, the
|
|
242
|
+
* callback included, rather than the om that happens to contain it.
|
|
243
|
+
*/
|
|
244
|
+
function innermostStatement(ts, sf, pos) {
|
|
245
|
+
let found;
|
|
246
|
+
const visit = (node) => {
|
|
247
|
+
if (pos < node.getStart(sf) || pos >= node.getEnd())
|
|
248
|
+
return;
|
|
249
|
+
const parent = node.parent;
|
|
250
|
+
if (parent?.statements?.includes(node))
|
|
251
|
+
found = node;
|
|
252
|
+
ts.forEachChild(node, visit);
|
|
253
|
+
};
|
|
254
|
+
ts.forEachChild(sf, visit);
|
|
255
|
+
return found;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The 1-based line range to show for a site: its innermost statement, or — when the file will
|
|
259
|
+
* not parse or nothing encloses the line — a window around it.
|
|
260
|
+
*/
|
|
261
|
+
function excerptRange(ts, file, text, line, total) {
|
|
262
|
+
try {
|
|
263
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
264
|
+
const lineStart = sf.getPositionOfLineAndCharacter(Math.min(line, sf.getLineStarts().length) - 1, 0);
|
|
265
|
+
// the line's first token, so indentation doesn't fall outside the statement it opens
|
|
266
|
+
const pos = lineStart + (/^\s*/.exec(text.slice(lineStart))?.[0].length ?? 0);
|
|
267
|
+
const stmt = innermostStatement(ts, sf, pos);
|
|
268
|
+
if (stmt) {
|
|
269
|
+
return {
|
|
270
|
+
from: sf.getLineAndCharacterOfPosition(stmt.getStart(sf, true)).line + 1,
|
|
271
|
+
to: sf.getLineAndCharacterOfPosition(stmt.getEnd()).line + 1,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
/* fall back to the window around the line */
|
|
277
|
+
}
|
|
278
|
+
return { from: Math.max(1, line - 10), to: Math.min(total, line + 20) };
|
|
279
|
+
}
|
|
280
|
+
/** omkit's own package folder — `src/output/writers` (or `dist/…`) is three levels in. */
|
|
281
|
+
const OMKIT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
282
|
+
/**
|
|
283
|
+
* Sites inside omkit itself or an installed package — a battery's definition or a step's
|
|
284
|
+
* wrapper — where the viewer shows the user's call instead of library source.
|
|
285
|
+
*/
|
|
286
|
+
function librarySites(root) {
|
|
287
|
+
const out = new Set();
|
|
288
|
+
const inside = (file) => {
|
|
289
|
+
const rel = path.relative(OMKIT_DIR, file);
|
|
290
|
+
return (!rel.startsWith("..") && !path.isAbsolute(rel)) || /[\\/]node_modules[\\/]/.test(file);
|
|
291
|
+
};
|
|
292
|
+
const visit = (n) => {
|
|
293
|
+
for (const site of [n.definedAt, n.calledAt]) {
|
|
294
|
+
if (site && inside(site.replace(/:\d+(:\d+)?$/, "")))
|
|
295
|
+
out.add(site);
|
|
296
|
+
}
|
|
297
|
+
n.children.forEach(visit);
|
|
298
|
+
};
|
|
299
|
+
visit(root);
|
|
300
|
+
return [...out];
|
|
301
|
+
}
|
|
235
302
|
/** Longest excerpt shown per definition; a body past this is cut, and says so. */
|
|
236
303
|
const CODE_LINES = 300;
|
|
237
304
|
/**
|
|
@@ -245,6 +312,8 @@ async function definitionCode(root) {
|
|
|
245
312
|
const visit = (n) => {
|
|
246
313
|
if (n.definedAt)
|
|
247
314
|
sites.add(n.definedAt);
|
|
315
|
+
if (n.calledAt)
|
|
316
|
+
sites.add(n.calledAt);
|
|
248
317
|
n.children.forEach(visit);
|
|
249
318
|
};
|
|
250
319
|
visit(root);
|
|
@@ -267,21 +336,7 @@ async function definitionCode(root) {
|
|
|
267
336
|
continue;
|
|
268
337
|
const lang = /\.[cm]?tsx?$/i.test(file) ? "ts" : "js";
|
|
269
338
|
const lines = text.split(/\r?\n/);
|
|
270
|
-
|
|
271
|
-
let to = Math.min(lines.length, line + 20);
|
|
272
|
-
try {
|
|
273
|
-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
274
|
-
const pos = sf.getPositionOfLineAndCharacter(Math.min(line, sf.getLineStarts().length) - 1, 0);
|
|
275
|
-
const stmt = sf.statements.find((s) => s.getStart(sf) <= pos && pos < s.getEnd()) ??
|
|
276
|
-
sf.statements.find((s) => s.getFullStart() <= pos && pos < s.getEnd());
|
|
277
|
-
if (stmt) {
|
|
278
|
-
from = sf.getLineAndCharacterOfPosition(stmt.getStart(sf, true)).line + 1;
|
|
279
|
-
to = sf.getLineAndCharacterOfPosition(stmt.getEnd()).line + 1;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
/* fall back to the window around the line */
|
|
284
|
-
}
|
|
339
|
+
const { from, to } = excerptRange(ts, file, text, line, lines.length);
|
|
285
340
|
const truncated = to - from + 1 > CODE_LINES;
|
|
286
341
|
out[site] = {
|
|
287
342
|
start: from,
|
|
@@ -337,6 +337,36 @@ a.icon-btn { text-decoration: none; }
|
|
|
337
337
|
.tk-k { color: var(--tk-k); }
|
|
338
338
|
.tk-f { color: var(--tk-f); }
|
|
339
339
|
.tk-t { color: var(--tk-t); }
|
|
340
|
+
.tk-p { color: var(--tk-f); }
|
|
341
|
+
|
|
342
|
+
/* ── popover tabs, prompt panel, flow snapshot strip ───── */
|
|
343
|
+
.code-pop .cp-tabs { display: flex; gap: 2px; padding: 0 8px; border-bottom: 1px solid var(--line); background: var(--panel-2); }
|
|
344
|
+
.code-pop .cp-tabs .tab { padding: 6px 9px; font-size: 12px; }
|
|
345
|
+
.pp { padding: 12px 14px 14px; display: grid; gap: 10px; }
|
|
346
|
+
.pp-q { font-size: 14px; font-weight: 600; white-space: pre-wrap; }
|
|
347
|
+
.pp-grid { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 4px 10px; font-size: 12px; }
|
|
348
|
+
.pp-k { color: var(--text-3); font-size: 12px; }
|
|
349
|
+
.pp-choices { margin: 0; padding-left: 18px; }
|
|
350
|
+
.pp-choices li.picked { color: var(--accent); font-weight: 600; }
|
|
351
|
+
.pp-answer { display: grid; gap: 6px; justify-items: start; }
|
|
352
|
+
.pp-answer pre { width: 100%; }
|
|
353
|
+
.tip .tprompt { font-weight: 600; white-space: pre-wrap; margin-bottom: 4px; }
|
|
354
|
+
.flow-shots .shot { cursor: pointer; }
|
|
355
|
+
.flow-shots .shot rect { fill: var(--panel-2); stroke: var(--line); }
|
|
356
|
+
.flow-shots .shot:hover rect { stroke: var(--accent); }
|
|
357
|
+
.flow-shots .shot text { fill: var(--text-3); font: 600 9px var(--mono); pointer-events: none; }
|
|
358
|
+
|
|
359
|
+
/* ── returned values ───────────────────────────────────── */
|
|
360
|
+
.code-btn path { fill: none; stroke: var(--text-2); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
|
|
361
|
+
.code-btn:hover path { stroke: var(--accent); }
|
|
362
|
+
.cp-tools { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 10px 6px 14px; border-bottom: 1px solid var(--line-soft); font-size: 12px; }
|
|
363
|
+
.btn.small { padding: 3px 9px; font-size: 12px; }
|
|
364
|
+
.btn.small svg { width: 13px; height: 13px; }
|
|
365
|
+
.code-box { border: 1px solid var(--line-soft); border-radius: 8px; background: var(--panel-2); overflow: auto; max-height: 60vh; }
|
|
366
|
+
.code-box .cp-code, .code-pop .cp-code { font: 11.5px/1.65 var(--mono); padding: 8px 0; min-width: max-content; }
|
|
367
|
+
.code-box .cl { display: grid; grid-template-columns: 40px auto; white-space: pre; }
|
|
368
|
+
.code-box .ln { color: var(--text-3); text-align: right; padding-right: 12px; user-select: none; }
|
|
369
|
+
.code-box .lc { padding-right: 14px; }
|
|
340
370
|
|
|
341
371
|
@media (max-width: 1100px) {
|
|
342
372
|
.bottom { grid-template-columns: minmax(0, 1fr); }
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
info: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v5M12 8h.01",
|
|
44
44
|
locate: "M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM12 2v3M12 19v3M2 12h3M19 12h3M12 10.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z",
|
|
45
45
|
code: "M8 7l-5 5 5 5M16 7l5 5-5 5",
|
|
46
|
+
ret: "M9 10l-4 4 4 4M5 14h11a4 4 0 0 0 4-4V5",
|
|
46
47
|
external: "M14 4h6v6M20 4l-9 9M18 14v6H4V6h6",
|
|
47
48
|
};
|
|
48
49
|
const svgIcon = (name, extra) =>
|
|
@@ -505,9 +506,12 @@
|
|
|
505
506
|
r.append("rect").attr("class", "bar").attr("rx", 5).attr("y", BAR_Y).attr("height", BAR_H);
|
|
506
507
|
r.append("text").attr("class", "bar-label").attr("y", BAR_Y + BAR_H / 2 + 4);
|
|
507
508
|
r.append("g").attr("class", "marks");
|
|
508
|
-
const cb = r.append("g").attr("class", "code-btn");
|
|
509
|
+
const cb = r.append("g").attr("class", "code-btn src");
|
|
509
510
|
cb.append("circle").attr("r", 9);
|
|
510
511
|
cb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
|
|
512
|
+
const rb = r.append("g").attr("class", "code-btn ret");
|
|
513
|
+
rb.append("circle").attr("r", 9);
|
|
514
|
+
rb.append("path").attr("d", ICON.ret).attr("transform", "translate(-6,-6) scale(0.5)");
|
|
511
515
|
return r;
|
|
512
516
|
})
|
|
513
517
|
.attr("transform", (_n, i) => `translate(0,${i * ROW_H})`)
|
|
@@ -547,18 +551,33 @@
|
|
|
547
551
|
// …and, for a block spanning the chart, inside it right after its label
|
|
548
552
|
const cx =
|
|
549
553
|
right + 8 <= W - 10 ? right + 8 : left - 8 >= 10 ? left - 8 : Math.min(W - 12, (flip ? x0 : lx + tw) + 14);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
554
|
+
// buttons stack away from the block: rightward when they sit after it, leftward before it
|
|
555
|
+
const dir = right + 8 <= W - 10 || left - 8 < 10 ? 1 : -1;
|
|
556
|
+
const src = hasCode(n);
|
|
557
|
+
const row = d3.select(this.parentNode);
|
|
558
|
+
row
|
|
559
|
+
.select("g.code-btn.src")
|
|
560
|
+
.style("display", src ? null : "none")
|
|
553
561
|
.attr("transform", `translate(${cx},${BAR_Y + BAR_H / 2})`);
|
|
562
|
+
row
|
|
563
|
+
.select("g.code-btn.ret")
|
|
564
|
+
.style("display", hasValue(n) ? null : "none")
|
|
565
|
+
.attr("transform", `translate(${src ? cx + dir * 21 : cx},${BAR_Y + BAR_H / 2})`);
|
|
554
566
|
});
|
|
555
|
-
g.select("g.code-btn")
|
|
567
|
+
g.select("g.code-btn.src")
|
|
556
568
|
.on("click", (ev, n) => {
|
|
557
569
|
ev.stopPropagation();
|
|
558
570
|
toggleCode(n, ev);
|
|
559
571
|
})
|
|
560
572
|
.on("mouseenter", (ev) => showTip(ev, "Show source"))
|
|
561
573
|
.on("mouseleave", hideTip);
|
|
574
|
+
g.select("g.code-btn.ret")
|
|
575
|
+
.on("click", (ev, n) => {
|
|
576
|
+
ev.stopPropagation();
|
|
577
|
+
toggleCode(n, ev, "value", true);
|
|
578
|
+
})
|
|
579
|
+
.on("mouseenter", (ev, n) => showTip(ev, `Returned: <span class="mono">${esc(valuePreview(n, 80))}</span>`))
|
|
580
|
+
.on("mouseleave", hideTip);
|
|
562
581
|
g.selectAll("rect.hit, text.bar-label").call(bindNode);
|
|
563
582
|
|
|
564
583
|
g.select("g.marks").each(function (n) {
|
|
@@ -733,7 +752,10 @@
|
|
|
733
752
|
const h = d3.hierarchy(model.root, (n) => n.children);
|
|
734
753
|
const CW = 210;
|
|
735
754
|
const CH = 46;
|
|
736
|
-
|
|
755
|
+
// cards that took snapshots carry a strip of them underneath, so leave room for it
|
|
756
|
+
const shotsOf = (n) => model.gallery.filter((g) => g.node === n);
|
|
757
|
+
const SH = model.gallery.some((g) => g.node) ? 36 : 0;
|
|
758
|
+
d3.tree().nodeSize([CH + 18 + SH, CW + 70])(h);
|
|
737
759
|
let minX = Infinity;
|
|
738
760
|
let maxX = -Infinity;
|
|
739
761
|
let maxY = 0;
|
|
@@ -799,6 +821,76 @@
|
|
|
799
821
|
});
|
|
800
822
|
fcb.append("circle").attr("r", 9);
|
|
801
823
|
fcb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
|
|
824
|
+
const frb = card
|
|
825
|
+
.filter((d) => hasValue(d.data))
|
|
826
|
+
.append("g")
|
|
827
|
+
.attr("class", "code-btn ret")
|
|
828
|
+
.attr("transform", (d) => `translate(${CW / 2 + (hasCode(d.data) ? 22 : 0)},0)`)
|
|
829
|
+
.on("click", (ev, d) => {
|
|
830
|
+
ev.stopPropagation();
|
|
831
|
+
toggleCode(d.data, ev, "value", true);
|
|
832
|
+
})
|
|
833
|
+
.on("mouseenter", (ev, d) => {
|
|
834
|
+
ev.stopPropagation();
|
|
835
|
+
showTip(ev, `Returned: <span class="mono">${esc(valuePreview(d.data, 80))}</span>`);
|
|
836
|
+
});
|
|
837
|
+
frb.append("circle").attr("r", 9);
|
|
838
|
+
frb.append("path").attr("d", ICON.ret).attr("transform", "translate(-6,-6) scale(0.5)");
|
|
839
|
+
const TW = 46;
|
|
840
|
+
const TH = 28;
|
|
841
|
+
const strip = card
|
|
842
|
+
.filter((d) => shotsOf(d.data).length > 0)
|
|
843
|
+
.append("g")
|
|
844
|
+
.attr("class", "flow-shots")
|
|
845
|
+
.attr("transform", `translate(6,${CH + 4})`);
|
|
846
|
+
strip.each(function (d) {
|
|
847
|
+
const shots = shotsOf(d.data);
|
|
848
|
+
const fit = Math.floor((CW - 12) / (TW + 4));
|
|
849
|
+
const shown = shots.length > fit ? shots.slice(0, fit - 1) : shots;
|
|
850
|
+
const g = d3.select(this);
|
|
851
|
+
const cell = g
|
|
852
|
+
.selectAll("g.shot")
|
|
853
|
+
.data(shown)
|
|
854
|
+
.join("g")
|
|
855
|
+
.attr("class", "shot")
|
|
856
|
+
.attr("transform", (_x, i) => `translate(${i * (TW + 4)},0)`)
|
|
857
|
+
.on("click", (ev, x) => {
|
|
858
|
+
ev.stopPropagation();
|
|
859
|
+
openGallery(x);
|
|
860
|
+
})
|
|
861
|
+
.on("mouseenter", (ev, x) => {
|
|
862
|
+
ev.stopPropagation();
|
|
863
|
+
showTip(ev, galleryTip(x));
|
|
864
|
+
})
|
|
865
|
+
.on("mousemove", (ev) => moveTip(ev))
|
|
866
|
+
.on("mouseleave", hideTip);
|
|
867
|
+
cell.append("rect").attr("width", TW).attr("height", TH).attr("rx", 4);
|
|
868
|
+
cell
|
|
869
|
+
.filter((x) => x.image)
|
|
870
|
+
.append("image")
|
|
871
|
+
.attr("href", (x) => model.asset(x.f.asset))
|
|
872
|
+
.attr("x", 1)
|
|
873
|
+
.attr("y", 1)
|
|
874
|
+
.attr("width", TW - 2)
|
|
875
|
+
.attr("height", TH - 2)
|
|
876
|
+
.attr("preserveAspectRatio", "xMidYMid slice");
|
|
877
|
+
cell
|
|
878
|
+
.filter((x) => !x.image)
|
|
879
|
+
.append("text")
|
|
880
|
+
.attr("x", TW / 2)
|
|
881
|
+
.attr("y", TH / 2 + 3.5)
|
|
882
|
+
.attr("text-anchor", "middle")
|
|
883
|
+
.text((x) => (x.type === "snapshot" ? "{ }" : "file"));
|
|
884
|
+
if (shots.length > shown.length) {
|
|
885
|
+
const more = g.append("g").attr("class", "shot more").attr("transform", `translate(${shown.length * (TW + 4)},0)`)
|
|
886
|
+
.on("click", (ev) => {
|
|
887
|
+
ev.stopPropagation();
|
|
888
|
+
select(d.data.id, "snapshots");
|
|
889
|
+
});
|
|
890
|
+
more.append("rect").attr("width", TW).attr("height", TH).attr("rx", 4);
|
|
891
|
+
more.append("text").attr("x", TW / 2).attr("y", TH / 2 + 3.5).attr("text-anchor", "middle").text(`+${shots.length - shown.length}`);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
802
894
|
flowPos = new Map(h.descendants().map((d) => [d.data.id, { x: d.y + CW / 2, y: d.x }]));
|
|
803
895
|
svg.call(flowZoom);
|
|
804
896
|
fitFlow(true);
|
|
@@ -946,6 +1038,7 @@
|
|
|
946
1038
|
const tabs = [
|
|
947
1039
|
["overview", "Overview"],
|
|
948
1040
|
["args", "Args"],
|
|
1041
|
+
...(hasValue(n) ? [["value", "Return"]] : []),
|
|
949
1042
|
["events", `Events`],
|
|
950
1043
|
["logs", "Logs"],
|
|
951
1044
|
["snapshots", "Snapshots"],
|
|
@@ -965,10 +1058,11 @@
|
|
|
965
1058
|
${n.v.description ? `<div class="k">Summary</div><div class="v">${esc(n.v.description.summary)}</div>` : ""}
|
|
966
1059
|
${n.v.mcp ? `<div class="k">MCP</div><div class="v"><span class="badge-inline">${esc(n.v.mcp.mode)}</span></div>` : ""}
|
|
967
1060
|
<div class="k">Path</div><div class="v">${esc(n.v.path)}</div>
|
|
968
|
-
${n
|
|
1061
|
+
${userSite(n) ? `<div class="k">${siteLabel(n)}</div><div class="v defined">
|
|
969
1062
|
<button class="icon-btn" data-copy-source aria-label="Copy relative path">${svgIcon("copy")}</button>
|
|
970
1063
|
${hasCode(n) ? `<button class="icon-btn" data-show-code aria-label="Show source">${svgIcon("code")}</button>` : ""}
|
|
971
|
-
|
|
1064
|
+
${hasValue(n) ? `<button class="icon-btn" data-show-value aria-label="Show returned value">${svgIcon("ret")}</button>` : ""}
|
|
1065
|
+
<a class="icon-btn" href="${esc(editorUrl(userSite(n)))}" aria-label="Open in VS Code" data-tip-text="Open in VS Code">${svgIcon("external")}</a>
|
|
972
1066
|
<span class="defined-name" >${esc(basename(sourceOf(n)))}</span>
|
|
973
1067
|
</div>` : ""}
|
|
974
1068
|
</div>
|
|
@@ -986,6 +1080,13 @@
|
|
|
986
1080
|
copySrc.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
987
1081
|
copySrc.addEventListener("mouseleave", hideTip);
|
|
988
1082
|
}
|
|
1083
|
+
const showValue = el.details.querySelector("[data-show-value]");
|
|
1084
|
+
if (showValue) {
|
|
1085
|
+
showValue.addEventListener("click", (ev) => toggleCode(n, ev, "value", true));
|
|
1086
|
+
showValue.addEventListener("mouseenter", (ev) => showTip(ev, `Returned: <span class="mono">${esc(valuePreview(n, 80))}</span>`));
|
|
1087
|
+
showValue.addEventListener("mouseleave", hideTip);
|
|
1088
|
+
}
|
|
1089
|
+
wireValue(el.details, n);
|
|
989
1090
|
const showCode = el.details.querySelector("[data-show-code]");
|
|
990
1091
|
if (showCode) {
|
|
991
1092
|
showCode.addEventListener("click", (ev) => toggleCode(n, ev));
|
|
@@ -1012,6 +1113,9 @@
|
|
|
1012
1113
|
|
|
1013
1114
|
function detailTabHtml(n) {
|
|
1014
1115
|
const tab = state.detailTab;
|
|
1116
|
+
if (tab === "value") {
|
|
1117
|
+
return hasValue(n) ? `<div class="code-box">${valueHtml(n)}</div>` : '<div class="muted">Returned nothing.</div>';
|
|
1118
|
+
}
|
|
1015
1119
|
if (tab === "args") {
|
|
1016
1120
|
const params = schemaFields(n.v.inputSchema);
|
|
1017
1121
|
const schema = params.length ? `<div class="section-title">Schema</div><div class="params">${params.map(paramHtml).join("")}</div><div class="section-title">Values</div>` : "";
|
|
@@ -1284,23 +1388,38 @@
|
|
|
1284
1388
|
["Duration", fmtDur(n.duration)],
|
|
1285
1389
|
["When", `${fmtRel(n.start)} → ${fmtRel(n.end)}`],
|
|
1286
1390
|
v.tags.length ? ["Tags", esc(v.tags.join(" · "))] : null,
|
|
1287
|
-
|
|
1391
|
+
userSite(n) ? [siteLabel(n) === "Called at" ? "Called" : "Defined", `<span class="mono">${esc(sourceOf(n))}</span>`] : null,
|
|
1392
|
+
hasValue(n) ? ["Returns", `<span class="mono">${esc(valuePreview(n, 90))}</span>`] : null,
|
|
1288
1393
|
n.children.length ? ["Children", String(n.children.length)] : null,
|
|
1289
1394
|
n.marks.length ? ["Events", String(n.marks.length)] : null,
|
|
1290
1395
|
].filter(Boolean);
|
|
1291
|
-
const args = v.args && v.args.length ? truncate(oneLine(v.args.length === 1 ? v.args[0] : v.args), 220) : "";
|
|
1396
|
+
const args = !promptOf(n) && v.args && v.args.length ? truncate(oneLine(v.args.length === 1 ? v.args[0] : v.args), 220) : "";
|
|
1292
1397
|
const params = schemaFields(v.inputSchema);
|
|
1398
|
+
const pr = promptOf(n);
|
|
1399
|
+
const promptRows = pr
|
|
1400
|
+
? `<div class="sec">Prompt</div><div class="tprompt">${esc(truncate(pr.message || "", 200))}</div>${
|
|
1401
|
+
pr.answer ? `<div class="tkv"><span class="k">Answer</span><span class="mono">${esc(truncate(typeof pr.answer.value === "string" ? pr.answer.value : oneLine(pr.answer.value), 120))}</span>${pr.answer.via ? `<span class="k">Via</span><span>${esc(pr.answer.via)}</span>` : ""}</div>` : ""
|
|
1402
|
+
}`
|
|
1403
|
+
: "";
|
|
1293
1404
|
const fails = model.run.failures.filter((f) => f.action === n.id || f.action === n.name || f.action === v.path);
|
|
1294
1405
|
return `<div class="tt">${esc(shown)}</div>
|
|
1295
1406
|
${shown !== n.name ? `<div class="ts mono">${esc(truncate(n.name, 140))}</div>` : ""}
|
|
1296
1407
|
<div class="badges">${badges}</div>
|
|
1297
1408
|
${v.description ? `<div class="summary">${esc(v.description.summary)}</div>` : '<div class="summary muted">No description.</div>'}
|
|
1298
1409
|
<div class="tkv">${rows.map(([k, val]) => `<span class="k">${k}</span><span>${val}</span>`).join("")}</div>
|
|
1410
|
+
${promptRows}
|
|
1299
1411
|
${params.length ? `<div class="sec">Args schema</div><div class="params">${params.map(paramHtml).join("")}</div>` : ""}
|
|
1300
1412
|
${args ? `<div class="sec">Args</div><div class="mono small">${esc(args)}</div>` : ""}
|
|
1301
1413
|
${v.definedAt ? '<div class="hint">Press <kbd>C</kbd> to copy the path</div>' : ""}
|
|
1302
1414
|
${fails.length ? `<div class="sec fail">Failed</div><div class="mono small">${esc(truncate(String(fails[0].error).split(String.fromCharCode(10))[0], 200))}</div>` : ""}`;
|
|
1303
1415
|
}
|
|
1416
|
+
function galleryTip(x) {
|
|
1417
|
+
const img = x.image ? `<img src="${model.asset(x.f.asset)}" alt="">` : "";
|
|
1418
|
+
const body = x.type === "snapshot"
|
|
1419
|
+
? `<div class="mono small">${esc(truncate(x.s.value !== undefined ? oneLine(x.s.value) : x.s.text || "", 200))}</div>`
|
|
1420
|
+
: `<div class="ts">${esc(x.f.mime)} · ${fmtSize(x.f.size)}</div>`;
|
|
1421
|
+
return `<div class="tt">${esc(x.name)}${x.t != null ? ` · ${fmtRel(x.t)}` : ""}</div>${img}${body}`;
|
|
1422
|
+
}
|
|
1304
1423
|
/** The om itself: its node tooltip, plus where the run's record lives. */
|
|
1305
1424
|
function omTip() {
|
|
1306
1425
|
return `${nodeTip(model.root)}<div class="sec">Run folder</div><div class="mono small">${esc(model.data.folder)}</div>`;
|
|
@@ -1319,48 +1438,149 @@
|
|
|
1319
1438
|
codeEl.hidden = true;
|
|
1320
1439
|
document.body.appendChild(codeEl);
|
|
1321
1440
|
let codeFor = null;
|
|
1441
|
+
let codeTab = null;
|
|
1322
1442
|
let codeAway = null;
|
|
1443
|
+
/** A site inside omkit or an installed package — library source, not the user's. */
|
|
1444
|
+
function isLib(site) {
|
|
1445
|
+
return !!(site && model.data.library && model.data.library.includes(site));
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* The site worth showing for `n`: where it is defined, unless that is library source (a
|
|
1449
|
+
* battery, a prompt), in which case the user's call that launched it.
|
|
1450
|
+
*/
|
|
1451
|
+
function userSite(n) {
|
|
1452
|
+
const { definedAt, calledAt } = n.v;
|
|
1453
|
+
if (definedAt && !isLib(definedAt)) return definedAt;
|
|
1454
|
+
if (calledAt && !isLib(calledAt)) return calledAt;
|
|
1455
|
+
return definedAt || calledAt;
|
|
1456
|
+
}
|
|
1457
|
+
function siteLabel(n) {
|
|
1458
|
+
return userSite(n) === n.v.definedAt ? "Defined at" : "Called at";
|
|
1459
|
+
}
|
|
1460
|
+
function codeOf(site) {
|
|
1461
|
+
return site && model.data.code ? model.data.code[site] : undefined;
|
|
1462
|
+
}
|
|
1463
|
+
/** The popover's tabs for `n`, most useful first. */
|
|
1464
|
+
function codeTabs(n) {
|
|
1465
|
+
const tabs = [];
|
|
1466
|
+
if (promptOf(n)) tabs.push({ key: "prompt", label: "Prompt" });
|
|
1467
|
+
if (hasValue(n)) tabs.push({ key: "value", label: "Returned" });
|
|
1468
|
+
const mine = userSite(n);
|
|
1469
|
+
if (codeOf(mine)) tabs.push({ key: "user", label: siteLabel(n) === "Called at" ? "Call site" : "Source", site: mine });
|
|
1470
|
+
const def = n.v.definedAt;
|
|
1471
|
+
if (def && def !== mine && codeOf(def)) tabs.push({ key: "def", label: isLib(def) ? "Definition (omkit)" : "Definition", site: def });
|
|
1472
|
+
return tabs;
|
|
1473
|
+
}
|
|
1323
1474
|
function hasCode(n) {
|
|
1324
|
-
return
|
|
1475
|
+
return codeTabs(n).some((t) => t.key !== "value");
|
|
1325
1476
|
}
|
|
1326
|
-
function
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1477
|
+
function relOf(site) {
|
|
1478
|
+
return (model.data.sources && model.data.sources[site]) || String(site).replace(/:\d+(:\d+)?$/, "");
|
|
1479
|
+
}
|
|
1480
|
+
function hasValue(n) {
|
|
1481
|
+
return n.v.value !== undefined || !!n.v.valueNote;
|
|
1482
|
+
}
|
|
1483
|
+
function valuePreview(n, max) {
|
|
1484
|
+
if (n.v.value === undefined) return n.v.valueNote || "";
|
|
1485
|
+
return truncate(oneLine(n.v.value), max);
|
|
1486
|
+
}
|
|
1487
|
+
/** The returned value in the code view: pretty JSON, highlighted, numbered, copyable. */
|
|
1488
|
+
function valueHtml(n) {
|
|
1489
|
+
if (n.v.value === undefined) {
|
|
1490
|
+
return `<div class="pp"><div class="muted">${esc(n.v.valueNote || "Returned nothing.")}</div></div>`;
|
|
1491
|
+
}
|
|
1492
|
+
const text = json(n.v.value);
|
|
1493
|
+
const lines = highlight(text);
|
|
1494
|
+
const kind = Array.isArray(n.v.value) ? `array · ${n.v.value.length} items` : n.v.value === null ? "null" : typeof n.v.value === "object" ? `object · ${Object.keys(n.v.value).length} keys` : typeof n.v.value;
|
|
1495
|
+
return `<div class="cp-tools"><span class="muted">${esc(kind)} · ${lines.length} line${lines.length > 1 ? "s" : ""} · ${fmtSize(text.length)}</span>
|
|
1496
|
+
<button class="btn solo small" data-a="copy-json">${svgIcon("copy")}Copy JSON</button></div>
|
|
1497
|
+
<div class="cp-code">${lines
|
|
1498
|
+
.map((html, i) => `<div class="cl"><span class="ln">${i + 1}</span><span class="lc">${html || " "}</span></div>`)
|
|
1499
|
+
.join("")}</div>`;
|
|
1500
|
+
}
|
|
1501
|
+
function wireValue(root, n) {
|
|
1502
|
+
const b = root.querySelector('[data-a="copy-json"]');
|
|
1503
|
+
if (b) b.addEventListener("click", () => copy(json(n.v.value), "Copied the returned value"));
|
|
1504
|
+
}
|
|
1505
|
+
function codeHtml(site) {
|
|
1506
|
+
const c = codeOf(site);
|
|
1333
1507
|
const line = Number((/:(\d+)(?::\d+)?$/.exec(site) || [])[1]);
|
|
1334
|
-
const rel = sourceOf(n);
|
|
1335
1508
|
const lines = highlight(c.text).map(
|
|
1336
1509
|
(html, i) => `<div class="cl${c.start + i === line ? " at" : ""}"><span class="ln">${c.start + i}</span><span class="lc">${html || " "}</span></div>`
|
|
1337
1510
|
);
|
|
1511
|
+
return `<div class="cp-code">${lines.join("")}</div>${c.truncated ? '<div class="muted cp-more">Cut short — open the file for the rest.</div>' : ""}`;
|
|
1512
|
+
}
|
|
1513
|
+
function promptHtml(p) {
|
|
1514
|
+
const choices = p.choices && p.choices.length
|
|
1515
|
+
? `<div class="pp-k">Choices</div><div class="pp-v"><ol class="pp-choices">${p.choices
|
|
1516
|
+
.map((c) => {
|
|
1517
|
+
const value = typeof c === "string" ? c : c.value;
|
|
1518
|
+
const label = typeof c === "string" ? c : c.label;
|
|
1519
|
+
const picked = p.answer && p.answer.value === value;
|
|
1520
|
+
return `<li class="${picked ? "picked" : ""}">${esc(label)}${label !== value ? ` <span class="muted">(${esc(value)})</span>` : ""}</li>`;
|
|
1521
|
+
})
|
|
1522
|
+
.join("")}</ol></div>`
|
|
1523
|
+
: "";
|
|
1524
|
+
const answer = p.answer
|
|
1525
|
+
? `<div class="pp-answer"><span class="pp-k">Answer</span><pre class="code">${esc(typeof p.answer.value === "string" ? p.answer.value : json(p.answer.value))}</pre>${
|
|
1526
|
+
p.answer.via ? `<span class="badge-inline">via ${esc(p.answer.via)}</span>` : ""
|
|
1527
|
+
}</div>`
|
|
1528
|
+
: '<div class="muted">No answer recorded.</div>';
|
|
1529
|
+
return `<div class="pp">
|
|
1530
|
+
<div class="pp-q">${esc(p.message || "(no message)")}</div>
|
|
1531
|
+
${p.hint ? `<div class="mono small">${esc(p.hint)}</div>` : ""}
|
|
1532
|
+
<div class="pp-grid">
|
|
1533
|
+
<div class="pp-k">Kind</div><div class="pp-v">${esc(p.kind)}</div>
|
|
1534
|
+
${p.def !== undefined ? `<div class="pp-k">Default</div><div class="pp-v mono">${esc(oneLine(p.def))}</div>` : ""}
|
|
1535
|
+
${p.timeoutMs !== undefined ? `<div class="pp-k">Timeout</div><div class="pp-v">${p.timeoutMs > 0 && isFinite(p.timeoutMs) ? fmtDur(p.timeoutMs) : "none"}</div>` : ""}
|
|
1536
|
+
${choices}
|
|
1537
|
+
</div>
|
|
1538
|
+
${answer}
|
|
1539
|
+
</div>`;
|
|
1540
|
+
}
|
|
1541
|
+
function toggleCode(n, ev, want, toggle) {
|
|
1542
|
+
// the <> closes whatever is open on its node; the return button closes only its own tab
|
|
1543
|
+
if (codeFor === n.id && (!want || (toggle && codeTab === want))) return closeCode();
|
|
1544
|
+
const tabs = codeTabs(n);
|
|
1545
|
+
if (!tabs.length) return;
|
|
1546
|
+
const tab = tabs.find((t) => t.key === want) || tabs.find((t) => t.key !== "value") || tabs[0];
|
|
1547
|
+
codeTab = tab.key;
|
|
1548
|
+
hideTip();
|
|
1549
|
+
const reopen = codeFor !== n.id;
|
|
1550
|
+
codeFor = n.id;
|
|
1551
|
+
const site = tab.site || userSite(n);
|
|
1552
|
+
const rel = site ? relOf(site) : "";
|
|
1553
|
+
const line = site ? (/:(\d+)(?::\d+)?$/.exec(site) || [])[1] : "";
|
|
1338
1554
|
codeEl.innerHTML = `<div class="cp-head">
|
|
1339
|
-
|
|
1555
|
+
${site ? `<span class="cp-path" title="${esc(site)}">${esc(rel)}<span class="muted">${line ? ":" + line : ""}</span></span>
|
|
1340
1556
|
<button class="icon-btn" data-a="copy" aria-label="Copy path">${svgIcon("copy")}</button>
|
|
1341
|
-
<a class="icon-btn" href="${esc(editorUrl(site))}" aria-label="Open in VS Code">${svgIcon("external")}</a
|
|
1557
|
+
<a class="icon-btn" href="${esc(editorUrl(site))}" aria-label="Open in VS Code">${svgIcon("external")}</a>` : ""}
|
|
1342
1558
|
<span class="cp-name">${esc(displayName(n))}</span>
|
|
1343
1559
|
<button class="icon-btn" data-a="close" aria-label="Close">${svgIcon("close")}</button>
|
|
1344
1560
|
</div>
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
codeEl.querySelector('[data-a="copy"]')
|
|
1561
|
+
${tabs.length > 1 ? `<div class="cp-tabs">${tabs.map((t) => `<button class="tab ${t === tab ? "active" : ""}" data-tab="${t.key}">${esc(t.label)}</button>`).join("")}</div>` : ""}
|
|
1562
|
+
<div class="cp-body">${tab.key === "prompt" ? promptHtml(promptOf(n)) : tab.key === "value" ? valueHtml(n) : codeHtml(site)}</div>`;
|
|
1563
|
+
wireValue(codeEl, n);
|
|
1564
|
+
const cp = codeEl.querySelector('[data-a="copy"]');
|
|
1565
|
+
if (cp) cp.addEventListener("click", () => copy(rel, `Copied ${rel}`));
|
|
1349
1566
|
codeEl.querySelector('[data-a="close"]').addEventListener("click", closeCode);
|
|
1567
|
+
codeEl.querySelectorAll(".cp-tabs .tab").forEach((b) => b.addEventListener("click", () => toggleCode(n, null, b.dataset.tab)));
|
|
1350
1568
|
codeEl.hidden = false;
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1569
|
+
if (reopen) {
|
|
1570
|
+
const x = ev && ev.clientX != null ? ev.clientX : window.innerWidth / 2;
|
|
1571
|
+
const y = ev && ev.clientY != null ? ev.clientY : window.innerHeight / 3;
|
|
1572
|
+
const w = codeEl.offsetWidth;
|
|
1573
|
+
const h = Math.min(codeEl.offsetHeight, window.innerHeight * 0.7);
|
|
1574
|
+
const left = x + 14 + w > window.innerWidth - 8 ? x - w - 14 : x + 14;
|
|
1575
|
+
const top = y + 14 + h > window.innerHeight - 8 ? y - h - 14 : y + 14;
|
|
1576
|
+
codeEl.style.left = `${Math.max(8, left)}px`;
|
|
1577
|
+
codeEl.style.top = `${Math.max(8, top)}px`;
|
|
1578
|
+
}
|
|
1359
1579
|
const at = codeEl.querySelector(".cl.at");
|
|
1360
1580
|
if (at) at.scrollIntoView({ block: "center" });
|
|
1361
1581
|
if (codeAway) document.removeEventListener("mousedown", codeAway, true);
|
|
1362
1582
|
codeAway = (e) => {
|
|
1363
|
-
if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code]")) closeCode();
|
|
1583
|
+
if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code], [data-show-value]")) closeCode();
|
|
1364
1584
|
};
|
|
1365
1585
|
setTimeout(() => codeAway && document.addEventListener("mousedown", codeAway, true), 0);
|
|
1366
1586
|
}
|
|
@@ -1372,11 +1592,10 @@
|
|
|
1372
1592
|
}
|
|
1373
1593
|
let tipTimer = 0;
|
|
1374
1594
|
let hovered = null;
|
|
1375
|
-
/** The file
|
|
1595
|
+
/** The file worth opening for `n` (see {@link userSite}), relative to the project root — no line. */
|
|
1376
1596
|
function sourceOf(n) {
|
|
1377
|
-
const site = n
|
|
1378
|
-
|
|
1379
|
-
return (model.data.sources && model.data.sources[site]) || site.replace(/:\d+(:\d+)?$/, "");
|
|
1597
|
+
const site = userSite(n);
|
|
1598
|
+
return site ? relOf(site) : "";
|
|
1380
1599
|
}
|
|
1381
1600
|
function copySource(n) {
|
|
1382
1601
|
const src = n && sourceOf(n);
|
|
@@ -1568,7 +1787,7 @@
|
|
|
1568
1787
|
if (m.index > last) parts.push([null, text.slice(last, m.index)]);
|
|
1569
1788
|
let cls = null;
|
|
1570
1789
|
if (m[1]) cls = "tk-c";
|
|
1571
|
-
else if (m[2]) cls = "tk-s";
|
|
1790
|
+
else if (m[2]) cls = /^\s*:/.test(text.slice(TOKEN.lastIndex, TOKEN.lastIndex + 8)) ? "tk-p" : "tk-s";
|
|
1572
1791
|
else if (m[3]) cls = "tk-n";
|
|
1573
1792
|
else if (m[4]) {
|
|
1574
1793
|
if (KEYWORDS.has(m[4])) cls = "tk-k";
|
|
@@ -1622,12 +1841,48 @@
|
|
|
1622
1841
|
f.def !== undefined ? `<span class="pd">= ${esc(truncate(f.def, 24))}</span>` : ""
|
|
1623
1842
|
}${f.desc ? `<div class="pdesc">${esc(f.desc)}</div>` : ""}</div>`;
|
|
1624
1843
|
}
|
|
1844
|
+
/**
|
|
1845
|
+
* What a `prompt` battery node asked and got back: its options (the node's one argument) plus
|
|
1846
|
+
* the `prompt` / `answer` events it emitted. `null` for anything that is not a prompt.
|
|
1847
|
+
*/
|
|
1848
|
+
function promptOf(n) {
|
|
1849
|
+
if (n._prompt !== undefined) return n._prompt;
|
|
1850
|
+
n._prompt = null;
|
|
1851
|
+
if (n.name !== "prompt" || n.depth === 0) return null;
|
|
1852
|
+
const opts = (n.v.args && n.v.args[0] && typeof n.v.args[0] === "object" ? n.v.args[0] : {}) || {};
|
|
1853
|
+
let message = opts.message;
|
|
1854
|
+
let answer = null;
|
|
1855
|
+
for (const it of n.entries) {
|
|
1856
|
+
if (it.e.level !== "event") continue;
|
|
1857
|
+
if (it.e.message.startsWith("prompt · ")) message = it.e.message.slice("prompt · ".length);
|
|
1858
|
+
else if (it.e.message.startsWith("answer · ")) {
|
|
1859
|
+
try {
|
|
1860
|
+
answer = JSON.parse(it.e.message.slice("answer · ".length));
|
|
1861
|
+
} catch {
|
|
1862
|
+
answer = { value: it.e.message.slice("answer · ".length) };
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
if (message === undefined && !answer) return null;
|
|
1867
|
+
n._prompt = {
|
|
1868
|
+
message,
|
|
1869
|
+
kind: opts.kind || "input",
|
|
1870
|
+
hint: opts.hint,
|
|
1871
|
+
choices: opts.choices,
|
|
1872
|
+
def: opts.default,
|
|
1873
|
+
timeoutMs: opts.timeoutMs,
|
|
1874
|
+
answer,
|
|
1875
|
+
};
|
|
1876
|
+
return n._prompt;
|
|
1877
|
+
}
|
|
1625
1878
|
function truncate(s, n) {
|
|
1626
1879
|
n = Math.max(1, Math.floor(n));
|
|
1627
1880
|
return s.length > n ? `${s.slice(0, Math.max(1, n - 1))}…` : s;
|
|
1628
1881
|
}
|
|
1629
1882
|
function displayName(n) {
|
|
1630
1883
|
if (n.depth === 0) return "main";
|
|
1884
|
+
const p = promptOf(n);
|
|
1885
|
+
if (p && p.message) return `prompt: ${truncate(p.message, 48)}`;
|
|
1631
1886
|
const tag = n.v.tags && n.v.tags[0];
|
|
1632
1887
|
// Battery steps are named after their kind (`command`, `chromePage`); their first tag
|
|
1633
1888
|
// is what the om called them, which is the more useful label.
|
|
@@ -9,6 +9,8 @@ export interface NodeView {
|
|
|
9
9
|
readonly name: string;
|
|
10
10
|
/** `file:line` where the action was defined (the `.run(...)` site); absent for the root. */
|
|
11
11
|
readonly definedAt?: string;
|
|
12
|
+
/** `file:line` of the call that launched this node; absent for the root. */
|
|
13
|
+
readonly calledAt?: string;
|
|
12
14
|
readonly path: string;
|
|
13
15
|
readonly parentId: string | null;
|
|
14
16
|
readonly tags: readonly string[];
|
|
@@ -29,6 +31,10 @@ export interface NodeView {
|
|
|
29
31
|
};
|
|
30
32
|
/** The definition's `.args()` schema as JSON Schema; absent without one. */
|
|
31
33
|
readonly inputSchema?: unknown;
|
|
34
|
+
/** What the node returned, when JSON can carry it; absent for `undefined` and for failures. */
|
|
35
|
+
readonly value?: unknown;
|
|
36
|
+
/** Why a returned value is not in {@link value}: what it was, and that it was too big or not JSON. */
|
|
37
|
+
readonly valueNote?: string;
|
|
32
38
|
readonly children: readonly NodeView[];
|
|
33
39
|
}
|
|
34
40
|
/** A curated artifact, as projected into `result.json`. Data-only — no core import. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.14",
|
|
4
4
|
"description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|