@copilotkit/web-inspector 1.61.1 → 1.61.2

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.
@@ -1,4 +1,4 @@
1
- import { WebInspectorElement, ɵCpkThreadDetails } from "../index";
1
+ import { WebInspectorElement, ɵCpkThreadDetails } from "../index.js";
2
2
  import type { CopilotKitCore } from "@copilotkit/core";
3
3
  import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
4
4
  import type { CopilotKitCoreSubscriber } from "@copilotkit/core";
@@ -321,6 +321,7 @@ type ThreadDetailsInternals = {
321
321
  _loadingState: boolean;
322
322
  _loadingEvents: boolean;
323
323
  _panelTplCache: Map<string, { key: readonly unknown[]; tpl: unknown }>;
324
+ fetchMessages: (threadId: string) => Promise<void>;
324
325
  fetchEvents: (threadId: string) => Promise<void>;
325
326
  fetchState: (threadId: string) => Promise<void>;
326
327
  renderConversation: () => unknown;
@@ -406,6 +407,43 @@ describe("ɵCpkThreadDetails caching", () => {
406
407
  }
407
408
  });
408
409
 
410
+ it("joins thread inspection URLs without double slashes when runtimeUrl has a trailing slash", async () => {
411
+ const fetchSpy = vi
412
+ .spyOn(globalThis, "fetch")
413
+ .mockImplementation(async (input) => {
414
+ const url = String(input);
415
+ if (url.endsWith("/messages")) {
416
+ return new Response(JSON.stringify({ messages: [] }), {
417
+ status: 200,
418
+ });
419
+ }
420
+ if (url.endsWith("/events")) {
421
+ return new Response(JSON.stringify({ events: [] }), { status: 200 });
422
+ }
423
+ return new Response(JSON.stringify({ state: null }), { status: 200 });
424
+ });
425
+ try {
426
+ const { el, internals } = createThreadDetails();
427
+ internals.runtimeUrl = "http://localhost:4000/api/";
428
+ internals.threadInspectionAvailable = true;
429
+ internals.threadId = "thread one";
430
+ await el.updateComplete;
431
+ fetchSpy.mockClear();
432
+
433
+ await internals.fetchMessages("thread one");
434
+ await internals.fetchEvents("thread one");
435
+ await internals.fetchState("thread one");
436
+
437
+ expect(fetchSpy.mock.calls.map((call) => String(call[0]))).toEqual([
438
+ "http://localhost:4000/api/threads/thread%20one/messages",
439
+ "http://localhost:4000/api/threads/thread%20one/events",
440
+ "http://localhost:4000/api/threads/thread%20one/state",
441
+ ]);
442
+ } finally {
443
+ fetchSpy.mockRestore();
444
+ }
445
+ });
446
+
409
447
  it("conversation cache invalidates when _conversation is reassigned", async () => {
410
448
  const { el, internals } = createThreadDetails();
411
449
  await settleThread(el, internals, "t1");
@@ -616,6 +654,7 @@ type HeaderMockCore = {
616
654
  agents: Record<string, AbstractAgent>;
617
655
  context: Record<string, unknown>;
618
656
  properties: Record<string, unknown>;
657
+ telemetryDisabled: boolean;
619
658
  runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
620
659
  runtimeUrl: string;
621
660
  headers: Record<string, string>;
@@ -637,12 +676,15 @@ type HeaderMockCore = {
637
676
  function createHeaderMockCore(
638
677
  agents: Record<string, AbstractAgent>,
639
678
  headers: Record<string, string>,
679
+ endpointOverrides: Partial<HeaderMockCore["threadEndpoints"]> = {},
680
+ telemetryDisabled = true,
640
681
  ) {
641
682
  const subscribers = new Set<CopilotKitCoreSubscriber>();
642
683
  const core: HeaderMockCore = {
643
684
  agents,
644
685
  context: {},
645
686
  properties: {},
687
+ telemetryDisabled,
646
688
  runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
647
689
  runtimeUrl: "http://localhost/api",
648
690
  headers,
@@ -651,6 +693,7 @@ function createHeaderMockCore(
651
693
  inspect: true,
652
694
  mutations: true,
653
695
  realtimeMetadata: true,
696
+ ...endpointOverrides,
654
697
  },
655
698
  subscribe(subscriber: CopilotKitCoreSubscriber) {
656
699
  subscribers.add(subscriber);
@@ -693,6 +736,21 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
693
736
  fetchMock.mock.calls.filter((call) =>
694
737
  String(call[0]).includes("/threads?"),
695
738
  );
739
+ const telemetryPosts = () =>
740
+ fetchMock.mock.calls
741
+ .filter(
742
+ (call) =>
743
+ String(call[0]) === "https://telemetry.copilotkit.ai/ingest" &&
744
+ (call[1] as RequestInit | undefined)?.method === "POST",
745
+ )
746
+ .map((call) => {
747
+ const body =
748
+ ((call[1] as RequestInit | undefined)?.body as string) ?? "{}";
749
+ return JSON.parse(body) as {
750
+ event: string;
751
+ properties: Record<string, unknown>;
752
+ };
753
+ });
696
754
 
697
755
  beforeEach(() => {
698
756
  document.body.innerHTML = "";
@@ -758,4 +816,160 @@ describe("WebInspectorElement owned thread store headers (#5581)", () => {
758
816
  "X-CSRF": "2",
759
817
  });
760
818
  });
819
+
820
+ it("shows the locked Intelligence state when thread listing is unavailable without fetching threads", async () => {
821
+ const { agent } = createMockAgent("alpha");
822
+ const harness = createHeaderMockCore(
823
+ { alpha: agent },
824
+ { "X-CSRF": "1" },
825
+ { list: false },
826
+ true,
827
+ );
828
+
829
+ const inspector = new WebInspectorElement();
830
+ document.body.appendChild(inspector);
831
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
832
+ harness.emitAgentsChanged();
833
+
834
+ const internals = inspector as unknown as {
835
+ isOpen: boolean;
836
+ handleMenuSelect: (key: "threads") => void;
837
+ };
838
+ internals.isOpen = true;
839
+ internals.handleMenuSelect("threads");
840
+ await inspector.updateComplete;
841
+
842
+ const text = inspector.shadowRoot?.textContent ?? "";
843
+ expect(text).toMatch(/Enable Intelligence to inspect Threads\./);
844
+ expect(text).toContain("Talk to an Engineer");
845
+ expect(text).toContain("Sign up for Intelligence");
846
+ const ctaLabels = Array.from(
847
+ inspector.shadowRoot?.querySelectorAll<HTMLAnchorElement>("a") ?? [],
848
+ ).map((anchor) => anchor.textContent?.trim());
849
+ expect(ctaLabels).toEqual([
850
+ "Talk to an Engineer",
851
+ "Sign up for Intelligence",
852
+ ]);
853
+ expect(text).not.toContain("No threads yet");
854
+ expect(
855
+ fetchMock.mock.calls.some((call) => String(call[0]).includes("/threads")),
856
+ ).toBe(false);
857
+ });
858
+
859
+ it("adds ref and posthog distinct ID attribution to locked-state CTAs", async () => {
860
+ const { agent } = createMockAgent("alpha");
861
+ const harness = createHeaderMockCore(
862
+ { alpha: agent },
863
+ {},
864
+ { list: false },
865
+ false,
866
+ );
867
+
868
+ const inspector = new WebInspectorElement();
869
+ document.body.appendChild(inspector);
870
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
871
+ harness.emitAgentsChanged();
872
+
873
+ const internals = inspector as unknown as {
874
+ isOpen: boolean;
875
+ handleMenuSelect: (key: "threads") => void;
876
+ };
877
+ internals.isOpen = true;
878
+ internals.handleMenuSelect("threads");
879
+ await inspector.updateComplete;
880
+
881
+ const signup = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
882
+ 'a[href^="https://go.copilotkit.ai/intelligence-signup"]',
883
+ );
884
+ const engineer = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
885
+ 'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
886
+ );
887
+
888
+ expect(signup).not.toBeNull();
889
+ expect(engineer).not.toBeNull();
890
+
891
+ const signupUrl = new URL(signup!.href);
892
+ expect(signupUrl.searchParams.get("ref")).toBe("cpk-inspector");
893
+ const distinctId = signupUrl.searchParams.get("posthog_distinct_id");
894
+ expect(distinctId).toMatch(/^[0-9a-f-]{36}$/);
895
+
896
+ const engineerUrl = new URL(engineer!.href);
897
+ expect(engineerUrl.origin).toBe("https://www.copilotkit.ai");
898
+ expect(engineerUrl.pathname).toBe("/talk-to-an-engineer");
899
+ expect(engineerUrl.searchParams.get("ref")).toBe("cpk-inspector-threads");
900
+ expect(engineerUrl.searchParams.get("posthog_distinct_id")).toBe(
901
+ distinctId,
902
+ );
903
+ });
904
+
905
+ it("tracks Threads tab clicks through the rendered inspector menu", async () => {
906
+ const { agent } = createMockAgent("alpha");
907
+ const harness = createHeaderMockCore(
908
+ { alpha: agent },
909
+ {},
910
+ { list: false },
911
+ false,
912
+ );
913
+
914
+ const inspector = new WebInspectorElement();
915
+ document.body.appendChild(inspector);
916
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
917
+ harness.emitAgentsChanged();
918
+
919
+ const internals = inspector as unknown as { isOpen: boolean };
920
+ internals.isOpen = true;
921
+ inspector.requestUpdate();
922
+ await inspector.updateComplete;
923
+
924
+ const threadsButton = Array.from(
925
+ inspector.shadowRoot?.querySelectorAll<HTMLButtonElement>("button") ?? [],
926
+ ).find((button) => button.textContent?.trim() === "Threads");
927
+ expect(threadsButton, "Threads menu button should render").toBeDefined();
928
+
929
+ threadsButton!.click();
930
+ await inspector.updateComplete;
931
+ await Promise.resolve();
932
+
933
+ const threadsTabClick = telemetryPosts().find(
934
+ (post) => post.event === "oss.inspector.threads_tab_clicked",
935
+ );
936
+ expect(threadsTabClick).toBeDefined();
937
+ expect(threadsTabClick!.properties).toMatchObject({
938
+ intelligence_status: "intelligence_not_enabled",
939
+ thread_service_status: "unavailable",
940
+ telemetry_disabled: false,
941
+ });
942
+ expect(threadsTabClick!.properties.distinct_id).toMatch(/^[0-9a-f-]{36}$/);
943
+ if (threadsTabClick!.properties.posthog_distinct_id !== undefined) {
944
+ expect(threadsTabClick!.properties.posthog_distinct_id).toBe(
945
+ threadsTabClick!.properties.distinct_id,
946
+ );
947
+ }
948
+ });
949
+
950
+ it("keeps the enabled empty Threads state when thread listing is available", async () => {
951
+ const { agent } = createMockAgent("alpha");
952
+ const harness = createHeaderMockCore({ alpha: agent }, {}, {}, true);
953
+
954
+ const inspector = new WebInspectorElement();
955
+ document.body.appendChild(inspector);
956
+ inspector.core = harness.core as unknown as WebInspectorElement["core"];
957
+ harness.emitAgentsChanged();
958
+
959
+ const internals = inspector as unknown as {
960
+ isOpen: boolean;
961
+ handleMenuSelect: (key: "threads") => void;
962
+ };
963
+ internals.isOpen = true;
964
+ internals.handleMenuSelect("threads");
965
+ await inspector.updateComplete;
966
+
967
+ expect(inspector.shadowRoot?.textContent ?? "").toContain("No threads yet");
968
+ const engineer = inspector.shadowRoot?.querySelector<HTMLAnchorElement>(
969
+ 'a[href^="https://www.copilotkit.ai/talk-to-an-engineer"]',
970
+ );
971
+ expect(engineer?.href).toBe(
972
+ "https://www.copilotkit.ai/talk-to-an-engineer?ref=cpk-inspector-threads",
973
+ );
974
+ });
761
975
  });