@tanstack/ai-client 0.18.5 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/esm/audio-recorder.d.ts +76 -0
  2. package/dist/esm/audio-recorder.js +215 -0
  3. package/dist/esm/audio-recorder.js.map +1 -0
  4. package/dist/esm/chat-client.d.ts +264 -0
  5. package/dist/esm/chat-client.js +1097 -0
  6. package/dist/esm/chat-client.js.map +1 -0
  7. package/dist/esm/client-persistor.d.ts +86 -0
  8. package/dist/esm/client-persistor.js +243 -0
  9. package/dist/esm/client-persistor.js.map +1 -0
  10. package/dist/esm/connection-adapters.d.ts +203 -0
  11. package/dist/esm/connection-adapters.js +611 -0
  12. package/dist/esm/connection-adapters.js.map +1 -0
  13. package/dist/esm/devtools-noop.d.ts +63 -0
  14. package/dist/esm/devtools-noop.js +141 -0
  15. package/dist/esm/devtools-noop.js.map +1 -0
  16. package/dist/esm/devtools.d.ts +276 -0
  17. package/dist/esm/devtools.js +1178 -0
  18. package/dist/esm/devtools.js.map +1 -0
  19. package/dist/esm/events.d.ts +129 -0
  20. package/dist/esm/events.js +227 -0
  21. package/dist/esm/events.js.map +1 -0
  22. package/dist/esm/generation-client.d.ts +99 -0
  23. package/dist/esm/generation-client.js +325 -0
  24. package/dist/esm/generation-client.js.map +1 -0
  25. package/dist/esm/generation-types.d.ts +261 -0
  26. package/dist/esm/generation-types.js +14 -0
  27. package/dist/esm/generation-types.js.map +1 -0
  28. package/dist/esm/index.d.ts +20 -0
  29. package/dist/esm/index.js +48 -0
  30. package/dist/esm/index.js.map +1 -0
  31. package/dist/esm/mcp-app-bridge.d.ts +27 -0
  32. package/dist/esm/mcp-app-bridge.js +70 -0
  33. package/dist/esm/mcp-app-bridge.js.map +1 -0
  34. package/dist/esm/realtime-client.d.ts +98 -0
  35. package/dist/esm/realtime-client.js +403 -0
  36. package/dist/esm/realtime-client.js.map +1 -0
  37. package/dist/esm/realtime-types.d.ts +78 -0
  38. package/dist/esm/response-stream.d.ts +7 -0
  39. package/dist/esm/response-stream.js +32 -0
  40. package/dist/esm/response-stream.js.map +1 -0
  41. package/dist/esm/sse-parser.d.ts +8 -0
  42. package/dist/esm/sse-parser.js +52 -0
  43. package/dist/esm/sse-parser.js.map +1 -0
  44. package/dist/esm/sse-utils.d.ts +1 -0
  45. package/dist/esm/sse-utils.js +11 -0
  46. package/dist/esm/sse-utils.js.map +1 -0
  47. package/dist/esm/tool-types.d.ts +20 -0
  48. package/dist/esm/types.d.ts +419 -0
  49. package/dist/esm/types.js +11 -0
  50. package/dist/esm/types.js.map +1 -0
  51. package/dist/esm/video-generation-client.d.ts +110 -0
  52. package/dist/esm/video-generation-client.js +382 -0
  53. package/dist/esm/video-generation-client.js.map +1 -0
  54. package/package.json +4 -3
  55. package/src/audio-recorder.ts +322 -0
  56. package/src/generation-types.ts +24 -4
  57. package/src/index.ts +10 -0
  58. package/src/mcp-app-bridge.ts +120 -0
  59. package/src/types.ts +2 -0
@@ -0,0 +1,1178 @@
1
+ import { emitAIDevtoolsEvent, aiEventClient, createAIDevtoolsEventEnvelope } from "@tanstack/ai-event-client";
2
+ import { convertSchemaToJsonSchema } from "@tanstack/ai/client";
3
+ import { DefaultChatClientEventEmitter } from "./events.js";
4
+ function createAIDevtoolsGenerationPreview(input) {
5
+ if (input.outputKind === "image") {
6
+ return imagePreviewFromResult(input.result);
7
+ }
8
+ if (input.outputKind === "audio") {
9
+ return audioPreviewFromResult(input.result);
10
+ }
11
+ if (input.outputKind === "video") {
12
+ return videoPreviewFromResult(input.result, input.videoStatus);
13
+ }
14
+ if (input.outputKind === "text") {
15
+ return textPreviewFromResult(input.result);
16
+ }
17
+ if (input.result === null || input.result === void 0) {
18
+ return { kind: "empty" };
19
+ }
20
+ return {
21
+ kind: "structured",
22
+ value: input.result
23
+ };
24
+ }
25
+ function imagePreviewFromResult(result) {
26
+ const record = asRecord(result);
27
+ const images = Array.isArray(record?.images) ? record.images : [];
28
+ const items = images.map((image) => mediaItemFromSource(image, "image/png")).filter(isGenerationMediaItem);
29
+ if (items.length === 0 && result !== null && result !== void 0) {
30
+ const directItem = mediaItemFromSource(result, "image/png");
31
+ if (directItem) {
32
+ items.push(directItem);
33
+ }
34
+ }
35
+ return { kind: "image", items };
36
+ }
37
+ function audioPreviewFromResult(result) {
38
+ const record = asRecord(result);
39
+ const audio = record?.audio;
40
+ const resultContentType = stringField(record, "contentType");
41
+ const format = stringField(record, "format");
42
+ const mimeType = resultContentType ?? mimeTypeFromAudioFormat(format);
43
+ const items = [];
44
+ const directItem = typeof audio === "string" ? base64MediaItem(audio, mimeType, {
45
+ format,
46
+ duration: numberField(record, "duration")
47
+ }) : mediaItemFromSource(audio, mimeType, {
48
+ format
49
+ });
50
+ if (directItem) {
51
+ items.push(directItem);
52
+ }
53
+ return { kind: "audio", items };
54
+ }
55
+ function videoPreviewFromResult(result, videoStatus) {
56
+ const resultRecord = asRecord(result);
57
+ const statusRecord = asRecord(videoStatus);
58
+ const item = mediaItemFromSource(result, "video/mp4") ?? mediaItemFromSource(videoStatus, "video/mp4");
59
+ const items = item ? [item] : [];
60
+ const job = videoJobFromStatus(statusRecord ?? resultRecord);
61
+ return {
62
+ kind: "video",
63
+ items,
64
+ ...job ? { job } : {}
65
+ };
66
+ }
67
+ function textPreviewFromResult(result) {
68
+ const record = asRecord(result);
69
+ const text = stringField(record, "text") ?? stringField(record, "summary") ?? stringField(record, "content") ?? (typeof result === "string" ? result : void 0);
70
+ if (text !== void 0) {
71
+ return { kind: "text", text };
72
+ }
73
+ if (result === null || result === void 0) {
74
+ return { kind: "empty" };
75
+ }
76
+ return {
77
+ kind: "structured",
78
+ value: result
79
+ };
80
+ }
81
+ function videoJobFromStatus(record) {
82
+ const jobId = stringField(record, "jobId");
83
+ if (!jobId) return void 0;
84
+ return {
85
+ jobId,
86
+ ...stringField(record, "status") ? { status: stringField(record, "status") } : {},
87
+ ...numberField(record, "progress") !== void 0 ? { progress: numberField(record, "progress") } : {},
88
+ ...stringField(record, "error") ? { error: stringField(record, "error") } : {}
89
+ };
90
+ }
91
+ function mediaItemFromSource(value, defaultMimeType, extras = {}) {
92
+ const record = asRecord(value);
93
+ if (!record) return void 0;
94
+ const explicitContentType = stringField(record, "contentType") ?? stringField(record, "mimeType");
95
+ const duration = numberField(record, "duration") ?? extras.duration;
96
+ const format = stringField(record, "format") ?? extras.format;
97
+ const url = stringField(record, "url");
98
+ if (url) {
99
+ return {
100
+ src: url,
101
+ sourceType: "url",
102
+ ...explicitContentType ? { mimeType: explicitContentType } : {},
103
+ ...format ? { format } : {},
104
+ ...duration !== void 0 ? { duration } : {}
105
+ };
106
+ }
107
+ const b64Json = stringField(record, "b64Json");
108
+ if (!b64Json) return void 0;
109
+ return base64MediaItem(b64Json, explicitContentType ?? defaultMimeType, {
110
+ format,
111
+ duration
112
+ });
113
+ }
114
+ function base64MediaItem(value, mimeType, extras = {}) {
115
+ const src = value.startsWith("data:") ? value : `data:${mimeType ?? "application/octet-stream"};base64,${value}`;
116
+ return {
117
+ src,
118
+ sourceType: "base64",
119
+ ...mimeType ? { mimeType } : {},
120
+ ...extras.format ? { format: extras.format } : {},
121
+ ...extras.duration !== void 0 ? { duration: extras.duration } : {}
122
+ };
123
+ }
124
+ function mimeTypeFromAudioFormat(format) {
125
+ if (!format) return "audio/mpeg";
126
+ if (format === "mp3") return "audio/mpeg";
127
+ return `audio/${format}`;
128
+ }
129
+ function asRecord(value) {
130
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
131
+ return void 0;
132
+ }
133
+ return value;
134
+ }
135
+ function stringField(record, field) {
136
+ const value = record?.[field];
137
+ return typeof value === "string" && value.length > 0 ? value : void 0;
138
+ }
139
+ function numberField(record, field) {
140
+ const value = record?.[field];
141
+ return typeof value === "number" ? value : void 0;
142
+ }
143
+ function isGenerationMediaItem(value) {
144
+ return Boolean(value);
145
+ }
146
+ const activeBridgeRegistryKey = /* @__PURE__ */ Symbol.for(
147
+ "tanstack.ai.devtools.activeBridgeByHookId"
148
+ );
149
+ function getActiveBridgeRegistry() {
150
+ const global = globalThis;
151
+ const existing = global[activeBridgeRegistryKey];
152
+ if (existing) return existing;
153
+ const registry = /* @__PURE__ */ new Map();
154
+ global[activeBridgeRegistryKey] = registry;
155
+ return registry;
156
+ }
157
+ class ClientDevtoolsBridge {
158
+ options;
159
+ bridgeId;
160
+ unsubscribers = [];
161
+ disposed = false;
162
+ superseded = false;
163
+ registered = false;
164
+ constructor(options) {
165
+ this.options = options;
166
+ this.bridgeId = createBridgeId(options.hookId);
167
+ }
168
+ emitRegistered() {
169
+ if (!this.prepareForMountEmit()) {
170
+ return;
171
+ }
172
+ this.registered = true;
173
+ emitAIDevtoolsEvent("hook:registered", {
174
+ ...this.createEnvelope("hook:registered"),
175
+ ...this.createMetadataPayload(),
176
+ lifecycle: "mounted"
177
+ });
178
+ }
179
+ emitUpdated() {
180
+ if (!this.prepareForEmit()) {
181
+ return;
182
+ }
183
+ emitAIDevtoolsEvent("hook:updated", {
184
+ ...this.createEnvelope("hook:updated"),
185
+ ...this.createMetadataPayload(),
186
+ lifecycle: "active"
187
+ });
188
+ }
189
+ emitSnapshot() {
190
+ if (!this.prepareForEmit()) {
191
+ return;
192
+ }
193
+ emitAIDevtoolsEvent("hook:state-snapshot", {
194
+ ...this.createEnvelope("hook:state-snapshot"),
195
+ ...this.createMetadataPayload(),
196
+ // Wire envelope uses Record<string, unknown>; widen the typed snapshot
197
+ // here so the typed-snapshot constraint above can stay narrow.
198
+ // eslint-disable-next-line no-restricted-syntax -- TSnapshot extends object is structurally compatible but TS can't see the missing index signature
199
+ state: this.options.getSnapshot()
200
+ });
201
+ }
202
+ emitToolsRegistered() {
203
+ if (!this.prepareForEmit()) {
204
+ return;
205
+ }
206
+ const tools = this.options.getTools ? Array.from(this.options.getTools()).map((tool) => ({
207
+ name: tool.name,
208
+ description: tool.description,
209
+ inputSchema: tool.inputSchema ? convertSchemaToJsonSchema(tool.inputSchema) : { type: "object" },
210
+ outputSchema: tool.outputSchema ? convertSchemaToJsonSchema(tool.outputSchema) : void 0,
211
+ needsApproval: tool.needsApproval,
212
+ metadata: tool.metadata
213
+ })) : [];
214
+ emitAIDevtoolsEvent("tools:registered", {
215
+ ...this.createEnvelope("tools:registered"),
216
+ ...this.createMetadataPayload(),
217
+ tools
218
+ });
219
+ }
220
+ emitRunLifecycle(eventType, runId, status, options = {}) {
221
+ if (!this.prepareForEmit()) {
222
+ return;
223
+ }
224
+ emitAIDevtoolsEvent(eventType, {
225
+ ...this.createEnvelope(eventType, "client-state", { runId }),
226
+ runId,
227
+ status,
228
+ ...options.error ? { error: options.error } : {}
229
+ });
230
+ }
231
+ deactivate() {
232
+ const activeBridgeByHookId = getActiveBridgeRegistry();
233
+ if (activeBridgeByHookId.get(this.options.hookId) === this) {
234
+ activeBridgeByHookId.delete(this.options.hookId);
235
+ }
236
+ for (const unsubscribe of this.unsubscribers.splice(0)) {
237
+ unsubscribe();
238
+ }
239
+ }
240
+ supersede() {
241
+ if (this.disposed) {
242
+ return;
243
+ }
244
+ this.superseded = true;
245
+ this.disposed = true;
246
+ this.deactivate();
247
+ }
248
+ dispose() {
249
+ if (this.disposed) {
250
+ return;
251
+ }
252
+ this.disposed = true;
253
+ if (!this.registered) {
254
+ this.deactivate();
255
+ return;
256
+ }
257
+ const payload = {
258
+ ...this.createEnvelope("hook:unregistered"),
259
+ ...this.createMetadataPayload(),
260
+ reason: "disposed"
261
+ };
262
+ emitAIDevtoolsEvent("hook:unregistered", payload);
263
+ this.deactivate();
264
+ }
265
+ prepareForEmit() {
266
+ if (this.disposed || this.superseded) {
267
+ return false;
268
+ }
269
+ this.activate();
270
+ return true;
271
+ }
272
+ prepareForMountEmit() {
273
+ if (this.superseded) {
274
+ return false;
275
+ }
276
+ if (this.disposed) {
277
+ this.disposed = false;
278
+ this.registered = false;
279
+ }
280
+ this.activate();
281
+ return true;
282
+ }
283
+ activate() {
284
+ if (this.disposed) {
285
+ return;
286
+ }
287
+ const activeBridgeByHookId = getActiveBridgeRegistry();
288
+ const activeBridge = activeBridgeByHookId.get(this.options.hookId);
289
+ if (activeBridge && activeBridge !== this) {
290
+ if (typeof activeBridge.supersede === "function") {
291
+ activeBridge.supersede();
292
+ } else {
293
+ activeBridge.deactivate();
294
+ }
295
+ }
296
+ activeBridgeByHookId.set(this.options.hookId, this);
297
+ if (this.unsubscribers.length > 0) {
298
+ return;
299
+ }
300
+ this.unsubscribers.push(
301
+ aiEventClient.on("devtools:request-state", (event) => {
302
+ this.handleRequestState(event);
303
+ })
304
+ );
305
+ if (this.options.applyToolFixture) {
306
+ this.unsubscribers.push(
307
+ aiEventClient.on("devtools:tool-fixture:apply", (event) => {
308
+ void this.handleToolFixtureApply(event);
309
+ })
310
+ );
311
+ }
312
+ }
313
+ handleRequestState(event) {
314
+ if (this.disposed || this.superseded) {
315
+ return;
316
+ }
317
+ const targetHookId = event.payload.targetHookId;
318
+ if (targetHookId && targetHookId !== this.options.hookId) {
319
+ return;
320
+ }
321
+ this.emitRegistered();
322
+ this.emitToolsRegistered();
323
+ this.emitSnapshot();
324
+ }
325
+ async handleToolFixtureApply(event) {
326
+ const fixture = event.payload;
327
+ if (!this.matchesFixtureTarget(fixture)) {
328
+ return;
329
+ }
330
+ await this.options.applyToolFixture?.(fixture);
331
+ }
332
+ matchesFixtureTarget(fixture) {
333
+ if (!fixture.hookId && !fixture.threadId) {
334
+ return false;
335
+ }
336
+ if (fixture.hookId) {
337
+ return fixture.hookId === this.options.hookId;
338
+ }
339
+ if (fixture.threadId && (!this.options.threadId || fixture.threadId !== this.options.threadId)) {
340
+ return false;
341
+ }
342
+ return true;
343
+ }
344
+ createEnvelope(eventType, visibility = "client-state", context = {}) {
345
+ return createAIDevtoolsEventEnvelope({
346
+ eventType,
347
+ source: "client",
348
+ visibility,
349
+ clientId: this.options.clientId,
350
+ hookId: this.options.hookId,
351
+ correlationId: this.bridgeId,
352
+ ...this.options.threadId ? { threadId: this.options.threadId } : {},
353
+ ...context.runId ? { runId: context.runId } : {},
354
+ timestamp: Date.now()
355
+ });
356
+ }
357
+ createMetadataPayload() {
358
+ return {
359
+ hookId: this.options.hookId,
360
+ hookName: this.options.metadata.hookName,
361
+ ...this.options.metadata.name ? { displayName: this.options.metadata.name } : {},
362
+ ...this.options.metadata.outputKind ? { outputKind: this.options.metadata.outputKind } : {},
363
+ ...this.options.metadata.framework ? { framework: this.options.metadata.framework } : {}
364
+ };
365
+ }
366
+ }
367
+ let bridgeIdSequence = 0;
368
+ function createBridgeId(hookId) {
369
+ const cryptoLike = globalThis.crypto;
370
+ if (cryptoLike?.randomUUID) {
371
+ return `bridge:${hookId}:${cryptoLike.randomUUID()}`;
372
+ }
373
+ bridgeIdSequence += 1;
374
+ return `bridge:${hookId}:${bridgeIdSequence}`;
375
+ }
376
+ class ChatDevtoolsBridge extends ClientDevtoolsBridge {
377
+ events;
378
+ chatOptions;
379
+ currentRunId = null;
380
+ currentRunThreadId = null;
381
+ currentStreamId = null;
382
+ lastStreamId = null;
383
+ lastRunEventContext;
384
+ constructor(options) {
385
+ super({
386
+ ...options,
387
+ // Thunk defers `this.applyFixture` lookup until after `super` returns.
388
+ applyToolFixture: (fixture) => this.applyFixture(fixture)
389
+ });
390
+ this.chatOptions = options;
391
+ this.events = new ChatDevtoolsAwareEventEmitter(options.clientId, this);
392
+ }
393
+ // --- Stream / run context API -------------------------------------------
394
+ setCurrentStreamId(streamId) {
395
+ this.currentStreamId = streamId;
396
+ if (streamId) {
397
+ this.lastStreamId = streamId;
398
+ }
399
+ }
400
+ /**
401
+ * Called by the auto-attaching emitter every time it sees a non-empty
402
+ * streamId pass through. Lets devtools track the latest stream id
403
+ * without the chat client wiring it up explicitly.
404
+ */
405
+ recordStreamId(streamId) {
406
+ if (streamId) this.lastStreamId = streamId;
407
+ }
408
+ mountWithTools(initialMessageCount) {
409
+ this.events.clientCreated(initialMessageCount);
410
+ this.emitRegistered();
411
+ this.emitToolsRegistered();
412
+ this.emitSnapshot();
413
+ }
414
+ notifyToolsChanged() {
415
+ this.emitToolsRegistered();
416
+ this.emitSnapshot();
417
+ }
418
+ getCurrentStreamId() {
419
+ return this.currentStreamId;
420
+ }
421
+ getLastStreamId() {
422
+ return this.lastStreamId;
423
+ }
424
+ resolveStreamId() {
425
+ return this.currentStreamId ?? this.lastStreamId ?? this.chatOptions.generateId("stream");
426
+ }
427
+ // Called when the chat client has just generated a runId for outbound emits;
428
+ // the matching RUN_STARTED chunk from the adapter lands later and
429
+ // observeChunk keeps the same context.
430
+ beginRun(runId, threadId) {
431
+ this.currentRunId = runId;
432
+ this.currentRunThreadId = threadId;
433
+ this.lastRunEventContext = { runId, threadId };
434
+ }
435
+ observeChunk(chunk) {
436
+ if (chunk.type === "RUN_STARTED") {
437
+ this.beginRun(chunk.runId, chunk.threadId);
438
+ return;
439
+ }
440
+ if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
441
+ const runId = chunk.type === "RUN_FINISHED" ? chunk.runId : chunk.runId;
442
+ if (!runId || runId === this.currentRunId) {
443
+ const context = this.getCurrentRunEventContext();
444
+ if (context) {
445
+ this.lastRunEventContext = context;
446
+ }
447
+ this.currentRunId = null;
448
+ this.currentRunThreadId = null;
449
+ }
450
+ }
451
+ }
452
+ getCurrentRunEventContext() {
453
+ if (!this.currentRunId) return void 0;
454
+ return {
455
+ threadId: this.currentRunThreadId ?? this.chatOptions.threadId ?? "",
456
+ runId: this.currentRunId
457
+ };
458
+ }
459
+ getCurrentOrLastRunEventContext() {
460
+ return this.getCurrentRunEventContext() ?? this.lastRunEventContext;
461
+ }
462
+ findToolCallContext(toolCallId) {
463
+ const base = { toolCallId };
464
+ const runContext = this.getCurrentRunEventContext();
465
+ if (runContext) {
466
+ return {
467
+ threadId: runContext.threadId,
468
+ runId: runContext.runId,
469
+ toolCallId
470
+ };
471
+ }
472
+ if (this.chatOptions.threadId) {
473
+ return { threadId: this.chatOptions.threadId, toolCallId };
474
+ }
475
+ return base;
476
+ }
477
+ // --- Fixture replay ------------------------------------------------------
478
+ /**
479
+ * Entry point invoked when the devtools panel emits
480
+ * `devtools:tool-fixture:apply`. The chat client never calls this
481
+ * directly; it is wired through the base bridge's fixture subscription.
482
+ */
483
+ async applyFixture(fixture) {
484
+ const messages = this.chatOptions.getMessages();
485
+ const threadId = fixture.threadId ?? this.chatOptions.threadId ?? "";
486
+ if (fixture.execute) {
487
+ await this.executeFixture(fixture, messages, threadId);
488
+ return;
489
+ }
490
+ const replay = this.createReplayMessageFromFixture(fixture, messages);
491
+ const { message, toolCallId } = replay;
492
+ const messageId = message.id;
493
+ this.events.messageAppended(message, void 0, {
494
+ threadId,
495
+ toolCallId,
496
+ ...fixture.runId ? { runId: fixture.runId } : {}
497
+ });
498
+ this.chatOptions.setMessages([...messages, message]);
499
+ this.events.toolFixtureApplied({
500
+ hookId: this.chatOptions.hookId,
501
+ threadId,
502
+ ...fixture.runId ? { runId: fixture.runId } : {},
503
+ toolName: fixture.toolName,
504
+ input: fixture.input,
505
+ output: fixture.output,
506
+ messageId,
507
+ toolCallId,
508
+ ...fixture.execute !== void 0 ? { execute: fixture.execute } : {},
509
+ ...fixture.message ? { message: fixture.message } : {},
510
+ ...fixture.errorText ? { errorText: fixture.errorText } : {}
511
+ });
512
+ this.emitSnapshot();
513
+ }
514
+ async executeFixture(fixture, messages, threadId) {
515
+ const toolCallId = this.resolveFixtureToolCallId(
516
+ fixture.toolCallId,
517
+ messages
518
+ );
519
+ const messageId = this.resolveFixtureMessageId(fixture.messageId, messages);
520
+ const message = {
521
+ id: messageId,
522
+ role: "assistant",
523
+ parts: [
524
+ {
525
+ type: "tool-call",
526
+ id: toolCallId,
527
+ name: fixture.toolName,
528
+ arguments: stringifyFixtureValue(fixture.input),
529
+ input: fixture.input,
530
+ state: "input-complete"
531
+ }
532
+ ],
533
+ createdAt: /* @__PURE__ */ new Date()
534
+ };
535
+ this.events.messageAppended(message, void 0, {
536
+ threadId,
537
+ toolCallId,
538
+ ...fixture.runId ? { runId: fixture.runId } : {}
539
+ });
540
+ this.chatOptions.setMessages([...messages, message]);
541
+ this.emitSnapshot();
542
+ const clientTool = this.findClientTool(fixture.toolName);
543
+ const executeFunc = clientTool?.execute;
544
+ if (!executeFunc) {
545
+ console.warn(
546
+ `[ai-devtools] tool fixture "${fixture.toolName}" requested execute=true but no client tool implementation is registered; replaying saved output instead.`
547
+ );
548
+ this.addToolResultForFixture({
549
+ fixture,
550
+ messageId,
551
+ toolCallId,
552
+ threadId,
553
+ output: fixture.output,
554
+ errorText: fixture.errorText
555
+ });
556
+ return;
557
+ }
558
+ let output;
559
+ try {
560
+ output = await executeFunc(fixture.input);
561
+ } catch (error) {
562
+ console.error(
563
+ `[ai-devtools] tool fixture "${fixture.toolName}" execute threw`,
564
+ error
565
+ );
566
+ this.addToolResultForFixture({
567
+ fixture,
568
+ messageId,
569
+ toolCallId,
570
+ threadId,
571
+ output: null,
572
+ errorText: error instanceof Error ? `${error.name}: ${error.message}` : `Tool execution failed: ${String(error)}`
573
+ });
574
+ return;
575
+ }
576
+ this.addToolResultForFixture({
577
+ fixture,
578
+ messageId,
579
+ toolCallId,
580
+ threadId,
581
+ output
582
+ });
583
+ }
584
+ addToolResultForFixture(input) {
585
+ const state = input.errorText ? "output-error" : "output-available";
586
+ this.events.toolResultAdded(
587
+ input.toolCallId,
588
+ input.fixture.toolName,
589
+ input.output,
590
+ state,
591
+ {
592
+ threadId: input.threadId,
593
+ ...input.fixture.runId ? { runId: input.fixture.runId } : {},
594
+ toolCallId: input.toolCallId
595
+ }
596
+ );
597
+ this.chatOptions.addToolResult(
598
+ input.toolCallId,
599
+ input.output,
600
+ input.errorText
601
+ );
602
+ this.events.toolFixtureApplied({
603
+ hookId: this.chatOptions.hookId,
604
+ threadId: input.threadId,
605
+ ...input.fixture.runId ? { runId: input.fixture.runId } : {},
606
+ toolName: input.fixture.toolName,
607
+ input: input.fixture.input,
608
+ output: input.output,
609
+ execute: true,
610
+ messageId: input.messageId,
611
+ toolCallId: input.toolCallId,
612
+ ...input.errorText ? { errorText: input.errorText } : {}
613
+ });
614
+ this.emitSnapshot();
615
+ }
616
+ createReplayMessageFromFixture(fixture, messages) {
617
+ const cloned = this.cloneFixtureSourceMessage(fixture, messages);
618
+ if (cloned) return cloned;
619
+ const toolCallId = this.resolveFixtureToolCallId(
620
+ fixture.toolCallId,
621
+ messages
622
+ );
623
+ const messageId = this.resolveFixtureMessageId(fixture.messageId, messages);
624
+ const state = fixture.errorText ? "error" : "complete";
625
+ return {
626
+ toolCallId,
627
+ message: {
628
+ id: messageId,
629
+ role: "assistant",
630
+ parts: [
631
+ {
632
+ type: "tool-call",
633
+ id: toolCallId,
634
+ name: fixture.toolName,
635
+ arguments: stringifyFixtureValue(fixture.input),
636
+ input: fixture.input,
637
+ state: "input-complete",
638
+ output: fixture.output
639
+ },
640
+ {
641
+ type: "tool-result",
642
+ toolCallId,
643
+ content: stringifyFixtureValue(fixture.output),
644
+ state,
645
+ ...fixture.errorText ? { error: fixture.errorText } : {}
646
+ }
647
+ ],
648
+ createdAt: /* @__PURE__ */ new Date()
649
+ }
650
+ };
651
+ }
652
+ cloneFixtureSourceMessage(fixture, messages) {
653
+ const sourceMessage = fixture.message;
654
+ if (!sourceMessage || !Array.isArray(sourceMessage.parts)) {
655
+ return void 0;
656
+ }
657
+ const toolCallIds = this.createFixtureToolCallIdMap(
658
+ sourceMessage.parts,
659
+ messages
660
+ );
661
+ const parts = sourceMessage.parts.map((part) => cloneFixtureMessagePart(part, toolCallIds)).filter((part) => Boolean(part));
662
+ const mappedFixtureToolCallId = fixture.toolCallId ? toolCallIds.get(fixture.toolCallId) : void 0;
663
+ hydrateToolCallOutputs(parts, {
664
+ ...mappedFixtureToolCallId ? { mappedToolCallId: mappedFixtureToolCallId } : {},
665
+ output: fixture.output
666
+ });
667
+ if (parts.length === 0) return void 0;
668
+ const toolCallId = (fixture.toolCallId ? toolCallIds.get(fixture.toolCallId) : void 0) ?? firstToolCallId(parts);
669
+ if (!toolCallId) return void 0;
670
+ return {
671
+ toolCallId,
672
+ message: {
673
+ id: this.resolveFixtureMessageId(sourceMessage.id, messages),
674
+ role: sourceMessage.role,
675
+ parts,
676
+ createdAt: /* @__PURE__ */ new Date()
677
+ }
678
+ };
679
+ }
680
+ createFixtureToolCallIdMap(parts, messages) {
681
+ const ids = /* @__PURE__ */ new Map();
682
+ for (const part of parts) {
683
+ if (!isRecord(part) || part.type !== "tool-call") continue;
684
+ if (typeof part.id !== "string") continue;
685
+ ids.set(part.id, this.resolveFixtureToolCallId(part.id, messages));
686
+ }
687
+ return ids;
688
+ }
689
+ resolveFixtureMessageId(messageId, messages) {
690
+ if (messageId && !messages.some((message) => message.id === messageId)) {
691
+ return messageId;
692
+ }
693
+ return this.chatOptions.generateId("fixture-msg");
694
+ }
695
+ resolveFixtureToolCallId(toolCallId, messages) {
696
+ if (toolCallId && !hasToolCallId(messages, toolCallId)) {
697
+ return toolCallId;
698
+ }
699
+ return this.chatOptions.generateId("fixture-tool-call");
700
+ }
701
+ findClientTool(name) {
702
+ const tools = this.chatOptions.getTools?.();
703
+ if (!tools) return void 0;
704
+ for (const tool of tools) {
705
+ if (tool.name === name) return tool;
706
+ }
707
+ return void 0;
708
+ }
709
+ }
710
+ function isRecord(value) {
711
+ return typeof value === "object" && value !== null;
712
+ }
713
+ function stringifyFixtureValue(value) {
714
+ if (typeof value === "string") return value;
715
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
716
+ return String(value);
717
+ }
718
+ try {
719
+ return JSON.stringify(value);
720
+ } catch (error) {
721
+ console.error(
722
+ "[ai-devtools] failed to JSON.stringify fixture value; falling back to String(). Tool call arguments may be malformed.",
723
+ { error, value }
724
+ );
725
+ return String(value);
726
+ }
727
+ }
728
+ function parseFixtureResultContent(content) {
729
+ try {
730
+ return JSON.parse(content);
731
+ } catch (error) {
732
+ console.error(
733
+ "[ai-devtools] failed to JSON.parse fixture result content; replaying as raw string. Fixture payload may be corrupted.",
734
+ { error, content }
735
+ );
736
+ return content;
737
+ }
738
+ }
739
+ function cloneFixtureMessagePart(part, toolCallIds) {
740
+ if (!isRecord(part) || typeof part.type !== "string") return void 0;
741
+ const cloned = { ...part };
742
+ if (part.type === "tool-call" && typeof part.id === "string") {
743
+ cloned.id = toolCallIds.get(part.id) ?? part.id;
744
+ }
745
+ if (part.type === "tool-result" && typeof part.toolCallId === "string") {
746
+ cloned.toolCallId = toolCallIds.get(part.toolCallId) ?? part.toolCallId;
747
+ }
748
+ return cloned;
749
+ }
750
+ function firstToolCallId(parts) {
751
+ const toolCall = parts.find((part) => part.type === "tool-call");
752
+ return toolCall?.type === "tool-call" ? toolCall.id : void 0;
753
+ }
754
+ function hydrateToolCallOutputs(parts, fixtureOutput) {
755
+ for (const part of parts) {
756
+ if (part.type !== "tool-result") continue;
757
+ const toolCall = parts.find(
758
+ (candidate) => candidate.type === "tool-call" && candidate.id === part.toolCallId && candidate.output === void 0
759
+ );
760
+ if (toolCall) {
761
+ toolCall.output = Array.isArray(part.content) ? part.content : parseFixtureResultContent(part.content);
762
+ }
763
+ }
764
+ if (fixtureOutput.mappedToolCallId && fixtureOutput.output !== void 0) {
765
+ const toolCall = parts.find(
766
+ (candidate) => candidate.type === "tool-call" && candidate.id === fixtureOutput.mappedToolCallId && candidate.output === void 0
767
+ );
768
+ if (toolCall) {
769
+ toolCall.output = fixtureOutput.output;
770
+ }
771
+ }
772
+ }
773
+ function hasToolCallId(messages, toolCallId) {
774
+ return messages.some(
775
+ (message) => message.parts.some((part) => {
776
+ if (part.type === "tool-call") return part.id === toolCallId;
777
+ if (part.type === "tool-result") return part.toolCallId === toolCallId;
778
+ return false;
779
+ })
780
+ );
781
+ }
782
+ class GenerationDevtoolsBridge extends ClientDevtoolsBridge {
783
+ activeRunId = null;
784
+ activeRunStarted = false;
785
+ devtoolsRuns = [];
786
+ maxRuns;
787
+ getCoreState;
788
+ constructor(options) {
789
+ super({
790
+ ...options,
791
+ getSnapshot: () => this.buildSnapshot()
792
+ });
793
+ this.maxRuns = options.maxRuns ?? 20;
794
+ this.getCoreState = options.getCoreState;
795
+ }
796
+ // --- Run lifecycle (called by GenerationClient) -----------------------
797
+ beginRun(input) {
798
+ const runId = this.generateRunId();
799
+ this.activeRunId = runId;
800
+ this.activeRunStarted = false;
801
+ this.upsertRun(runId, {
802
+ input,
803
+ result: null,
804
+ preview: this.createPreview(null),
805
+ progress: null,
806
+ status: "generating",
807
+ isLoading: true,
808
+ clearError: true
809
+ });
810
+ return runId;
811
+ }
812
+ ensureRunStarted(runId) {
813
+ if (this.activeRunStarted && this.activeRunId === runId) return;
814
+ if (!this.activeRunStarted && this.activeRunId && this.activeRunId !== runId) {
815
+ this.renameRun(this.activeRunId, runId);
816
+ }
817
+ this.activeRunId = runId;
818
+ this.activeRunStarted = true;
819
+ this.upsertRun(runId, {
820
+ status: "generating",
821
+ isLoading: true,
822
+ clearError: true
823
+ });
824
+ this.emitRunLifecycle("run:started", runId, "started");
825
+ this.emitState();
826
+ }
827
+ finishRun(runId, eventType, status, error) {
828
+ this.ensureRunStarted(runId);
829
+ const completedAt = Date.now();
830
+ const completedProgress = status === "completed" ? this.completeProgress() : this.getProgress();
831
+ const runStatus = status === "completed" ? "success" : status === "errored" ? "error" : "cancelled";
832
+ this.upsertRun(runId, {
833
+ status: runStatus,
834
+ isLoading: false,
835
+ progress: completedProgress,
836
+ completedAt,
837
+ ...error ? { error } : { clearError: true }
838
+ });
839
+ if (this.activeRunId === runId) {
840
+ this.activeRunId = null;
841
+ }
842
+ this.activeRunStarted = false;
843
+ this.emitRunLifecycle(eventType, runId, status, {
844
+ ...error ? { error } : {}
845
+ });
846
+ this.emitState();
847
+ }
848
+ getActiveRunId() {
849
+ return this.activeRunId;
850
+ }
851
+ /** Clear all per-run history. Called when the client `reset()`s. */
852
+ resetRuns() {
853
+ this.activeRunId = null;
854
+ this.activeRunStarted = false;
855
+ this.devtoolsRuns = [];
856
+ }
857
+ /** Record state changes from the client and emit the matching snapshot. */
858
+ recordResultChange() {
859
+ this.updateActiveRun({
860
+ result: this.getCoreState().result,
861
+ preview: this.createPreview(this.getCoreState().result),
862
+ clearError: true
863
+ });
864
+ this.emitState();
865
+ }
866
+ recordLoadingChange() {
867
+ this.updateActiveRun({ isLoading: this.getCoreState().isLoading });
868
+ this.emitState();
869
+ }
870
+ recordErrorChange(error) {
871
+ this.updateActiveRun(
872
+ error ? { error: error.message } : { clearError: true }
873
+ );
874
+ this.emitState();
875
+ }
876
+ recordStatusChange(status) {
877
+ this.updateActiveRun({ status });
878
+ this.emitState();
879
+ }
880
+ recordProgressChange() {
881
+ this.updateActiveRun({ progress: this.getCoreState().progress });
882
+ this.emitState();
883
+ }
884
+ /** Emit the latest snapshot without touching run state. */
885
+ emitState() {
886
+ this.emitUpdated();
887
+ this.emitSnapshot();
888
+ }
889
+ // --- Internal ---------------------------------------------------------
890
+ buildSnapshot() {
891
+ const core = this.getCoreState();
892
+ return {
893
+ input: core.input,
894
+ result: core.result,
895
+ preview: this.createPreview(core.result),
896
+ progress: core.progress,
897
+ status: core.status,
898
+ isLoading: core.isLoading,
899
+ activeRunId: this.activeRunId,
900
+ runs: this.devtoolsRuns,
901
+ ...core.error ? { error: core.error } : {}
902
+ };
903
+ }
904
+ updateActiveRun(patch) {
905
+ if (!this.activeRunId) return;
906
+ this.upsertRun(this.activeRunId, patch);
907
+ }
908
+ upsertRun(runId, patch) {
909
+ const now = Date.now();
910
+ const index = this.devtoolsRuns.findIndex((run) => run.id === runId);
911
+ const existing = index >= 0 ? this.devtoolsRuns[index] : void 0;
912
+ const next = existing ? { ...existing } : {
913
+ id: runId,
914
+ input: this.getCoreState().input,
915
+ result: null,
916
+ preview: this.createPreview(null),
917
+ progress: null,
918
+ status: "idle",
919
+ isLoading: false,
920
+ startedAt: now,
921
+ updatedAt: now
922
+ };
923
+ if ("input" in patch) next.input = patch.input ?? null;
924
+ if ("result" in patch) next.result = patch.result ?? null;
925
+ if (patch.preview) next.preview = patch.preview;
926
+ if ("progress" in patch) next.progress = patch.progress ?? null;
927
+ if (patch.status) next.status = patch.status;
928
+ if ("isLoading" in patch) next.isLoading = patch.isLoading === true;
929
+ if (patch.completedAt !== void 0) next.completedAt = patch.completedAt;
930
+ if (patch.clearError) delete next.error;
931
+ if (patch.error !== void 0) next.error = patch.error;
932
+ next.updatedAt = now;
933
+ if (index >= 0) {
934
+ this.devtoolsRuns = this.devtoolsRuns.map(
935
+ (run) => run.id === runId ? next : run
936
+ );
937
+ } else {
938
+ this.devtoolsRuns = [...this.devtoolsRuns, next];
939
+ }
940
+ if (this.devtoolsRuns.length > this.maxRuns) {
941
+ this.devtoolsRuns = this.devtoolsRuns.slice(-this.maxRuns);
942
+ }
943
+ }
944
+ renameRun(previousRunId, nextRunId) {
945
+ if (previousRunId === nextRunId) return;
946
+ const existing = this.devtoolsRuns.find((run) => run.id === previousRunId);
947
+ if (!existing) return;
948
+ const renamed = { ...existing, id: nextRunId, updatedAt: Date.now() };
949
+ this.devtoolsRuns = this.devtoolsRuns.filter((run) => run.id !== nextRunId).map((run) => run.id === previousRunId ? renamed : run);
950
+ }
951
+ getProgress() {
952
+ return this.getCoreState().progress;
953
+ }
954
+ completeProgress() {
955
+ const progress = this.getCoreState().progress;
956
+ if (!progress) return null;
957
+ return {
958
+ value: 100,
959
+ ...progress.message ? { message: progress.message } : {}
960
+ };
961
+ }
962
+ createPreview(result) {
963
+ return createAIDevtoolsGenerationPreview({
964
+ outputKind: this.options.metadata.outputKind,
965
+ result
966
+ });
967
+ }
968
+ generateRunId() {
969
+ return `run-${Date.now()}-${Math.random().toString(36).substring(7)}`;
970
+ }
971
+ }
972
+ class VideoDevtoolsBridge extends GenerationDevtoolsBridge {
973
+ constructor(options) {
974
+ super(options);
975
+ }
976
+ recordJobIdChange() {
977
+ this.updateActiveRun({
978
+ jobId: this.getCoreState().jobId
979
+ });
980
+ this.emitState();
981
+ }
982
+ recordVideoStatusChange() {
983
+ const core = this.getCoreState();
984
+ this.updateActiveRun({
985
+ videoStatus: core.videoStatus,
986
+ preview: this.createVideoPreview(core.result, core.videoStatus)
987
+ });
988
+ this.emitState();
989
+ }
990
+ buildSnapshot() {
991
+ const core = this.getCoreState();
992
+ return {
993
+ input: core.input,
994
+ result: core.result,
995
+ preview: this.createVideoPreview(core.result, core.videoStatus),
996
+ progress: core.progress,
997
+ status: core.status,
998
+ isLoading: core.isLoading,
999
+ activeRunId: this.activeRunId,
1000
+ runs: this.devtoolsRuns,
1001
+ jobId: core.jobId,
1002
+ videoStatus: core.videoStatus,
1003
+ ...core.error ? { error: core.error } : {}
1004
+ };
1005
+ }
1006
+ upsertRun(runId, patch) {
1007
+ super.upsertRun(runId, patch);
1008
+ if (!("jobId" in patch || "videoStatus" in patch)) return;
1009
+ const index = this.devtoolsRuns.findIndex((run) => run.id === runId);
1010
+ if (index < 0) return;
1011
+ const target = this.devtoolsRuns[index];
1012
+ if (!target) return;
1013
+ const merged = { ...target };
1014
+ if ("jobId" in patch) merged.jobId = patch.jobId ?? null;
1015
+ if ("videoStatus" in patch) merged.videoStatus = patch.videoStatus ?? null;
1016
+ this.devtoolsRuns = this.devtoolsRuns.map(
1017
+ (run) => run.id === runId ? merged : run
1018
+ );
1019
+ }
1020
+ // Override so record* methods inherited from GenerationDevtoolsBridge
1021
+ // (e.g. recordResultChange) thread the latest videoStatus into the preview.
1022
+ createPreview(result) {
1023
+ const core = this.getCoreState();
1024
+ return this.createVideoPreview(result, core.videoStatus);
1025
+ }
1026
+ createVideoPreview(result, videoStatus) {
1027
+ return createAIDevtoolsGenerationPreview({
1028
+ outputKind: this.options.metadata.outputKind,
1029
+ result,
1030
+ videoStatus
1031
+ });
1032
+ }
1033
+ }
1034
+ class ChatDevtoolsAwareEventEmitter extends DefaultChatClientEventEmitter {
1035
+ constructor(clientId, helper) {
1036
+ super(clientId);
1037
+ this.helper = helper;
1038
+ }
1039
+ helper;
1040
+ afterEmit(streamId) {
1041
+ if (streamId) this.helper.recordStreamId(streamId);
1042
+ this.helper.emitSnapshot();
1043
+ }
1044
+ // -- methods with run context --------------------------------------------
1045
+ textUpdated(streamId, messageId, content, context) {
1046
+ super.textUpdated(
1047
+ streamId,
1048
+ messageId,
1049
+ content,
1050
+ context ?? this.helper.getCurrentRunEventContext()
1051
+ );
1052
+ this.afterEmit(streamId);
1053
+ }
1054
+ thinkingUpdated(streamId, messageId, content, delta, context) {
1055
+ super.thinkingUpdated(
1056
+ streamId,
1057
+ messageId,
1058
+ content,
1059
+ delta,
1060
+ context ?? this.helper.getCurrentRunEventContext()
1061
+ );
1062
+ this.afterEmit(streamId);
1063
+ }
1064
+ messageAppended(uiMessage, streamId, context) {
1065
+ super.messageAppended(
1066
+ uiMessage,
1067
+ streamId,
1068
+ context ?? this.helper.getCurrentRunEventContext()
1069
+ );
1070
+ this.afterEmit(streamId);
1071
+ }
1072
+ toolCallStateChanged(streamId, messageId, toolCallId, toolName, state, args, context) {
1073
+ super.toolCallStateChanged(
1074
+ streamId,
1075
+ messageId,
1076
+ toolCallId,
1077
+ toolName,
1078
+ state,
1079
+ args,
1080
+ context ?? this.helper.getCurrentRunEventContext()
1081
+ );
1082
+ this.afterEmit(streamId);
1083
+ }
1084
+ structuredOutputChanged(eventName, streamId, messageId, output, context) {
1085
+ super.structuredOutputChanged(
1086
+ eventName,
1087
+ streamId,
1088
+ messageId,
1089
+ output,
1090
+ context ?? this.helper.getCurrentOrLastRunEventContext()
1091
+ );
1092
+ this.afterEmit(streamId);
1093
+ }
1094
+ approvalRequested(streamId, messageId, toolCallId, toolName, input, approvalId, context) {
1095
+ super.approvalRequested(
1096
+ streamId,
1097
+ messageId,
1098
+ toolCallId,
1099
+ toolName,
1100
+ input,
1101
+ approvalId,
1102
+ context ?? this.helper.getCurrentOrLastRunEventContext()
1103
+ );
1104
+ this.afterEmit(streamId);
1105
+ }
1106
+ toolResultAdded(toolCallId, toolName, output, state, context) {
1107
+ super.toolResultAdded(
1108
+ toolCallId,
1109
+ toolName,
1110
+ output,
1111
+ state,
1112
+ context ?? this.helper.getCurrentRunEventContext()
1113
+ );
1114
+ this.afterEmit();
1115
+ }
1116
+ toolApprovalResponded(approvalId, toolCallId, approved, context) {
1117
+ super.toolApprovalResponded(
1118
+ approvalId,
1119
+ toolCallId,
1120
+ approved,
1121
+ context ?? this.helper.getCurrentRunEventContext()
1122
+ );
1123
+ this.afterEmit();
1124
+ }
1125
+ // -- methods without context (just auto-emit snapshot) -------------------
1126
+ clientCreated(initialMessageCount) {
1127
+ super.clientCreated(initialMessageCount);
1128
+ this.afterEmit();
1129
+ }
1130
+ loadingChanged(isLoading) {
1131
+ super.loadingChanged(isLoading);
1132
+ this.afterEmit();
1133
+ }
1134
+ errorChanged(error) {
1135
+ super.errorChanged(error);
1136
+ this.afterEmit();
1137
+ }
1138
+ reloaded(fromMessageIndex) {
1139
+ super.reloaded(fromMessageIndex);
1140
+ this.afterEmit();
1141
+ }
1142
+ stopped() {
1143
+ super.stopped();
1144
+ this.afterEmit();
1145
+ }
1146
+ messagesCleared() {
1147
+ super.messagesCleared();
1148
+ this.afterEmit();
1149
+ }
1150
+ messageSent(messageId, content) {
1151
+ super.messageSent(messageId, content);
1152
+ this.afterEmit();
1153
+ }
1154
+ toolFixtureApplied(fixture) {
1155
+ super.toolFixtureApplied(fixture);
1156
+ this.afterEmit();
1157
+ }
1158
+ }
1159
+ function createChatDevtoolsBridge(options) {
1160
+ return new ChatDevtoolsBridge(options);
1161
+ }
1162
+ function createGenerationDevtoolsBridge(options) {
1163
+ return new GenerationDevtoolsBridge(options);
1164
+ }
1165
+ function createVideoDevtoolsBridge(options) {
1166
+ return new VideoDevtoolsBridge(options);
1167
+ }
1168
+ export {
1169
+ ChatDevtoolsBridge,
1170
+ ClientDevtoolsBridge,
1171
+ GenerationDevtoolsBridge,
1172
+ VideoDevtoolsBridge,
1173
+ createAIDevtoolsGenerationPreview,
1174
+ createChatDevtoolsBridge,
1175
+ createGenerationDevtoolsBridge,
1176
+ createVideoDevtoolsBridge
1177
+ };
1178
+ //# sourceMappingURL=devtools.js.map