jev-devtools 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -3
- package/lib/flow/index.d.ts +82 -0
- package/lib/flow/index.js +588 -0
- package/lib/server/flows.d.ts +25 -0
- package/lib/server/flows.js +115 -0
- package/lib/server/index.d.ts +8 -0
- package/lib/server/index.js +85 -1
- package/lib/server/recorder.d.ts +4 -1
- package/lib/server/recorder.js +12 -1
- package/lib/src/ChoiceProbabilities.js +1 -1
- package/lib/src/ConnectedInspector.js +13 -4
- package/lib/src/DiagramContent.d.ts +6 -0
- package/lib/src/DiagramContent.js +32 -0
- package/lib/src/FlowOverview.d.ts +7 -0
- package/lib/src/FlowOverview.js +228 -0
- package/lib/src/JevDevtools.d.ts +6 -1
- package/lib/src/JevDevtools.js +18 -4
- package/lib/src/TraceDiagram.d.ts +4 -1
- package/lib/src/TraceDiagram.js +30 -12
- package/lib/src/condition-inputs.d.ts +9 -0
- package/lib/src/condition-inputs.js +100 -0
- package/lib/src/flow-condition.d.ts +4 -0
- package/lib/src/flow-condition.js +47 -0
- package/lib/src/flow-model.d.ts +7 -2
- package/lib/src/flow-model.js +2 -11
- package/lib/src/flow-protocol.d.ts +68 -0
- package/lib/src/flow-protocol.js +1 -0
- package/lib/src/flow-stream.d.ts +8 -0
- package/lib/src/flow-stream.js +80 -0
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +501 -8
- package/lib/src/protocol.d.ts +5 -0
- package/lib/src/useDiagramViewport.d.ts +4 -0
- package/lib/src/useDiagramViewport.js +20 -3
- package/package.json +6 -2
- package/skills/setup-jev-devtools/SKILL.md +6 -2
- package/skills/setup-jev-devtools/references/flows.md +150 -0
- package/skills/setup-jev-devtools/references/nextjs.md +1 -1
- package/skills/setup-jev-devtools/references/verify.md +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { capture } from "./recorder.js";
|
|
2
|
+
/** Independent bounded run store. Definitions are retained only while referenced. */
|
|
3
|
+
export class FlowRecorder {
|
|
4
|
+
options;
|
|
5
|
+
runs = new Map();
|
|
6
|
+
definitions = new Map();
|
|
7
|
+
sessions = new Map();
|
|
8
|
+
definitionRevisions = new Map();
|
|
9
|
+
revisions = new Map();
|
|
10
|
+
revision = 0;
|
|
11
|
+
listeners = new Set();
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
subscribe(fn) {
|
|
16
|
+
this.listeners.add(fn);
|
|
17
|
+
return () => {
|
|
18
|
+
this.listeners.delete(fn);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
notify(sessions) {
|
|
22
|
+
for (const fn of this.listeners) {
|
|
23
|
+
try {
|
|
24
|
+
fn(sessions);
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
start(definition, run, session) {
|
|
30
|
+
if (!this.definitions.has(definition.id)) {
|
|
31
|
+
const stored = structuredClone(definition);
|
|
32
|
+
for (const node of stored.nodes) {
|
|
33
|
+
if (node.conditionRule === undefined)
|
|
34
|
+
continue;
|
|
35
|
+
try {
|
|
36
|
+
node.conditionRule = capture(node.conditionRule, new Set(), this.options.redact
|
|
37
|
+
? (path, value) => this.options.redact(["conditions", node.id, ...path], value)
|
|
38
|
+
: undefined);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
node.conditionRule = "[rule capture unavailable]";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this.definitions.set(definition.id, stored);
|
|
45
|
+
this.definitionRevisions.set(definition.id, ++this.revision);
|
|
46
|
+
}
|
|
47
|
+
this.runs.set(run.id, run);
|
|
48
|
+
this.sessions.set(run.id, session);
|
|
49
|
+
this.revisions.set(run.id, ++this.revision);
|
|
50
|
+
const sessions = new Set([session]);
|
|
51
|
+
const limit = Math.max(1, Math.min(200, this.options.limit ?? 50));
|
|
52
|
+
while (this.runs.size > limit) {
|
|
53
|
+
const id = this.runs.keys().next().value;
|
|
54
|
+
sessions.add(this.sessions.get(id));
|
|
55
|
+
this.remove(id);
|
|
56
|
+
}
|
|
57
|
+
this.prune();
|
|
58
|
+
this.notify(sessions);
|
|
59
|
+
}
|
|
60
|
+
update(run) {
|
|
61
|
+
if (!this.runs.has(run.id))
|
|
62
|
+
return; // Clear/eviction cannot resurrect pending runs.
|
|
63
|
+
const stored = { ...run };
|
|
64
|
+
if ("value" in stored) {
|
|
65
|
+
try {
|
|
66
|
+
stored.value = capture(stored.value, new Set(), this.options.redact);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
stored.value = "[capture unavailable]";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.runs.set(run.id, stored);
|
|
73
|
+
this.revisions.set(run.id, ++this.revision);
|
|
74
|
+
this.notify(new Set([this.sessions.get(run.id)]));
|
|
75
|
+
}
|
|
76
|
+
remove(id) {
|
|
77
|
+
this.runs.delete(id);
|
|
78
|
+
this.sessions.delete(id);
|
|
79
|
+
this.revisions.delete(id);
|
|
80
|
+
}
|
|
81
|
+
prune() {
|
|
82
|
+
const used = new Set([...this.runs.values()].map((r) => r.definitionId));
|
|
83
|
+
for (const id of this.definitions.keys())
|
|
84
|
+
if (!used.has(id)) {
|
|
85
|
+
this.definitions.delete(id);
|
|
86
|
+
this.definitionRevisions.delete(id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
clear(session) {
|
|
90
|
+
for (const id of this.runs.keys())
|
|
91
|
+
if (this.sessions.get(id) === session)
|
|
92
|
+
this.remove(id);
|
|
93
|
+
this.prune();
|
|
94
|
+
this.revision++;
|
|
95
|
+
this.notify(new Set([session]));
|
|
96
|
+
}
|
|
97
|
+
changes(session, cursor) {
|
|
98
|
+
const current = [...this.runs.values()]
|
|
99
|
+
.filter((r) => this.sessions.get(r.id) === session)
|
|
100
|
+
.reverse();
|
|
101
|
+
const used = new Set(current.map((r) => r.definitionId));
|
|
102
|
+
return {
|
|
103
|
+
revision: this.revision,
|
|
104
|
+
data: structuredClone({
|
|
105
|
+
version: 1,
|
|
106
|
+
definitions: [...used]
|
|
107
|
+
.filter((id) => cursor === undefined ||
|
|
108
|
+
this.definitionRevisions.get(id) > cursor)
|
|
109
|
+
.map((id) => this.definitions.get(id)),
|
|
110
|
+
upserts: current.filter((r) => cursor === undefined || this.revisions.get(r.id) > cursor),
|
|
111
|
+
order: current.map((r) => r.id),
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
package/lib/server/index.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export type ClientMetadata = {
|
|
|
19
19
|
};
|
|
20
20
|
/** Node runtime adapter: framework-neutral Request/Response plus Node middleware. */
|
|
21
21
|
export declare function createJevDevtools(options: JevDevtoolsServerOptions): {
|
|
22
|
+
startFlow: (definition: import("../src/flow-protocol.js").FlowDefinition, run: import("../src/flow-protocol.js").FlowRun) => {
|
|
23
|
+
update(run: import("../src/flow-protocol.js").FlowRun): void;
|
|
24
|
+
request<T>(link: {
|
|
25
|
+
runId: string;
|
|
26
|
+
stepId: string;
|
|
27
|
+
executionId: string;
|
|
28
|
+
}, onTrace: (id: string) => void, callback: () => T): T;
|
|
29
|
+
} | undefined;
|
|
22
30
|
handle: (request: Request) => Promise<Response>;
|
|
23
31
|
/**
|
|
24
32
|
* For Node framework routes whose Request URL uses an internal hostname.
|
package/lib/server/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FlowRecorder } from "./flows.js";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import { Readable } from "node:stream";
|
|
3
4
|
import { pipeline } from "node:stream/promises";
|
|
@@ -17,6 +18,7 @@ export function createJevDevtools(options) {
|
|
|
17
18
|
if (options.enabled && !local && !options.authorize)
|
|
18
19
|
throw new Error("Non-loopback inspector endpoints require authorize");
|
|
19
20
|
const recorder = new JevRecorder(options), scope = new AsyncLocalStorage();
|
|
21
|
+
const flows = new FlowRecorder(options);
|
|
20
22
|
const streams = new Set();
|
|
21
23
|
const pending = new Set();
|
|
22
24
|
const encoder = new TextEncoder();
|
|
@@ -91,15 +93,84 @@ export function createJevDevtools(options) {
|
|
|
91
93
|
return json({ error: "Unauthorized" }, 403);
|
|
92
94
|
const session = await sessionFor(request);
|
|
93
95
|
if (url.pathname === endpoint && request.method === "GET")
|
|
94
|
-
return json({ version: 1, enabled: true });
|
|
96
|
+
return json({ version: 1, enabled: true, flows: true });
|
|
95
97
|
if (url.pathname === endpoint + "/clear") {
|
|
96
98
|
if (request.method !== "DELETE")
|
|
97
99
|
return json({ error: "Method not allowed" }, 405);
|
|
98
100
|
if (request.headers.get("origin") !== origin)
|
|
99
101
|
return json({ error: "Same-origin requests required" }, 403);
|
|
100
102
|
recorder.clear(session);
|
|
103
|
+
flows.clear(session);
|
|
101
104
|
return json({ ok: true });
|
|
102
105
|
}
|
|
106
|
+
let flowPull = () => { };
|
|
107
|
+
if (url.pathname === endpoint + "/flows/events") {
|
|
108
|
+
if (request.method !== "GET")
|
|
109
|
+
return json({ error: "Method not allowed" }, 405);
|
|
110
|
+
let stop = () => { };
|
|
111
|
+
const body = new ReadableStream({
|
|
112
|
+
start(controller) {
|
|
113
|
+
let closed = false, dirty = true, cursor, timer;
|
|
114
|
+
const flush = () => {
|
|
115
|
+
timer = undefined;
|
|
116
|
+
if (closed || !dirty || (controller.desiredSize ?? 0) <= 0)
|
|
117
|
+
return;
|
|
118
|
+
dirty = false;
|
|
119
|
+
const update = flows.changes(session, cursor);
|
|
120
|
+
cursor = update.revision;
|
|
121
|
+
controller.enqueue(encoder.encode(`event: flows\ndata: ${JSON.stringify(update.data)}\n\n`));
|
|
122
|
+
};
|
|
123
|
+
const wake = () => {
|
|
124
|
+
if (!closed && dirty && !timer)
|
|
125
|
+
timer = setImmediate(flush);
|
|
126
|
+
};
|
|
127
|
+
const unsubscribe = flows.subscribe((sessions) => {
|
|
128
|
+
if (sessions.has(session)) {
|
|
129
|
+
dirty = true;
|
|
130
|
+
wake();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const heartbeat = setInterval(() => {
|
|
134
|
+
if (!closed && (controller.desiredSize ?? 0) > 0)
|
|
135
|
+
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
136
|
+
}, 15000);
|
|
137
|
+
stop = () => {
|
|
138
|
+
if (closed)
|
|
139
|
+
return;
|
|
140
|
+
closed = true;
|
|
141
|
+
unsubscribe();
|
|
142
|
+
clearImmediate(timer);
|
|
143
|
+
clearInterval(heartbeat);
|
|
144
|
+
request.signal.removeEventListener("abort", stop);
|
|
145
|
+
streams.delete(stop);
|
|
146
|
+
try {
|
|
147
|
+
controller.close();
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
};
|
|
151
|
+
streams.add(stop);
|
|
152
|
+
request.signal.addEventListener("abort", stop, { once: true });
|
|
153
|
+
flowPull = wake;
|
|
154
|
+
if (request.signal.aborted)
|
|
155
|
+
stop();
|
|
156
|
+
else
|
|
157
|
+
flush();
|
|
158
|
+
},
|
|
159
|
+
pull() {
|
|
160
|
+
flowPull();
|
|
161
|
+
},
|
|
162
|
+
cancel() {
|
|
163
|
+
stop();
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
return new Response(body, {
|
|
167
|
+
headers: {
|
|
168
|
+
"content-type": "text/event-stream",
|
|
169
|
+
"cache-control": "no-store",
|
|
170
|
+
"x-accel-buffering": "no",
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
103
174
|
if (url.pathname !== endpoint + "/events")
|
|
104
175
|
return json({ error: "Not found" }, 404);
|
|
105
176
|
if (request.method !== "GET")
|
|
@@ -209,7 +280,20 @@ export function createJevDevtools(options) {
|
|
|
209
280
|
res.destroy();
|
|
210
281
|
}
|
|
211
282
|
}
|
|
283
|
+
const startFlow = (definition, run) => {
|
|
284
|
+
if (!enabled())
|
|
285
|
+
return;
|
|
286
|
+
flows.start(definition, run, scope.getStore() ?? "local");
|
|
287
|
+
return {
|
|
288
|
+
update: (next) => {
|
|
289
|
+
if (enabled())
|
|
290
|
+
flows.update(next);
|
|
291
|
+
},
|
|
292
|
+
request: (link, onTrace, callback) => recorder.withFlow(link, onTrace, callback),
|
|
293
|
+
};
|
|
294
|
+
};
|
|
212
295
|
return {
|
|
296
|
+
startFlow,
|
|
213
297
|
handle,
|
|
214
298
|
/**
|
|
215
299
|
* For Node framework routes whose Request URL uses an internal hostname.
|
package/lib/server/recorder.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { TypeSafeClient } from "@typesafe-ai/sdk";
|
|
2
2
|
import type { Fetch } from "@typesafe-ai/sdk";
|
|
3
|
-
import type { Trace } from "../src/protocol.js";
|
|
3
|
+
import type { Json, Trace } from "../src/protocol.js";
|
|
4
4
|
export type CaptureOptions = {
|
|
5
5
|
limit?: number;
|
|
6
6
|
redact?: (path: string[], value: unknown) => unknown;
|
|
7
7
|
};
|
|
8
|
+
export declare function capture(value: unknown, warnings: Set<string>, redact?: CaptureOptions["redact"]): Json;
|
|
8
9
|
export declare class JevRecorder {
|
|
9
10
|
private options;
|
|
10
11
|
private traces;
|
|
@@ -12,6 +13,8 @@ export declare class JevRecorder {
|
|
|
12
13
|
private revision;
|
|
13
14
|
private revisions;
|
|
14
15
|
private context;
|
|
16
|
+
private flowContext;
|
|
17
|
+
withFlow<T>(link: NonNullable<Trace["flow"]>, onTrace: (id: string) => void, callback: () => T): T;
|
|
15
18
|
private limit;
|
|
16
19
|
constructor(options?: CaptureOptions);
|
|
17
20
|
snapshot(session: string): Trace[];
|
package/lib/server/recorder.js
CHANGED
|
@@ -5,7 +5,7 @@ const secretKey = /^(authorization|cookie|set-cookie|api[-_]?key|password|secret
|
|
|
5
5
|
const safeMessage = (value) => value.replace(/Bearer\s+\S+/gi, "Bearer [redacted]").slice(0, 500);
|
|
6
6
|
// Snapshot before the caller can mutate its request. Bounded and cycle-safe;
|
|
7
7
|
// image handles become explicit previews/references instead of opaque SDK objects.
|
|
8
|
-
function capture(value, warnings, redact) {
|
|
8
|
+
export function capture(value, warnings, redact) {
|
|
9
9
|
const seen = new WeakSet();
|
|
10
10
|
let budget = 96_000;
|
|
11
11
|
const visit = (input, path, depth) => {
|
|
@@ -82,6 +82,10 @@ export class JevRecorder {
|
|
|
82
82
|
revision = 0;
|
|
83
83
|
revisions = new WeakMap();
|
|
84
84
|
context = new AsyncLocalStorage();
|
|
85
|
+
flowContext = new AsyncLocalStorage();
|
|
86
|
+
withFlow(link, onTrace, callback) {
|
|
87
|
+
return this.flowContext.run({ link, onTrace }, callback);
|
|
88
|
+
}
|
|
85
89
|
limit;
|
|
86
90
|
constructor(options = {}) {
|
|
87
91
|
this.options = options;
|
|
@@ -196,6 +200,7 @@ export class JevRecorder {
|
|
|
196
200
|
version: 1,
|
|
197
201
|
id,
|
|
198
202
|
...(typeof meta === "function" ? meta() : meta),
|
|
203
|
+
flow: this.flowContext.getStore()?.link,
|
|
199
204
|
startedAt: Date.now(),
|
|
200
205
|
status: "pending",
|
|
201
206
|
request: captured,
|
|
@@ -203,6 +208,12 @@ export class JevRecorder {
|
|
|
203
208
|
captureWarnings: [...warnings],
|
|
204
209
|
});
|
|
205
210
|
const trace = this.traces.get(id);
|
|
211
|
+
try {
|
|
212
|
+
this.flowContext.getStore()?.onTrace(id);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* Observer only. */
|
|
216
|
+
}
|
|
206
217
|
this.revisions.set(trace, ++this.revision);
|
|
207
218
|
const sessions = new Set([trace.session]);
|
|
208
219
|
while (this.traces.size > this.limit) {
|
|
@@ -10,7 +10,7 @@ export function ChoiceProbabilities({ answer }) {
|
|
|
10
10
|
}, [rows, search]);
|
|
11
11
|
const pages = Math.max(1, Math.ceil(filtered.length / 20));
|
|
12
12
|
const current = Math.min(page, pages - 1);
|
|
13
|
-
return (_jsxs("div", { className: "lab-choice-browser", role: "region", "aria-label": "All Choice probabilities", children: [_jsxs("div", { className: "lab-choice-controls", children: [_jsxs("label", { children: ["Search categories", _jsx("input", { autoFocus: true, type: "search", value: search, onChange: (event) => {
|
|
13
|
+
return (_jsxs("div", { className: "lab-choice-browser", role: "region", "aria-label": "All Choice probabilities", "data-diagram-scroll": "", children: [_jsxs("div", { className: "lab-choice-controls", children: [_jsxs("label", { children: ["Search categories", _jsx("input", { autoFocus: true, type: "search", value: search, onChange: (event) => {
|
|
14
14
|
setSearch(event.target.value);
|
|
15
15
|
setPage(0);
|
|
16
16
|
} })] }), _jsxs("span", { role: "status", children: [filtered.length, " of ", rows.length, " categories \u00B7 highest probability first"] })] }), _jsxs("div", { className: "lab-choice-table-scroll", tabIndex: 0, "aria-label": "Probability table", children: [_jsxs("table", { children: [_jsxs("caption", { children: ["Captured probabilities. Values are not renormalized. Returned choice: ", answer.choice ?? "unavailable", "."] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Category" }), _jsx("th", { scope: "col", children: "Probability" }), _jsx("th", { scope: "col", children: "Exact value (0\u20131)" })] }) }), _jsx("tbody", { children: filtered
|
|
@@ -3,6 +3,7 @@ import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
|
3
3
|
import { JevPanel } from "./JevDevtools.js";
|
|
4
4
|
import { JevDrawer } from "./JevDrawer.js";
|
|
5
5
|
import { connectTraceStream } from "./trace-stream.js";
|
|
6
|
+
import { connectFlowStream, emptyFlows, } from "./flow-stream.js";
|
|
6
7
|
const RetainedPanel = memo(JevPanel);
|
|
7
8
|
function endpointUrl(endpoint) {
|
|
8
9
|
const url = new URL(endpoint, window.location.origin);
|
|
@@ -18,7 +19,11 @@ export default function ConnectedInspector({ endpoint = "/api/jev-devtools", def
|
|
|
18
19
|
const [open, setOpen] = useState(defaultOpen);
|
|
19
20
|
const [visited, setVisited] = useState(defaultOpen);
|
|
20
21
|
const visible = useRef(defaultOpen);
|
|
21
|
-
const latest = useRef({
|
|
22
|
+
const latest = useRef({
|
|
23
|
+
traces: [],
|
|
24
|
+
connection: "connecting",
|
|
25
|
+
flows: emptyFlows,
|
|
26
|
+
});
|
|
22
27
|
const [snapshot, setSnapshot] = useState(latest.current);
|
|
23
28
|
const toggle = (value) => {
|
|
24
29
|
visible.current = value;
|
|
@@ -29,20 +34,21 @@ export default function ConnectedInspector({ endpoint = "/api/jev-devtools", def
|
|
|
29
34
|
}
|
|
30
35
|
};
|
|
31
36
|
useEffect(() => {
|
|
32
|
-
let alive = true, disconnect = () => { };
|
|
37
|
+
let alive = true, disconnect = () => { }, disconnectFlows = () => { };
|
|
33
38
|
const abort = new AbortController();
|
|
34
39
|
// Keep capture current while closed without scheduling React work. Reopening
|
|
35
40
|
// publishes only the latest snapshot; the retained panel keeps its UI state.
|
|
36
41
|
const update = (patch) => {
|
|
37
42
|
const next = { ...latest.current, ...patch };
|
|
38
43
|
if (next.traces === latest.current.traces &&
|
|
39
|
-
next.connection === latest.current.connection
|
|
44
|
+
next.connection === latest.current.connection &&
|
|
45
|
+
next.flows === latest.current.flows)
|
|
40
46
|
return;
|
|
41
47
|
latest.current = next;
|
|
42
48
|
if (visible.current)
|
|
43
49
|
setSnapshot(latest.current);
|
|
44
50
|
};
|
|
45
|
-
update({ traces: [], connection: "connecting" });
|
|
51
|
+
update({ traces: [], connection: "connecting", flows: emptyFlows });
|
|
46
52
|
void (async () => {
|
|
47
53
|
try {
|
|
48
54
|
const url = endpointUrl(endpoint);
|
|
@@ -59,6 +65,8 @@ export default function ConnectedInspector({ endpoint = "/api/jev-devtools", def
|
|
|
59
65
|
throw new Error("Unsupported inspector endpoint");
|
|
60
66
|
if (!alive)
|
|
61
67
|
return;
|
|
68
|
+
if (config.flows)
|
|
69
|
+
disconnectFlows = connectFlowStream(url, (flows) => update({ flows }));
|
|
62
70
|
disconnect = connectTraceStream(url, (traces) => update({ traces }), (connection) => update({ connection }));
|
|
63
71
|
}
|
|
64
72
|
catch (error) {
|
|
@@ -72,6 +80,7 @@ export default function ConnectedInspector({ endpoint = "/api/jev-devtools", def
|
|
|
72
80
|
alive = false;
|
|
73
81
|
abort.abort();
|
|
74
82
|
disconnect();
|
|
83
|
+
disconnectFlows();
|
|
75
84
|
};
|
|
76
85
|
}, [endpoint]);
|
|
77
86
|
const clear = useCallback(async () => {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Json } from "./protocol.js";
|
|
2
|
+
/** Bounded, lazy property rendering inside a fixed-size diagram card. */
|
|
3
|
+
export declare const DiagramContent: import("react").MemoExoticComponent<({ value, label, }: {
|
|
4
|
+
value: Json | undefined;
|
|
5
|
+
label: string;
|
|
6
|
+
}) => import("react").JSX.Element>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useMemo, useState } from "react";
|
|
3
|
+
const PAGE_SIZE = 12;
|
|
4
|
+
function TextValue({ value }) {
|
|
5
|
+
const [expanded, setExpanded] = useState(false);
|
|
6
|
+
const long = value.length > 140 || value.split("\n").length > 3;
|
|
7
|
+
return (_jsxs("span", { className: "diagram-text-value", children: [_jsx("span", { className: long && !expanded ? "diagram-text-preview" : undefined, children: value === "" ? (_jsx("span", { className: "diagram-value-empty", children: "empty string" })) : long && !expanded ? (`${value.slice(0, 140)}${value.length > 140 ? "…" : ""}`) : (value) }), long && (_jsx("button", { type: "button", className: "diagram-content-toggle", "aria-expanded": expanded, onClick: () => setExpanded(!expanded), children: expanded ? "Less text" : "Full text" }))] }));
|
|
8
|
+
}
|
|
9
|
+
function Properties({ value }) {
|
|
10
|
+
const [visible, setVisible] = useState(PAGE_SIZE);
|
|
11
|
+
const entries = useMemo(() => Object.entries(value), [value]);
|
|
12
|
+
if (!entries.length)
|
|
13
|
+
return (_jsx("span", { className: "diagram-value-empty", children: Array.isArray(value) ? "[] Empty array" : "{} Empty object" }));
|
|
14
|
+
return (_jsxs("div", { className: "diagram-properties", children: [entries.slice(0, visible).map(([name, child]) => (_jsx(Property, { name: name, value: child }, name))), visible < entries.length && (_jsxs("button", { type: "button", className: "diagram-content-toggle", onClick: () => setVisible((count) => count + PAGE_SIZE), children: ["Show ", Math.min(PAGE_SIZE, entries.length - visible), " more (", entries.length - visible, " remaining)"] })), visible > PAGE_SIZE && (_jsx("button", { type: "button", className: "diagram-content-toggle", onClick: () => setVisible(PAGE_SIZE), children: "Fewer properties" }))] }));
|
|
15
|
+
}
|
|
16
|
+
function Scalar({ value }) {
|
|
17
|
+
if (typeof value === "string")
|
|
18
|
+
return _jsx(TextValue, { value: value });
|
|
19
|
+
return (_jsx("span", { className: `diagram-value-${value === null || value === undefined ? "empty" : typeof value}`, children: value === undefined ? "Not captured" : String(value) }));
|
|
20
|
+
}
|
|
21
|
+
function Property({ name, value }) {
|
|
22
|
+
const [expanded, setExpanded] = useState(false);
|
|
23
|
+
const object = value !== null && typeof value === "object";
|
|
24
|
+
if (!object)
|
|
25
|
+
return (_jsxs("div", { className: `diagram-property ${typeof value === "string" ? "diagram-property-text" : ""}`, children: [_jsx("span", { className: "diagram-property-key", children: name || '""' }), _jsx(Scalar, { value: value })] }));
|
|
26
|
+
const count = Object.keys(value).length;
|
|
27
|
+
return (_jsxs("div", { className: "diagram-property-group", children: [_jsxs("button", { type: "button", className: "diagram-property-disclosure", "aria-label": `${expanded ? "Collapse" : "Expand"} ${name || '""'}`, "aria-expanded": expanded, onClick: () => setExpanded(!expanded), children: [_jsx("span", { "aria-hidden": "true", className: "diagram-property-caret", children: expanded ? "▾" : "▸" }), _jsx("span", { className: "diagram-property-key", children: name || '""' }), _jsx("span", { className: "diagram-property-type", children: Array.isArray(value) ? `[${count}]` : `{${count}}` })] }), expanded && (_jsx("div", { className: "diagram-property-children", children: _jsx(Properties, { value: value }) }))] }));
|
|
28
|
+
}
|
|
29
|
+
/** Bounded, lazy property rendering inside a fixed-size diagram card. */
|
|
30
|
+
export const DiagramContent = memo(function DiagramContent({ value, label, }) {
|
|
31
|
+
return (_jsx("div", { className: "diagram-content", "data-diagram-scroll": true, role: "region", "aria-label": label, tabIndex: 0, children: value !== null && typeof value === "object" ? (_jsx(Properties, { value: value })) : (_jsx(Scalar, { value: value })) }));
|
|
32
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FlowDefinition, FlowRun } from "./flow-protocol.js";
|
|
2
|
+
import type { Trace } from "./protocol.js";
|
|
3
|
+
export declare const FlowOverview: import("react").MemoExoticComponent<({ definition, run, traces, }: {
|
|
4
|
+
definition: FlowDefinition;
|
|
5
|
+
run: FlowRun;
|
|
6
|
+
traces: Trace[];
|
|
7
|
+
}) => import("react").JSX.Element>;
|