@copilotkit/web-inspector 1.61.2 → 1.62.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 +21 -0
- package/dev/css-raw-import.spec.ts +31 -0
- package/dev/index.html +115 -0
- package/dev/main.ts +165 -0
- package/dev/vite.config.ts +30 -0
- package/dist/index.cjs +752 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +90 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +751 -137
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +807 -163
- package/dist/index.umd.js.map +1 -1
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/package.json +4 -2
- package/src/__tests__/web-inspector.spec.ts +717 -13
- package/src/index.ts +1017 -193
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CpkThreadInspector,
|
|
3
|
+
WebInspectorElement,
|
|
4
|
+
ɵCpkThreadDetails,
|
|
5
|
+
} from "../index.js";
|
|
6
|
+
import type { ThreadDebuggerProvider } from "../index.js";
|
|
2
7
|
import type { CopilotKitCore } from "@copilotkit/core";
|
|
3
8
|
import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
|
|
4
9
|
import type { CopilotKitCoreSubscriber } from "@copilotkit/core";
|
|
@@ -16,6 +21,18 @@ type InspectorInternals = {
|
|
|
16
21
|
cachedTools: Array<{ name: string }>;
|
|
17
22
|
};
|
|
18
23
|
|
|
24
|
+
type InspectorThreadViewInternals = {
|
|
25
|
+
isOpen: boolean;
|
|
26
|
+
selectedMenu: "threads";
|
|
27
|
+
selectedThreadId: string | null;
|
|
28
|
+
_threads: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
name?: string | null;
|
|
31
|
+
agentId: string;
|
|
32
|
+
updatedAt?: string | null;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
19
36
|
type InspectorContextInternals = {
|
|
20
37
|
contextStore: Record<string, { description?: string; value: unknown }>;
|
|
21
38
|
copyContextValue: (value: unknown, id: string) => Promise<void>;
|
|
@@ -295,7 +312,7 @@ describe("WebInspectorElement", () => {
|
|
|
295
312
|
// CpkThreadDetails — per-panel TemplateResult cache invariants
|
|
296
313
|
// ─────────────────────────────────────────────────────────────────────────
|
|
297
314
|
//
|
|
298
|
-
// The
|
|
315
|
+
// The timeline / state / raw-events panels each cache the rendered
|
|
299
316
|
// TemplateResult by reference of the underlying data so tab switches don't
|
|
300
317
|
// re-iterate over hundreds of items. These tests pin down the cache-key
|
|
301
318
|
// contract: when the data reassigns, the cache MUST drop, and when the
|
|
@@ -310,9 +327,16 @@ type ThreadDetailsInternals = {
|
|
|
310
327
|
headers: Record<string, string>;
|
|
311
328
|
threadInspectionAvailable: boolean;
|
|
312
329
|
liveMessageVersion: number;
|
|
330
|
+
provider: ThreadDebuggerProvider | null;
|
|
331
|
+
_fetchedMetadata: Record<string, unknown> | null;
|
|
313
332
|
_conversation: Array<Record<string, unknown>>;
|
|
333
|
+
agentEventsInput: Array<Record<string, unknown>>;
|
|
314
334
|
_fetchedState: Record<string, unknown> | null;
|
|
315
|
-
_fetchedEvents: Array<unknown
|
|
335
|
+
_fetchedEvents: Array<Record<string, unknown>> | null;
|
|
336
|
+
_timelineItemsCache: {
|
|
337
|
+
events: Array<Record<string, unknown>>;
|
|
338
|
+
items: Array<Record<string, unknown>>;
|
|
339
|
+
} | null;
|
|
316
340
|
_expandedTools: Set<string>;
|
|
317
341
|
_expandedMessages: Set<string>;
|
|
318
342
|
_stateNotAvailable: boolean;
|
|
@@ -320,13 +344,18 @@ type ThreadDetailsInternals = {
|
|
|
320
344
|
_loadingMessages: boolean;
|
|
321
345
|
_loadingState: boolean;
|
|
322
346
|
_loadingEvents: boolean;
|
|
347
|
+
activeTimelineItems: Array<Record<string, unknown>>;
|
|
323
348
|
_panelTplCache: Map<string, { key: readonly unknown[]; tpl: unknown }>;
|
|
324
349
|
fetchMessages: (threadId: string) => Promise<void>;
|
|
325
350
|
fetchEvents: (threadId: string) => Promise<void>;
|
|
326
351
|
fetchState: (threadId: string) => Promise<void>;
|
|
352
|
+
renderTimeline: () => unknown;
|
|
327
353
|
renderConversation: () => unknown;
|
|
328
354
|
renderState: () => unknown;
|
|
329
355
|
renderEvents: () => unknown;
|
|
356
|
+
timelineItemsFromEvents: (
|
|
357
|
+
events: Array<Record<string, unknown>>,
|
|
358
|
+
) => Array<Record<string, unknown>>;
|
|
330
359
|
};
|
|
331
360
|
|
|
332
361
|
function createThreadDetails(): {
|
|
@@ -341,6 +370,35 @@ function createThreadDetails(): {
|
|
|
341
370
|
return { el, internals };
|
|
342
371
|
}
|
|
343
372
|
|
|
373
|
+
function createThreadInspector(): {
|
|
374
|
+
el: CpkThreadInspector;
|
|
375
|
+
internals: ThreadDetailsInternals;
|
|
376
|
+
} {
|
|
377
|
+
const el = new CpkThreadInspector();
|
|
378
|
+
document.body.appendChild(el);
|
|
379
|
+
const internals = el as unknown as ThreadDetailsInternals;
|
|
380
|
+
return { el, internals };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function flushProviderWork(el: CpkThreadInspector): Promise<void> {
|
|
384
|
+
await Promise.resolve();
|
|
385
|
+
await el.updateComplete;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function createDeferred<T>(): {
|
|
389
|
+
promise: Promise<T>;
|
|
390
|
+
resolve: (value: T) => void;
|
|
391
|
+
reject: (reason?: unknown) => void;
|
|
392
|
+
} {
|
|
393
|
+
let resolve!: (value: T) => void;
|
|
394
|
+
let reject!: (reason?: unknown) => void;
|
|
395
|
+
const promise = new Promise<T>((res, rej) => {
|
|
396
|
+
resolve = res;
|
|
397
|
+
reject = rej;
|
|
398
|
+
});
|
|
399
|
+
return { promise, resolve, reject };
|
|
400
|
+
}
|
|
401
|
+
|
|
344
402
|
describe("ɵCpkThreadDetails caching", () => {
|
|
345
403
|
beforeEach(() => {
|
|
346
404
|
document.body.innerHTML = "";
|
|
@@ -364,16 +422,25 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
364
422
|
await el.updateComplete;
|
|
365
423
|
}
|
|
366
424
|
|
|
367
|
-
it("threadId change drops
|
|
425
|
+
it("threadId change drops template and timeline item caches", async () => {
|
|
368
426
|
const { el, internals } = createThreadDetails();
|
|
369
427
|
await settleThread(el, internals, "t1");
|
|
370
428
|
|
|
371
429
|
// Hand-build cache entries for all three panels so we don't have to
|
|
372
430
|
// drive every render path through the DOM. The presence of any entry
|
|
373
431
|
// is what the assertion below checks for; what they hold is irrelevant.
|
|
374
|
-
internals._panelTplCache.set("
|
|
375
|
-
internals._panelTplCache.set("
|
|
376
|
-
internals._panelTplCache.set("
|
|
432
|
+
internals._panelTplCache.set("timeline", { key: [], tpl: "c" });
|
|
433
|
+
internals._panelTplCache.set("state", { key: [], tpl: "s" });
|
|
434
|
+
internals._panelTplCache.set("raw-events", { key: [], tpl: "e" });
|
|
435
|
+
internals.agentEventsInput = [
|
|
436
|
+
{
|
|
437
|
+
type: "RUN_STARTED",
|
|
438
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
439
|
+
payload: {},
|
|
440
|
+
},
|
|
441
|
+
];
|
|
442
|
+
internals.renderTimeline();
|
|
443
|
+
expect(internals._timelineItemsCache).not.toBeNull();
|
|
377
444
|
|
|
378
445
|
// Switch to thread t2 — the threadId branch in `updated()` should
|
|
379
446
|
// empty the cache map.
|
|
@@ -381,6 +448,7 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
381
448
|
await el.updateComplete;
|
|
382
449
|
|
|
383
450
|
expect(internals._panelTplCache.size).toBe(0);
|
|
451
|
+
expect(internals._timelineItemsCache).toBeNull();
|
|
384
452
|
});
|
|
385
453
|
|
|
386
454
|
it("does not fetch messages, events, or state when threadInspectionAvailable is omitted", async () => {
|
|
@@ -434,11 +502,21 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
434
502
|
await internals.fetchEvents("thread one");
|
|
435
503
|
await internals.fetchState("thread one");
|
|
436
504
|
|
|
437
|
-
|
|
505
|
+
const requestedUrls = fetchSpy.mock.calls.map((call) => String(call[0]));
|
|
506
|
+
expect(requestedUrls).toContain(
|
|
438
507
|
"http://localhost:4000/api/threads/thread%20one/messages",
|
|
508
|
+
);
|
|
509
|
+
expect(requestedUrls).toContain(
|
|
439
510
|
"http://localhost:4000/api/threads/thread%20one/events",
|
|
511
|
+
);
|
|
512
|
+
expect(requestedUrls).toContain(
|
|
440
513
|
"http://localhost:4000/api/threads/thread%20one/state",
|
|
441
|
-
|
|
514
|
+
);
|
|
515
|
+
expect(
|
|
516
|
+
requestedUrls.every(
|
|
517
|
+
(url) => !url.includes("/api//threads/") && !url.includes("one//"),
|
|
518
|
+
),
|
|
519
|
+
).toBe(true);
|
|
442
520
|
} finally {
|
|
443
521
|
fetchSpy.mockRestore();
|
|
444
522
|
}
|
|
@@ -453,7 +531,7 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
453
531
|
];
|
|
454
532
|
|
|
455
533
|
const tplA = internals.renderConversation();
|
|
456
|
-
expect(internals._panelTplCache.get("
|
|
534
|
+
expect(internals._panelTplCache.get("timeline-fallback")?.tpl).toBe(tplA);
|
|
457
535
|
|
|
458
536
|
// Cache hit: same array reference, same expand sets — same TemplateResult.
|
|
459
537
|
expect(internals.renderConversation()).toBe(tplA);
|
|
@@ -468,7 +546,7 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
468
546
|
|
|
469
547
|
const tplB = internals.renderConversation();
|
|
470
548
|
expect(tplB).not.toBe(tplA);
|
|
471
|
-
expect(internals._panelTplCache.get("
|
|
549
|
+
expect(internals._panelTplCache.get("timeline-fallback")?.tpl).toBe(tplB);
|
|
472
550
|
});
|
|
473
551
|
|
|
474
552
|
it("conversation cache invalidates when expand state changes", async () => {
|
|
@@ -507,12 +585,95 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
507
585
|
expect(internals.renderConversation()).not.toBe(expanded);
|
|
508
586
|
});
|
|
509
587
|
|
|
588
|
+
it("keeps timeline and message fallback template caches separate", async () => {
|
|
589
|
+
const { el, internals } = createThreadDetails();
|
|
590
|
+
await settleThread(el, internals, "t1");
|
|
591
|
+
|
|
592
|
+
const events = [
|
|
593
|
+
{
|
|
594
|
+
type: "RUN_STARTED",
|
|
595
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
596
|
+
payload: {},
|
|
597
|
+
sourceIndex: 1,
|
|
598
|
+
},
|
|
599
|
+
];
|
|
600
|
+
internals._fetchedEvents = events;
|
|
601
|
+
|
|
602
|
+
const timelineTpl = internals.renderTimeline();
|
|
603
|
+
expect(internals._panelTplCache.get("timeline")?.tpl).toBe(timelineTpl);
|
|
604
|
+
|
|
605
|
+
internals._fetchedEvents = [];
|
|
606
|
+
internals._conversation = [
|
|
607
|
+
{ id: "m1", type: "user", content: "fallback", createdAt: "" },
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
const fallbackTpl = internals.renderTimeline();
|
|
611
|
+
expect(fallbackTpl).not.toBe(timelineTpl);
|
|
612
|
+
expect(internals._panelTplCache.get("timeline")?.tpl).toBe(timelineTpl);
|
|
613
|
+
expect(internals._panelTplCache.get("timeline-fallback")?.tpl).toBe(
|
|
614
|
+
fallbackTpl,
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
internals._fetchedEvents = events;
|
|
618
|
+
expect(internals.renderTimeline()).toBe(timelineTpl);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("does not recompute timeline items when renderTimeline returns a cached template", async () => {
|
|
622
|
+
const { el, internals } = createThreadDetails();
|
|
623
|
+
await settleThread(el, internals, "t1");
|
|
624
|
+
|
|
625
|
+
internals._fetchedEvents = [
|
|
626
|
+
{
|
|
627
|
+
type: "RUN_STARTED",
|
|
628
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
629
|
+
payload: {},
|
|
630
|
+
sourceIndex: 1,
|
|
631
|
+
},
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
const normalizeSpy = vi.spyOn(internals, "timelineItemsFromEvents");
|
|
635
|
+
|
|
636
|
+
const timelineTpl = internals.renderTimeline();
|
|
637
|
+
|
|
638
|
+
expect(internals.renderTimeline()).toBe(timelineTpl);
|
|
639
|
+
expect(normalizeSpy).toHaveBeenCalledTimes(1);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("keeps live fallback event references stable for timeline and raw event caches", async () => {
|
|
643
|
+
const { el, internals } = createThreadDetails();
|
|
644
|
+
await settleThread(el, internals, "t1");
|
|
645
|
+
|
|
646
|
+
internals.agentEventsInput = [
|
|
647
|
+
{
|
|
648
|
+
type: "RUN_STARTED",
|
|
649
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
650
|
+
payload: {},
|
|
651
|
+
},
|
|
652
|
+
];
|
|
653
|
+
|
|
654
|
+
const normalizeSpy = vi.spyOn(internals, "timelineItemsFromEvents");
|
|
655
|
+
|
|
656
|
+
const timelineTpl = internals.renderTimeline();
|
|
657
|
+
const eventsTpl = internals.renderEvents();
|
|
658
|
+
|
|
659
|
+
expect(internals.renderTimeline()).toBe(timelineTpl);
|
|
660
|
+
expect(internals.renderEvents()).toBe(eventsTpl);
|
|
661
|
+
expect(normalizeSpy).toHaveBeenCalledTimes(1);
|
|
662
|
+
});
|
|
663
|
+
|
|
510
664
|
it("state and events caches invalidate when their fetched data is reassigned", async () => {
|
|
511
665
|
const { el, internals } = createThreadDetails();
|
|
512
666
|
await settleThread(el, internals, "t1");
|
|
513
667
|
|
|
514
668
|
internals._fetchedState = { foo: "bar" };
|
|
515
|
-
internals._fetchedEvents = [
|
|
669
|
+
internals._fetchedEvents = [
|
|
670
|
+
{
|
|
671
|
+
type: "RUN_STARTED",
|
|
672
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
673
|
+
payload: {},
|
|
674
|
+
sourceIndex: 1,
|
|
675
|
+
},
|
|
676
|
+
];
|
|
516
677
|
|
|
517
678
|
const stateA = internals.renderState();
|
|
518
679
|
const eventsA = internals.renderEvents();
|
|
@@ -521,13 +682,487 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
521
682
|
|
|
522
683
|
// Reassign both — fresh references after a refetch.
|
|
523
684
|
internals._fetchedState = { foo: "baz" };
|
|
524
|
-
internals._fetchedEvents = [
|
|
685
|
+
internals._fetchedEvents = [
|
|
686
|
+
{
|
|
687
|
+
type: "RUN_FINISHED",
|
|
688
|
+
timestamp: "2026-06-25T10:00:01.000Z",
|
|
689
|
+
payload: {},
|
|
690
|
+
sourceIndex: 1,
|
|
691
|
+
},
|
|
692
|
+
];
|
|
525
693
|
|
|
526
694
|
expect(internals.renderState()).not.toBe(stateA);
|
|
527
695
|
expect(internals.renderEvents()).not.toBe(eventsA);
|
|
528
696
|
});
|
|
529
697
|
});
|
|
530
698
|
|
|
699
|
+
describe("CpkThreadInspector provider contract", () => {
|
|
700
|
+
beforeEach(() => {
|
|
701
|
+
document.body.innerHTML = "";
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
afterEach(() => {
|
|
705
|
+
vi.unstubAllGlobals();
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("registers the stable custom element and renders a normalized provider-backed timeline", async () => {
|
|
709
|
+
expect(customElements.get("cpk-thread-inspector")).toBe(CpkThreadInspector);
|
|
710
|
+
|
|
711
|
+
const provider: ThreadDebuggerProvider = {
|
|
712
|
+
getThreadMetadata: vi.fn().mockResolvedValue({
|
|
713
|
+
id: "thread-1234567890",
|
|
714
|
+
agentId: "agent-a",
|
|
715
|
+
endUserId: "user-a",
|
|
716
|
+
status: "active",
|
|
717
|
+
createdAt: "2026-06-25T10:00:00.000Z",
|
|
718
|
+
updatedAt: "2026-06-25T10:00:01.000Z",
|
|
719
|
+
}),
|
|
720
|
+
getMessages: vi.fn().mockResolvedValue([]),
|
|
721
|
+
getEvents: vi.fn().mockResolvedValue([
|
|
722
|
+
{
|
|
723
|
+
type: "RUN_STARTED",
|
|
724
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
725
|
+
payload: { runId: "run-1" },
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
type: "TEXT_MESSAGE_START",
|
|
729
|
+
timestamp: "2026-06-25T10:00:00.100Z",
|
|
730
|
+
payload: { messageId: "m1", role: "assistant" },
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
734
|
+
timestamp: "2026-06-25T10:00:00.200Z",
|
|
735
|
+
payload: { messageId: "m1", delta: "hello from events" },
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
type: "TOOL_CALL_START",
|
|
739
|
+
timestamp: "2026-06-25T10:00:00.300Z",
|
|
740
|
+
payload: { toolCallId: "tc1", toolCallName: "lookup_docs" },
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
type: "TOOL_CALL_ARGS",
|
|
744
|
+
timestamp: "2026-06-25T10:00:00.400Z",
|
|
745
|
+
payload: { toolCallId: "tc1", delta: '{"broken":' },
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
type: "TOOL_CALL_END",
|
|
749
|
+
timestamp: "2026-06-25T10:00:00.500Z",
|
|
750
|
+
payload: { toolCallId: "tc1" },
|
|
751
|
+
},
|
|
752
|
+
]),
|
|
753
|
+
};
|
|
754
|
+
const { el, internals } = createThreadInspector();
|
|
755
|
+
|
|
756
|
+
internals.provider = provider;
|
|
757
|
+
internals.threadId = "thread-1234567890";
|
|
758
|
+
await flushProviderWork(el);
|
|
759
|
+
|
|
760
|
+
expect(provider.getThreadMetadata).toHaveBeenCalledWith(
|
|
761
|
+
"thread-1234567890",
|
|
762
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
763
|
+
);
|
|
764
|
+
expect(provider.getEvents).toHaveBeenCalledWith(
|
|
765
|
+
"thread-1234567890",
|
|
766
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
767
|
+
);
|
|
768
|
+
expect(provider.getMessages).not.toHaveBeenCalled();
|
|
769
|
+
expect(internals._fetchedMetadata?.agentId).toBe("agent-a");
|
|
770
|
+
expect(internals._fetchedEvents).toHaveLength(6);
|
|
771
|
+
|
|
772
|
+
const text = el.shadowRoot?.textContent ?? "";
|
|
773
|
+
expect(text).toContain("Timeline");
|
|
774
|
+
expect(text).toContain("Raw AG-UI Events");
|
|
775
|
+
expect(text).toContain("State");
|
|
776
|
+
expect(text).toContain("Run started");
|
|
777
|
+
expect(text).toContain("assistant message");
|
|
778
|
+
expect(text).toContain("hello from events");
|
|
779
|
+
expect(text).toContain("lookup_docs");
|
|
780
|
+
expect(text).toContain("Could not decode tool call arguments");
|
|
781
|
+
expect(text).toContain("Source event #1");
|
|
782
|
+
expect(text).toContain("Source event #6");
|
|
783
|
+
expect(text).toContain("agent-a");
|
|
784
|
+
expect(text).toContain("user-a");
|
|
785
|
+
expect(text).not.toContain("Rename");
|
|
786
|
+
expect(text).not.toContain("Archive");
|
|
787
|
+
expect(text).not.toContain("Delete");
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("source-event references reveal raw events and state stays lazy-loaded", async () => {
|
|
791
|
+
const provider: ThreadDebuggerProvider = {
|
|
792
|
+
getMessages: vi.fn().mockResolvedValue([]),
|
|
793
|
+
getEvents: vi.fn().mockResolvedValue([
|
|
794
|
+
{
|
|
795
|
+
type: "RUN_STARTED",
|
|
796
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
797
|
+
payload: { runId: "run-1" },
|
|
798
|
+
},
|
|
799
|
+
]),
|
|
800
|
+
getState: vi.fn().mockResolvedValue({ step: 1 }),
|
|
801
|
+
};
|
|
802
|
+
const { el, internals } = createThreadInspector();
|
|
803
|
+
|
|
804
|
+
internals.provider = provider;
|
|
805
|
+
internals.threadId = "thread-1";
|
|
806
|
+
await flushProviderWork(el);
|
|
807
|
+
|
|
808
|
+
expect(provider.getEvents).toHaveBeenCalledTimes(1);
|
|
809
|
+
expect(provider.getState).not.toHaveBeenCalled();
|
|
810
|
+
expect(internals._fetchedEvents?.[0]).toMatchObject({
|
|
811
|
+
type: "RUN_STARTED",
|
|
812
|
+
payload: { runId: "run-1" },
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
el.shadowRoot
|
|
816
|
+
?.querySelector<HTMLButtonElement>(".cpk-td__source-link")
|
|
817
|
+
?.click();
|
|
818
|
+
await flushProviderWork(el);
|
|
819
|
+
|
|
820
|
+
expect(
|
|
821
|
+
el.shadowRoot?.querySelector<HTMLElement>(
|
|
822
|
+
'.cpk-td__event[data-source-index="1"]',
|
|
823
|
+
),
|
|
824
|
+
).not.toBeNull();
|
|
825
|
+
expect(provider.getEvents).toHaveBeenCalledTimes(1);
|
|
826
|
+
|
|
827
|
+
el.shadowRoot
|
|
828
|
+
?.querySelectorAll<HTMLButtonElement>('[role="tab"]')[2]
|
|
829
|
+
?.click();
|
|
830
|
+
await flushProviderWork(el);
|
|
831
|
+
|
|
832
|
+
expect(provider.getState).toHaveBeenCalledWith(
|
|
833
|
+
"thread-1",
|
|
834
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
835
|
+
);
|
|
836
|
+
expect(internals._fetchedState).toEqual({ step: 1 });
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("reloads when a provider arrives after threadId and when provider identity swaps for the same thread", async () => {
|
|
840
|
+
const firstEvents =
|
|
841
|
+
createDeferred<
|
|
842
|
+
Awaited<ReturnType<NonNullable<ThreadDebuggerProvider["getEvents"]>>>
|
|
843
|
+
>();
|
|
844
|
+
const firstSignals: AbortSignal[] = [];
|
|
845
|
+
const firstProvider: ThreadDebuggerProvider = {
|
|
846
|
+
getEvents: vi.fn((_threadId, options) => {
|
|
847
|
+
firstSignals.push(options.signal);
|
|
848
|
+
return firstEvents.promise;
|
|
849
|
+
}),
|
|
850
|
+
};
|
|
851
|
+
const secondProvider: ThreadDebuggerProvider = {
|
|
852
|
+
getEvents: vi.fn().mockResolvedValue([
|
|
853
|
+
{
|
|
854
|
+
type: "RUN_FINISHED",
|
|
855
|
+
timestamp: "2026-06-25T10:00:01.000Z",
|
|
856
|
+
payload: { runId: "second-run" },
|
|
857
|
+
},
|
|
858
|
+
]),
|
|
859
|
+
};
|
|
860
|
+
const { el, internals } = createThreadInspector();
|
|
861
|
+
|
|
862
|
+
internals.threadId = "thread-1";
|
|
863
|
+
await flushProviderWork(el);
|
|
864
|
+
|
|
865
|
+
expect(firstProvider.getEvents).not.toHaveBeenCalled();
|
|
866
|
+
|
|
867
|
+
internals.provider = firstProvider;
|
|
868
|
+
await flushProviderWork(el);
|
|
869
|
+
|
|
870
|
+
expect(firstProvider.getEvents).toHaveBeenCalledWith(
|
|
871
|
+
"thread-1",
|
|
872
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
internals.provider = secondProvider;
|
|
876
|
+
await flushProviderWork(el);
|
|
877
|
+
|
|
878
|
+
expect(firstSignals[0]?.aborted).toBe(true);
|
|
879
|
+
await vi.waitFor(() => {
|
|
880
|
+
expect(secondProvider.getEvents).toHaveBeenCalledWith(
|
|
881
|
+
"thread-1",
|
|
882
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
883
|
+
);
|
|
884
|
+
expect(internals._fetchedEvents?.[0]?.type).toBe("RUN_FINISHED");
|
|
885
|
+
expect(internals._fetchedEvents?.[0]?.payload).toEqual({
|
|
886
|
+
runId: "second-run",
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
firstEvents.resolve([
|
|
891
|
+
{
|
|
892
|
+
type: "RUN_STARTED",
|
|
893
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
894
|
+
payload: { runId: "stale-run" },
|
|
895
|
+
},
|
|
896
|
+
]);
|
|
897
|
+
await Promise.resolve();
|
|
898
|
+
|
|
899
|
+
expect(internals._fetchedEvents?.[0]?.payload).toEqual({
|
|
900
|
+
runId: "second-run",
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("renders raw AG-UI events with top-level fields preserved when payload is present", async () => {
|
|
905
|
+
const provider: ThreadDebuggerProvider = {
|
|
906
|
+
getEvents: vi.fn().mockResolvedValue([
|
|
907
|
+
{
|
|
908
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
909
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
910
|
+
payload: { messageId: "m1", delta: "hello" },
|
|
911
|
+
runId: "top-level-run",
|
|
912
|
+
sequence: 42,
|
|
913
|
+
},
|
|
914
|
+
]),
|
|
915
|
+
};
|
|
916
|
+
const { el, internals } = createThreadInspector();
|
|
917
|
+
|
|
918
|
+
internals.provider = provider;
|
|
919
|
+
internals.threadId = "thread-raw";
|
|
920
|
+
await flushProviderWork(el);
|
|
921
|
+
|
|
922
|
+
expect(internals._fetchedEvents?.[0]?.payload).toEqual({
|
|
923
|
+
messageId: "m1",
|
|
924
|
+
delta: "hello",
|
|
925
|
+
});
|
|
926
|
+
expect(internals._fetchedEvents?.[0]?.rawEvent).toMatchObject({
|
|
927
|
+
runId: "top-level-run",
|
|
928
|
+
sequence: 42,
|
|
929
|
+
payload: { messageId: "m1", delta: "hello" },
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
el.shadowRoot
|
|
933
|
+
?.querySelector<HTMLButtonElement>(".cpk-td__source-link")
|
|
934
|
+
?.click();
|
|
935
|
+
await flushProviderWork(el);
|
|
936
|
+
|
|
937
|
+
const text = el.shadowRoot?.textContent ?? "";
|
|
938
|
+
expect(text).toContain("top-level-run");
|
|
939
|
+
expect(text).toContain("sequence");
|
|
940
|
+
expect(text).toContain("messageId");
|
|
941
|
+
expect(text).toContain("hello");
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it("ignores stale runtime 501 availability results after the selected thread changes", async () => {
|
|
945
|
+
const t1Events = createDeferred<Response>();
|
|
946
|
+
const t1State = createDeferred<Response>();
|
|
947
|
+
const t2Events = createDeferred<Response>();
|
|
948
|
+
const t2State = createDeferred<Response>();
|
|
949
|
+
const fetchMock = vi.fn((url: string) => {
|
|
950
|
+
if (url.endsWith("/threads/t1/events")) return t1Events.promise;
|
|
951
|
+
if (url.endsWith("/threads/t1/state")) return t1State.promise;
|
|
952
|
+
if (url.endsWith("/threads/t2/events")) return t2Events.promise;
|
|
953
|
+
if (url.endsWith("/threads/t2/state")) return t2State.promise;
|
|
954
|
+
if (url.endsWith("/threads/t2/messages")) {
|
|
955
|
+
return Promise.resolve(
|
|
956
|
+
new Response(JSON.stringify({ messages: [] }), { status: 200 }),
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
return Promise.reject(new Error(`Unexpected URL ${url}`));
|
|
960
|
+
});
|
|
961
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
962
|
+
const { el, internals } = createThreadInspector();
|
|
963
|
+
|
|
964
|
+
internals.runtimeUrl = "http://runtime";
|
|
965
|
+
internals.threadInspectionAvailable = true;
|
|
966
|
+
internals.threadId = "t1";
|
|
967
|
+
await flushProviderWork(el);
|
|
968
|
+
const t1StatePromise = internals.fetchState("t1");
|
|
969
|
+
|
|
970
|
+
internals.threadId = "t2";
|
|
971
|
+
await flushProviderWork(el);
|
|
972
|
+
const t2StatePromise = internals.fetchState("t2");
|
|
973
|
+
|
|
974
|
+
t1Events.resolve(new Response(null, { status: 501 }));
|
|
975
|
+
t1State.resolve(new Response(null, { status: 501 }));
|
|
976
|
+
t2Events.resolve(
|
|
977
|
+
new Response(
|
|
978
|
+
JSON.stringify({
|
|
979
|
+
events: [
|
|
980
|
+
{
|
|
981
|
+
type: "RUN_STARTED",
|
|
982
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
983
|
+
payload: { runId: "fresh" },
|
|
984
|
+
},
|
|
985
|
+
],
|
|
986
|
+
}),
|
|
987
|
+
{ status: 200 },
|
|
988
|
+
),
|
|
989
|
+
);
|
|
990
|
+
t2State.resolve(
|
|
991
|
+
new Response(JSON.stringify({ state: { current: "fresh" } }), {
|
|
992
|
+
status: 200,
|
|
993
|
+
}),
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
await Promise.all([t1StatePromise, t2StatePromise]);
|
|
997
|
+
await vi.waitFor(() => {
|
|
998
|
+
expect(internals._eventsNotAvailable).toBe(false);
|
|
999
|
+
expect(internals._stateNotAvailable).toBe(false);
|
|
1000
|
+
expect(internals._fetchedEvents?.[0]?.payload).toEqual({
|
|
1001
|
+
runId: "fresh",
|
|
1002
|
+
});
|
|
1003
|
+
expect(internals._fetchedState).toEqual({ current: "fresh" });
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it("refetches runtime thread data when headers change for the same thread", async () => {
|
|
1008
|
+
const fetchMock = vi.fn(
|
|
1009
|
+
(_url: string, init?: { headers?: Record<string, string> }) =>
|
|
1010
|
+
Promise.resolve(
|
|
1011
|
+
new Response(
|
|
1012
|
+
JSON.stringify({
|
|
1013
|
+
events: [
|
|
1014
|
+
{
|
|
1015
|
+
type: "RUN_STARTED",
|
|
1016
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
1017
|
+
payload: {
|
|
1018
|
+
auth: init?.headers?.Authorization,
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
}),
|
|
1023
|
+
{ status: 200 },
|
|
1024
|
+
),
|
|
1025
|
+
),
|
|
1026
|
+
);
|
|
1027
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1028
|
+
const { el, internals } = createThreadInspector();
|
|
1029
|
+
|
|
1030
|
+
internals.runtimeUrl = "http://runtime";
|
|
1031
|
+
internals.threadInspectionAvailable = true;
|
|
1032
|
+
internals.headers = { Authorization: "Bearer first" };
|
|
1033
|
+
internals.threadId = "thread-1";
|
|
1034
|
+
await flushProviderWork(el);
|
|
1035
|
+
|
|
1036
|
+
await vi.waitFor(() => {
|
|
1037
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1038
|
+
expect(internals._fetchedEvents?.[0]?.payload).toEqual({
|
|
1039
|
+
auth: "Bearer first",
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
internals.headers = { Authorization: "Bearer second" };
|
|
1044
|
+
await flushProviderWork(el);
|
|
1045
|
+
|
|
1046
|
+
await vi.waitFor(() => {
|
|
1047
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
1048
|
+
expect(internals._fetchedEvents?.[0]?.payload).toEqual({
|
|
1049
|
+
auth: "Bearer second",
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
expect(headersOf(fetchMock.mock.calls.at(-1)!)).toMatchObject({
|
|
1053
|
+
Authorization: "Bearer second",
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it("loads runtime messages when event history is empty so timeline fallback and counts work", async () => {
|
|
1058
|
+
const fetchMock = vi.fn((url: string) => {
|
|
1059
|
+
if (url.endsWith("/threads/thread-1/events")) {
|
|
1060
|
+
return Promise.resolve(
|
|
1061
|
+
new Response(JSON.stringify({ events: [] }), { status: 200 }),
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
if (url.endsWith("/threads/thread-1/messages")) {
|
|
1065
|
+
return Promise.resolve(
|
|
1066
|
+
new Response(
|
|
1067
|
+
JSON.stringify({
|
|
1068
|
+
messages: [
|
|
1069
|
+
{ id: "u1", role: "user", content: "historical hello" },
|
|
1070
|
+
{ id: "a1", role: "assistant", content: "historical reply" },
|
|
1071
|
+
],
|
|
1072
|
+
}),
|
|
1073
|
+
{ status: 200 },
|
|
1074
|
+
),
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
return Promise.reject(new Error(`Unexpected URL ${url}`));
|
|
1078
|
+
});
|
|
1079
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
1080
|
+
const { el, internals } = createThreadInspector();
|
|
1081
|
+
|
|
1082
|
+
internals.runtimeUrl = "http://runtime";
|
|
1083
|
+
internals.threadInspectionAvailable = true;
|
|
1084
|
+
internals.threadId = "thread-1";
|
|
1085
|
+
await flushProviderWork(el);
|
|
1086
|
+
|
|
1087
|
+
await vi.waitFor(() => {
|
|
1088
|
+
expect(internals._fetchedEvents).toEqual([]);
|
|
1089
|
+
expect(internals._conversation).toHaveLength(2);
|
|
1090
|
+
expect(el.shadowRoot?.textContent ?? "").toContain("historical hello");
|
|
1091
|
+
expect(el.shadowRoot?.textContent ?? "").toContain("historical reply");
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
el.shadowRoot
|
|
1095
|
+
?.querySelector<HTMLButtonElement>(".cpk-td__panel-toggle")
|
|
1096
|
+
?.click();
|
|
1097
|
+
await flushProviderWork(el);
|
|
1098
|
+
|
|
1099
|
+
expect(el.shadowRoot?.textContent ?? "").toContain("Messages");
|
|
1100
|
+
expect(el.shadowRoot?.textContent ?? "").toContain("2");
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
it("renders unsupported raw events as timeline rows instead of leaving the first tab empty", async () => {
|
|
1104
|
+
const provider: ThreadDebuggerProvider = {
|
|
1105
|
+
getEvents: vi.fn().mockResolvedValue([
|
|
1106
|
+
{
|
|
1107
|
+
type: "THREAD_STATE_WRITTEN",
|
|
1108
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
1109
|
+
payload: { checkpointId: "state-1", status: "ok" },
|
|
1110
|
+
},
|
|
1111
|
+
]),
|
|
1112
|
+
};
|
|
1113
|
+
const { el, internals } = createThreadInspector();
|
|
1114
|
+
|
|
1115
|
+
internals.provider = provider;
|
|
1116
|
+
internals.threadId = "thread-raw-event-only";
|
|
1117
|
+
await flushProviderWork(el);
|
|
1118
|
+
|
|
1119
|
+
expect(internals.activeTimelineItems).toHaveLength(1);
|
|
1120
|
+
expect(el.shadowRoot?.textContent ?? "").toContain("THREAD_STATE_WRITTEN");
|
|
1121
|
+
expect(el.shadowRoot?.textContent ?? "").toContain("checkpointId");
|
|
1122
|
+
expect(el.shadowRoot?.textContent ?? "").toContain("Source event #1");
|
|
1123
|
+
expect(el.shadowRoot?.textContent ?? "").not.toContain(
|
|
1124
|
+
"No timeline events captured",
|
|
1125
|
+
);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it("keeps the first-visible timeline intentional while provider message fallback is loading", async () => {
|
|
1129
|
+
const messages =
|
|
1130
|
+
createDeferred<
|
|
1131
|
+
Awaited<ReturnType<NonNullable<ThreadDebuggerProvider["getMessages"]>>>
|
|
1132
|
+
>();
|
|
1133
|
+
const provider: ThreadDebuggerProvider = {
|
|
1134
|
+
getEvents: vi.fn().mockResolvedValue([]),
|
|
1135
|
+
getMessages: vi.fn().mockReturnValue(messages.promise),
|
|
1136
|
+
};
|
|
1137
|
+
const { el, internals } = createThreadInspector();
|
|
1138
|
+
|
|
1139
|
+
internals.provider = provider;
|
|
1140
|
+
internals.threadId = "thread-messages-only";
|
|
1141
|
+
await flushProviderWork(el);
|
|
1142
|
+
|
|
1143
|
+
expect(provider.getMessages).toHaveBeenCalledWith(
|
|
1144
|
+
"thread-messages-only",
|
|
1145
|
+
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
1146
|
+
);
|
|
1147
|
+
expect(internals._loadingMessages).toBe(true);
|
|
1148
|
+
expect(el.shadowRoot?.textContent ?? "").toContain("Loading messages");
|
|
1149
|
+
expect(el.shadowRoot?.textContent ?? "").not.toContain(
|
|
1150
|
+
"No timeline events captured",
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
messages.resolve([
|
|
1154
|
+
{ id: "u1", role: "user", content: "provider hello" },
|
|
1155
|
+
{ id: "a1", role: "assistant", content: "provider reply" },
|
|
1156
|
+
]);
|
|
1157
|
+
|
|
1158
|
+
await vi.waitFor(() => {
|
|
1159
|
+
const text = el.shadowRoot?.textContent ?? "";
|
|
1160
|
+
expect(text).toContain("provider hello");
|
|
1161
|
+
expect(text).toContain("provider reply");
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
|
|
531
1166
|
// ─────────────────────────────────────────────────────────────────────────
|
|
532
1167
|
// Announcement preview (popout) dismissal MUST persist
|
|
533
1168
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -817,6 +1452,75 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
|
|
|
817
1452
|
});
|
|
818
1453
|
});
|
|
819
1454
|
|
|
1455
|
+
it("rerenders selected thread details so core header changes refetch events with new headers", async () => {
|
|
1456
|
+
fetchMock.mockImplementation(
|
|
1457
|
+
(url: string, init?: { headers?: Record<string, string> }) => {
|
|
1458
|
+
if (url.endsWith("/threads/thread-1/events")) {
|
|
1459
|
+
return Promise.resolve(
|
|
1460
|
+
new Response(
|
|
1461
|
+
JSON.stringify({
|
|
1462
|
+
events: [
|
|
1463
|
+
{
|
|
1464
|
+
type: "RUN_STARTED",
|
|
1465
|
+
timestamp: "2026-06-25T10:00:00.000Z",
|
|
1466
|
+
payload: { csrf: init?.headers?.["X-CSRF"] },
|
|
1467
|
+
},
|
|
1468
|
+
],
|
|
1469
|
+
}),
|
|
1470
|
+
{ status: 200 },
|
|
1471
|
+
),
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
if (url.includes("/threads?")) {
|
|
1475
|
+
return Promise.resolve(
|
|
1476
|
+
new Response(JSON.stringify({ threads: [] }), { status: 200 }),
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
return Promise.reject(new Error(`Unexpected URL ${url}`));
|
|
1480
|
+
},
|
|
1481
|
+
);
|
|
1482
|
+
const harness = createHeaderMockCore({}, { "X-CSRF": "1" });
|
|
1483
|
+
const inspector = new WebInspectorElement();
|
|
1484
|
+
document.body.appendChild(inspector);
|
|
1485
|
+
inspector.core = harness.core as unknown as WebInspectorElement["core"];
|
|
1486
|
+
|
|
1487
|
+
const internals = inspector as unknown as InspectorThreadViewInternals;
|
|
1488
|
+
internals.isOpen = true;
|
|
1489
|
+
internals.selectedMenu = "threads";
|
|
1490
|
+
internals.selectedThreadId = "thread-1";
|
|
1491
|
+
internals._threads = [
|
|
1492
|
+
{
|
|
1493
|
+
id: "thread-1",
|
|
1494
|
+
name: "Thread 1",
|
|
1495
|
+
agentId: "alpha",
|
|
1496
|
+
updatedAt: "2026-06-25T10:00:00.000Z",
|
|
1497
|
+
},
|
|
1498
|
+
];
|
|
1499
|
+
inspector.requestUpdate();
|
|
1500
|
+
await inspector.updateComplete;
|
|
1501
|
+
|
|
1502
|
+
await vi.waitFor(() => {
|
|
1503
|
+
expect(
|
|
1504
|
+
fetchMock.mock.calls.filter((call) =>
|
|
1505
|
+
String(call[0]).endsWith("/threads/thread-1/events"),
|
|
1506
|
+
),
|
|
1507
|
+
).toHaveLength(1);
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
harness.emitHeadersChanged({ "X-CSRF": "2" });
|
|
1511
|
+
|
|
1512
|
+
await vi.waitFor(() => {
|
|
1513
|
+
expect(
|
|
1514
|
+
fetchMock.mock.calls.filter((call) =>
|
|
1515
|
+
String(call[0]).endsWith("/threads/thread-1/events"),
|
|
1516
|
+
),
|
|
1517
|
+
).toHaveLength(2);
|
|
1518
|
+
});
|
|
1519
|
+
expect(headersOf(fetchMock.mock.calls.at(-1)!)).toMatchObject({
|
|
1520
|
+
"X-CSRF": "2",
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
1523
|
+
|
|
820
1524
|
it("shows the locked Intelligence state when thread listing is unavailable without fetching threads", async () => {
|
|
821
1525
|
const { agent } = createMockAgent("alpha");
|
|
822
1526
|
const harness = createHeaderMockCore(
|