@ai-gui/devtools 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Liang Li
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @ai-gui/devtools
2
+
3
+ Framework-agnostic AIGUI runtime timeline and deterministic stream simulator.
4
+
5
+ ```ts
6
+ import { Renderer, createActionRuntime, CardStore } from "@ai-gui/core"
7
+ import { createDevTools, createStreamSimulator } from "@ai-gui/devtools"
8
+
9
+ const renderer = new Renderer({ debug: true })
10
+ const cards = new CardStore({ debug: true })
11
+ const actions = createActionRuntime({ registry, cardStore: cards, debug: true })
12
+ const devtools = createDevTools({ maxEvents: 500 })
13
+
14
+ devtools.attach(renderer, actions, cards)
15
+ devtools.subscribe(console.log)
16
+
17
+ const simulator = createStreamSimulator(markdown, { chunkSize: 4, delayMs: 20 })
18
+ await renderer.feed(simulator.stream)
19
+ ```
20
+
21
+ `snapshot()` returns a bounded, globally ordered timeline. Core redacts credentials and text-level Bearer/query secrets before observers receive events and bounds payload construction. Debug may still contain application business data or form PII that cannot be recognized automatically, so use custom `redact` rules for those fields. Devtools also supports bounded event retention, `clear()`, and `destroy()`.
package/dist/index.cjs ADDED
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+
3
+ //#region src/index.ts
4
+ const DEFAULT_SENSITIVE_KEY_PARTS = [
5
+ "authorization",
6
+ "auth",
7
+ "accesstoken",
8
+ "refreshtoken",
9
+ "clientsecret",
10
+ "credentials",
11
+ "cookie",
12
+ "setcookie",
13
+ "proxyauthorization",
14
+ "password",
15
+ "passwd",
16
+ "apikey",
17
+ "secret",
18
+ "token"
19
+ ];
20
+ function createDevTools(options = {}) {
21
+ const maxEvents = positiveInteger(options.maxEvents, 1e3, "maxEvents");
22
+ const maxStringLength = positiveInteger(options.maxStringLength, 4096, "maxStringLength");
23
+ const maxDepth = positiveInteger(options.maxDepth, 24, "maxDepth");
24
+ const maxNodes = positiveInteger(options.maxNodes, 2e4, "maxNodes");
25
+ const now = options.now ?? Date.now;
26
+ const timeline = [];
27
+ const listeners = new Set();
28
+ const detachments = new Set();
29
+ let sequence = 0;
30
+ let dropped = 0;
31
+ let destroyed = false;
32
+ const record = (event) => {
33
+ if (destroyed) return;
34
+ const entry = Object.freeze({
35
+ type: event.type,
36
+ source: event.source,
37
+ timestamp: now(),
38
+ sequence: ++sequence,
39
+ sourceSequence: event.sequence,
40
+ data: limitValue(event.data, {
41
+ maxStringLength,
42
+ maxDepth,
43
+ maxNodes,
44
+ redact: options.redact
45
+ })
46
+ });
47
+ timeline.push(entry);
48
+ if (timeline.length > maxEvents) {
49
+ dropped += timeline.length - maxEvents;
50
+ timeline.splice(0, timeline.length - maxEvents);
51
+ }
52
+ for (const listener of listeners) try {
53
+ listener(entry);
54
+ } catch {}
55
+ };
56
+ const api = {
57
+ attach(...targets) {
58
+ if (destroyed) throw new Error("DevTools has been destroyed");
59
+ const listener = (event) => record(event);
60
+ const current = [];
61
+ try {
62
+ for (const target of targets) current.push(target.subscribeDebug(listener));
63
+ } catch (error) {
64
+ for (const unsubscribe of current) unsubscribe();
65
+ throw error;
66
+ }
67
+ const detach = () => {
68
+ if (!detachments.delete(detach)) return;
69
+ for (const unsubscribe of current) unsubscribe();
70
+ };
71
+ detachments.add(detach);
72
+ return detach;
73
+ },
74
+ subscribe(listener) {
75
+ if (destroyed) return () => {};
76
+ listeners.add(listener);
77
+ return () => listeners.delete(listener);
78
+ },
79
+ snapshot() {
80
+ return timeline.map((event) => ({
81
+ ...event,
82
+ data: cloneValue(event.data)
83
+ }));
84
+ },
85
+ clear() {
86
+ timeline.length = 0;
87
+ dropped = 0;
88
+ },
89
+ stats() {
90
+ return {
91
+ retained: timeline.length,
92
+ dropped
93
+ };
94
+ },
95
+ destroy() {
96
+ if (destroyed) return;
97
+ destroyed = true;
98
+ for (const detach of [...detachments]) detach();
99
+ listeners.clear();
100
+ timeline.length = 0;
101
+ }
102
+ };
103
+ return api;
104
+ }
105
+ function limitValue(value, options) {
106
+ const seen = new WeakSet();
107
+ let nodes = 0;
108
+ const visit = (input, key, path, depth) => {
109
+ nodes++;
110
+ if (nodes > options.maxNodes) return "[MAX_NODES]";
111
+ if (isSensitiveKey(key) || options.redact?.({
112
+ key,
113
+ path,
114
+ value: input
115
+ })) return "[REDACTED]";
116
+ if (typeof input === "string") {
117
+ const safe = redactText(input);
118
+ return safe.length > options.maxStringLength ? `${safe.slice(0, options.maxStringLength)}[TRUNCATED]` : safe;
119
+ }
120
+ if (input === null || typeof input === "number" || typeof input === "boolean") return input;
121
+ if (typeof input !== "object") return String(input);
122
+ if (depth >= options.maxDepth) return "[MAX_DEPTH]";
123
+ if (seen.has(input)) return "[CIRCULAR]";
124
+ seen.add(input);
125
+ try {
126
+ if (Array.isArray(input)) return input.map((item, index) => visit(item, "", [...path, index], depth + 1));
127
+ const output = {};
128
+ for (const [childKey, child] of Object.entries(input)) output[childKey] = visit(child, childKey, [...path, childKey], depth + 1);
129
+ return output;
130
+ } finally {
131
+ seen.delete(input);
132
+ }
133
+ };
134
+ return visit(value, "", [], 0);
135
+ }
136
+ function isSensitiveKey(key) {
137
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
138
+ return normalized !== "" && DEFAULT_SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
139
+ }
140
+ function redactText(value) {
141
+ return value.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]").replace(/([?&](?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|password|passwd|token|secret)=)[^&#\s]*/gi, "$1[REDACTED]").replace(/\b((?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|password|passwd|token|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[REDACTED]");
142
+ }
143
+ function cloneValue(value) {
144
+ if (Array.isArray(value)) return value.map(cloneValue);
145
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cloneValue(child)]));
146
+ return value;
147
+ }
148
+ function positiveInteger(value, fallback, name) {
149
+ if (value === void 0) return fallback;
150
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`);
151
+ return value;
152
+ }
153
+ const STREAM_FIXTURES = Object.freeze({
154
+ markdown: "# Streaming demo\n\nThis paragraph arrives in deterministic chunks.",
155
+ card: "```card:demo\n{\"id\":\"demo-card\",\"title\":\"Fixture card\",\"count\":1}\n```",
156
+ unicode: "UTF-8 boundaries: 你好, مرحبا, 🙂."
157
+ });
158
+ function createStreamSimulator(input, options = {}) {
159
+ const chunkSize = positiveInteger(options.chunkSize, 8, "chunkSize");
160
+ const delayMs = nonNegativeNumber(options.delayMs, 0, "delayMs");
161
+ const bytes = new TextEncoder().encode(input);
162
+ let currentState = "running";
163
+ let offset = 0;
164
+ let wake;
165
+ let timer;
166
+ let settleDelay;
167
+ let pending;
168
+ const waitUntilRunning = async () => {
169
+ while (currentState === "paused") await new Promise((resolve) => {
170
+ wake = resolve;
171
+ });
172
+ };
173
+ const finishWaits = () => {
174
+ if (timer !== void 0) clearTimeout(timer);
175
+ timer = void 0;
176
+ settleDelay?.();
177
+ settleDelay = void 0;
178
+ wake?.();
179
+ wake = void 0;
180
+ };
181
+ const waitDelay = () => new Promise((resolve) => {
182
+ if (delayMs === 0 || isCancelled()) return resolve();
183
+ settleDelay = resolve;
184
+ timer = setTimeout(() => {
185
+ timer = void 0;
186
+ settleDelay = void 0;
187
+ resolve();
188
+ }, delayMs);
189
+ });
190
+ const iterator = {
191
+ next() {
192
+ if (pending) return pending;
193
+ pending = (async () => {
194
+ await waitUntilRunning();
195
+ if (isCancelled()) return {
196
+ done: true,
197
+ value: void 0
198
+ };
199
+ await waitDelay();
200
+ if (isCancelled()) return {
201
+ done: true,
202
+ value: void 0
203
+ };
204
+ await waitUntilRunning();
205
+ if (isCancelled()) return {
206
+ done: true,
207
+ value: void 0
208
+ };
209
+ if (offset >= bytes.length) {
210
+ currentState = "completed";
211
+ return {
212
+ done: true,
213
+ value: void 0
214
+ };
215
+ }
216
+ const chunk = bytes.slice(offset, Math.min(offset + chunkSize, bytes.length));
217
+ offset += chunk.length;
218
+ return {
219
+ done: false,
220
+ value: chunk
221
+ };
222
+ })().finally(() => {
223
+ pending = void 0;
224
+ });
225
+ return pending;
226
+ },
227
+ async return() {
228
+ if (currentState !== "completed") currentState = "cancelled";
229
+ finishWaits();
230
+ await pending;
231
+ return {
232
+ done: true,
233
+ value: void 0
234
+ };
235
+ }
236
+ };
237
+ const stream = { [Symbol.asyncIterator]: () => iterator };
238
+ return {
239
+ stream,
240
+ pause() {
241
+ if (currentState === "running") currentState = "paused";
242
+ },
243
+ resume() {
244
+ if (currentState !== "paused") return;
245
+ currentState = "running";
246
+ wake?.();
247
+ wake = void 0;
248
+ },
249
+ cancel() {
250
+ if (currentState === "completed" || currentState === "cancelled") return;
251
+ currentState = "cancelled";
252
+ finishWaits();
253
+ },
254
+ state() {
255
+ return currentState;
256
+ }
257
+ };
258
+ function isCancelled() {
259
+ return currentState === "cancelled";
260
+ }
261
+ }
262
+ function nonNegativeNumber(value, fallback, name) {
263
+ if (value === void 0) return fallback;
264
+ if (!Number.isFinite(value) || value < 0) throw new TypeError(`${name} must be a non-negative number`);
265
+ return value;
266
+ }
267
+
268
+ //#endregion
269
+ exports.STREAM_FIXTURES = STREAM_FIXTURES
270
+ exports.createDevTools = createDevTools
271
+ exports.createStreamSimulator = createStreamSimulator
@@ -0,0 +1,56 @@
1
+ import { DebugEventTarget, DebugSource } from "@ai-gui/core";
2
+
3
+ //#region src/index.d.ts
4
+ interface TimelineEvent {
5
+ type: string;
6
+ source: DebugSource;
7
+ timestamp: number;
8
+ sequence: number;
9
+ sourceSequence: number;
10
+ data: Record<string, unknown>;
11
+ }
12
+ interface RedactContext {
13
+ key: string;
14
+ path: readonly (string | number)[];
15
+ value: unknown;
16
+ }
17
+ interface DevToolsOptions {
18
+ maxEvents?: number;
19
+ maxStringLength?: number;
20
+ maxDepth?: number;
21
+ maxNodes?: number;
22
+ redact?: (context: RedactContext) => boolean;
23
+ now?: () => number;
24
+ }
25
+ interface DevToolsStats {
26
+ retained: number;
27
+ dropped: number;
28
+ }
29
+ interface DevTools {
30
+ attach(...targets: DebugEventTarget[]): () => void;
31
+ subscribe(listener: (event: TimelineEvent) => void): () => void;
32
+ snapshot(): TimelineEvent[];
33
+ clear(): void;
34
+ stats(): DevToolsStats;
35
+ destroy(): void;
36
+ }
37
+ declare function createDevTools(options?: DevToolsOptions): DevTools;
38
+ type StreamSimulatorState = "running" | "paused" | "completed" | "cancelled";
39
+ interface StreamSimulatorOptions {
40
+ chunkSize?: number;
41
+ delayMs?: number;
42
+ }
43
+ interface StreamSimulator {
44
+ stream: AsyncIterable<Uint8Array>;
45
+ pause(): void;
46
+ resume(): void;
47
+ cancel(): void;
48
+ state(): StreamSimulatorState;
49
+ }
50
+ declare const STREAM_FIXTURES: Readonly<{
51
+ markdown: "# Streaming demo\n\nThis paragraph arrives in deterministic chunks.";
52
+ card: "```card:demo\n{\"id\":\"demo-card\",\"title\":\"Fixture card\",\"count\":1}\n```";
53
+ unicode: "UTF-8 boundaries: 你好, مرحبا, 🙂.";
54
+ }>;
55
+ declare function createStreamSimulator(input: string, options?: StreamSimulatorOptions): StreamSimulator; //#endregion
56
+ export { DevTools, DevToolsOptions, DevToolsStats, RedactContext, STREAM_FIXTURES, StreamSimulator, StreamSimulatorOptions, StreamSimulatorState, TimelineEvent, createDevTools, createStreamSimulator };
@@ -0,0 +1,56 @@
1
+ import { DebugEventTarget, DebugSource } from "@ai-gui/core";
2
+
3
+ //#region src/index.d.ts
4
+ interface TimelineEvent {
5
+ type: string;
6
+ source: DebugSource;
7
+ timestamp: number;
8
+ sequence: number;
9
+ sourceSequence: number;
10
+ data: Record<string, unknown>;
11
+ }
12
+ interface RedactContext {
13
+ key: string;
14
+ path: readonly (string | number)[];
15
+ value: unknown;
16
+ }
17
+ interface DevToolsOptions {
18
+ maxEvents?: number;
19
+ maxStringLength?: number;
20
+ maxDepth?: number;
21
+ maxNodes?: number;
22
+ redact?: (context: RedactContext) => boolean;
23
+ now?: () => number;
24
+ }
25
+ interface DevToolsStats {
26
+ retained: number;
27
+ dropped: number;
28
+ }
29
+ interface DevTools {
30
+ attach(...targets: DebugEventTarget[]): () => void;
31
+ subscribe(listener: (event: TimelineEvent) => void): () => void;
32
+ snapshot(): TimelineEvent[];
33
+ clear(): void;
34
+ stats(): DevToolsStats;
35
+ destroy(): void;
36
+ }
37
+ declare function createDevTools(options?: DevToolsOptions): DevTools;
38
+ type StreamSimulatorState = "running" | "paused" | "completed" | "cancelled";
39
+ interface StreamSimulatorOptions {
40
+ chunkSize?: number;
41
+ delayMs?: number;
42
+ }
43
+ interface StreamSimulator {
44
+ stream: AsyncIterable<Uint8Array>;
45
+ pause(): void;
46
+ resume(): void;
47
+ cancel(): void;
48
+ state(): StreamSimulatorState;
49
+ }
50
+ declare const STREAM_FIXTURES: Readonly<{
51
+ markdown: "# Streaming demo\n\nThis paragraph arrives in deterministic chunks.";
52
+ card: "```card:demo\n{\"id\":\"demo-card\",\"title\":\"Fixture card\",\"count\":1}\n```";
53
+ unicode: "UTF-8 boundaries: 你好, مرحبا, 🙂.";
54
+ }>;
55
+ declare function createStreamSimulator(input: string, options?: StreamSimulatorOptions): StreamSimulator; //#endregion
56
+ export { DevTools, DevToolsOptions, DevToolsStats, RedactContext, STREAM_FIXTURES, StreamSimulator, StreamSimulatorOptions, StreamSimulatorState, TimelineEvent, createDevTools, createStreamSimulator };
package/dist/index.js ADDED
@@ -0,0 +1,267 @@
1
+ //#region src/index.ts
2
+ const DEFAULT_SENSITIVE_KEY_PARTS = [
3
+ "authorization",
4
+ "auth",
5
+ "accesstoken",
6
+ "refreshtoken",
7
+ "clientsecret",
8
+ "credentials",
9
+ "cookie",
10
+ "setcookie",
11
+ "proxyauthorization",
12
+ "password",
13
+ "passwd",
14
+ "apikey",
15
+ "secret",
16
+ "token"
17
+ ];
18
+ function createDevTools(options = {}) {
19
+ const maxEvents = positiveInteger(options.maxEvents, 1e3, "maxEvents");
20
+ const maxStringLength = positiveInteger(options.maxStringLength, 4096, "maxStringLength");
21
+ const maxDepth = positiveInteger(options.maxDepth, 24, "maxDepth");
22
+ const maxNodes = positiveInteger(options.maxNodes, 2e4, "maxNodes");
23
+ const now = options.now ?? Date.now;
24
+ const timeline = [];
25
+ const listeners = new Set();
26
+ const detachments = new Set();
27
+ let sequence = 0;
28
+ let dropped = 0;
29
+ let destroyed = false;
30
+ const record = (event) => {
31
+ if (destroyed) return;
32
+ const entry = Object.freeze({
33
+ type: event.type,
34
+ source: event.source,
35
+ timestamp: now(),
36
+ sequence: ++sequence,
37
+ sourceSequence: event.sequence,
38
+ data: limitValue(event.data, {
39
+ maxStringLength,
40
+ maxDepth,
41
+ maxNodes,
42
+ redact: options.redact
43
+ })
44
+ });
45
+ timeline.push(entry);
46
+ if (timeline.length > maxEvents) {
47
+ dropped += timeline.length - maxEvents;
48
+ timeline.splice(0, timeline.length - maxEvents);
49
+ }
50
+ for (const listener of listeners) try {
51
+ listener(entry);
52
+ } catch {}
53
+ };
54
+ const api = {
55
+ attach(...targets) {
56
+ if (destroyed) throw new Error("DevTools has been destroyed");
57
+ const listener = (event) => record(event);
58
+ const current = [];
59
+ try {
60
+ for (const target of targets) current.push(target.subscribeDebug(listener));
61
+ } catch (error) {
62
+ for (const unsubscribe of current) unsubscribe();
63
+ throw error;
64
+ }
65
+ const detach = () => {
66
+ if (!detachments.delete(detach)) return;
67
+ for (const unsubscribe of current) unsubscribe();
68
+ };
69
+ detachments.add(detach);
70
+ return detach;
71
+ },
72
+ subscribe(listener) {
73
+ if (destroyed) return () => {};
74
+ listeners.add(listener);
75
+ return () => listeners.delete(listener);
76
+ },
77
+ snapshot() {
78
+ return timeline.map((event) => ({
79
+ ...event,
80
+ data: cloneValue(event.data)
81
+ }));
82
+ },
83
+ clear() {
84
+ timeline.length = 0;
85
+ dropped = 0;
86
+ },
87
+ stats() {
88
+ return {
89
+ retained: timeline.length,
90
+ dropped
91
+ };
92
+ },
93
+ destroy() {
94
+ if (destroyed) return;
95
+ destroyed = true;
96
+ for (const detach of [...detachments]) detach();
97
+ listeners.clear();
98
+ timeline.length = 0;
99
+ }
100
+ };
101
+ return api;
102
+ }
103
+ function limitValue(value, options) {
104
+ const seen = new WeakSet();
105
+ let nodes = 0;
106
+ const visit = (input, key, path, depth) => {
107
+ nodes++;
108
+ if (nodes > options.maxNodes) return "[MAX_NODES]";
109
+ if (isSensitiveKey(key) || options.redact?.({
110
+ key,
111
+ path,
112
+ value: input
113
+ })) return "[REDACTED]";
114
+ if (typeof input === "string") {
115
+ const safe = redactText(input);
116
+ return safe.length > options.maxStringLength ? `${safe.slice(0, options.maxStringLength)}[TRUNCATED]` : safe;
117
+ }
118
+ if (input === null || typeof input === "number" || typeof input === "boolean") return input;
119
+ if (typeof input !== "object") return String(input);
120
+ if (depth >= options.maxDepth) return "[MAX_DEPTH]";
121
+ if (seen.has(input)) return "[CIRCULAR]";
122
+ seen.add(input);
123
+ try {
124
+ if (Array.isArray(input)) return input.map((item, index) => visit(item, "", [...path, index], depth + 1));
125
+ const output = {};
126
+ for (const [childKey, child] of Object.entries(input)) output[childKey] = visit(child, childKey, [...path, childKey], depth + 1);
127
+ return output;
128
+ } finally {
129
+ seen.delete(input);
130
+ }
131
+ };
132
+ return visit(value, "", [], 0);
133
+ }
134
+ function isSensitiveKey(key) {
135
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
136
+ return normalized !== "" && DEFAULT_SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
137
+ }
138
+ function redactText(value) {
139
+ return value.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]").replace(/([?&](?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|password|passwd|token|secret)=)[^&#\s]*/gi, "$1[REDACTED]").replace(/\b((?:access[_-]?token|refresh[_-]?token|client[_-]?secret|api[_-]?key|password|passwd|token|secret)\s*[:=]\s*)[^\s,;]+/gi, "$1[REDACTED]");
140
+ }
141
+ function cloneValue(value) {
142
+ if (Array.isArray(value)) return value.map(cloneValue);
143
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cloneValue(child)]));
144
+ return value;
145
+ }
146
+ function positiveInteger(value, fallback, name) {
147
+ if (value === void 0) return fallback;
148
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`);
149
+ return value;
150
+ }
151
+ const STREAM_FIXTURES = Object.freeze({
152
+ markdown: "# Streaming demo\n\nThis paragraph arrives in deterministic chunks.",
153
+ card: "```card:demo\n{\"id\":\"demo-card\",\"title\":\"Fixture card\",\"count\":1}\n```",
154
+ unicode: "UTF-8 boundaries: 你好, مرحبا, 🙂."
155
+ });
156
+ function createStreamSimulator(input, options = {}) {
157
+ const chunkSize = positiveInteger(options.chunkSize, 8, "chunkSize");
158
+ const delayMs = nonNegativeNumber(options.delayMs, 0, "delayMs");
159
+ const bytes = new TextEncoder().encode(input);
160
+ let currentState = "running";
161
+ let offset = 0;
162
+ let wake;
163
+ let timer;
164
+ let settleDelay;
165
+ let pending;
166
+ const waitUntilRunning = async () => {
167
+ while (currentState === "paused") await new Promise((resolve) => {
168
+ wake = resolve;
169
+ });
170
+ };
171
+ const finishWaits = () => {
172
+ if (timer !== void 0) clearTimeout(timer);
173
+ timer = void 0;
174
+ settleDelay?.();
175
+ settleDelay = void 0;
176
+ wake?.();
177
+ wake = void 0;
178
+ };
179
+ const waitDelay = () => new Promise((resolve) => {
180
+ if (delayMs === 0 || isCancelled()) return resolve();
181
+ settleDelay = resolve;
182
+ timer = setTimeout(() => {
183
+ timer = void 0;
184
+ settleDelay = void 0;
185
+ resolve();
186
+ }, delayMs);
187
+ });
188
+ const iterator = {
189
+ next() {
190
+ if (pending) return pending;
191
+ pending = (async () => {
192
+ await waitUntilRunning();
193
+ if (isCancelled()) return {
194
+ done: true,
195
+ value: void 0
196
+ };
197
+ await waitDelay();
198
+ if (isCancelled()) return {
199
+ done: true,
200
+ value: void 0
201
+ };
202
+ await waitUntilRunning();
203
+ if (isCancelled()) return {
204
+ done: true,
205
+ value: void 0
206
+ };
207
+ if (offset >= bytes.length) {
208
+ currentState = "completed";
209
+ return {
210
+ done: true,
211
+ value: void 0
212
+ };
213
+ }
214
+ const chunk = bytes.slice(offset, Math.min(offset + chunkSize, bytes.length));
215
+ offset += chunk.length;
216
+ return {
217
+ done: false,
218
+ value: chunk
219
+ };
220
+ })().finally(() => {
221
+ pending = void 0;
222
+ });
223
+ return pending;
224
+ },
225
+ async return() {
226
+ if (currentState !== "completed") currentState = "cancelled";
227
+ finishWaits();
228
+ await pending;
229
+ return {
230
+ done: true,
231
+ value: void 0
232
+ };
233
+ }
234
+ };
235
+ const stream = { [Symbol.asyncIterator]: () => iterator };
236
+ return {
237
+ stream,
238
+ pause() {
239
+ if (currentState === "running") currentState = "paused";
240
+ },
241
+ resume() {
242
+ if (currentState !== "paused") return;
243
+ currentState = "running";
244
+ wake?.();
245
+ wake = void 0;
246
+ },
247
+ cancel() {
248
+ if (currentState === "completed" || currentState === "cancelled") return;
249
+ currentState = "cancelled";
250
+ finishWaits();
251
+ },
252
+ state() {
253
+ return currentState;
254
+ }
255
+ };
256
+ function isCancelled() {
257
+ return currentState === "cancelled";
258
+ }
259
+ }
260
+ function nonNegativeNumber(value, fallback, name) {
261
+ if (value === void 0) return fallback;
262
+ if (!Number.isFinite(value) || value < 0) throw new TypeError(`${name} must be a non-negative number`);
263
+ return value;
264
+ }
265
+
266
+ //#endregion
267
+ export { STREAM_FIXTURES, createDevTools, createStreamSimulator };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@ai-gui/devtools",
3
+ "version": "0.3.0",
4
+ "description": "Framework-agnostic runtime timeline and stream simulator for AIGUI.",
5
+ "keywords": [
6
+ "aigui",
7
+ "devtools",
8
+ "timeline",
9
+ "debug",
10
+ "streaming"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Liang Li <ll_faw@hotmail.com>",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/liliang-cn/aigui.git",
17
+ "directory": "packages/devtools"
18
+ },
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "import": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "require": {
34
+ "types": "./dist/index.d.cts",
35
+ "default": "./dist/index.cjs"
36
+ }
37
+ }
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "@ai-gui/core": "0.3.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsdown",
52
+ "test": "pnpm --dir ../.. exec vitest run --project devtools",
53
+ "typecheck": "tsc --noEmit"
54
+ }
55
+ }