omkit 0.8.11 → 0.8.13
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 +2 -2
- package/dist/core/ActionRun.d.ts +2 -0
- package/dist/core/ActionRun.js +2 -0
- package/dist/core/ExecutionTree.js +2 -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 +155 -28
- package/dist/output/writers/report/report.css +63 -0
- package/dist/output/writers/report/report.js +424 -16
- package/dist/output/writers/views.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -185,11 +185,11 @@ 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 `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.
|
|
192
|
-
- `report.html` — the whole run as one self-contained page: a timeline of every action with its events, a flame graph, a flow graph of the tree, per-action details, logs, assertions,
|
|
192
|
+
- `report.html` — the whole run as one self-contained page: a timeline of every action with its events, a flame graph, a flow graph of the tree, per-action details, logs, assertions, the run's snapshots and images, and each action's source (the `<>` button next to a block). Everything is inlined (data, base64 images, d3), so it opens straight from disk and survives being zipped or attached.
|
|
193
193
|
|
|
194
194
|
`ctx` also gives each action `assert(cond, msg)` (tallies into the run's verdict), `snapshot(name, value)` (captures JSON state to the run folder), and `artifact(name, file, opts?)` (labels a file the action wrote — name, optional description, MIME inferred from the extension — and returns its absolute path) — so a run is an inspectable artifact, not just an exit code. Write the files you label under `ctx.artifactsFolder`, the run's own output folder.
|
|
195
195
|
|
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],
|
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
|
|
@@ -40,6 +40,8 @@ export async function writeReport(file, input) {
|
|
|
40
40
|
folder: input.folder,
|
|
41
41
|
projectRoot: root,
|
|
42
42
|
sources: definitionSources(input.run.root, root),
|
|
43
|
+
code: await definitionCode(input.run.root),
|
|
44
|
+
library: librarySites(input.run.root),
|
|
43
45
|
generatedAt: Date.now(),
|
|
44
46
|
run: input.run,
|
|
45
47
|
entries: input.entries,
|
|
@@ -58,29 +60,29 @@ function renderPage(input, payload, assets, code) {
|
|
|
58
60
|
const blobs = assets
|
|
59
61
|
.map((a) => `<script type="text/plain" id="${a.id}" data-mime="${a.mime}">${a.data}</script>`)
|
|
60
62
|
.join("\n");
|
|
61
|
-
return `<!doctype html>
|
|
62
|
-
<html lang="en">
|
|
63
|
-
<head>
|
|
64
|
-
<meta charset="utf-8">
|
|
65
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
66
|
-
<title>${escapeHtml(title)}</title>
|
|
67
|
-
<style>${code.css}</style>
|
|
68
|
-
</head>
|
|
69
|
-
<body>
|
|
70
|
-
<div id="loader" role="status" aria-live="polite">
|
|
71
|
-
<div class="loader-card">
|
|
72
|
-
<div class="loader-spin"></div>
|
|
73
|
-
<div class="loader-title">${escapeHtml(title)}</div>
|
|
74
|
-
<div class="loader-step" id="loader-step">Loading the run record…</div>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
<div id="app" hidden></div>
|
|
78
|
-
<script id="omkit-data" type="application/json">${inertJson(payload)}</script>
|
|
79
|
-
${blobs}
|
|
80
|
-
<script>${inertScript(code.d3)}</script>
|
|
81
|
-
<script>${inertScript(code.client)}</script>
|
|
82
|
-
</body>
|
|
83
|
-
</html>
|
|
63
|
+
return `<!doctype html>
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="utf-8">
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
68
|
+
<title>${escapeHtml(title)}</title>
|
|
69
|
+
<style>${code.css}</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<div id="loader" role="status" aria-live="polite">
|
|
73
|
+
<div class="loader-card">
|
|
74
|
+
<div class="loader-spin"></div>
|
|
75
|
+
<div class="loader-title">${escapeHtml(title)}</div>
|
|
76
|
+
<div class="loader-step" id="loader-step">Loading the run record…</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<div id="app" hidden></div>
|
|
80
|
+
<script id="omkit-data" type="application/json">${inertJson(payload)}</script>
|
|
81
|
+
${blobs}
|
|
82
|
+
<script>${inertScript(code.d3)}</script>
|
|
83
|
+
<script>${inertScript(code.client)}</script>
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
84
86
|
`;
|
|
85
87
|
}
|
|
86
88
|
async function collectFiles(input, assets) {
|
|
@@ -120,7 +122,11 @@ async function collectFiles(input, assets) {
|
|
|
120
122
|
out.push({ ...base, size, text: await readFile(abs, "utf8") });
|
|
121
123
|
}
|
|
122
124
|
else {
|
|
123
|
-
out.push({
|
|
125
|
+
out.push({
|
|
126
|
+
...base,
|
|
127
|
+
size,
|
|
128
|
+
omitted: isText(meta.mime) ? `larger than ${mb(TEXT_CAP)}` : "binary",
|
|
129
|
+
});
|
|
124
130
|
}
|
|
125
131
|
};
|
|
126
132
|
for (const a of input.artifacts) {
|
|
@@ -153,7 +159,14 @@ async function collectSnapshots(input) {
|
|
|
153
159
|
continue;
|
|
154
160
|
const name = e.message.slice(0, split);
|
|
155
161
|
const abs = e.message.slice(split + 3);
|
|
156
|
-
const base = {
|
|
162
|
+
const base = {
|
|
163
|
+
seq: ++seq,
|
|
164
|
+
name,
|
|
165
|
+
nodeId: e.nodeId,
|
|
166
|
+
ts: e.ts,
|
|
167
|
+
file: abs,
|
|
168
|
+
rel: relTo(input.folder, abs),
|
|
169
|
+
};
|
|
157
170
|
try {
|
|
158
171
|
const raw = await readFile(abs, "utf8");
|
|
159
172
|
if (raw.length > SNAPSHOT_CAP) {
|
|
@@ -211,15 +224,129 @@ async function projectRoot(folder) {
|
|
|
211
224
|
function definitionSources(root, projectDir) {
|
|
212
225
|
const out = {};
|
|
213
226
|
const visit = (n) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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;
|
|
217
232
|
}
|
|
218
233
|
n.children.forEach(visit);
|
|
219
234
|
};
|
|
220
235
|
visit(root);
|
|
221
236
|
return out;
|
|
222
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
|
+
}
|
|
302
|
+
/** Longest excerpt shown per definition; a body past this is cut, and says so. */
|
|
303
|
+
const CODE_LINES = 300;
|
|
304
|
+
/**
|
|
305
|
+
* Each distinct `definedAt` site → the source of the definition there: the whole top-level
|
|
306
|
+
* statement holding that line (`export const x = action(…)…run(…)`, or the `om(…).run(…)` call),
|
|
307
|
+
* found with the TypeScript parser so it is the real extent, not a guess at one. A site whose
|
|
308
|
+
* file is gone or unparseable gets a window of lines around it instead, or nothing.
|
|
309
|
+
*/
|
|
310
|
+
async function definitionCode(root) {
|
|
311
|
+
const sites = new Set();
|
|
312
|
+
const visit = (n) => {
|
|
313
|
+
if (n.definedAt)
|
|
314
|
+
sites.add(n.definedAt);
|
|
315
|
+
if (n.calledAt)
|
|
316
|
+
sites.add(n.calledAt);
|
|
317
|
+
n.children.forEach(visit);
|
|
318
|
+
};
|
|
319
|
+
visit(root);
|
|
320
|
+
if (sites.size === 0)
|
|
321
|
+
return {};
|
|
322
|
+
// Loaded only here: the parser is heavy, and a run that never writes a report never pays for it.
|
|
323
|
+
const ts = (await import("typescript")).default;
|
|
324
|
+
const out = {};
|
|
325
|
+
const texts = new Map();
|
|
326
|
+
for (const site of sites) {
|
|
327
|
+
const m = /^(.*):(\d+)(?::\d+)?$/.exec(site);
|
|
328
|
+
if (!m)
|
|
329
|
+
continue;
|
|
330
|
+
const file = m[1];
|
|
331
|
+
const line = Number(m[2]);
|
|
332
|
+
if (!texts.has(file))
|
|
333
|
+
texts.set(file, await readFile(file, "utf8").catch(() => null));
|
|
334
|
+
const text = texts.get(file);
|
|
335
|
+
if (!text)
|
|
336
|
+
continue;
|
|
337
|
+
const lang = /\.[cm]?tsx?$/i.test(file) ? "ts" : "js";
|
|
338
|
+
const lines = text.split(/\r?\n/);
|
|
339
|
+
const { from, to } = excerptRange(ts, file, text, line, lines.length);
|
|
340
|
+
const truncated = to - from + 1 > CODE_LINES;
|
|
341
|
+
out[site] = {
|
|
342
|
+
start: from,
|
|
343
|
+
text: lines.slice(from - 1, truncated ? from - 1 + CODE_LINES : to).join("\n"),
|
|
344
|
+
lang,
|
|
345
|
+
...(truncated ? { truncated } : {}),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
223
350
|
async function walk(dir, depth = 0) {
|
|
224
351
|
if (depth > 4)
|
|
225
352
|
return [];
|
|
@@ -292,6 +292,69 @@ table.list tr:hover td { background: var(--raise); }
|
|
|
292
292
|
.menu button:hover { background: var(--raise); }
|
|
293
293
|
.toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); background: var(--raise); border: 1px solid var(--line); padding: 7px 14px; border-radius: 8px; z-index: 70; }
|
|
294
294
|
|
|
295
|
+
/* ── title info, locate, defined-at, code popover ───────── */
|
|
296
|
+
.title-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
297
|
+
.title-row h1 { min-width: 0; }
|
|
298
|
+
.info-btn { flex: none; width: 22px; height: 22px; border: 0; border-radius: 50%; background: none; color: var(--text-3); display: grid; place-items: center; cursor: help; padding: 0; }
|
|
299
|
+
.info-btn:hover, .info-btn:focus-visible { color: var(--accent); background: var(--accent-soft); }
|
|
300
|
+
.info-btn svg { width: 17px; height: 17px; }
|
|
301
|
+
.gal-item .cap .locate { margin-left: auto; width: 22px; height: 20px; }
|
|
302
|
+
.kv .v.defined { flex-wrap: wrap; }
|
|
303
|
+
.kv .v.defined .defined-name { color: var(--text-2); font-family: var(--mono); font-size: 12px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
304
|
+
a.icon-btn { text-decoration: none; }
|
|
305
|
+
.chart .code-btn, .flow-card .code-btn { cursor: pointer; opacity: 0; transition: opacity 0.12s; }
|
|
306
|
+
.chart g.r:hover .code-btn, .chart g.r.sel .code-btn, .flow-card:hover .code-btn, .flow-card.selected .code-btn { opacity: 1; }
|
|
307
|
+
.code-btn circle { fill: var(--panel); stroke: var(--text-3); stroke-width: 1.2; }
|
|
308
|
+
.code-btn text { fill: var(--text-2); font: 700 9px var(--mono); pointer-events: none; }
|
|
309
|
+
.code-btn:hover circle { stroke: var(--accent); }
|
|
310
|
+
.code-btn:hover text { fill: var(--accent); }
|
|
311
|
+
.chart line.pin.flash, .flame-view line.pin.flash { animation: pin-flash 0.6s ease-in-out 3; }
|
|
312
|
+
.chart circle.mark.flash { animation: mark-flash 0.6s ease-in-out 3; transform-box: fill-box; transform-origin: center; }
|
|
313
|
+
@keyframes pin-flash { 50% { stroke-width: 5; } }
|
|
314
|
+
@keyframes mark-flash { 50% { transform: scale(1.9); } }
|
|
315
|
+
.code-pop {
|
|
316
|
+
position: fixed; z-index: 58; width: 640px; max-width: 92vw; max-height: 70vh; display: flex; flex-direction: column;
|
|
317
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); overflow: hidden;
|
|
318
|
+
}
|
|
319
|
+
.code-pop .cp-head { display: flex; align-items: center; gap: 6px; padding: 6px 8px 6px 12px; background: var(--panel-2); border-bottom: 1px solid var(--line); }
|
|
320
|
+
.code-pop .cp-path { font: 12px var(--mono); color: var(--text-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
321
|
+
.code-pop .cp-name { flex: 1; text-align: right; color: var(--text-3); font-size: 12px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
322
|
+
.code-pop .cp-body { overflow: auto; min-height: 0; }
|
|
323
|
+
.code-pop .cp-code { font: 11.5px/1.65 var(--mono); padding: 8px 0; min-width: max-content; }
|
|
324
|
+
.code-pop .cl { display: grid; grid-template-columns: 48px auto; white-space: pre; }
|
|
325
|
+
.code-pop .cl.at { background: var(--accent-soft); }
|
|
326
|
+
.code-pop .ln { color: var(--text-3); text-align: right; padding-right: 14px; user-select: none; }
|
|
327
|
+
.code-pop .lc { padding-right: 16px; }
|
|
328
|
+
.code-pop .cp-more { padding: 6px 14px 10px; font-size: 12px; }
|
|
329
|
+
:root { --tk-c: #6f7a99; --tk-s: #7ee0a3; --tk-n: #f0b26b; --tk-k: #c792ea; --tk-f: #82aaff; --tk-t: #ffcb6b; }
|
|
330
|
+
:root[data-theme="light"] { --tk-c: #6a737d; --tk-s: #116329; --tk-n: #b35900; --tk-k: #cf222e; --tk-f: #6639ba; --tk-t: #953800; }
|
|
331
|
+
@media (prefers-color-scheme: light) {
|
|
332
|
+
:root:not([data-theme="dark"]) { --tk-c: #6a737d; --tk-s: #116329; --tk-n: #b35900; --tk-k: #cf222e; --tk-f: #6639ba; --tk-t: #953800; }
|
|
333
|
+
}
|
|
334
|
+
.tk-c { color: var(--tk-c); font-style: italic; }
|
|
335
|
+
.tk-s { color: var(--tk-s); }
|
|
336
|
+
.tk-n { color: var(--tk-n); }
|
|
337
|
+
.tk-k { color: var(--tk-k); }
|
|
338
|
+
.tk-f { color: var(--tk-f); }
|
|
339
|
+
.tk-t { color: var(--tk-t); }
|
|
340
|
+
|
|
341
|
+
/* ── popover tabs, prompt panel, flow snapshot strip ───── */
|
|
342
|
+
.code-pop .cp-tabs { display: flex; gap: 2px; padding: 0 8px; border-bottom: 1px solid var(--line); background: var(--panel-2); }
|
|
343
|
+
.code-pop .cp-tabs .tab { padding: 6px 9px; font-size: 12px; }
|
|
344
|
+
.pp { padding: 12px 14px 14px; display: grid; gap: 10px; }
|
|
345
|
+
.pp-q { font-size: 14px; font-weight: 600; white-space: pre-wrap; }
|
|
346
|
+
.pp-grid { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 4px 10px; font-size: 12px; }
|
|
347
|
+
.pp-k { color: var(--text-3); font-size: 12px; }
|
|
348
|
+
.pp-choices { margin: 0; padding-left: 18px; }
|
|
349
|
+
.pp-choices li.picked { color: var(--accent); font-weight: 600; }
|
|
350
|
+
.pp-answer { display: grid; gap: 6px; justify-items: start; }
|
|
351
|
+
.pp-answer pre { width: 100%; }
|
|
352
|
+
.tip .tprompt { font-weight: 600; white-space: pre-wrap; margin-bottom: 4px; }
|
|
353
|
+
.flow-shots .shot { cursor: pointer; }
|
|
354
|
+
.flow-shots .shot rect { fill: var(--panel-2); stroke: var(--line); }
|
|
355
|
+
.flow-shots .shot:hover rect { stroke: var(--accent); }
|
|
356
|
+
.flow-shots .shot text { fill: var(--text-3); font: 600 9px var(--mono); pointer-events: none; }
|
|
357
|
+
|
|
295
358
|
@media (max-width: 1100px) {
|
|
296
359
|
.bottom { grid-template-columns: minmax(0, 1fr); }
|
|
297
360
|
.legend, .overview { display: none; }
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
download: "M12 4v11M7 10l5 5 5-5M5 20h14",
|
|
41
41
|
copy: "M9 9h11v11H9zM5 15V4h11",
|
|
42
42
|
stop: "M6 6h12v12H6z",
|
|
43
|
+
info: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v5M12 8h.01",
|
|
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
|
+
code: "M8 7l-5 5 5 5M16 7l5 5-5 5",
|
|
46
|
+
external: "M14 4h6v6M20 4l-9 9M18 14v6H4V6h6",
|
|
43
47
|
};
|
|
44
48
|
const svgIcon = (name, extra) =>
|
|
45
49
|
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"${
|
|
@@ -297,6 +301,11 @@
|
|
|
297
301
|
zoomToDomain(Math.max(0, d0), Math.max(0, d0) + ms);
|
|
298
302
|
});
|
|
299
303
|
app.querySelector("#more").addEventListener("click", (ev) => openMenu(ev.currentTarget));
|
|
304
|
+
const info = app.querySelector("#om-info");
|
|
305
|
+
info.addEventListener("mouseenter", (ev) => showTip(ev, omTip(), true));
|
|
306
|
+
info.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
307
|
+
info.addEventListener("mouseleave", hideTip);
|
|
308
|
+
info.addEventListener("click", () => select(model.root.id));
|
|
300
309
|
|
|
301
310
|
// ── zoom behaviours ──
|
|
302
311
|
const chartZoom = d3
|
|
@@ -496,9 +505,13 @@
|
|
|
496
505
|
r.append("rect").attr("class", "bar").attr("rx", 5).attr("y", BAR_Y).attr("height", BAR_H);
|
|
497
506
|
r.append("text").attr("class", "bar-label").attr("y", BAR_Y + BAR_H / 2 + 4);
|
|
498
507
|
r.append("g").attr("class", "marks");
|
|
508
|
+
const cb = r.append("g").attr("class", "code-btn");
|
|
509
|
+
cb.append("circle").attr("r", 9);
|
|
510
|
+
cb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
|
|
499
511
|
return r;
|
|
500
512
|
})
|
|
501
|
-
.attr("transform", (_n, i) => `translate(0,${i * ROW_H})`)
|
|
513
|
+
.attr("transform", (_n, i) => `translate(0,${i * ROW_H})`)
|
|
514
|
+
.classed("sel", (n) => n.id === state.selected);
|
|
502
515
|
|
|
503
516
|
g.select("rect.bar")
|
|
504
517
|
.attr("x", (n) => x(n.start))
|
|
@@ -530,7 +543,22 @@
|
|
|
530
543
|
const left = Math.min(x0, flip ? lx - tw : lx) - 6;
|
|
531
544
|
const right = Math.max(x0 + Math.max(w, 3), flip ? x0 : lx + tw) + 6;
|
|
532
545
|
d3.select(this.parentNode).select("rect.hit").attr("x", left).attr("width", Math.max(16, right - left));
|
|
546
|
+
// the <> sits just past the block (or just before it, when that would leave the chart)
|
|
547
|
+
// …and, for a block spanning the chart, inside it right after its label
|
|
548
|
+
const cx =
|
|
549
|
+
right + 8 <= W - 10 ? right + 8 : left - 8 >= 10 ? left - 8 : Math.min(W - 12, (flip ? x0 : lx + tw) + 14);
|
|
550
|
+
d3.select(this.parentNode)
|
|
551
|
+
.select("g.code-btn")
|
|
552
|
+
.style("display", hasCode(n) ? null : "none")
|
|
553
|
+
.attr("transform", `translate(${cx},${BAR_Y + BAR_H / 2})`);
|
|
533
554
|
});
|
|
555
|
+
g.select("g.code-btn")
|
|
556
|
+
.on("click", (ev, n) => {
|
|
557
|
+
ev.stopPropagation();
|
|
558
|
+
toggleCode(n, ev);
|
|
559
|
+
})
|
|
560
|
+
.on("mouseenter", (ev) => showTip(ev, "Show source"))
|
|
561
|
+
.on("mouseleave", hideTip);
|
|
534
562
|
g.selectAll("rect.hit, text.bar-label").call(bindNode);
|
|
535
563
|
|
|
536
564
|
g.select("g.marks").each(function (n) {
|
|
@@ -645,12 +673,15 @@
|
|
|
645
673
|
.on("mouseleave", () => moveCursor(null));
|
|
646
674
|
flameSvg.append("g").attr("class", "grid");
|
|
647
675
|
flameSvg.append("g").attr("class", "cells");
|
|
676
|
+
flameSvg.append("line").attr("class", "pin").attr("stroke", css("--accent")).attr("stroke-width", 1.5).attr("y1", 0).attr("y2", 4000);
|
|
648
677
|
flameSvg.append("line").attr("class", "cursor").attr("y1", 0).attr("y2", 4000).attr("visibility", "hidden");
|
|
649
678
|
flameSvg.call(flameZoom).call(flameZoom.transform, state.zoom);
|
|
650
679
|
redrawX();
|
|
651
680
|
}
|
|
652
681
|
function drawFlame(x) {
|
|
653
682
|
if (!flameSvg) return;
|
|
683
|
+
const px = state.pin != null ? x(state.pin) : -10;
|
|
684
|
+
flameSvg.select("line.pin").attr("x1", px).attr("x2", px);
|
|
654
685
|
const H = el.flame.clientHeight;
|
|
655
686
|
const ticks = x.ticks(Math.max(2, Math.floor(W / 90)));
|
|
656
687
|
flameSvg
|
|
@@ -693,6 +724,7 @@
|
|
|
693
724
|
|
|
694
725
|
// ── flow view ──
|
|
695
726
|
let flowG = null;
|
|
727
|
+
let flowPos = new Map();
|
|
696
728
|
let flowBounds = null;
|
|
697
729
|
function renderFlow() {
|
|
698
730
|
d3.select(el.flow).selectAll("svg").remove();
|
|
@@ -701,7 +733,10 @@
|
|
|
701
733
|
const h = d3.hierarchy(model.root, (n) => n.children);
|
|
702
734
|
const CW = 210;
|
|
703
735
|
const CH = 46;
|
|
704
|
-
|
|
736
|
+
// cards that took snapshots carry a strip of them underneath, so leave room for it
|
|
737
|
+
const shotsOf = (n) => model.gallery.filter((g) => g.node === n);
|
|
738
|
+
const SH = model.gallery.some((g) => g.node) ? 36 : 0;
|
|
739
|
+
d3.tree().nodeSize([CH + 18 + SH, CW + 70])(h);
|
|
705
740
|
let minX = Infinity;
|
|
706
741
|
let maxX = -Infinity;
|
|
707
742
|
let maxY = 0;
|
|
@@ -752,6 +787,77 @@
|
|
|
752
787
|
.attr("cy", 14)
|
|
753
788
|
.attr("r", 4)
|
|
754
789
|
.attr("fill", (d) => css(statusVar(d.data.status)));
|
|
790
|
+
const fcb = card
|
|
791
|
+
.filter((d) => hasCode(d.data))
|
|
792
|
+
.append("g")
|
|
793
|
+
.attr("class", "code-btn")
|
|
794
|
+
.attr("transform", `translate(${CW / 2},0)`)
|
|
795
|
+
.on("click", (ev, d) => {
|
|
796
|
+
ev.stopPropagation();
|
|
797
|
+
toggleCode(d.data, ev);
|
|
798
|
+
})
|
|
799
|
+
.on("mouseenter", (ev) => {
|
|
800
|
+
ev.stopPropagation();
|
|
801
|
+
showTip(ev, "Show source");
|
|
802
|
+
});
|
|
803
|
+
fcb.append("circle").attr("r", 9);
|
|
804
|
+
fcb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
|
|
805
|
+
const TW = 46;
|
|
806
|
+
const TH = 28;
|
|
807
|
+
const strip = card
|
|
808
|
+
.filter((d) => shotsOf(d.data).length > 0)
|
|
809
|
+
.append("g")
|
|
810
|
+
.attr("class", "flow-shots")
|
|
811
|
+
.attr("transform", `translate(6,${CH + 4})`);
|
|
812
|
+
strip.each(function (d) {
|
|
813
|
+
const shots = shotsOf(d.data);
|
|
814
|
+
const fit = Math.floor((CW - 12) / (TW + 4));
|
|
815
|
+
const shown = shots.length > fit ? shots.slice(0, fit - 1) : shots;
|
|
816
|
+
const g = d3.select(this);
|
|
817
|
+
const cell = g
|
|
818
|
+
.selectAll("g.shot")
|
|
819
|
+
.data(shown)
|
|
820
|
+
.join("g")
|
|
821
|
+
.attr("class", "shot")
|
|
822
|
+
.attr("transform", (_x, i) => `translate(${i * (TW + 4)},0)`)
|
|
823
|
+
.on("click", (ev, x) => {
|
|
824
|
+
ev.stopPropagation();
|
|
825
|
+
openGallery(x);
|
|
826
|
+
})
|
|
827
|
+
.on("mouseenter", (ev, x) => {
|
|
828
|
+
ev.stopPropagation();
|
|
829
|
+
showTip(ev, galleryTip(x));
|
|
830
|
+
})
|
|
831
|
+
.on("mousemove", (ev) => moveTip(ev))
|
|
832
|
+
.on("mouseleave", hideTip);
|
|
833
|
+
cell.append("rect").attr("width", TW).attr("height", TH).attr("rx", 4);
|
|
834
|
+
cell
|
|
835
|
+
.filter((x) => x.image)
|
|
836
|
+
.append("image")
|
|
837
|
+
.attr("href", (x) => model.asset(x.f.asset))
|
|
838
|
+
.attr("x", 1)
|
|
839
|
+
.attr("y", 1)
|
|
840
|
+
.attr("width", TW - 2)
|
|
841
|
+
.attr("height", TH - 2)
|
|
842
|
+
.attr("preserveAspectRatio", "xMidYMid slice");
|
|
843
|
+
cell
|
|
844
|
+
.filter((x) => !x.image)
|
|
845
|
+
.append("text")
|
|
846
|
+
.attr("x", TW / 2)
|
|
847
|
+
.attr("y", TH / 2 + 3.5)
|
|
848
|
+
.attr("text-anchor", "middle")
|
|
849
|
+
.text((x) => (x.type === "snapshot" ? "{ }" : "file"));
|
|
850
|
+
if (shots.length > shown.length) {
|
|
851
|
+
const more = g.append("g").attr("class", "shot more").attr("transform", `translate(${shown.length * (TW + 4)},0)`)
|
|
852
|
+
.on("click", (ev) => {
|
|
853
|
+
ev.stopPropagation();
|
|
854
|
+
select(d.data.id, "snapshots");
|
|
855
|
+
});
|
|
856
|
+
more.append("rect").attr("width", TW).attr("height", TH).attr("rx", 4);
|
|
857
|
+
more.append("text").attr("x", TW / 2).attr("y", TH / 2 + 3.5).attr("text-anchor", "middle").text(`+${shots.length - shown.length}`);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
flowPos = new Map(h.descendants().map((d) => [d.data.id, { x: d.y + CW / 2, y: d.x }]));
|
|
755
861
|
svg.call(flowZoom);
|
|
756
862
|
fitFlow(true);
|
|
757
863
|
}
|
|
@@ -820,7 +926,10 @@
|
|
|
820
926
|
state.details = true;
|
|
821
927
|
el.main.classList.remove("no-details");
|
|
822
928
|
el.tree.querySelectorAll(".row").forEach((r) => r.classList.toggle("selected", r.dataset.id === id));
|
|
823
|
-
if (chartSvg)
|
|
929
|
+
if (chartSvg) {
|
|
930
|
+
chartSvg.selectAll("rect.bar").classed("selected", (n) => n.id === id);
|
|
931
|
+
chartSvg.selectAll("g.r").classed("sel", (n) => n.id === id);
|
|
932
|
+
}
|
|
824
933
|
if (flameSvg) flameSvg.selectAll("rect.flame-rect").classed("selected", (n) => n.id === id);
|
|
825
934
|
d3.select(el.flow).selectAll("g.flow-card").classed("selected", (d) => d.data.id === id);
|
|
826
935
|
renderDetails();
|
|
@@ -829,7 +938,7 @@
|
|
|
829
938
|
const pad = Math.max(n.end - n.start, 50) * 0.08;
|
|
830
939
|
zoomToDomain(n.start - pad, n.end + pad);
|
|
831
940
|
}
|
|
832
|
-
function revealRow(n) {
|
|
941
|
+
function revealRow(n, block) {
|
|
833
942
|
let p = n.parent;
|
|
834
943
|
let changed = false;
|
|
835
944
|
while (p) {
|
|
@@ -838,7 +947,55 @@
|
|
|
838
947
|
}
|
|
839
948
|
if (changed && state.view === "timeline") renderTimeline();
|
|
840
949
|
const row = el.tree.querySelector(`.row[data-id="${CSS.escape(n.id)}"]`);
|
|
841
|
-
if (row) row.scrollIntoView({ block: "nearest" });
|
|
950
|
+
if (row) row.scrollIntoView({ block: block || "nearest" });
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Bring a gallery item into view in whichever graph is showing: select its node, pin its
|
|
955
|
+
* time, pan (keeping the zoom) so that time is on screen, and pulse its mark.
|
|
956
|
+
*/
|
|
957
|
+
function locate(g) {
|
|
958
|
+
if (!g) return;
|
|
959
|
+
if (g.node) select(g.node.id, "snapshots");
|
|
960
|
+
if (state.view === "flow") {
|
|
961
|
+
if (g.node) centerFlowOn(g.node);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (g.t != null) {
|
|
965
|
+
state.pin = g.t;
|
|
966
|
+
const [a, b] = xz().domain();
|
|
967
|
+
if (g.t < a || g.t > b) {
|
|
968
|
+
const k = state.zoom.k;
|
|
969
|
+
const t = d3.zoomIdentity.translate(W / 2 - k * baseX(g.t), 0).scale(k);
|
|
970
|
+
const { z, sel } = activeZoom();
|
|
971
|
+
sel.call(z.transform, t);
|
|
972
|
+
} else redrawX();
|
|
973
|
+
}
|
|
974
|
+
if (g.node && state.view === "timeline") revealRow(g.node, "center");
|
|
975
|
+
flash(g);
|
|
976
|
+
}
|
|
977
|
+
function flash(g) {
|
|
978
|
+
const svgs = [chartSvg, flameSvg].filter(Boolean);
|
|
979
|
+
for (const s of svgs) {
|
|
980
|
+
s.selectAll("line.pin").classed("flash", false).node()?.getBoundingClientRect();
|
|
981
|
+
s.selectAll("line.pin").classed("flash", true);
|
|
982
|
+
}
|
|
983
|
+
if (chartSvg) {
|
|
984
|
+
const hit = chartSvg
|
|
985
|
+
.selectAll("circle.mark")
|
|
986
|
+
.filter((d) => d.m.gallery === g || d.more.some((m) => m.gallery === g));
|
|
987
|
+
hit.classed("flash", false).node()?.getBoundingClientRect();
|
|
988
|
+
hit.classed("flash", true);
|
|
989
|
+
}
|
|
990
|
+
setTimeout(() => d3.selectAll(".flash").classed("flash", false), 1800);
|
|
991
|
+
}
|
|
992
|
+
function centerFlowOn(n) {
|
|
993
|
+
const svg = d3.select(el.flow).select("svg");
|
|
994
|
+
const p = flowPos.get(n.id);
|
|
995
|
+
if (svg.empty() || !p) return;
|
|
996
|
+
const k = Math.max(0.8, d3.zoomTransform(svg.node()).k);
|
|
997
|
+
const t = d3.zoomIdentity.translate(el.flow.clientWidth / 2 - k * p.x, el.flow.clientHeight / 2 - k * p.y).scale(k);
|
|
998
|
+
svg.transition().duration(300).call(flowZoom.transform, t);
|
|
842
999
|
}
|
|
843
1000
|
|
|
844
1001
|
function renderDetails() {
|
|
@@ -866,7 +1023,12 @@
|
|
|
866
1023
|
${n.v.description ? `<div class="k">Summary</div><div class="v">${esc(n.v.description.summary)}</div>` : ""}
|
|
867
1024
|
${n.v.mcp ? `<div class="k">MCP</div><div class="v"><span class="badge-inline">${esc(n.v.mcp.mode)}</span></div>` : ""}
|
|
868
1025
|
<div class="k">Path</div><div class="v">${esc(n.v.path)}</div>
|
|
869
|
-
${n
|
|
1026
|
+
${userSite(n) ? `<div class="k">${siteLabel(n)}</div><div class="v defined">
|
|
1027
|
+
<button class="icon-btn" data-copy-source aria-label="Copy relative path">${svgIcon("copy")}</button>
|
|
1028
|
+
${hasCode(n) ? `<button class="icon-btn" data-show-code aria-label="Show source">${svgIcon("code")}</button>` : ""}
|
|
1029
|
+
<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>
|
|
1030
|
+
<span class="defined-name" >${esc(basename(sourceOf(n)))}</span>
|
|
1031
|
+
</div>` : ""}
|
|
870
1032
|
</div>
|
|
871
1033
|
<div class="tabs">${tabs
|
|
872
1034
|
.map(([k, l]) => `<button class="tab ${state.detailTab === k ? "active" : ""}" data-tab="${k}">${l}</button>`)
|
|
@@ -874,7 +1036,24 @@
|
|
|
874
1036
|
<div class="tabbody">${detailTabHtml(n)}</div>
|
|
875
1037
|
</div>`;
|
|
876
1038
|
const copySrc = el.details.querySelector("[data-copy-source]");
|
|
877
|
-
if (copySrc)
|
|
1039
|
+
if (copySrc) {
|
|
1040
|
+
copySrc.addEventListener("click", () => copySource(n));
|
|
1041
|
+
copySrc.addEventListener("mouseenter", (ev) =>
|
|
1042
|
+
showTip(ev, `<div class="mono">${esc(sourceOf(n))}</div><div class="hint">Click (or press <kbd>C</kbd>) to copy</div>`)
|
|
1043
|
+
);
|
|
1044
|
+
copySrc.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
1045
|
+
copySrc.addEventListener("mouseleave", hideTip);
|
|
1046
|
+
}
|
|
1047
|
+
const showCode = el.details.querySelector("[data-show-code]");
|
|
1048
|
+
if (showCode) {
|
|
1049
|
+
showCode.addEventListener("click", (ev) => toggleCode(n, ev));
|
|
1050
|
+
showCode.addEventListener("mouseenter", (ev) => showTip(ev, "Show source"));
|
|
1051
|
+
showCode.addEventListener("mouseleave", hideTip);
|
|
1052
|
+
}
|
|
1053
|
+
el.details.querySelectorAll("[data-tip-text]").forEach((a) => {
|
|
1054
|
+
a.addEventListener("mouseenter", (ev) => showTip(ev, esc(a.dataset.tipText)));
|
|
1055
|
+
a.addEventListener("mouseleave", hideTip);
|
|
1056
|
+
});
|
|
878
1057
|
el.details.querySelector(".x").addEventListener("click", () => {
|
|
879
1058
|
state.details = false;
|
|
880
1059
|
el.main.classList.add("no-details");
|
|
@@ -1002,6 +1181,14 @@
|
|
|
1002
1181
|
body.querySelectorAll("tr[data-row]").forEach((tr) =>
|
|
1003
1182
|
tr.addEventListener("click", () => openItem(model.items[+tr.dataset.row]))
|
|
1004
1183
|
);
|
|
1184
|
+
body.querySelectorAll("[data-locate]").forEach((b) => {
|
|
1185
|
+
b.addEventListener("click", (ev) => {
|
|
1186
|
+
ev.stopPropagation();
|
|
1187
|
+
locate(model.gallery[+b.dataset.locate]);
|
|
1188
|
+
});
|
|
1189
|
+
b.addEventListener("mouseenter", (ev) => showTip(ev, `Show in the ${state.view === "flow" ? "flow" : "graph"}`));
|
|
1190
|
+
b.addEventListener("mouseleave", hideTip);
|
|
1191
|
+
});
|
|
1005
1192
|
}
|
|
1006
1193
|
function matches(...parts) {
|
|
1007
1194
|
const q = state.filter.trim().toLowerCase();
|
|
@@ -1015,7 +1202,8 @@
|
|
|
1015
1202
|
return `<div class="gallery">${g
|
|
1016
1203
|
.map((x) => {
|
|
1017
1204
|
const sub = x.type === "snapshot" ? `snapshot · ${x.node ? esc(displayName(x.node)) : ""}` : `${esc(x.f.mime)} · ${fmtSize(x.f.size)}${x.f.omitted ? " · not embedded" : ""}`;
|
|
1018
|
-
|
|
1205
|
+
const at = x.t != null || x.node ? `<button class="icon-btn locate" data-locate="${model.gallery.indexOf(x)}" aria-label="Show in graph">${svgIcon("locate")}</button>` : "";
|
|
1206
|
+
return `<div class="gal-item">${thumbHtml(x)}<div class="cap"><span class="t">${x.t == null ? "–" : fmtTick(x.t)}</span><span class="n" title="${esc(x.name)}">${esc(x.name)}</span>${at}</div><div class="sz">${sub}</div></div>`;
|
|
1019
1207
|
})
|
|
1020
1208
|
.join("")}</div>`;
|
|
1021
1209
|
}
|
|
@@ -1144,7 +1332,7 @@
|
|
|
1144
1332
|
function nodeTip(n) {
|
|
1145
1333
|
hovered = n; // the node `C` copies while its tooltip is up
|
|
1146
1334
|
const v = n.v;
|
|
1147
|
-
const shown = displayName(n);
|
|
1335
|
+
const shown = n.depth === 0 ? model.data.name : displayName(n);
|
|
1148
1336
|
const badges = [
|
|
1149
1337
|
`<span class="badge"><span class="dot ${n.status}"></span>${esc(n.status)}</span>`,
|
|
1150
1338
|
v.mcp ? `<span class="badge mcp" title="Exposed over MCP">MCP · ${esc(v.mcp.mode)}</span>` : "",
|
|
@@ -1154,23 +1342,41 @@
|
|
|
1154
1342
|
["Duration", fmtDur(n.duration)],
|
|
1155
1343
|
["When", `${fmtRel(n.start)} → ${fmtRel(n.end)}`],
|
|
1156
1344
|
v.tags.length ? ["Tags", esc(v.tags.join(" · "))] : null,
|
|
1157
|
-
|
|
1345
|
+
userSite(n) ? [siteLabel(n) === "Called at" ? "Called" : "Defined", `<span class="mono">${esc(sourceOf(n))}</span>`] : null,
|
|
1158
1346
|
n.children.length ? ["Children", String(n.children.length)] : null,
|
|
1159
1347
|
n.marks.length ? ["Events", String(n.marks.length)] : null,
|
|
1160
1348
|
].filter(Boolean);
|
|
1161
|
-
const args = v.args && v.args.length ? truncate(oneLine(v.args.length === 1 ? v.args[0] : v.args), 220) : "";
|
|
1349
|
+
const args = !promptOf(n) && v.args && v.args.length ? truncate(oneLine(v.args.length === 1 ? v.args[0] : v.args), 220) : "";
|
|
1162
1350
|
const params = schemaFields(v.inputSchema);
|
|
1351
|
+
const pr = promptOf(n);
|
|
1352
|
+
const promptRows = pr
|
|
1353
|
+
? `<div class="sec">Prompt</div><div class="tprompt">${esc(truncate(pr.message || "", 200))}</div>${
|
|
1354
|
+
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>` : ""
|
|
1355
|
+
}`
|
|
1356
|
+
: "";
|
|
1163
1357
|
const fails = model.run.failures.filter((f) => f.action === n.id || f.action === n.name || f.action === v.path);
|
|
1164
1358
|
return `<div class="tt">${esc(shown)}</div>
|
|
1165
1359
|
${shown !== n.name ? `<div class="ts mono">${esc(truncate(n.name, 140))}</div>` : ""}
|
|
1166
1360
|
<div class="badges">${badges}</div>
|
|
1167
1361
|
${v.description ? `<div class="summary">${esc(v.description.summary)}</div>` : '<div class="summary muted">No description.</div>'}
|
|
1168
1362
|
<div class="tkv">${rows.map(([k, val]) => `<span class="k">${k}</span><span>${val}</span>`).join("")}</div>
|
|
1363
|
+
${promptRows}
|
|
1169
1364
|
${params.length ? `<div class="sec">Args schema</div><div class="params">${params.map(paramHtml).join("")}</div>` : ""}
|
|
1170
1365
|
${args ? `<div class="sec">Args</div><div class="mono small">${esc(args)}</div>` : ""}
|
|
1171
1366
|
${v.definedAt ? '<div class="hint">Press <kbd>C</kbd> to copy the path</div>' : ""}
|
|
1172
1367
|
${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>` : ""}`;
|
|
1173
1368
|
}
|
|
1369
|
+
function galleryTip(x) {
|
|
1370
|
+
const img = x.image ? `<img src="${model.asset(x.f.asset)}" alt="">` : "";
|
|
1371
|
+
const body = x.type === "snapshot"
|
|
1372
|
+
? `<div class="mono small">${esc(truncate(x.s.value !== undefined ? oneLine(x.s.value) : x.s.text || "", 200))}</div>`
|
|
1373
|
+
: `<div class="ts">${esc(x.f.mime)} · ${fmtSize(x.f.size)}</div>`;
|
|
1374
|
+
return `<div class="tt">${esc(x.name)}${x.t != null ? ` · ${fmtRel(x.t)}` : ""}</div>${img}${body}`;
|
|
1375
|
+
}
|
|
1376
|
+
/** The om itself: its node tooltip, plus where the run's record lives. */
|
|
1377
|
+
function omTip() {
|
|
1378
|
+
return `${nodeTip(model.root)}<div class="sec">Run folder</div><div class="mono small">${esc(model.data.folder)}</div>`;
|
|
1379
|
+
}
|
|
1174
1380
|
function markTip(d) {
|
|
1175
1381
|
const m = d.m;
|
|
1176
1382
|
const mk = MARK[m.kind] || MARK.event;
|
|
@@ -1179,13 +1385,140 @@
|
|
|
1179
1385
|
const extra = m.gallery && m.gallery.image ? `<div class="ts">${esc(m.gallery.f.rel)} · ${fmtSize(m.gallery.f.size)}</div>` : "";
|
|
1180
1386
|
return `<div class="tt">${esc(mk.label)} · ${fmtRel(m.t)}</div>${img}<div>${esc(truncate(m.kind === "snapshot" || m.kind === "artifact" ? m.label : m.e.message, 260))}</div>${extra}${more}`;
|
|
1181
1387
|
}
|
|
1388
|
+
// ── source popover (the <> buttons) — click-toggled, like the tskb explorer's ──
|
|
1389
|
+
const codeEl = document.createElement("div");
|
|
1390
|
+
codeEl.className = "code-pop";
|
|
1391
|
+
codeEl.hidden = true;
|
|
1392
|
+
document.body.appendChild(codeEl);
|
|
1393
|
+
let codeFor = null;
|
|
1394
|
+
let codeAway = null;
|
|
1395
|
+
/** A site inside omkit or an installed package — library source, not the user's. */
|
|
1396
|
+
function isLib(site) {
|
|
1397
|
+
return !!(site && model.data.library && model.data.library.includes(site));
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* The site worth showing for `n`: where it is defined, unless that is library source (a
|
|
1401
|
+
* battery, a prompt), in which case the user's call that launched it.
|
|
1402
|
+
*/
|
|
1403
|
+
function userSite(n) {
|
|
1404
|
+
const { definedAt, calledAt } = n.v;
|
|
1405
|
+
if (definedAt && !isLib(definedAt)) return definedAt;
|
|
1406
|
+
if (calledAt && !isLib(calledAt)) return calledAt;
|
|
1407
|
+
return definedAt || calledAt;
|
|
1408
|
+
}
|
|
1409
|
+
function siteLabel(n) {
|
|
1410
|
+
return userSite(n) === n.v.definedAt ? "Defined at" : "Called at";
|
|
1411
|
+
}
|
|
1412
|
+
function codeOf(site) {
|
|
1413
|
+
return site && model.data.code ? model.data.code[site] : undefined;
|
|
1414
|
+
}
|
|
1415
|
+
/** The popover's tabs for `n`, most useful first. */
|
|
1416
|
+
function codeTabs(n) {
|
|
1417
|
+
const tabs = [];
|
|
1418
|
+
if (promptOf(n)) tabs.push({ key: "prompt", label: "Prompt" });
|
|
1419
|
+
const mine = userSite(n);
|
|
1420
|
+
if (codeOf(mine)) tabs.push({ key: "user", label: siteLabel(n) === "Called at" ? "Call site" : "Source", site: mine });
|
|
1421
|
+
const def = n.v.definedAt;
|
|
1422
|
+
if (def && def !== mine && codeOf(def)) tabs.push({ key: "def", label: isLib(def) ? "Definition (omkit)" : "Definition", site: def });
|
|
1423
|
+
return tabs;
|
|
1424
|
+
}
|
|
1425
|
+
function hasCode(n) {
|
|
1426
|
+
return codeTabs(n).length > 0;
|
|
1427
|
+
}
|
|
1428
|
+
function relOf(site) {
|
|
1429
|
+
return (model.data.sources && model.data.sources[site]) || String(site).replace(/:\d+(:\d+)?$/, "");
|
|
1430
|
+
}
|
|
1431
|
+
function codeHtml(site) {
|
|
1432
|
+
const c = codeOf(site);
|
|
1433
|
+
const line = Number((/:(\d+)(?::\d+)?$/.exec(site) || [])[1]);
|
|
1434
|
+
const lines = highlight(c.text).map(
|
|
1435
|
+
(html, i) => `<div class="cl${c.start + i === line ? " at" : ""}"><span class="ln">${c.start + i}</span><span class="lc">${html || " "}</span></div>`
|
|
1436
|
+
);
|
|
1437
|
+
return `<div class="cp-code">${lines.join("")}</div>${c.truncated ? '<div class="muted cp-more">Cut short — open the file for the rest.</div>' : ""}`;
|
|
1438
|
+
}
|
|
1439
|
+
function promptHtml(p) {
|
|
1440
|
+
const choices = p.choices && p.choices.length
|
|
1441
|
+
? `<div class="pp-k">Choices</div><div class="pp-v"><ol class="pp-choices">${p.choices
|
|
1442
|
+
.map((c) => {
|
|
1443
|
+
const value = typeof c === "string" ? c : c.value;
|
|
1444
|
+
const label = typeof c === "string" ? c : c.label;
|
|
1445
|
+
const picked = p.answer && p.answer.value === value;
|
|
1446
|
+
return `<li class="${picked ? "picked" : ""}">${esc(label)}${label !== value ? ` <span class="muted">(${esc(value)})</span>` : ""}</li>`;
|
|
1447
|
+
})
|
|
1448
|
+
.join("")}</ol></div>`
|
|
1449
|
+
: "";
|
|
1450
|
+
const answer = p.answer
|
|
1451
|
+
? `<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>${
|
|
1452
|
+
p.answer.via ? `<span class="badge-inline">via ${esc(p.answer.via)}</span>` : ""
|
|
1453
|
+
}</div>`
|
|
1454
|
+
: '<div class="muted">No answer recorded.</div>';
|
|
1455
|
+
return `<div class="pp">
|
|
1456
|
+
<div class="pp-q">${esc(p.message || "(no message)")}</div>
|
|
1457
|
+
${p.hint ? `<div class="mono small">${esc(p.hint)}</div>` : ""}
|
|
1458
|
+
<div class="pp-grid">
|
|
1459
|
+
<div class="pp-k">Kind</div><div class="pp-v">${esc(p.kind)}</div>
|
|
1460
|
+
${p.def !== undefined ? `<div class="pp-k">Default</div><div class="pp-v mono">${esc(oneLine(p.def))}</div>` : ""}
|
|
1461
|
+
${p.timeoutMs !== undefined ? `<div class="pp-k">Timeout</div><div class="pp-v">${p.timeoutMs > 0 && isFinite(p.timeoutMs) ? fmtDur(p.timeoutMs) : "none"}</div>` : ""}
|
|
1462
|
+
${choices}
|
|
1463
|
+
</div>
|
|
1464
|
+
${answer}
|
|
1465
|
+
</div>`;
|
|
1466
|
+
}
|
|
1467
|
+
function toggleCode(n, ev, want) {
|
|
1468
|
+
if (codeFor === n.id && !want) return closeCode();
|
|
1469
|
+
const tabs = codeTabs(n);
|
|
1470
|
+
if (!tabs.length) return;
|
|
1471
|
+
const tab = tabs.find((t) => t.key === want) || tabs[0];
|
|
1472
|
+
hideTip();
|
|
1473
|
+
const reopen = codeFor !== n.id;
|
|
1474
|
+
codeFor = n.id;
|
|
1475
|
+
const site = tab.site || userSite(n);
|
|
1476
|
+
const rel = site ? relOf(site) : "";
|
|
1477
|
+
const line = site ? (/:(\d+)(?::\d+)?$/.exec(site) || [])[1] : "";
|
|
1478
|
+
codeEl.innerHTML = `<div class="cp-head">
|
|
1479
|
+
${site ? `<span class="cp-path" title="${esc(site)}">${esc(rel)}<span class="muted">${line ? ":" + line : ""}</span></span>
|
|
1480
|
+
<button class="icon-btn" data-a="copy" aria-label="Copy path">${svgIcon("copy")}</button>
|
|
1481
|
+
<a class="icon-btn" href="${esc(editorUrl(site))}" aria-label="Open in VS Code">${svgIcon("external")}</a>` : ""}
|
|
1482
|
+
<span class="cp-name">${esc(displayName(n))}</span>
|
|
1483
|
+
<button class="icon-btn" data-a="close" aria-label="Close">${svgIcon("close")}</button>
|
|
1484
|
+
</div>
|
|
1485
|
+
${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>` : ""}
|
|
1486
|
+
<div class="cp-body">${tab.key === "prompt" ? promptHtml(promptOf(n)) : codeHtml(site)}</div>`;
|
|
1487
|
+
const cp = codeEl.querySelector('[data-a="copy"]');
|
|
1488
|
+
if (cp) cp.addEventListener("click", () => copy(rel, `Copied ${rel}`));
|
|
1489
|
+
codeEl.querySelector('[data-a="close"]').addEventListener("click", closeCode);
|
|
1490
|
+
codeEl.querySelectorAll(".cp-tabs .tab").forEach((b) => b.addEventListener("click", () => toggleCode(n, null, b.dataset.tab)));
|
|
1491
|
+
codeEl.hidden = false;
|
|
1492
|
+
if (reopen) {
|
|
1493
|
+
const x = ev && ev.clientX != null ? ev.clientX : window.innerWidth / 2;
|
|
1494
|
+
const y = ev && ev.clientY != null ? ev.clientY : window.innerHeight / 3;
|
|
1495
|
+
const w = codeEl.offsetWidth;
|
|
1496
|
+
const h = Math.min(codeEl.offsetHeight, window.innerHeight * 0.7);
|
|
1497
|
+
const left = x + 14 + w > window.innerWidth - 8 ? x - w - 14 : x + 14;
|
|
1498
|
+
const top = y + 14 + h > window.innerHeight - 8 ? y - h - 14 : y + 14;
|
|
1499
|
+
codeEl.style.left = `${Math.max(8, left)}px`;
|
|
1500
|
+
codeEl.style.top = `${Math.max(8, top)}px`;
|
|
1501
|
+
}
|
|
1502
|
+
const at = codeEl.querySelector(".cl.at");
|
|
1503
|
+
if (at) at.scrollIntoView({ block: "center" });
|
|
1504
|
+
if (codeAway) document.removeEventListener("mousedown", codeAway, true);
|
|
1505
|
+
codeAway = (e) => {
|
|
1506
|
+
if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code]")) closeCode();
|
|
1507
|
+
};
|
|
1508
|
+
setTimeout(() => codeAway && document.addEventListener("mousedown", codeAway, true), 0);
|
|
1509
|
+
}
|
|
1510
|
+
function closeCode() {
|
|
1511
|
+
codeFor = null;
|
|
1512
|
+
codeEl.hidden = true;
|
|
1513
|
+
if (codeAway) document.removeEventListener("mousedown", codeAway, true);
|
|
1514
|
+
codeAway = null;
|
|
1515
|
+
}
|
|
1182
1516
|
let tipTimer = 0;
|
|
1183
1517
|
let hovered = null;
|
|
1184
|
-
/** The file
|
|
1518
|
+
/** The file worth opening for `n` (see {@link userSite}), relative to the project root — no line. */
|
|
1185
1519
|
function sourceOf(n) {
|
|
1186
|
-
const site = n
|
|
1187
|
-
|
|
1188
|
-
return (model.data.sources && model.data.sources[site]) || site.replace(/:\d+(:\d+)?$/, "");
|
|
1520
|
+
const site = userSite(n);
|
|
1521
|
+
return site ? relOf(site) : "";
|
|
1189
1522
|
}
|
|
1190
1523
|
function copySource(n) {
|
|
1191
1524
|
const src = n && sourceOf(n);
|
|
@@ -1242,6 +1575,7 @@
|
|
|
1242
1575
|
resizeTimer = setTimeout(relayout, 120);
|
|
1243
1576
|
});
|
|
1244
1577
|
document.addEventListener("keydown", (e) => {
|
|
1578
|
+
if (e.key === "Escape" && codeFor) return closeCode();
|
|
1245
1579
|
if (e.target.closest && e.target.closest("input")) return;
|
|
1246
1580
|
if (e.key === "+" || e.key === "=") zoomBy(1.6);
|
|
1247
1581
|
else if (e.key === "-") zoomBy(1 / 1.6);
|
|
@@ -1278,7 +1612,10 @@
|
|
|
1278
1612
|
return `
|
|
1279
1613
|
<header class="top">
|
|
1280
1614
|
<div class="titles">
|
|
1281
|
-
<
|
|
1615
|
+
<div class="title-row">
|
|
1616
|
+
<h1 title="${esc(model.data.folder)}">Run: ${esc(model.data.name)}</h1>
|
|
1617
|
+
<button class="info-btn" id="om-info" aria-label="About this om">${svgIcon("info")}</button>
|
|
1618
|
+
</div>
|
|
1282
1619
|
<div class="meta"><span class="dot ${ok ? "ok" : "failed"}"></span><span>${ok ? "passed" : "failed"}</span>
|
|
1283
1620
|
<span class="sep">•</span><span>${fmtDate(run.startedAt)}</span>
|
|
1284
1621
|
<span class="sep">•</span><span title="body ${fmtDur(run.duration)} · record ${fmtDur(model.span)}">${fmtDur(run.duration)}</span>
|
|
@@ -1358,6 +1695,41 @@
|
|
|
1358
1695
|
c.opacity = a;
|
|
1359
1696
|
return c.formatRgb();
|
|
1360
1697
|
}
|
|
1698
|
+
const KEYWORDS = new Set(
|
|
1699
|
+
"abstract as async await break case catch class const continue default delete do else enum export extends false finally for from function get if implements import in instanceof interface keyof let new null of private protected public readonly return satisfies set static super switch this throw true try type typeof undefined var void while yield".split(" ")
|
|
1700
|
+
);
|
|
1701
|
+
const TOKEN =
|
|
1702
|
+
/(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(`(?:\\[\s\S]|[^`\\])*`|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*')|(\b\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?n?\b)|([A-Za-z_$][\w$]*)/g;
|
|
1703
|
+
/** Highlighted HTML per source line. Tokens that span lines are closed and reopened at each break. */
|
|
1704
|
+
function highlight(text) {
|
|
1705
|
+
const parts = [];
|
|
1706
|
+
let last = 0;
|
|
1707
|
+
let m;
|
|
1708
|
+
TOKEN.lastIndex = 0;
|
|
1709
|
+
while ((m = TOKEN.exec(text))) {
|
|
1710
|
+
if (m.index > last) parts.push([null, text.slice(last, m.index)]);
|
|
1711
|
+
let cls = null;
|
|
1712
|
+
if (m[1]) cls = "tk-c";
|
|
1713
|
+
else if (m[2]) cls = "tk-s";
|
|
1714
|
+
else if (m[3]) cls = "tk-n";
|
|
1715
|
+
else if (m[4]) {
|
|
1716
|
+
if (KEYWORDS.has(m[4])) cls = "tk-k";
|
|
1717
|
+
else if (/^\s*\(/.test(text.slice(TOKEN.lastIndex, TOKEN.lastIndex + 40))) cls = "tk-f";
|
|
1718
|
+
else if (/^[A-Z]/.test(m[4])) cls = "tk-t";
|
|
1719
|
+
}
|
|
1720
|
+
parts.push([cls, m[0]]);
|
|
1721
|
+
last = TOKEN.lastIndex;
|
|
1722
|
+
}
|
|
1723
|
+
if (last < text.length) parts.push([null, text.slice(last)]);
|
|
1724
|
+
const lines = [""];
|
|
1725
|
+
for (const [cls, chunk] of parts) {
|
|
1726
|
+
chunk.split("\n").forEach((piece, i) => {
|
|
1727
|
+
if (i > 0) lines.push("");
|
|
1728
|
+
if (piece) lines[lines.length - 1] += cls ? `<span class="${cls}">${esc(piece)}</span>` : esc(piece);
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
return lines;
|
|
1732
|
+
}
|
|
1361
1733
|
function oneLine(v) {
|
|
1362
1734
|
try {
|
|
1363
1735
|
return typeof v === "string" ? v : JSON.stringify(v);
|
|
@@ -1392,12 +1764,48 @@
|
|
|
1392
1764
|
f.def !== undefined ? `<span class="pd">= ${esc(truncate(f.def, 24))}</span>` : ""
|
|
1393
1765
|
}${f.desc ? `<div class="pdesc">${esc(f.desc)}</div>` : ""}</div>`;
|
|
1394
1766
|
}
|
|
1767
|
+
/**
|
|
1768
|
+
* What a `prompt` battery node asked and got back: its options (the node's one argument) plus
|
|
1769
|
+
* the `prompt` / `answer` events it emitted. `null` for anything that is not a prompt.
|
|
1770
|
+
*/
|
|
1771
|
+
function promptOf(n) {
|
|
1772
|
+
if (n._prompt !== undefined) return n._prompt;
|
|
1773
|
+
n._prompt = null;
|
|
1774
|
+
if (n.name !== "prompt" || n.depth === 0) return null;
|
|
1775
|
+
const opts = (n.v.args && n.v.args[0] && typeof n.v.args[0] === "object" ? n.v.args[0] : {}) || {};
|
|
1776
|
+
let message = opts.message;
|
|
1777
|
+
let answer = null;
|
|
1778
|
+
for (const it of n.entries) {
|
|
1779
|
+
if (it.e.level !== "event") continue;
|
|
1780
|
+
if (it.e.message.startsWith("prompt · ")) message = it.e.message.slice("prompt · ".length);
|
|
1781
|
+
else if (it.e.message.startsWith("answer · ")) {
|
|
1782
|
+
try {
|
|
1783
|
+
answer = JSON.parse(it.e.message.slice("answer · ".length));
|
|
1784
|
+
} catch {
|
|
1785
|
+
answer = { value: it.e.message.slice("answer · ".length) };
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (message === undefined && !answer) return null;
|
|
1790
|
+
n._prompt = {
|
|
1791
|
+
message,
|
|
1792
|
+
kind: opts.kind || "input",
|
|
1793
|
+
hint: opts.hint,
|
|
1794
|
+
choices: opts.choices,
|
|
1795
|
+
def: opts.default,
|
|
1796
|
+
timeoutMs: opts.timeoutMs,
|
|
1797
|
+
answer,
|
|
1798
|
+
};
|
|
1799
|
+
return n._prompt;
|
|
1800
|
+
}
|
|
1395
1801
|
function truncate(s, n) {
|
|
1396
1802
|
n = Math.max(1, Math.floor(n));
|
|
1397
1803
|
return s.length > n ? `${s.slice(0, Math.max(1, n - 1))}…` : s;
|
|
1398
1804
|
}
|
|
1399
1805
|
function displayName(n) {
|
|
1400
1806
|
if (n.depth === 0) return "main";
|
|
1807
|
+
const p = promptOf(n);
|
|
1808
|
+
if (p && p.message) return `prompt: ${truncate(p.message, 48)}`;
|
|
1401
1809
|
const tag = n.v.tags && n.v.tags[0];
|
|
1402
1810
|
// Battery steps are named after their kind (`command`, `chromePage`); their first tag
|
|
1403
1811
|
// 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[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.13",
|
|
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",
|