@copilotkit/web-inspector 1.61.0 → 1.61.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/web-inspector",
3
- "version": "1.61.0",
3
+ "version": "1.61.1",
4
4
  "description": "Lit-based web component for the CopilotKit web inspector",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  "lit": "^3.2.0",
28
28
  "lucide": "^0.525.0",
29
29
  "marked": "^12.0.2",
30
- "@copilotkit/core": "1.61.0"
30
+ "@copilotkit/core": "1.61.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@tailwindcss/cli": "^4.1.11",
@@ -306,6 +306,9 @@ describe("WebInspectorElement", () => {
306
306
 
307
307
  type ThreadDetailsInternals = {
308
308
  threadId: string | null;
309
+ runtimeUrl: string;
310
+ headers: Record<string, string>;
311
+ threadInspectionAvailable: boolean;
309
312
  liveMessageVersion: number;
310
313
  _conversation: Array<Record<string, unknown>>;
311
314
  _fetchedState: Record<string, unknown> | null;
@@ -318,6 +321,8 @@ type ThreadDetailsInternals = {
318
321
  _loadingState: boolean;
319
322
  _loadingEvents: boolean;
320
323
  _panelTplCache: Map<string, { key: readonly unknown[]; tpl: unknown }>;
324
+ fetchEvents: (threadId: string) => Promise<void>;
325
+ fetchState: (threadId: string) => Promise<void>;
321
326
  renderConversation: () => unknown;
322
327
  renderState: () => unknown;
323
328
  renderEvents: () => unknown;
@@ -377,6 +382,30 @@ describe("ɵCpkThreadDetails caching", () => {
377
382
  expect(internals._panelTplCache.size).toBe(0);
378
383
  });
379
384
 
385
+ it("does not fetch messages, events, or state when threadInspectionAvailable is omitted", async () => {
386
+ const fetchSpy = vi
387
+ .spyOn(globalThis, "fetch")
388
+ .mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
389
+ try {
390
+ const { el, internals } = createThreadDetails();
391
+
392
+ internals.runtimeUrl = "http://localhost:4000";
393
+ internals.headers = { Authorization: "Bearer test-token" };
394
+ internals.threadId = "t1";
395
+ await el.updateComplete;
396
+
397
+ expect(internals.threadInspectionAvailable).toBe(false);
398
+ expect(fetchSpy).not.toHaveBeenCalled();
399
+
400
+ await internals.fetchEvents("t1");
401
+ await internals.fetchState("t1");
402
+
403
+ expect(fetchSpy).not.toHaveBeenCalled();
404
+ } finally {
405
+ fetchSpy.mockRestore();
406
+ }
407
+ });
408
+
380
409
  it("conversation cache invalidates when _conversation is reassigned", async () => {
381
410
  const { el, internals } = createThreadDetails();
382
411
  await settleThread(el, internals, "t1");
@@ -575,3 +604,158 @@ describe("WebInspectorElement announcement preview dismissal", () => {
575
604
  expect(a.hasUnseenAnnouncement).toBe(true);
576
605
  });
577
606
  });
607
+
608
+ // --- Owned thread store header forwarding (issue #5581) ---
609
+ //
610
+ // When useThreads() isn't mounted, the inspector creates its own thread store
611
+ // per agent (ensureOwnedThreadStore). That store's /threads requests must carry
612
+ // the headers configured on <CopilotKit> (e.g. X-CSRF / auth), otherwise the
613
+ // requests 403 in environments that enforce CSRF/auth checks.
614
+
615
+ type HeaderMockCore = {
616
+ agents: Record<string, AbstractAgent>;
617
+ context: Record<string, unknown>;
618
+ properties: Record<string, unknown>;
619
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
620
+ runtimeUrl: string;
621
+ headers: Record<string, string>;
622
+ threadEndpoints: {
623
+ list: boolean;
624
+ inspect: boolean;
625
+ mutations: boolean;
626
+ realtimeMetadata: boolean;
627
+ };
628
+ subscribe: (subscriber: CopilotKitCoreSubscriber) => {
629
+ unsubscribe: () => void;
630
+ };
631
+ getThreadStores: () => Record<string, never>;
632
+ getThreadStore: (agentId: string) => undefined;
633
+ registerThreadStore: (agentId: string, store: unknown) => void;
634
+ unregisterThreadStore: (agentId: string) => void;
635
+ };
636
+
637
+ function createHeaderMockCore(
638
+ agents: Record<string, AbstractAgent>,
639
+ headers: Record<string, string>,
640
+ ) {
641
+ const subscribers = new Set<CopilotKitCoreSubscriber>();
642
+ const core: HeaderMockCore = {
643
+ agents,
644
+ context: {},
645
+ properties: {},
646
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
647
+ runtimeUrl: "http://localhost/api",
648
+ headers,
649
+ threadEndpoints: {
650
+ list: true,
651
+ inspect: true,
652
+ mutations: true,
653
+ realtimeMetadata: true,
654
+ },
655
+ subscribe(subscriber: CopilotKitCoreSubscriber) {
656
+ subscribers.add(subscriber);
657
+ return { unsubscribe: () => subscribers.delete(subscriber) };
658
+ },
659
+ getThreadStores() {
660
+ return {};
661
+ },
662
+ getThreadStore() {
663
+ return undefined;
664
+ },
665
+ registerThreadStore() {},
666
+ unregisterThreadStore() {},
667
+ };
668
+
669
+ const asCore = () => core as unknown as CopilotKitCore;
670
+ return {
671
+ core,
672
+ emitAgentsChanged() {
673
+ subscribers.forEach((s) =>
674
+ s.onAgentsChanged?.({ copilotkit: asCore(), agents: core.agents }),
675
+ );
676
+ },
677
+ emitHeadersChanged(nextHeaders: Record<string, string>) {
678
+ core.headers = nextHeaders;
679
+ subscribers.forEach((s) =>
680
+ s.onHeadersChanged?.({ copilotkit: asCore(), headers: nextHeaders }),
681
+ );
682
+ },
683
+ };
684
+ }
685
+
686
+ const headersOf = (call: unknown[]) =>
687
+ (call[1] as { headers?: Record<string, string> } | undefined)?.headers ?? {};
688
+
689
+ describe("WebInspectorElement owned thread store headers (#5581)", () => {
690
+ let fetchMock: ReturnType<typeof vi.fn>;
691
+
692
+ const threadListCalls = () =>
693
+ fetchMock.mock.calls.filter((call) =>
694
+ String(call[0]).includes("/threads?"),
695
+ );
696
+
697
+ beforeEach(() => {
698
+ document.body.innerHTML = "";
699
+ fetchMock = vi.fn(() =>
700
+ Promise.resolve({
701
+ ok: true,
702
+ status: 200,
703
+ json: () => Promise.resolve({ threads: [] }),
704
+ }),
705
+ );
706
+ // The owned store captures globalThis.fetch when it's created, so stub
707
+ // before the inspector attaches to the core.
708
+ vi.stubGlobal("fetch", fetchMock);
709
+ });
710
+
711
+ afterEach(() => {
712
+ vi.unstubAllGlobals();
713
+ });
714
+
715
+ it("forwards core headers on the owned store's /threads request", async () => {
716
+ const { agent } = createMockAgent("alpha");
717
+ const harness = createHeaderMockCore(
718
+ { alpha: agent },
719
+ { "X-CSRF": "1", Authorization: "Bearer abc" },
720
+ );
721
+
722
+ const inspector = new WebInspectorElement();
723
+ document.body.appendChild(inspector);
724
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
725
+ harness.emitAgentsChanged();
726
+
727
+ await vi.waitFor(() => {
728
+ expect(threadListCalls().length).toBeGreaterThan(0);
729
+ });
730
+
731
+ expect(headersOf(threadListCalls()[0]!)).toMatchObject({
732
+ "X-CSRF": "1",
733
+ Authorization: "Bearer abc",
734
+ });
735
+ });
736
+
737
+ it("re-applies headers on the owned store when core headers change", async () => {
738
+ const { agent } = createMockAgent("alpha");
739
+ const harness = createHeaderMockCore({ alpha: agent }, { "X-CSRF": "1" });
740
+
741
+ const inspector = new WebInspectorElement();
742
+ document.body.appendChild(inspector);
743
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
744
+ harness.emitAgentsChanged();
745
+
746
+ await vi.waitFor(() => {
747
+ expect(threadListCalls().length).toBeGreaterThan(0);
748
+ });
749
+ const callsBefore = threadListCalls().length;
750
+
751
+ harness.emitHeadersChanged({ "X-CSRF": "2" });
752
+
753
+ await vi.waitFor(() => {
754
+ expect(threadListCalls().length).toBeGreaterThan(callsBefore);
755
+ });
756
+
757
+ expect(headersOf(threadListCalls().at(-1)!)).toMatchObject({
758
+ "X-CSRF": "2",
759
+ });
760
+ });
761
+ });
package/src/index.ts CHANGED
@@ -674,6 +674,7 @@ export class ɵCpkThreadDetails extends LitElement {
674
674
  thread: { attribute: false },
675
675
  runtimeUrl: { attribute: false },
676
676
  headers: { attribute: false },
677
+ threadInspectionAvailable: { attribute: false },
677
678
  agentStateInput: { attribute: false },
678
679
  agentEventsInput: { attribute: false },
679
680
  liveMessageVersion: { attribute: false },
@@ -701,6 +702,7 @@ export class ɵCpkThreadDetails extends LitElement {
701
702
  thread: ɵThread | null = null;
702
703
  runtimeUrl = "";
703
704
  headers: Record<string, string> = {};
705
+ threadInspectionAvailable = false;
704
706
  agentStateInput: Record<string, unknown> | null = null;
705
707
  agentEventsInput: ApiAgentEvent[] = [];
706
708
  /**
@@ -1456,7 +1458,7 @@ export class ɵCpkThreadDetails extends LitElement {
1456
1458
  threadId: string,
1457
1459
  silent: boolean = false,
1458
1460
  ): Promise<void> {
1459
- if (!this.runtimeUrl) {
1461
+ if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1460
1462
  if (!silent) this._conversation = [];
1461
1463
  return;
1462
1464
  }
@@ -1494,7 +1496,7 @@ export class ɵCpkThreadDetails extends LitElement {
1494
1496
 
1495
1497
  private async fetchEvents(threadId: string): Promise<void> {
1496
1498
  this._eventsNotAvailable = false;
1497
- if (!this.runtimeUrl) {
1499
+ if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1498
1500
  this._fetchedEvents = null;
1499
1501
  return;
1500
1502
  }
@@ -1541,7 +1543,7 @@ export class ɵCpkThreadDetails extends LitElement {
1541
1543
 
1542
1544
  private async fetchState(threadId: string): Promise<void> {
1543
1545
  this._stateNotAvailable = false;
1544
- if (!this.runtimeUrl) {
1546
+ if (!this.runtimeUrl || !this.threadInspectionAvailable) {
1545
1547
  this._fetchedState = null;
1546
1548
  return;
1547
1549
  }
@@ -2585,12 +2587,14 @@ export class WebInspectorElement extends LitElement {
2585
2587
  if (this.core?.getThreadStore(agentId)) return;
2586
2588
  const core = this.core;
2587
2589
  if (!core?.runtimeUrl) return;
2590
+ if (core.threadEndpoints?.list === false) return;
2588
2591
 
2589
2592
  const store = ɵcreateThreadStore({ fetch: globalThis.fetch });
2590
2593
  store.start();
2591
2594
  store.setContext({
2592
2595
  runtimeUrl: core.runtimeUrl,
2593
- headers: {},
2596
+ headers: { ...core.headers },
2597
+ wsUrl: core.intelligence?.wsUrl,
2594
2598
  agentId,
2595
2599
  });
2596
2600
  this._ownedThreadStores.set(agentId, store);
@@ -2609,6 +2613,24 @@ export class WebInspectorElement extends LitElement {
2609
2613
  store.refresh();
2610
2614
  }
2611
2615
 
2616
+ // Keep inspector-owned thread stores in sync when the host updates headers
2617
+ // at runtime (e.g. a refreshed auth/CSRF token via core.setHeaders). Mirrors
2618
+ // useThreads(), which re-dispatches the context whenever core.headers change,
2619
+ // so the owned stores' /threads requests stay authorized.
2620
+ private updateOwnedThreadStoreHeaders(
2621
+ headers: Readonly<Record<string, string>>,
2622
+ ): void {
2623
+ const core = this.core;
2624
+ if (!core?.runtimeUrl) return;
2625
+ for (const [agentId, store] of this._ownedThreadStores) {
2626
+ store.setContext({
2627
+ runtimeUrl: core.runtimeUrl,
2628
+ headers: { ...headers },
2629
+ agentId,
2630
+ });
2631
+ }
2632
+ }
2633
+
2612
2634
  private removeOwnedThreadStore(agentId: string): void {
2613
2635
  const store = this._ownedThreadStores.get(agentId);
2614
2636
  if (!store) return;
@@ -2639,8 +2661,12 @@ export class WebInspectorElement extends LitElement {
2639
2661
  maybeShowDisclosure();
2640
2662
  }
2641
2663
  this.flushPendingBannerViewed();
2642
- for (const agentId of this._ownedThreadStores.keys()) {
2643
- this.refreshOwnedThreadStore(agentId);
2664
+ if (core.threadEndpoints?.list !== false) {
2665
+ for (const agentId of this._ownedThreadStores.keys()) {
2666
+ this.refreshOwnedThreadStore(agentId);
2667
+ }
2668
+ } else {
2669
+ this.teardownOwnedThreadStores();
2644
2670
  }
2645
2671
  } else {
2646
2672
  // Clear stale thread data immediately when the server goes away
@@ -2653,6 +2679,9 @@ export class WebInspectorElement extends LitElement {
2653
2679
  this.coreProperties = properties;
2654
2680
  this.requestUpdate();
2655
2681
  },
2682
+ onHeadersChanged: ({ headers }) => {
2683
+ this.updateOwnedThreadStoreHeaders(headers);
2684
+ },
2656
2685
  onError: ({ code, error }) => {
2657
2686
  this.lastCoreError = { code, message: error.message };
2658
2687
  this.requestUpdate();
@@ -5855,6 +5884,9 @@ ${argsString}</pre
5855
5884
  .thread=${selectedThread}
5856
5885
  .runtimeUrl=${this._core?.runtimeUrl ?? ""}
5857
5886
  .headers=${this._core?.headers ?? {}}
5887
+ .threadInspectionAvailable=${
5888
+ this._core?.threadEndpoints?.inspect !== false
5889
+ }
5858
5890
  .liveMessageVersion=${
5859
5891
  this.selectedThreadId
5860
5892
  ? (this.liveMessageVersion.get(this.selectedThreadId) ??