@copilotkit/web-inspector 1.61.1 → 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 +1241 -209
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +107 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1241 -211
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1397 -267
- package/dist/index.umd.js.map +1 -1
- package/dist/lib/context-helpers.cjs.map +1 -1
- package/dist/lib/context-helpers.mjs.map +1 -1
- package/dist/lib/persistence.cjs.map +1 -1
- package/dist/lib/persistence.mjs.map +1 -1
- package/dist/lib/telemetry.cjs +78 -12
- package/dist/lib/telemetry.cjs.map +1 -1
- package/dist/lib/telemetry.mjs +72 -13
- package/dist/lib/telemetry.mjs.map +1 -1
- package/dist/lib/types.d.cts +8 -0
- package/dist/lib/types.d.cts.map +1 -0
- package/dist/lib/types.d.mts +8 -0
- package/dist/lib/types.d.mts.map +1 -0
- package/dist/package.cjs +12 -0
- package/dist/package.cjs.map +1 -0
- package/dist/package.mjs +6 -0
- package/dist/package.mjs.map +1 -0
- package/package.json +4 -2
- package/src/__tests__/web-inspector.spec.ts +929 -11
- package/src/index.ts +1650 -318
- package/src/lib/__tests__/telemetry.test.ts +150 -10
- package/src/lib/context-helpers.ts +1 -1
- package/src/lib/persistence.ts +1 -1
- package/src/lib/telemetry.ts +162 -18
|
@@ -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,12 +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 }>;
|
|
349
|
+
fetchMessages: (threadId: string) => Promise<void>;
|
|
324
350
|
fetchEvents: (threadId: string) => Promise<void>;
|
|
325
351
|
fetchState: (threadId: string) => Promise<void>;
|
|
352
|
+
renderTimeline: () => unknown;
|
|
326
353
|
renderConversation: () => unknown;
|
|
327
354
|
renderState: () => unknown;
|
|
328
355
|
renderEvents: () => unknown;
|
|
356
|
+
timelineItemsFromEvents: (
|
|
357
|
+
events: Array<Record<string, unknown>>,
|
|
358
|
+
) => Array<Record<string, unknown>>;
|
|
329
359
|
};
|
|
330
360
|
|
|
331
361
|
function createThreadDetails(): {
|
|
@@ -340,6 +370,35 @@ function createThreadDetails(): {
|
|
|
340
370
|
return { el, internals };
|
|
341
371
|
}
|
|
342
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
|
+
|
|
343
402
|
describe("ɵCpkThreadDetails caching", () => {
|
|
344
403
|
beforeEach(() => {
|
|
345
404
|
document.body.innerHTML = "";
|
|
@@ -363,16 +422,25 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
363
422
|
await el.updateComplete;
|
|
364
423
|
}
|
|
365
424
|
|
|
366
|
-
it("threadId change drops
|
|
425
|
+
it("threadId change drops template and timeline item caches", async () => {
|
|
367
426
|
const { el, internals } = createThreadDetails();
|
|
368
427
|
await settleThread(el, internals, "t1");
|
|
369
428
|
|
|
370
429
|
// Hand-build cache entries for all three panels so we don't have to
|
|
371
430
|
// drive every render path through the DOM. The presence of any entry
|
|
372
431
|
// is what the assertion below checks for; what they hold is irrelevant.
|
|
373
|
-
internals._panelTplCache.set("
|
|
374
|
-
internals._panelTplCache.set("
|
|
375
|
-
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();
|
|
376
444
|
|
|
377
445
|
// Switch to thread t2 — the threadId branch in `updated()` should
|
|
378
446
|
// empty the cache map.
|
|
@@ -380,6 +448,7 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
380
448
|
await el.updateComplete;
|
|
381
449
|
|
|
382
450
|
expect(internals._panelTplCache.size).toBe(0);
|
|
451
|
+
expect(internals._timelineItemsCache).toBeNull();
|
|
383
452
|
});
|
|
384
453
|
|
|
385
454
|
it("does not fetch messages, events, or state when threadInspectionAvailable is omitted", async () => {
|
|
@@ -406,6 +475,53 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
406
475
|
}
|
|
407
476
|
});
|
|
408
477
|
|
|
478
|
+
it("joins thread inspection URLs without double slashes when runtimeUrl has a trailing slash", async () => {
|
|
479
|
+
const fetchSpy = vi
|
|
480
|
+
.spyOn(globalThis, "fetch")
|
|
481
|
+
.mockImplementation(async (input) => {
|
|
482
|
+
const url = String(input);
|
|
483
|
+
if (url.endsWith("/messages")) {
|
|
484
|
+
return new Response(JSON.stringify({ messages: [] }), {
|
|
485
|
+
status: 200,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (url.endsWith("/events")) {
|
|
489
|
+
return new Response(JSON.stringify({ events: [] }), { status: 200 });
|
|
490
|
+
}
|
|
491
|
+
return new Response(JSON.stringify({ state: null }), { status: 200 });
|
|
492
|
+
});
|
|
493
|
+
try {
|
|
494
|
+
const { el, internals } = createThreadDetails();
|
|
495
|
+
internals.runtimeUrl = "http://localhost:4000/api/";
|
|
496
|
+
internals.threadInspectionAvailable = true;
|
|
497
|
+
internals.threadId = "thread one";
|
|
498
|
+
await el.updateComplete;
|
|
499
|
+
fetchSpy.mockClear();
|
|
500
|
+
|
|
501
|
+
await internals.fetchMessages("thread one");
|
|
502
|
+
await internals.fetchEvents("thread one");
|
|
503
|
+
await internals.fetchState("thread one");
|
|
504
|
+
|
|
505
|
+
const requestedUrls = fetchSpy.mock.calls.map((call) => String(call[0]));
|
|
506
|
+
expect(requestedUrls).toContain(
|
|
507
|
+
"http://localhost:4000/api/threads/thread%20one/messages",
|
|
508
|
+
);
|
|
509
|
+
expect(requestedUrls).toContain(
|
|
510
|
+
"http://localhost:4000/api/threads/thread%20one/events",
|
|
511
|
+
);
|
|
512
|
+
expect(requestedUrls).toContain(
|
|
513
|
+
"http://localhost:4000/api/threads/thread%20one/state",
|
|
514
|
+
);
|
|
515
|
+
expect(
|
|
516
|
+
requestedUrls.every(
|
|
517
|
+
(url) => !url.includes("/api//threads/") && !url.includes("one//"),
|
|
518
|
+
),
|
|
519
|
+
).toBe(true);
|
|
520
|
+
} finally {
|
|
521
|
+
fetchSpy.mockRestore();
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
409
525
|
it("conversation cache invalidates when _conversation is reassigned", async () => {
|
|
410
526
|
const { el, internals } = createThreadDetails();
|
|
411
527
|
await settleThread(el, internals, "t1");
|
|
@@ -415,7 +531,7 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
415
531
|
];
|
|
416
532
|
|
|
417
533
|
const tplA = internals.renderConversation();
|
|
418
|
-
expect(internals._panelTplCache.get("
|
|
534
|
+
expect(internals._panelTplCache.get("timeline-fallback")?.tpl).toBe(tplA);
|
|
419
535
|
|
|
420
536
|
// Cache hit: same array reference, same expand sets — same TemplateResult.
|
|
421
537
|
expect(internals.renderConversation()).toBe(tplA);
|
|
@@ -430,7 +546,7 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
430
546
|
|
|
431
547
|
const tplB = internals.renderConversation();
|
|
432
548
|
expect(tplB).not.toBe(tplA);
|
|
433
|
-
expect(internals._panelTplCache.get("
|
|
549
|
+
expect(internals._panelTplCache.get("timeline-fallback")?.tpl).toBe(tplB);
|
|
434
550
|
});
|
|
435
551
|
|
|
436
552
|
it("conversation cache invalidates when expand state changes", async () => {
|
|
@@ -469,12 +585,95 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
469
585
|
expect(internals.renderConversation()).not.toBe(expanded);
|
|
470
586
|
});
|
|
471
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
|
+
|
|
472
664
|
it("state and events caches invalidate when their fetched data is reassigned", async () => {
|
|
473
665
|
const { el, internals } = createThreadDetails();
|
|
474
666
|
await settleThread(el, internals, "t1");
|
|
475
667
|
|
|
476
668
|
internals._fetchedState = { foo: "bar" };
|
|
477
|
-
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
|
+
];
|
|
478
677
|
|
|
479
678
|
const stateA = internals.renderState();
|
|
480
679
|
const eventsA = internals.renderEvents();
|
|
@@ -483,13 +682,487 @@ describe("ɵCpkThreadDetails caching", () => {
|
|
|
483
682
|
|
|
484
683
|
// Reassign both — fresh references after a refetch.
|
|
485
684
|
internals._fetchedState = { foo: "baz" };
|
|
486
|
-
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
|
+
];
|
|
487
693
|
|
|
488
694
|
expect(internals.renderState()).not.toBe(stateA);
|
|
489
695
|
expect(internals.renderEvents()).not.toBe(eventsA);
|
|
490
696
|
});
|
|
491
697
|
});
|
|
492
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
|
+
|
|
493
1166
|
// ─────────────────────────────────────────────────────────────────────────
|
|
494
1167
|
// Announcement preview (popout) dismissal MUST persist
|
|
495
1168
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -616,6 +1289,7 @@ type HeaderMockCore = {
|
|
|
616
1289
|
agents: Record<string, AbstractAgent>;
|
|
617
1290
|
context: Record<string, unknown>;
|
|
618
1291
|
properties: Record<string, unknown>;
|
|
1292
|
+
telemetryDisabled: boolean;
|
|
619
1293
|
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
|
|
620
1294
|
runtimeUrl: string;
|
|
621
1295
|
headers: Record<string, string>;
|
|
@@ -637,12 +1311,15 @@ type HeaderMockCore = {
|
|
|
637
1311
|
function createHeaderMockCore(
|
|
638
1312
|
agents: Record<string, AbstractAgent>,
|
|
639
1313
|
headers: Record<string, string>,
|
|
1314
|
+
endpointOverrides: Partial<HeaderMockCore["threadEndpoints"]> = {},
|
|
1315
|
+
telemetryDisabled = true,
|
|
640
1316
|
) {
|
|
641
1317
|
const subscribers = new Set<CopilotKitCoreSubscriber>();
|
|
642
1318
|
const core: HeaderMockCore = {
|
|
643
1319
|
agents,
|
|
644
1320
|
context: {},
|
|
645
1321
|
properties: {},
|
|
1322
|
+
telemetryDisabled,
|
|
646
1323
|
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
647
1324
|
runtimeUrl: "http://localhost/api",
|
|
648
1325
|
headers,
|
|
@@ -651,6 +1328,7 @@ function createHeaderMockCore(
|
|
|
651
1328
|
inspect: true,
|
|
652
1329
|
mutations: true,
|
|
653
1330
|
realtimeMetadata: true,
|
|
1331
|
+
...endpointOverrides,
|
|
654
1332
|
},
|
|
655
1333
|
subscribe(subscriber: CopilotKitCoreSubscriber) {
|
|
656
1334
|
subscribers.add(subscriber);
|
|
@@ -693,6 +1371,21 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
|
|
|
693
1371
|
fetchMock.mock.calls.filter((call) =>
|
|
694
1372
|
String(call[0]).includes("/threads?"),
|
|
695
1373
|
);
|
|
1374
|
+
const telemetryPosts = () =>
|
|
1375
|
+
fetchMock.mock.calls
|
|
1376
|
+
.filter(
|
|
1377
|
+
(call) =>
|
|
1378
|
+
String(call[0]) === "https://telemetry.copilotkit.ai/ingest" &&
|
|
1379
|
+
(call[1] as RequestInit | undefined)?.method === "POST",
|
|
1380
|
+
)
|
|
1381
|
+
.map((call) => {
|
|
1382
|
+
const body =
|
|
1383
|
+
((call[1] as RequestInit | undefined)?.body as string) ?? "{}";
|
|
1384
|
+
return JSON.parse(body) as {
|
|
1385
|
+
event: string;
|
|
1386
|
+
properties: Record<string, unknown>;
|
|
1387
|
+
};
|
|
1388
|
+
});
|
|
696
1389
|
|
|
697
1390
|
beforeEach(() => {
|
|
698
1391
|
document.body.innerHTML = "";
|
|
@@ -758,4 +1451,229 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
|
|
|
758
1451
|
"X-CSRF": "2",
|
|
759
1452
|
});
|
|
760
1453
|
});
|
|
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
|
+
|
|
1524
|
+
it("shows the locked Intelligence state when thread listing is unavailable without fetching threads", async () => {
|
|
1525
|
+
const { agent } = createMockAgent("alpha");
|
|
1526
|
+
const harness = createHeaderMockCore(
|
|
1527
|
+
{ alpha: agent },
|
|
1528
|
+
{ "X-CSRF": "1" },
|
|
1529
|
+
{ list: false },
|
|
1530
|
+
true,
|
|
1531
|
+
);
|
|
1532
|
+
|
|
1533
|
+
const inspector = new WebInspectorElement();
|
|
1534
|
+
document.body.appendChild(inspector);
|
|
1535
|
+
inspector.core = harness.core as unknown as WebInspectorElement["core"];
|
|
1536
|
+
harness.emitAgentsChanged();
|
|
1537
|
+
|
|
1538
|
+
const internals = inspector as unknown as {
|
|
1539
|
+
isOpen: boolean;
|
|
1540
|
+
handleMenuSelect: (key: "threads") => void;
|
|
1541
|
+
};
|
|
1542
|
+
internals.isOpen = true;
|
|
1543
|
+
internals.handleMenuSelect("threads");
|
|
1544
|
+
await inspector.updateComplete;
|
|
1545
|
+
|
|
1546
|
+
const text = inspector.shadowRoot?.textContent ?? "";
|
|
1547
|
+
expect(text).toMatch(/Enable Intelligence to inspect Threads\./);
|
|
1548
|
+
expect(text).toContain("Talk to an Engineer");
|
|
1549
|
+
expect(text).toContain("Sign up for Intelligence");
|
|
1550
|
+
const ctaLabels = Array.from(
|
|
1551
|
+
inspector.shadowRoot?.querySelectorAll<HTMLAnchorElement>("a") ?? [],
|
|
1552
|
+
).map((anchor) => anchor.textContent?.trim());
|
|
1553
|
+
expect(ctaLabels).toEqual([
|
|
1554
|
+
"Talk to an Engineer",
|
|
1555
|
+
"Sign up for Intelligence",
|
|
1556
|
+
]);
|
|
1557
|
+
expect(text).not.toContain("No threads yet");
|
|
1558
|
+
expect(
|
|
1559
|
+
fetchMock.mock.calls.some((call) => String(call[0]).includes("/threads")),
|
|
1560
|
+
).toBe(false);
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
it("adds ref and posthog distinct ID attribution to locked-state CTAs", async () => {
|
|
1564
|
+
const { agent } = createMockAgent("alpha");
|
|
1565
|
+
const harness = createHeaderMockCore(
|
|
1566
|
+
{ alpha: agent },
|
|
1567
|
+
{},
|
|
1568
|
+
{ list: false },
|
|
1569
|
+
false,
|
|
1570
|
+
);
|
|
1571
|
+
|
|
1572
|
+
const inspector = new WebInspectorElement();
|
|
1573
|
+
document.body.appendChild(inspector);
|
|
1574
|
+
inspector.core = harness.core as unknown as WebInspectorElement["core"];
|
|
1575
|
+
harness.emitAgentsChanged();
|
|
1576
|
+
|
|
1577
|
+
const internals = inspector as unknown as {
|
|
1578
|
+
isOpen: boolean;
|
|
1579
|
+
handleMenuSelect: (key: "threads") => void;
|
|
1580
|
+
};
|
|
1581
|
+
internals.isOpen = true;
|
|
1582
|
+
internals.handleMenuSelect("threads");
|
|
1583
|
+
await inspector.updateComplete;
|
|
1584
|
+
|
|
1585
|
+
const signup = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
|
|
1586
|
+
'a[href^="https://go.copilotkit.ai/intelligence-signup"]',
|
|
1587
|
+
);
|
|
1588
|
+
const engineer = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
|
|
1589
|
+
'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
|
|
1590
|
+
);
|
|
1591
|
+
|
|
1592
|
+
expect(signup).not.toBeNull();
|
|
1593
|
+
expect(engineer).not.toBeNull();
|
|
1594
|
+
|
|
1595
|
+
const signupUrl = new URL(signup!.href);
|
|
1596
|
+
expect(signupUrl.searchParams.get("ref")).toBe("cpk-inspector");
|
|
1597
|
+
const distinctId = signupUrl.searchParams.get("posthog_distinct_id");
|
|
1598
|
+
expect(distinctId).toMatch(/^[0-9a-f-]{36}$/);
|
|
1599
|
+
|
|
1600
|
+
const engineerUrl = new URL(engineer!.href);
|
|
1601
|
+
expect(engineerUrl.origin).toBe("https://www.copilotkit.ai");
|
|
1602
|
+
expect(engineerUrl.pathname).toBe("/talk-to-an-engineer");
|
|
1603
|
+
expect(engineerUrl.searchParams.get("ref")).toBe("cpk-inspector-threads");
|
|
1604
|
+
expect(engineerUrl.searchParams.get("posthog_distinct_id")).toBe(
|
|
1605
|
+
distinctId,
|
|
1606
|
+
);
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
it("tracks Threads tab clicks through the rendered inspector menu", async () => {
|
|
1610
|
+
const { agent } = createMockAgent("alpha");
|
|
1611
|
+
const harness = createHeaderMockCore(
|
|
1612
|
+
{ alpha: agent },
|
|
1613
|
+
{},
|
|
1614
|
+
{ list: false },
|
|
1615
|
+
false,
|
|
1616
|
+
);
|
|
1617
|
+
|
|
1618
|
+
const inspector = new WebInspectorElement();
|
|
1619
|
+
document.body.appendChild(inspector);
|
|
1620
|
+
inspector.core = harness.core as unknown as WebInspectorElement["core"];
|
|
1621
|
+
harness.emitAgentsChanged();
|
|
1622
|
+
|
|
1623
|
+
const internals = inspector as unknown as { isOpen: boolean };
|
|
1624
|
+
internals.isOpen = true;
|
|
1625
|
+
inspector.requestUpdate();
|
|
1626
|
+
await inspector.updateComplete;
|
|
1627
|
+
|
|
1628
|
+
const threadsButton = Array.from(
|
|
1629
|
+
inspector.shadowRoot?.querySelectorAll<HTMLButtonElement>("button") ?? [],
|
|
1630
|
+
).find((button) => button.textContent?.trim() === "Threads");
|
|
1631
|
+
expect(threadsButton, "Threads menu button should render").toBeDefined();
|
|
1632
|
+
|
|
1633
|
+
threadsButton!.click();
|
|
1634
|
+
await inspector.updateComplete;
|
|
1635
|
+
await Promise.resolve();
|
|
1636
|
+
|
|
1637
|
+
const threadsTabClick = telemetryPosts().find(
|
|
1638
|
+
(post) => post.event === "oss.inspector.threads_tab_clicked",
|
|
1639
|
+
);
|
|
1640
|
+
expect(threadsTabClick).toBeDefined();
|
|
1641
|
+
expect(threadsTabClick!.properties).toMatchObject({
|
|
1642
|
+
intelligence_status: "intelligence_not_enabled",
|
|
1643
|
+
thread_service_status: "unavailable",
|
|
1644
|
+
telemetry_disabled: false,
|
|
1645
|
+
});
|
|
1646
|
+
expect(threadsTabClick!.properties.distinct_id).toMatch(/^[0-9a-f-]{36}$/);
|
|
1647
|
+
if (threadsTabClick!.properties.posthog_distinct_id !== undefined) {
|
|
1648
|
+
expect(threadsTabClick!.properties.posthog_distinct_id).toBe(
|
|
1649
|
+
threadsTabClick!.properties.distinct_id,
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
it("keeps the enabled empty Threads state when thread listing is available", async () => {
|
|
1655
|
+
const { agent } = createMockAgent("alpha");
|
|
1656
|
+
const harness = createHeaderMockCore({ alpha: agent }, {}, {}, true);
|
|
1657
|
+
|
|
1658
|
+
const inspector = new WebInspectorElement();
|
|
1659
|
+
document.body.appendChild(inspector);
|
|
1660
|
+
inspector.core = harness.core as unknown as WebInspectorElement["core"];
|
|
1661
|
+
harness.emitAgentsChanged();
|
|
1662
|
+
|
|
1663
|
+
const internals = inspector as unknown as {
|
|
1664
|
+
isOpen: boolean;
|
|
1665
|
+
handleMenuSelect: (key: "threads") => void;
|
|
1666
|
+
};
|
|
1667
|
+
internals.isOpen = true;
|
|
1668
|
+
internals.handleMenuSelect("threads");
|
|
1669
|
+
await inspector.updateComplete;
|
|
1670
|
+
|
|
1671
|
+
expect(inspector.shadowRoot?.textContent ?? "").toContain("No threads yet");
|
|
1672
|
+
const engineer = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
|
|
1673
|
+
'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
|
|
1674
|
+
);
|
|
1675
|
+
expect(engineer?.href).toBe(
|
|
1676
|
+
"https://www.copilotkit.ai/talk-to-an-engineer?ref=cpk-inspector-threads",
|
|
1677
|
+
);
|
|
1678
|
+
});
|
|
761
1679
|
});
|