@copilotkit/vue 2.0.0-rc.20 → 2.0.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.mjs +13 -13
  3. package/dist/use-render-activity-message-BAhbpSDh.js +16200 -0
  4. package/dist/use-render-activity-message-BAhbpSDh.js.map +1 -0
  5. package/dist/use-render-activity-message-DJ6q1qHV.cjs +114 -0
  6. package/dist/use-render-activity-message-DJ6q1qHV.cjs.map +1 -0
  7. package/dist/v2/components/A2UIMessageRenderer.d.ts +14 -0
  8. package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
  9. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts +10 -3
  10. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
  11. package/dist/v2/components/CopilotKitInspector.vue.d.ts +5 -1
  12. package/dist/v2/components/CopilotKitInspector.vue.d.ts.map +1 -1
  13. package/dist/v2/components/MCPAppsActivityRenderer.d.ts.map +1 -1
  14. package/dist/v2/components/OpenGenerativeUIRenderer.d.ts +7 -21
  15. package/dist/v2/components/OpenGenerativeUIRenderer.d.ts.map +1 -1
  16. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +0 -5
  17. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
  18. package/dist/v2/components/a2ui/A2UICatalogContext.d.ts +10 -15
  19. package/dist/v2/components/a2ui/A2UICatalogContext.d.ts.map +1 -1
  20. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts +14 -138
  21. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts.map +1 -1
  22. package/dist/v2/components/a2ui/catalog.d.ts.map +1 -1
  23. package/dist/v2/components/a2ui/utils.d.ts +3 -15
  24. package/dist/v2/components/a2ui/utils.d.ts.map +1 -1
  25. package/dist/v2/components/a2ui.d.ts +6 -1
  26. package/dist/v2/components/a2ui.d.ts.map +1 -1
  27. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  28. package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
  29. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts +4 -8
  30. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts.map +1 -1
  31. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  32. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts.map +1 -1
  33. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  34. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -1
  35. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
  36. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
  37. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -1
  38. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  39. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  40. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts +1 -1
  41. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts +1 -1
  42. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  43. package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -1
  44. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts +1 -0
  45. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -1
  46. package/dist/v2/components/chat/audioRecorder.d.ts +3 -4
  47. package/dist/v2/components/chat/audioRecorder.d.ts.map +1 -1
  48. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts +3 -2
  49. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
  50. package/dist/v2/components/chat/index.d.ts +6 -6
  51. package/dist/v2/components/chat/tool-call-display-context.d.ts +3 -1
  52. package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -1
  53. package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -1
  54. package/dist/v2/components/chat/types.d.ts +1 -1
  55. package/dist/v2/components/chat/types.d.ts.map +1 -1
  56. package/dist/v2/components/chat/use-lightbox.d.ts.map +1 -1
  57. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts +1 -1
  58. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts.map +1 -1
  59. package/dist/v2/hooks/use-agent-context.d.ts +2 -3
  60. package/dist/v2/hooks/use-agent-context.d.ts.map +1 -1
  61. package/dist/v2/hooks/use-agent.d.ts +4 -1
  62. package/dist/v2/hooks/use-agent.d.ts.map +1 -1
  63. package/dist/v2/hooks/use-attachments.d.ts +7 -0
  64. package/dist/v2/hooks/use-attachments.d.ts.map +1 -1
  65. package/dist/v2/hooks/use-capabilities.d.ts.map +1 -1
  66. package/dist/v2/hooks/use-component.d.ts +1 -0
  67. package/dist/v2/hooks/use-component.d.ts.map +1 -1
  68. package/dist/v2/hooks/use-configure-suggestions.d.ts +4 -9
  69. package/dist/v2/hooks/use-configure-suggestions.d.ts.map +1 -1
  70. package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
  71. package/dist/v2/hooks/use-human-in-the-loop.d.ts +2 -1
  72. package/dist/v2/hooks/use-human-in-the-loop.d.ts.map +1 -1
  73. package/dist/v2/hooks/use-interrupt.d.ts +0 -1
  74. package/dist/v2/hooks/use-interrupt.d.ts.map +1 -1
  75. package/dist/v2/hooks/use-render-activity-message.d.ts.map +1 -1
  76. package/dist/v2/hooks/use-render-custom-messages.d.ts.map +1 -1
  77. package/dist/v2/hooks/use-suggestions.d.ts.map +1 -1
  78. package/dist/v2/hooks/use-threads.d.ts +0 -8
  79. package/dist/v2/hooks/use-threads.d.ts.map +1 -1
  80. package/dist/v2/index.cjs +1 -1
  81. package/dist/v2/index.mjs +1 -1
  82. package/dist/v2/lib/vue-core.d.ts +12 -11
  83. package/dist/v2/lib/vue-core.d.ts.map +1 -1
  84. package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
  85. package/dist/v2/providers/CopilotKitProvider.types.d.ts +9 -124
  86. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  87. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +9 -10
  88. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  89. package/dist/v2/providers/chat-configuration-store-key.d.ts +8 -0
  90. package/dist/v2/providers/chat-configuration-store-key.d.ts.map +1 -0
  91. package/dist/v2/providers/markdown-renderer.d.ts +3 -1
  92. package/dist/v2/providers/markdown-renderer.d.ts.map +1 -1
  93. package/dist/v2/providers/use-inspector-thread-override.d.ts +5 -0
  94. package/dist/v2/providers/use-inspector-thread-override.d.ts.map +1 -1
  95. package/package.json +8 -8
  96. package/src/v2/__tests__/exports.test.ts +10 -0
  97. package/src/v2/__tests__/mocks/web-inspector.ts +6 -0
  98. package/src/v2/__tests__/utils/test-helpers.ts +34 -4
  99. package/src/v2/components/A2UIMessageRenderer.ts +98 -68
  100. package/src/v2/components/A2UISurfaceActivityRenderer.vue +68 -103
  101. package/src/v2/components/CopilotKitInspector.vue +104 -51
  102. package/src/v2/components/MCPAppsActivityRenderer.ts +75 -201
  103. package/src/v2/components/OpenGenerativeUIRenderer.ts +78 -80
  104. package/src/v2/components/__tests__/A2UIMessageRenderer.test.ts +171 -8
  105. package/src/v2/components/__tests__/CopilotKitInspector.test.ts +42 -0
  106. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.ts +9 -2
  107. package/src/v2/components/__tests__/OpenGenerativeUIToolRenderer.test.ts +38 -0
  108. package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +1 -6
  109. package/src/v2/components/a2ui/A2UICatalogContext.ts +27 -93
  110. package/src/v2/components/a2ui/A2UIRecoveryStates.ts +127 -327
  111. package/src/v2/components/a2ui/catalog.ts +2 -3
  112. package/src/v2/components/a2ui/utils.ts +13 -67
  113. package/src/v2/components/a2ui.ts +14 -21
  114. package/src/v2/components/chat/CopilotChat.vue +136 -324
  115. package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +22 -45
  116. package/src/v2/components/chat/CopilotChatAudioRecorder.vue +19 -246
  117. package/src/v2/components/chat/CopilotChatInput.vue +69 -407
  118. package/src/v2/components/chat/CopilotChatLightbox.vue +10 -31
  119. package/src/v2/components/chat/CopilotChatMessageView.vue +52 -112
  120. package/src/v2/components/chat/CopilotChatToolCallGroup.vue +14 -45
  121. package/src/v2/components/chat/CopilotChatToolCallItem.vue +6 -4
  122. package/src/v2/components/chat/CopilotChatToolCallsView.vue +16 -19
  123. package/src/v2/components/chat/CopilotChatView.vue +28 -23
  124. package/src/v2/components/chat/CopilotPopupViewInternal.vue +21 -97
  125. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +24 -25
  126. package/src/v2/components/chat/CopilotThreadsDrawer.vue +37 -102
  127. package/src/v2/components/chat/StreamingMarkdownDefault.vue +13 -26
  128. package/src/v2/components/chat/__tests__/CopilotChat.test.ts +12 -10
  129. package/src/v2/components/chat/__tests__/CopilotChatAttachmentQueue.test.ts +28 -0
  130. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +56 -0
  131. package/src/v2/components/chat/__tests__/CopilotPopupView.test.ts +19 -0
  132. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.ts +32 -8
  133. package/src/v2/components/chat/__tests__/CopilotSidebarView.test.ts +2 -1
  134. package/src/v2/components/chat/__tests__/StreamingMarkdownDefault.test.ts +54 -0
  135. package/src/v2/components/chat/audioRecorder.ts +4 -7
  136. package/src/v2/components/chat/copilotChatToolCallItemRender.ts +22 -71
  137. package/src/v2/components/chat/tool-call-display-context.ts +6 -1
  138. package/src/v2/components/chat/tool-call-group-parts.ts +13 -33
  139. package/src/v2/components/chat/types.ts +1 -1
  140. package/src/v2/components/chat/use-lightbox.ts +15 -33
  141. package/src/v2/hooks/__tests__/use-agent-error-state.test.ts +5 -3
  142. package/src/v2/hooks/__tests__/use-agent-stability.test.ts +25 -4
  143. package/src/v2/hooks/__tests__/use-agent-throttle.test.ts +5 -1
  144. package/src/v2/hooks/__tests__/use-agent.test.ts +6 -3
  145. package/src/v2/hooks/__tests__/use-component.test.ts +21 -0
  146. package/src/v2/hooks/__tests__/use-configure-suggestions.test.ts +30 -1
  147. package/src/v2/hooks/__tests__/use-default-render-tool.test.ts +10 -10
  148. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +4 -0
  149. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.ts +6 -1
  150. package/src/v2/hooks/__tests__/use-human-in-the-loop.test.ts +187 -12
  151. package/src/v2/hooks/__tests__/use-interrupt.test.ts +97 -0
  152. package/src/v2/hooks/__tests__/use-render-activity-message.test.ts +2 -1
  153. package/src/v2/hooks/__tests__/use-threads.test.ts +48 -6
  154. package/src/v2/hooks/use-agent-context.ts +4 -10
  155. package/src/v2/hooks/use-agent.ts +86 -290
  156. package/src/v2/hooks/use-attachments.ts +42 -166
  157. package/src/v2/hooks/use-capabilities.ts +2 -7
  158. package/src/v2/hooks/use-component.ts +7 -4
  159. package/src/v2/hooks/use-configure-suggestions.ts +28 -218
  160. package/src/v2/hooks/use-default-render-tool.ts +34 -94
  161. package/src/v2/hooks/use-human-in-the-loop.ts +11 -80
  162. package/src/v2/hooks/use-interrupt.ts +29 -308
  163. package/src/v2/hooks/use-render-activity-message.ts +17 -20
  164. package/src/v2/hooks/use-render-custom-messages.ts +20 -63
  165. package/src/v2/hooks/use-suggestions.ts +9 -33
  166. package/src/v2/hooks/use-threads.ts +43 -273
  167. package/src/v2/lib/__tests__/vue-core-renderers.test.ts +42 -0
  168. package/src/v2/lib/vue-core.ts +67 -72
  169. package/src/v2/providers/CopilotChatConfigurationProvider.vue +48 -186
  170. package/src/v2/providers/CopilotKitProvider.types.ts +12 -127
  171. package/src/v2/providers/CopilotKitProvider.vue +188 -402
  172. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.ts +22 -2
  173. package/src/v2/providers/__tests__/CopilotKitProvider.a2ui.test.ts +19 -1
  174. package/src/v2/providers/__tests__/CopilotKitProvider.config.test.ts +124 -0
  175. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.ts +4 -1
  176. package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +45 -2
  177. package/src/v2/providers/chat-configuration-store-key.ts +9 -0
  178. package/src/v2/providers/markdown-renderer.ts +7 -1
  179. package/src/v2/providers/use-inspector-thread-override.ts +22 -80
  180. package/vite.config.ts +5 -1
  181. package/vitest.config.ts +12 -7
  182. package/dist/use-render-activity-message-CsWYyd6r.cjs +0 -91
  183. package/dist/use-render-activity-message-CsWYyd6r.cjs.map +0 -1
  184. package/dist/use-render-activity-message-DntKKNdd.js +0 -17418
  185. package/dist/use-render-activity-message-DntKKNdd.js.map +0 -1
  186. package/dist/v2/components/chat/normalize-auto-scroll.d.ts +0 -3
  187. package/dist/v2/components/chat/normalize-auto-scroll.d.ts.map +0 -1
  188. package/dist/v2/lib/is-mobile-viewport.d.ts +0 -2
  189. package/dist/v2/lib/is-mobile-viewport.d.ts.map +0 -1
  190. package/src/v2/components/__tests__/A2UICatalogContext.test.ts +0 -63
  191. package/src/v2/components/a2ui/__tests__/A2UIRecoveryStates.test.ts +0 -175
  192. package/src/v2/components/chat/normalize-auto-scroll.ts +0 -3
  193. package/src/v2/lib/is-mobile-viewport.ts +0 -5
@@ -12,7 +12,10 @@ import type { Observable } from "rxjs";
12
12
  import { useConfigureSuggestions } from "../use-configure-suggestions";
13
13
  import { useSuggestions } from "../use-suggestions";
14
14
  import { useCopilotKit } from "../../providers/useCopilotKit";
15
- import { mountWithProvider } from "../../__tests__/utils/mount";
15
+ import {
16
+ mountWithProvider,
17
+ renderWithCopilotKit,
18
+ } from "../../__tests__/utils/mount";
16
19
  import {
17
20
  StateCapturingAgent,
18
21
  SuggestionsProviderAgent,
@@ -396,4 +399,30 @@ describe("useConfigureSuggestions", () => {
396
399
 
397
400
  expect(wrapper.find("[data-testid=json]").text()).toContain("Deferred");
398
401
  });
402
+
403
+ it("loads a global static config for the chat's agent before any agent registers", async () => {
404
+ const Harness = defineComponent({
405
+ setup() {
406
+ useConfigureSuggestions({
407
+ suggestions: [{ title: "Welcome", message: "Say hi" }],
408
+ available: "always",
409
+ });
410
+ const { suggestions } = useSuggestions({ agentId: "support" });
411
+ return () =>
412
+ h(
413
+ "span",
414
+ { "data-testid": "titles" },
415
+ suggestions.value.map((suggestion) => suggestion.title).join(","),
416
+ );
417
+ },
418
+ });
419
+
420
+ const { wrapper } = renderWithCopilotKit(() => h(Harness), {
421
+ agentId: "support",
422
+ agents: {},
423
+ });
424
+ await nextTick();
425
+
426
+ expect(wrapper.find("[data-testid=titles]").text()).toBe("Welcome");
427
+ });
399
428
  });
@@ -530,10 +530,8 @@ describe("useDefaultRenderTool", () => {
530
530
  expect(forwarded.result).toBe("ok");
531
531
  });
532
532
 
533
- // Fix #5: circular-ref parameters must not crash the vue render; log.
534
- it("default renderer survives circular-ref parameters and logs", async () => {
535
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
536
-
533
+ // Fix #5: circular-ref parameters must not crash the vue render.
534
+ it("default renderer survives circular-ref parameters", async () => {
537
535
  const Harness = defineComponent({
538
536
  setup() {
539
537
  useDefaultRenderTool();
@@ -567,14 +565,16 @@ describe("useDefaultRenderTool", () => {
567
565
  }),
568
566
  ).not.toThrow();
569
567
 
568
+ const wrapper = screen.getByTestId("copilot-tool-render");
569
+ expect(wrapper.getAttribute("data-args")).toBe(
570
+ '{"a":1,"self":"[Circular]"}',
571
+ );
572
+
570
573
  const headerButton = screen
571
574
  .getByTestId("copilot-tool-render-name")
572
575
  .closest("button");
573
- if (headerButton) {
574
- await expect(fireEvent.click(headerButton)).resolves.not.toThrow();
575
- }
576
-
577
- expect(warnSpy).toHaveBeenCalled();
578
- warnSpy.mockRestore();
576
+ expect(headerButton).not.toBeNull();
577
+ await fireEvent.click(headerButton!);
578
+ expect(wrapper.textContent).toContain('"self": "[Circular]"');
579
579
  });
580
580
  });
@@ -887,6 +887,10 @@ describe("useFrontendTool E2E - Dynamic Registration", () => {
887
887
  expect(firstVersion.textContent).toContain("before override");
888
888
  });
889
889
 
890
+ await agent.emit(runFinishedEvent());
891
+ // The tool call starts a follow-up run. A send waits for the run in
892
+ // flight (#5195), so finish the follow-up before the next message.
893
+ await agent.emit(runStartedEvent());
890
894
  await agent.emit(runFinishedEvent());
891
895
 
892
896
  const overrideButton = screen.getByTestId("activate-override");
@@ -720,7 +720,12 @@ describe("useHumanInTheLoop E2E - HITL Tool Rendering", () => {
720
720
  expect(screen.queryByTestId("hitl-enabled")).toBeNull();
721
721
  });
722
722
 
723
- await submitMessage("Test after disable");
723
+ // The first run still waits for the unanswered HITL prompt, and a send
724
+ // waits for the run in flight (#5195), so this message stays queued.
725
+ // What this test pins is that the unregistered tool no longer renders.
726
+ const input = await screen.findByRole("textbox");
727
+ await fireEvent.update(input, "Test after disable");
728
+ await fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
724
729
 
725
730
  const messageId2 = testId("msg2");
726
731
  const toolCallId2 = testId("tc2");
@@ -4,7 +4,22 @@ import { z } from "zod";
4
4
  import type { AbstractAgent } from "@ag-ui/client";
5
5
  import { useHumanInTheLoop } from "../use-human-in-the-loop";
6
6
  import { mountWithProvider } from "../../__tests__/utils/mount";
7
- import { StateCapturingAgent } from "../../__tests__/utils/agents";
7
+ import {
8
+ SequencedRunAgent,
9
+ StateCapturingAgent,
10
+ toolCallMessage,
11
+ } from "../../__tests__/utils/agents";
12
+
13
+ /** Handler context for one tool call; core always passes the call. */
14
+ function callContext(id: string, name = "tool") {
15
+ return {
16
+ toolCall: {
17
+ id,
18
+ type: "function" as const,
19
+ function: { name, arguments: "{}" },
20
+ },
21
+ };
22
+ }
8
23
 
9
24
  describe("useHumanInTheLoop", () => {
10
25
  beforeEach(() => {
@@ -53,7 +68,7 @@ describe("useHumanInTheLoop", () => {
53
68
 
54
69
  const pending = registeredTool!.handler!(
55
70
  { question: "Proceed?" },
56
- {} as never,
71
+ callContext("tc-approve"),
57
72
  );
58
73
 
59
74
  const executingVNode = renderer!.render({
@@ -107,8 +122,8 @@ describe("useHumanInTheLoop", () => {
107
122
  const renderA = getCore().renderToolCalls.find((rc) => rc.name === "toolA");
108
123
  const renderB = getCore().renderToolCalls.find((rc) => rc.name === "toolB");
109
124
 
110
- const pA = toolA!.handler!({}, {} as never);
111
- const pB = toolB!.handler!({}, {} as never);
125
+ const pA = toolA!.handler!({}, callContext("tc-a"));
126
+ const pB = toolB!.handler!({}, callContext("tc-b"));
112
127
 
113
128
  const respondA = (
114
129
  renderA!.render({
@@ -140,6 +155,72 @@ describe("useHumanInTheLoop", () => {
140
155
  await expect(pB).resolves.toBe("B");
141
156
  });
142
157
 
158
+ it("gives parallel calls of one tool their own respond", async () => {
159
+ const RenderComp = defineComponent({
160
+ props: ["respond"],
161
+ setup() {
162
+ return () => h("div");
163
+ },
164
+ });
165
+ const Child = defineComponent({
166
+ setup() {
167
+ useHumanInTheLoop({
168
+ name: "parallelTool",
169
+ parameters: z.object({}),
170
+ render: RenderComp,
171
+ });
172
+ return () => null;
173
+ },
174
+ });
175
+ const { getCore } = mountWithProvider(() => h(Child));
176
+ const handler = getCore().getTool({ toolName: "parallelTool" })!.handler!;
177
+ const renderer = getCore().renderToolCalls.find(
178
+ (rc) => rc.name === "parallelTool",
179
+ )!;
180
+ const respondFor = (toolCallId: string) =>
181
+ (
182
+ renderer.render({
183
+ name: "parallelTool",
184
+ toolCallId,
185
+ args: {},
186
+ status: "executing",
187
+ result: undefined,
188
+ }) as { props?: { respond?: (result: unknown) => Promise<void> } }
189
+ ).props!.respond!;
190
+
191
+ const first = handler({}, callContext("call-1"));
192
+ const second = handler({}, callContext("call-2"));
193
+ await respondFor("call-2")("second");
194
+ await respondFor("call-1")("first");
195
+
196
+ await expect(first).resolves.toBe("first");
197
+ await expect(second).resolves.toBe("second");
198
+ });
199
+
200
+ it("passes the called tool's name to a catch-all render", () => {
201
+ const RenderComp = defineComponent({
202
+ setup() {
203
+ return () => h("div");
204
+ },
205
+ });
206
+ const Child = defineComponent({
207
+ setup() {
208
+ useHumanInTheLoop({ name: "*", render: RenderComp });
209
+ return () => null;
210
+ },
211
+ });
212
+ const { getCore } = mountWithProvider(() => h(Child));
213
+ const renderer = getCore().renderToolCalls.find((rc) => rc.name === "*")!;
214
+ const vnode = renderer.render({
215
+ name: "deleteFile",
216
+ toolCallId: "tc-any",
217
+ args: {},
218
+ status: "inProgress",
219
+ result: undefined,
220
+ }) as { props?: Record<string, unknown> };
221
+ expect(vnode.props?.name).toBe("deleteFile");
222
+ });
223
+
143
224
  it("only exposes respond during executing status", () => {
144
225
  const RenderComp = defineComponent({
145
226
  props: ["respond"],
@@ -323,9 +404,9 @@ describe("useHumanInTheLoop", () => {
323
404
  const controller = new AbortController();
324
405
  controller.abort();
325
406
 
326
- await expect(handler!({}, { signal: controller.signal })).rejects.toEqual(
327
- new Error("Human-in-the-loop interaction aborted"),
328
- );
407
+ await expect(
408
+ handler!({}, { ...callContext("tc-x"), signal: controller.signal }),
409
+ ).rejects.toEqual(new Error("Human-in-the-loop interaction aborted"));
329
410
  });
330
411
 
331
412
  it("rejects once on abort, clears pending refs, and installs a one-shot listener", async () => {
@@ -344,7 +425,7 @@ describe("useHumanInTheLoop", () => {
344
425
  }),
345
426
  } as unknown as AbortSignal;
346
427
 
347
- const pending = handler!({}, { signal });
428
+ const pending = handler!({}, { ...callContext("tc-aborted"), signal });
348
429
  expect(listeners.size).toBe(1);
349
430
  const onAbort = [...listeners][0];
350
431
  onAbort();
@@ -379,7 +460,13 @@ describe("useHumanInTheLoop", () => {
379
460
  }),
380
461
  } as unknown as AbortSignal;
381
462
 
382
- const pending = handler!({}, { signal }).then((result) => {
463
+ const pending = handler!(
464
+ {},
465
+ {
466
+ ...callContext("tc-respond"),
467
+ signal,
468
+ },
469
+ ).then((result) => {
383
470
  events.push(`resolved:${String(result)}`);
384
471
  return result;
385
472
  });
@@ -511,10 +598,98 @@ describe("useHumanInTheLoop", () => {
511
598
  expect(vnode.props?.agentId).toBeUndefined();
512
599
  });
513
600
 
601
+ it("lets a message queued behind an unanswered prompt send after the prompt unmounts", async () => {
602
+ const prompt = toolCallMessage("unmountApproval");
603
+ const toolCallId = (prompt as { toolCalls: { id: string }[] })
604
+ .toolCalls[0]!.id;
605
+ const agent = new SequencedRunAgent(
606
+ [
607
+ () => ({ result: undefined, newMessages: [prompt] }),
608
+ () => ({
609
+ result: undefined,
610
+ newMessages: [{ id: "reply", role: "assistant", content: "ok" }],
611
+ }),
612
+ ],
613
+ "default",
614
+ );
615
+ const RenderComp = defineComponent({
616
+ setup() {
617
+ return () => h("div");
618
+ },
619
+ });
620
+ const shown = ref(true);
621
+ const Child = defineComponent({
622
+ setup() {
623
+ useHumanInTheLoop({
624
+ name: "unmountApproval",
625
+ parameters: z.object({}),
626
+ render: RenderComp,
627
+ });
628
+ return () => null;
629
+ },
630
+ });
631
+ const { getCore } = mountWithProvider(
632
+ () => (shown.value ? h(Child) : null),
633
+ {
634
+ agents__unsafe_dev_only: {
635
+ default: agent as unknown as AbstractAgent,
636
+ },
637
+ },
638
+ );
639
+ const core = getCore();
640
+ const sendMessage = async (content: string) => {
641
+ agent.addMessage({ id: content, role: "user", content });
642
+ await core.runAgent({ agent: agent as unknown as AbstractAgent });
643
+ };
644
+
645
+ const first = sendMessage("approve this");
646
+ const queued = first.then(() => sendMessage("next question"));
647
+ await vi.waitFor(() =>
648
+ expect(core.ɵhumanInTheLoop.isPending(toolCallId)).toBe(true),
649
+ );
650
+
651
+ shown.value = false;
652
+ await nextTick();
653
+
654
+ await expect(queued).resolves.toBeUndefined();
655
+ expect(agent.messages).toEqual(
656
+ expect.arrayContaining([
657
+ expect.objectContaining({
658
+ role: "tool",
659
+ toolCallId,
660
+ error: "Human-in-the-loop interaction aborted",
661
+ }),
662
+ expect.objectContaining({ id: "reply", content: "ok" }),
663
+ ]),
664
+ );
665
+ });
666
+
514
667
  it("does not settle a pending HITL promise when its renderer unmounts", async () => {
515
- const mounted = mountHitlTool();
516
- const pending = mounted.handler({}, {});
517
- mounted.wrapper.unmount();
668
+ const RenderComp = defineComponent({
669
+ setup() {
670
+ return () => h("div");
671
+ },
672
+ });
673
+ const shown = ref(true);
674
+ const Child = defineComponent({
675
+ setup() {
676
+ useHumanInTheLoop({
677
+ name: "unmountTool",
678
+ parameters: z.object({}),
679
+ render: RenderComp,
680
+ });
681
+ return () => null;
682
+ },
683
+ });
684
+ const { getCore } = mountWithProvider(() =>
685
+ shown.value ? h(Child) : null,
686
+ );
687
+ const pending = getCore().getTool({ toolName: "unmountTool" })!.handler!(
688
+ {},
689
+ callContext("tc-unmount"),
690
+ );
691
+ shown.value = false;
692
+ await nextTick();
518
693
 
519
694
  const outcome = await Promise.race([
520
695
  pending.then(
@@ -347,6 +347,32 @@ describe("useInterrupt", () => {
347
347
  expect(wrapper.get("[data-testid=interrupt-state]").text()).toBe("idle");
348
348
  });
349
349
 
350
+ it("treats a throwing enabled predicate as disabled instead of crashing", async () => {
351
+ vi.spyOn(console, "error").mockImplementation(() => {});
352
+ const wrapper = mountHarness({
353
+ enabled: () => {
354
+ throw new Error("predicate");
355
+ },
356
+ });
357
+ emitInterrupt("hidden");
358
+ await nextTick();
359
+ expect(wrapper.get("[data-testid=interrupt-state]").text()).toBe("idle");
360
+ });
361
+
362
+ it("shows the interrupt with a null result when the handler throws synchronously", async () => {
363
+ vi.spyOn(console, "error").mockImplementation(() => {});
364
+ const wrapper = mountHarness({
365
+ handler: () => {
366
+ throw new Error("handler");
367
+ },
368
+ });
369
+ emitInterrupt("visible");
370
+ await nextTick();
371
+ expect(wrapper.get("[data-testid=interrupt-state]").text()).toBe(
372
+ "no-result:visible",
373
+ );
374
+ });
375
+
350
376
  it("keeps the latest interrupt when multiple interrupts arrive within one run", async () => {
351
377
  const wrapper = mountHarness({ renderInChat: false });
352
378
 
@@ -412,6 +438,77 @@ describe("useInterrupt", () => {
412
438
  handlers.onRunFinalized?.();
413
439
  }
414
440
 
441
+ it("picks up the agent's pending interrupts when it mounts", async () => {
442
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
443
+ (mockAgent as any).pendingInterrupts = [INT];
444
+ const wrapper = mountStandardHarness();
445
+ await nextTick();
446
+ expect(wrapper.find("[data-testid=active]").exists()).toBe(true);
447
+ });
448
+
449
+ it("appends a tool result for a frontend tool-call interrupt before resuming", async () => {
450
+ const addMessage = vi.fn();
451
+ Object.assign(mockAgent, { messages: [], addMessage });
452
+ const TOOL_INT: Interrupt = {
453
+ id: "tool-int",
454
+ reason: "tool_call",
455
+ toolCallId: "call-1",
456
+ };
457
+ const wrapper = mountStandardHarness();
458
+ fireStandardInterrupt([TOOL_INT]);
459
+ await nextTick();
460
+
461
+ await wrapper.get("[data-testid=resolve-btn]").trigger("click");
462
+
463
+ expect(addMessage).toHaveBeenCalledWith(
464
+ expect.objectContaining({
465
+ role: "tool",
466
+ toolCallId: "call-1",
467
+ content: JSON.stringify({ ok: true }),
468
+ }),
469
+ );
470
+ expect(runAgentMock).toHaveBeenCalledWith({
471
+ agent: mockAgent,
472
+ resume: [
473
+ {
474
+ interruptId: "tool-int",
475
+ status: "resolved",
476
+ payload: { ok: true },
477
+ },
478
+ ],
479
+ });
480
+ });
481
+
482
+ it("resumes when the only expired interrupt was cancelled", async () => {
483
+ const EXPIRED: Interrupt = {
484
+ id: "old",
485
+ reason: "confirmation",
486
+ expiresAt: "2000-01-01T00:00:00.000Z",
487
+ };
488
+ let api: ReturnType<typeof useInterrupt> | undefined;
489
+ mount(
490
+ defineComponent({
491
+ setup() {
492
+ api = useInterrupt({ renderInChat: false });
493
+ return () => null;
494
+ },
495
+ }),
496
+ );
497
+ fireStandardInterrupt([EXPIRED, INT]);
498
+ await nextTick();
499
+
500
+ await api!.cancel("old");
501
+ await api!.resolve("yes", INT.id);
502
+
503
+ expect(runAgentMock).toHaveBeenCalledWith({
504
+ agent: mockAgent,
505
+ resume: [
506
+ { interruptId: "old", status: "cancelled" },
507
+ { interruptId: INT.id, status: "resolved", payload: "yes" },
508
+ ],
509
+ });
510
+ });
511
+
415
512
  it("resolveInterrupt resumes with a resolved ResumeEntry", async () => {
416
513
  runAgentMock.mockResolvedValue({ result: undefined, newMessages: [] });
417
514
  const wrapper = mountStandardHarness();
@@ -99,7 +99,8 @@ describe("useRenderActivityMessage", () => {
99
99
 
100
100
  expect(result).toBeNull();
101
101
  expect(consoleWarnSpy).toHaveBeenCalledWith(
102
- "Failed to validate content for activity message 'progress'.",
102
+ "[CopilotKit] Failed to parse content for activity message 'progress':",
103
+ expect.any(Array),
103
104
  );
104
105
  });
105
106
  });
@@ -4,6 +4,19 @@ import type { Ref } from "vue";
4
4
  import { describe, expect, it, beforeEach, vi } from "vitest";
5
5
  import { useCopilotKit } from "../../providers/useCopilotKit";
6
6
 
7
+ /** Core's `ɵruntimeInfo` snapshot, built from a fake core's runtime fields. */
8
+ function runtimeInfoOf(core: {
9
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
10
+ threadEndpoints?: unknown;
11
+ intelligence?: unknown;
12
+ }) {
13
+ return {
14
+ status: core.runtimeConnectionStatus,
15
+ threadEndpoints: core.threadEndpoints,
16
+ intelligence: core.intelligence,
17
+ };
18
+ }
19
+
7
20
  type ThreadRecord = {
8
21
  id: string;
9
22
  organizationId: string;
@@ -54,7 +67,8 @@ function compareThreadsByActivity(left: ThreadRecord, right: ThreadRecord) {
54
67
  return rightKey.localeCompare(leftKey);
55
68
  }
56
69
 
57
- vi.mock("@copilotkit/core", () => {
70
+ vi.mock("@copilotkit/core", async (importOriginal) => {
71
+ const actual = await importOriginal<typeof import("@copilotkit/core")>();
58
72
  class MockChannel {
59
73
  topic: string;
60
74
  left = false;
@@ -118,6 +132,16 @@ vi.mock("@copilotkit/core", () => {
118
132
  private listeners = new Set<() => void>();
119
133
  private socket: MockSocket | null = null;
120
134
 
135
+ readonly selectors = {
136
+ threads: (state: ThreadState) => state.threads,
137
+ isLoading: (state: ThreadState) => state.isLoading,
138
+ error: (state: ThreadState) => state.error,
139
+ fetchMoreError: (state: ThreadState) => state.fetchMoreError,
140
+ hasNextPage: (state: ThreadState) => state.hasNextPage,
141
+ isFetchingNextPage: (state: ThreadState) => state.isFetchingNextPage,
142
+ isMutating: (state: ThreadState) => state.isMutating,
143
+ };
144
+
121
145
  start(): void {}
122
146
 
123
147
  stop(): void {
@@ -317,6 +341,10 @@ vi.mock("@copilotkit/core", () => {
317
341
  return this.state;
318
342
  }
319
343
 
344
+ getServerState(): ThreadState {
345
+ return { ...this.state, threads: [] };
346
+ }
347
+
320
348
  select<T>(selector: (state: ThreadState) => T) {
321
349
  return {
322
350
  subscribe: (callback: () => void) => {
@@ -357,6 +385,8 @@ vi.mock("@copilotkit/core", () => {
357
385
  ɵselectHasNextPage: select((state) => state.hasNextPage),
358
386
  ɵselectIsFetchingNextPage: select((state) => state.isFetchingNextPage),
359
387
  ɵselectIsMutating: select((state) => state.isMutating),
388
+ // The real view model, driving the mock store above.
389
+ ɵThreadsController: actual.ɵThreadsController,
360
390
  };
361
391
  });
362
392
 
@@ -394,6 +424,7 @@ const supportedThreadEndpoints: ThreadEndpointRuntimeInfo = {
394
424
 
395
425
  type MockCopilotKit = {
396
426
  runtimeUrl: string | undefined;
427
+ readonly ɵruntimeInfo: unknown;
397
428
  runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
398
429
  headers: Record<string, string>;
399
430
  /**
@@ -417,6 +448,9 @@ function setupCopilotKit(
417
448
  ) {
418
449
  const copilotkit = ref<MockCopilotKit>({
419
450
  runtimeUrl,
451
+ get ɵruntimeInfo() {
452
+ return runtimeInfoOf(this);
453
+ },
420
454
  runtimeConnectionStatus,
421
455
  headers: { Authorization: "Bearer test-token" },
422
456
  ɵheadersGeneration: 0,
@@ -965,6 +999,9 @@ describe("useThreads", () => {
965
999
  // exactly one list fetch (+ one subscribe after it lands).
966
1000
  copilotkit.value = {
967
1001
  ...copilotkit.value,
1002
+ get ɵruntimeInfo() {
1003
+ return runtimeInfoOf(this);
1004
+ },
968
1005
  runtimeConnectionStatus:
969
1006
  CopilotKitCoreRuntimeConnectionStatus.Connected,
970
1007
  intelligence: { wsUrl: "ws://localhost:4000/client" },
@@ -1020,6 +1057,9 @@ describe("useThreads", () => {
1020
1057
 
1021
1058
  copilotkit.value = {
1022
1059
  ...copilotkit.value,
1060
+ get ɵruntimeInfo() {
1061
+ return runtimeInfoOf(this);
1062
+ },
1023
1063
  runtimeConnectionStatus:
1024
1064
  CopilotKitCoreRuntimeConnectionStatus.Disconnected,
1025
1065
  };
@@ -1216,8 +1256,9 @@ describe("useThreads", () => {
1216
1256
 
1217
1257
  expect(fetchMock).not.toHaveBeenCalled();
1218
1258
  expect(getResult().threads.value).toEqual([]);
1219
- const lastDispatched = getDispatchedContexts().at(-1);
1220
- expect(lastDispatched).toBeNull();
1259
+ // Nothing was ever sent, so nothing is cleared either (no extra
1260
+ // queued list fetch).
1261
+ expect(getDispatchedContexts()).toEqual([]);
1221
1262
  // A disabled consumer must not be stuck in a permanent loading state:
1222
1263
  // preConnectLoading must factor in `enabled`, otherwise isLoading would
1223
1264
  // stay true forever since hasDispatchedContext never gets set.
@@ -1240,7 +1281,7 @@ describe("useThreads", () => {
1240
1281
  // Disabled: inert, not loading, no fetch.
1241
1282
  expect(getResult().isLoading.value).toBe(false);
1242
1283
  expect(fetchMock).not.toHaveBeenCalled();
1243
- expect(getDispatchedContexts().at(-1)).toBeNull();
1284
+ expect(getDispatchedContexts().filter(Boolean)).toEqual([]);
1244
1285
 
1245
1286
  // Re-enable: the pre-connect loading indicator must re-arm (i.e.
1246
1287
  // hasDispatchedContext was reset to false on disable) so consumers see
@@ -1343,8 +1384,9 @@ describe("useThreads", () => {
1343
1384
 
1344
1385
  expect(fetchMock).not.toHaveBeenCalled();
1345
1386
  expect(getResult().threads.value).toEqual([]);
1346
- const lastDispatched = getDispatchedContexts().at(-1);
1347
- expect(lastDispatched).toBeNull();
1387
+ // Nothing was ever sent, so nothing is cleared either (no extra
1388
+ // queued list fetch).
1389
+ expect(getDispatchedContexts()).toEqual([]);
1348
1390
  expect(getResult().error.value?.message).toBe(
1349
1391
  "Thread endpoints are not available on this CopilotKit runtime",
1350
1392
  );
@@ -1,14 +1,10 @@
1
1
  import { computed, toValue, watch } from "vue";
2
2
  import type { MaybeRefOrGetter } from "vue";
3
+ import { ɵserializeContextValue } from "@copilotkit/core";
4
+ import type { ɵJsonSerializable } from "@copilotkit/core";
3
5
  import { useCopilotKit } from "../providers/useCopilotKit";
4
6
 
5
- export type JsonSerializable =
6
- | string
7
- | number
8
- | boolean
9
- | null
10
- | JsonSerializable[]
11
- | { [key: string]: JsonSerializable };
7
+ export type JsonSerializable = ɵJsonSerializable;
12
8
 
13
9
  export interface AgentContextInput {
14
10
  description: MaybeRefOrGetter<string>;
@@ -35,9 +31,7 @@ export function useAgentContext(context: AgentContextInput): void {
35
31
  const resolvedValue = computed(() => toValue(context.value));
36
32
 
37
33
  const stringValue = computed(() =>
38
- typeof resolvedValue.value === "string"
39
- ? resolvedValue.value
40
- : JSON.stringify(resolvedValue.value),
34
+ ɵserializeContextValue(resolvedValue.value),
41
35
  );
42
36
 
43
37
  watch(