@copilotkit/vue 1.72.1-canary.sp-longthread-perf → 1.73.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.
Files changed (77) hide show
  1. package/PARITY.md +15 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.mjs +42 -41
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/use-render-activity-message-6LwUTCNm.cjs +85 -0
  7. package/dist/use-render-activity-message-6LwUTCNm.cjs.map +1 -0
  8. package/dist/{use-render-activity-message-Ciee03Rx.js → use-render-activity-message-Di_L8HCv.js} +4501 -4400
  9. package/dist/use-render-activity-message-Di_L8HCv.js.map +1 -0
  10. package/dist/v2/components/LicenseWarningBanner.vue.d.ts +3 -5
  11. package/dist/v2/components/LicenseWarningBanner.vue.d.ts.map +1 -1
  12. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  13. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +4 -0
  14. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  15. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +7 -0
  16. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  17. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  18. package/dist/v2/components/chat/CopilotChatUserMessage.vue.d.ts +2 -0
  19. package/dist/v2/components/chat/CopilotChatUserMessage.vue.d.ts.map +1 -1
  20. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +8 -2
  21. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  22. package/dist/v2/components/chat/CopilotPopup.vue.d.ts.map +1 -1
  23. package/dist/v2/components/chat/CopilotPopupView.vue.d.ts.map +1 -1
  24. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  25. package/dist/v2/components/chat/CopilotPopupWelcomeScreen.vue.d.ts +7 -4
  26. package/dist/v2/components/chat/CopilotPopupWelcomeScreen.vue.d.ts.map +1 -1
  27. package/dist/v2/components/chat/CopilotSidebar.vue.d.ts.map +1 -1
  28. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts.map +1 -1
  29. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  30. package/dist/v2/components/chat/CopilotSidebarWelcomeScreen.vue.d.ts +8 -5
  31. package/dist/v2/components/chat/CopilotSidebarWelcomeScreen.vue.d.ts.map +1 -1
  32. package/dist/v2/components/chat/index.d.ts +50 -26
  33. package/dist/v2/components/chat/index.d.ts.map +1 -1
  34. package/dist/v2/components/chat/types.d.ts +44 -17
  35. package/dist/v2/components/chat/types.d.ts.map +1 -1
  36. package/dist/v2/hooks/use-human-in-the-loop.d.ts.map +1 -1
  37. package/dist/v2/index.cjs +1 -1
  38. package/dist/v2/index.mjs +1 -1
  39. package/dist/v2/providers/CopilotKitProvider.types.d.ts +25 -1
  40. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  41. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  42. package/package.json +6 -6
  43. package/src/v2/components/LicenseWarningBanner.vue +5 -17
  44. package/src/v2/components/__tests__/LicenseWarningBanner.test.ts +23 -0
  45. package/src/v2/components/chat/CopilotChat.vue +23 -22
  46. package/src/v2/components/chat/CopilotChatAssistantMessage.vue +12 -21
  47. package/src/v2/components/chat/CopilotChatInput.vue +32 -26
  48. package/src/v2/components/chat/CopilotChatMessageView.vue +29 -3
  49. package/src/v2/components/chat/CopilotChatUserMessage.vue +19 -19
  50. package/src/v2/components/chat/CopilotChatView.vue +31 -39
  51. package/src/v2/components/chat/CopilotPopup.vue +8 -10
  52. package/src/v2/components/chat/CopilotPopupView.vue +8 -18
  53. package/src/v2/components/chat/CopilotPopupViewInternal.vue +5 -19
  54. package/src/v2/components/chat/CopilotPopupWelcomeScreen.vue +36 -6
  55. package/src/v2/components/chat/CopilotSidebar.vue +8 -10
  56. package/src/v2/components/chat/CopilotSidebarView.vue +8 -18
  57. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +5 -19
  58. package/src/v2/components/chat/CopilotSidebarWelcomeScreen.vue +36 -6
  59. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.ts +156 -5
  60. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +34 -0
  61. package/src/v2/components/chat/__tests__/CopilotChatMessageView.rowKeys.test.ts +140 -0
  62. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.test.ts +46 -0
  63. package/src/v2/components/chat/__tests__/CopilotChatView.test.ts +118 -0
  64. package/src/v2/components/chat/__tests__/CopilotPopupView.test.ts +79 -0
  65. package/src/v2/components/chat/__tests__/CopilotSidebarView.test.ts +93 -0
  66. package/src/v2/components/chat/types.ts +73 -18
  67. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.ts +32 -6
  68. package/src/v2/hooks/use-human-in-the-loop.ts +1 -0
  69. package/src/v2/providers/CopilotKitProvider.types.ts +28 -1
  70. package/src/v2/providers/CopilotKitProvider.vue +86 -6
  71. package/src/v2/providers/__tests__/CopilotKitProvider.hitl.e2e.test.ts +192 -0
  72. package/src/v2/providers/__tests__/CopilotKitProvider.messageFilter.test.ts +159 -0
  73. package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +275 -21
  74. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.ts +19 -1
  75. package/dist/use-render-activity-message-Ciee03Rx.js.map +0 -1
  76. package/dist/use-render-activity-message-DXJDZGkW.cjs +0 -85
  77. package/dist/use-render-activity-message-DXJDZGkW.cjs.map +0 -1
@@ -157,6 +157,43 @@ describe("CopilotSidebarView", () => {
157
157
  expect(onSelectSuggestion).toHaveBeenCalledTimes(1);
158
158
  });
159
159
 
160
+ it("only exposes stop capability when an onStop callback is available", async () => {
161
+ const withoutStop = mountSidebarView(
162
+ {},
163
+ {
164
+ "welcome-screen": ({ canStop }: { canStop: boolean }) =>
165
+ h("div", { "data-testid": "sidebar-can-stop" }, String(canStop)),
166
+ },
167
+ );
168
+
169
+ expect(withoutStop.get("[data-testid='sidebar-can-stop']").text()).toBe(
170
+ "false",
171
+ );
172
+
173
+ const onStop = vi.fn();
174
+ const withStop = mountSidebarView(
175
+ { onStop },
176
+ {
177
+ "welcome-screen": ({
178
+ canStop,
179
+ onStop: stop,
180
+ }: {
181
+ canStop: boolean;
182
+ onStop: () => void;
183
+ }) =>
184
+ h(
185
+ "button",
186
+ { "data-testid": "sidebar-stop", onClick: stop },
187
+ String(canStop),
188
+ ),
189
+ },
190
+ );
191
+
192
+ expect(withStop.get("[data-testid='sidebar-stop']").text()).toBe("true");
193
+ await withStop.get("[data-testid='sidebar-stop']").trigger("click");
194
+ expect(onStop).toHaveBeenCalledTimes(1);
195
+ });
196
+
160
197
  it("uses the default chat welcome screen and still forwards welcome sub-slots", () => {
161
198
  const wrapper = mountSidebarView(
162
199
  {
@@ -211,4 +248,60 @@ describe("CopilotSidebarView", () => {
211
248
  "Hello",
212
249
  );
213
250
  });
251
+
252
+ it("exposes reactive capability flags from the namespaced WelcomeScreen input slot", async () => {
253
+ const wrapper = mount(CopilotSidebarView.WelcomeScreen, {
254
+ props: {
255
+ suggestions: [],
256
+ loadingIndexes: [],
257
+ modelValue: "",
258
+ isRunning: false,
259
+ inputMode: "input",
260
+ inputToolsMenu: [],
261
+ onUpdateModelValue: vi.fn(),
262
+ onSubmitMessage: vi.fn(),
263
+ onSelectSuggestion: vi.fn(),
264
+ },
265
+ slots: {
266
+ input: ({
267
+ canStop,
268
+ canAddFile,
269
+ canTranscribe,
270
+ }: {
271
+ canStop: boolean;
272
+ canAddFile: boolean;
273
+ canTranscribe: boolean;
274
+ }) =>
275
+ h(
276
+ "div",
277
+ { "data-testid": "sidebar-welcome-capabilities" },
278
+ `${canStop}:${canAddFile}:${canTranscribe}`,
279
+ ),
280
+ },
281
+ });
282
+
283
+ expect(
284
+ wrapper.get("[data-testid='sidebar-welcome-capabilities']").text(),
285
+ ).toBe("false:false:false");
286
+
287
+ await wrapper.setProps({
288
+ onStop: vi.fn(),
289
+ onAddFile: vi.fn(),
290
+ onStartTranscribe: vi.fn(),
291
+ });
292
+
293
+ expect(
294
+ wrapper.get("[data-testid='sidebar-welcome-capabilities']").text(),
295
+ ).toBe("true:true:true");
296
+
297
+ await wrapper.setProps({
298
+ canStop: false,
299
+ canAddFile: false,
300
+ canTranscribe: false,
301
+ });
302
+
303
+ expect(
304
+ wrapper.get("[data-testid='sidebar-welcome-capabilities']").text(),
305
+ ).toBe("false:false:false");
306
+ });
214
307
  });
@@ -57,6 +57,22 @@ export interface CopilotChatViewProps {
57
57
  inputValue?: string;
58
58
  inputMode?: CopilotChatInputMode;
59
59
  inputToolsMenu?: (ToolsMenuItem | "-")[];
60
+ /** Optional event callbacks used to drive the default input controls. */
61
+ onSubmitMessage?: (value: string) => void | Promise<void>;
62
+ /**
63
+ * Presence drives reactive gating of the default stop control.
64
+ * Prefer `@stop` / `emit("stop")` for invocation; do not call this prop
65
+ * directly or the handler can fire twice.
66
+ */
67
+ onStop?: () => void;
68
+ onInputChange?: (value: string) => void;
69
+ onSelectSuggestion?: (
70
+ suggestion: Suggestion,
71
+ index: number,
72
+ ) => void | Promise<void>;
73
+ onStartTranscribe?: () => void;
74
+ onCancelTranscribe?: () => void;
75
+ onFinishTranscribe?: () => void;
60
76
  /**
61
77
  * When `true`, suppresses the welcome screen while a thread's initial
62
78
  * connect is in flight. Prevents the "How can I help you today?" flash
@@ -93,6 +109,13 @@ export interface CopilotChatProps extends Omit<
93
109
  | "onDragOver"
94
110
  | "onDragLeave"
95
111
  | "onDrop"
112
+ | "onSubmitMessage"
113
+ | "onStop"
114
+ | "onInputChange"
115
+ | "onSelectSuggestion"
116
+ | "onStartTranscribe"
117
+ | "onCancelTranscribe"
118
+ | "onFinishTranscribe"
96
119
  > {
97
120
  agentId?: string;
98
121
  threadId?: string;
@@ -107,18 +130,21 @@ export interface CopilotChatProps extends Omit<
107
130
  }
108
131
 
109
132
  export interface CopilotChatViewOverrideSlotProps extends CopilotChatViewProps {
133
+ canStop: boolean;
134
+ canAddFile: boolean;
135
+ canTranscribe: boolean;
110
136
  onSubmitMessage: (value: string) => void | Promise<void>;
111
- onStop?: () => void;
137
+ onStop: () => void;
112
138
  onInputChange: (value: string) => void;
113
139
  onSelectSuggestion: (
114
140
  suggestion: Suggestion,
115
141
  index: number,
116
142
  ) => void | Promise<void>;
117
- onAddFile?: () => void;
118
- onStartTranscribe?: () => void;
119
- onCancelTranscribe?: () => void;
120
- onFinishTranscribe?: () => void;
121
- onFinishTranscribeWithAudio?: (audioBlob: Blob) => void | Promise<void>;
143
+ onAddFile: () => void;
144
+ onStartTranscribe: () => void;
145
+ onCancelTranscribe: () => void;
146
+ onFinishTranscribe: () => void;
147
+ onFinishTranscribeWithAudio: (audioBlob: Blob) => void | Promise<void>;
122
148
  }
123
149
 
124
150
  export interface CopilotChatMessageViewSlotProps {
@@ -154,9 +180,12 @@ export interface CopilotChatInputSlotProps {
154
180
  isRunning: boolean;
155
181
  inputMode: CopilotChatInputMode;
156
182
  inputToolsMenu: (ToolsMenuItem | "-")[];
183
+ canStop: boolean;
184
+ canAddFile: boolean;
185
+ canTranscribe: boolean;
157
186
  onUpdateModelValue: (value: string) => void;
158
187
  onSubmitMessage: (value: string) => void;
159
- onStop?: () => void;
188
+ onStop: () => void;
160
189
  onAddFile: () => void;
161
190
  onStartTranscribe: () => void;
162
191
  onCancelTranscribe: () => void;
@@ -189,9 +218,12 @@ export interface CopilotChatWelcomeScreenSlotProps extends CopilotChatSuggestion
189
218
  isRunning: boolean;
190
219
  inputMode: CopilotChatInputMode;
191
220
  inputToolsMenu: (ToolsMenuItem | "-")[];
221
+ canStop: boolean;
222
+ canAddFile: boolean;
223
+ canTranscribe: boolean;
192
224
  onUpdateModelValue: (value: string) => void;
193
225
  onSubmitMessage: (value: string) => void;
194
- onStop?: () => void;
226
+ onStop: () => void;
195
227
  onAddFile: () => void;
196
228
  onStartTranscribe: () => void;
197
229
  onCancelTranscribe: () => void;
@@ -402,14 +434,17 @@ export interface CopilotSidebarWelcomeScreenInputSlotProps {
402
434
  isRunning: boolean;
403
435
  inputMode: CopilotChatInputMode;
404
436
  inputToolsMenu: (ToolsMenuItem | "-")[];
437
+ canStop: boolean;
438
+ canAddFile: boolean;
439
+ canTranscribe: boolean;
405
440
  onUpdateModelValue: (value: string) => void;
406
441
  onSubmitMessage: (value: string) => void;
407
- onStop?: () => void;
408
- onAddFile?: () => void;
409
- onStartTranscribe?: () => void;
410
- onCancelTranscribe?: () => void;
411
- onFinishTranscribe?: () => void;
412
- onFinishTranscribeWithAudio?: (audioBlob: Blob) => void | Promise<void>;
442
+ onStop: () => void;
443
+ onAddFile: () => void;
444
+ onStartTranscribe: () => void;
445
+ onCancelTranscribe: () => void;
446
+ onFinishTranscribe: () => void;
447
+ onFinishTranscribeWithAudio: (audioBlob: Blob) => void | Promise<void>;
413
448
  }
414
449
 
415
450
  export interface CopilotSidebarWelcomeScreenSuggestionViewSlotProps {
@@ -423,8 +458,29 @@ export interface CopilotSidebarWelcomeScreenLayoutSlotProps
423
458
  CopilotSidebarWelcomeScreenInputSlotProps,
424
459
  CopilotSidebarWelcomeScreenSuggestionViewSlotProps {}
425
460
 
426
- export type CopilotSidebarWelcomeScreenProps =
427
- CopilotSidebarWelcomeScreenLayoutSlotProps;
461
+ type WelcomeScreenCommandProps =
462
+ | "onStop"
463
+ | "onAddFile"
464
+ | "onStartTranscribe"
465
+ | "onCancelTranscribe"
466
+ | "onFinishTranscribe"
467
+ | "onFinishTranscribeWithAudio";
468
+
469
+ type WelcomeScreenDerivedCapabilityProps =
470
+ | "canStop"
471
+ | "canAddFile"
472
+ | "canTranscribe";
473
+
474
+ export type CopilotSidebarWelcomeScreenProps = Omit<
475
+ CopilotSidebarWelcomeScreenLayoutSlotProps,
476
+ WelcomeScreenCommandProps | WelcomeScreenDerivedCapabilityProps
477
+ > &
478
+ Partial<
479
+ Pick<
480
+ CopilotSidebarWelcomeScreenLayoutSlotProps,
481
+ WelcomeScreenCommandProps | WelcomeScreenDerivedCapabilityProps
482
+ >
483
+ >;
428
484
 
429
485
  export interface CopilotSidebarViewHeaderSlotProps {
430
486
  title: string;
@@ -458,8 +514,7 @@ export type CopilotPopupWelcomeScreenSuggestionViewSlotProps =
458
514
  export type CopilotPopupWelcomeScreenLayoutSlotProps =
459
515
  CopilotSidebarWelcomeScreenLayoutSlotProps;
460
516
 
461
- export type CopilotPopupWelcomeScreenProps =
462
- CopilotPopupWelcomeScreenLayoutSlotProps;
517
+ export type CopilotPopupWelcomeScreenProps = CopilotSidebarWelcomeScreenProps;
463
518
 
464
519
  export interface CopilotPopupViewProps extends CopilotChatViewProps {
465
520
  width?: number | string;
@@ -1,7 +1,7 @@
1
1
  import { defineComponent, ref, watch } from "vue";
2
2
  import type { PropType } from "vue";
3
3
  import { screen, fireEvent, waitFor, cleanup } from "@testing-library/vue";
4
- import { afterEach, describe, expect, it } from "vitest";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import { z } from "zod";
6
6
  import type { AssistantMessage, Message } from "@ag-ui/core";
7
7
  import { ToolCallStatus } from "@copilotkit/core";
@@ -1100,6 +1100,7 @@ describe("HITL Run Abort", () => {
1100
1100
  describe("HITL Thread Reconnection Bug", () => {
1101
1101
  it("should show executing status when reconnecting to thread with pending HITL", async () => {
1102
1102
  const agent = new MockReconnectableAgent();
1103
+ const handlerStarted = vi.fn();
1103
1104
 
1104
1105
  const HITLRenderer = defineComponent({
1105
1106
  props: {
@@ -1116,7 +1117,7 @@ describe("HITL Thread Reconnection Bug", () => {
1116
1117
  <div data-testid="hitl-tool">
1117
1118
  <div data-testid="hitl-status">{{ status }}</div>
1118
1119
  <div data-testid="hitl-action">{{ args.action ?? "no-action" }}</div>
1119
- <button v-if="respond" data-testid="hitl-respond">Respond</button>
1120
+ <button v-if="respond" data-testid="hitl-respond" @click="respond('approved')">Respond</button>
1120
1121
  </div>
1121
1122
  `,
1122
1123
  });
@@ -1130,6 +1131,19 @@ describe("HITL Thread Reconnection Bug", () => {
1130
1131
  render: HITLRenderer,
1131
1132
  };
1132
1133
  useHumanInTheLoop(hitlTool);
1134
+ const { copilotkit } = useCopilotKit();
1135
+ const registered = copilotkit.value.getTool({
1136
+ toolName: "approvalTool",
1137
+ });
1138
+ if (!registered?.handler) {
1139
+ throw new Error("HITL handler was not registered");
1140
+ }
1141
+ const originalHandler = registered.handler;
1142
+ registered.handler = (...args) => {
1143
+ const pending = originalHandler(...args);
1144
+ handlerStarted();
1145
+ return pending;
1146
+ };
1133
1147
  return {};
1134
1148
  },
1135
1149
  template: `<div />`,
@@ -1196,11 +1210,23 @@ describe("HITL Thread Reconnection Bug", () => {
1196
1210
  expect(screen.getByTestId("hitl-action").textContent).toBe("delete");
1197
1211
  });
1198
1212
 
1199
- // Core-level coverage asserts that passive replay does not re-invoke local
1200
- // frontend handlers for replayed assistant tool calls.
1201
1213
  await waitFor(() => {
1202
- expect(screen.getByTestId("hitl-status").textContent).toMatch(
1203
- /^(executing|inProgress)$/,
1214
+ expect(screen.getByTestId("hitl-status").textContent).toBe(
1215
+ ToolCallStatus.Executing,
1216
+ );
1217
+ });
1218
+ // Vue can paint Executing during the awaited execution-start notification,
1219
+ // before core invokes the handler. Wait for the remounted hook to install
1220
+ // its response resolver before dispatching a synchronous test click.
1221
+ await waitFor(() => expect(handlerStarted).toHaveBeenCalledTimes(2));
1222
+ await fireEvent.click(screen.getByTestId("hitl-respond"));
1223
+ await waitFor(() => {
1224
+ expect(agent.messages).toContainEqual(
1225
+ expect.objectContaining({
1226
+ role: "tool",
1227
+ toolCallId,
1228
+ content: "approved",
1229
+ }),
1204
1230
  );
1205
1231
  });
1206
1232
  });
@@ -111,6 +111,7 @@ export function useHumanInTheLoop<T extends Record<string, unknown>>(
111
111
 
112
112
  const frontendTool: VueFrontendTool<T> = {
113
113
  ...tool,
114
+ type: "human-in-the-loop",
114
115
  handler,
115
116
  render: RenderComponent,
116
117
  };
@@ -1,5 +1,8 @@
1
1
  import type { AbstractAgent } from "@ag-ui/client";
2
- import type { CopilotKitCoreErrorCode } from "@copilotkit/core";
2
+ import type {
3
+ CopilotKitCoreErrorCode,
4
+ CopilotKitMessageFilter,
5
+ } from "@copilotkit/core";
3
6
  import type { DebugConfig } from "@copilotkit/shared";
4
7
  import type {
5
8
  A2UITheme,
@@ -16,6 +19,30 @@ export interface CopilotKitProviderProps {
16
19
  runtimeUrl?: string;
17
20
  headers?: Record<string, string> | (() => Record<string, string>);
18
21
  credentials?: RequestCredentials;
22
+ /**
23
+ * Rewrites the message list sent to runtime agents on every run.
24
+ *
25
+ * CopilotKit sends the whole thread each time. When your agent already
26
+ * stores the conversation, most of that payload is waste, and an agent that
27
+ * merges the inbound list with its own store can show the model every turn
28
+ * twice. Return the messages to send:
29
+ *
30
+ * ```vue
31
+ * <CopilotKitProvider
32
+ * runtime-url="/api/copilotkit"
33
+ * :message-filter="(messages) => messages.slice(-1)"
34
+ * />
35
+ * ```
36
+ *
37
+ * The filter changes the request body only. The transcript the UI renders is
38
+ * untouched. Broken tool-call pairs are repaired before the request is sent,
39
+ * so a filter this blunt cannot strand a tool result mid-HITL.
40
+ *
41
+ * Agents reached through your CopilotRuntime honor this. An agent your app
42
+ * passes in directly does not, and neither Intelligence runs nor suggestion
43
+ * runs are ever filtered.
44
+ */
45
+ messageFilter?: CopilotKitMessageFilter;
19
46
  defaultThrottleMs?: number;
20
47
  publicApiKey?: string;
21
48
  publicLicenseKey?: string;
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import {
3
3
  computed,
4
+ h,
4
5
  onMounted,
5
6
  provide,
6
7
  ref,
@@ -10,6 +11,7 @@ import {
10
11
  } from "vue";
11
12
  import { z } from "zod";
12
13
  import type { AbstractAgent } from "@ag-ui/client";
14
+ import { ToolCallStatus } from "@copilotkit/core";
13
15
  import type {
14
16
  CopilotKitCoreErrorCode,
15
17
  CopilotKitCoreSubscriber,
@@ -52,6 +54,7 @@ import type {
52
54
  VueActivityMessageRenderer,
53
55
  VueFrontendTool,
54
56
  VueToolCallRenderer,
57
+ VueToolCallRendererRenderProps,
55
58
  } from "../types";
56
59
 
57
60
  const HEADER_NAME = "X-CopilotCloud-Public-Api-Key";
@@ -70,6 +73,10 @@ const RENDER_ACTIVITY_MESSAGES_STABLE_WARNING =
70
73
  "renderActivityMessages must be a stable array.";
71
74
  const SANDBOX_FUNCTIONS_STABLE_WARNING =
72
75
  "openGenerativeUI.sandboxFunctions must be a stable array.";
76
+ // Matches the message React's `useHumanInTheLoop` rejects with, so an aborted
77
+ // interrupt reads the same across frameworks (see #5554).
78
+ const HUMAN_IN_THE_LOOP_ABORTED_MESSAGE =
79
+ "Human-in-the-loop interaction aborted";
73
80
  const DEFAULT_DESIGN_SKILL = `When generating UI with generateSandboxedUi, follow these design principles inspired by shadcn/ui:
74
81
 
75
82
  - Use a minimal, flat aesthetic. Avoid drop shadows and gradients — rely on subtle borders (1px solid, light gray like #e5e7eb) to define surfaces.
@@ -228,29 +235,99 @@ watch(
228
235
  { immediate: true },
229
236
  );
230
237
 
238
+ /**
239
+ * A human-in-the-loop tool call from the `humanInTheLoop` prop that is waiting
240
+ * on the user. Keyed by tool call id so parallel interrupts on the same tool
241
+ * stay independent.
242
+ */
243
+ type PendingHumanInTheLoop = {
244
+ resolve: (result: unknown) => void;
245
+ detachAbort?: () => void;
246
+ };
247
+
248
+ const pendingHumanInTheLoop = new Map<string, PendingHumanInTheLoop>();
249
+
250
+ /** Removes a pending interaction and detaches its abort listener. */
251
+ const takePendingHumanInTheLoop = (key: string) => {
252
+ const pending = pendingHumanInTheLoop.get(key);
253
+ if (!pending) return undefined;
254
+ pending.detachAbort?.();
255
+ pendingHumanInTheLoop.delete(key);
256
+ return pending;
257
+ };
258
+
231
259
  const processedHumanInTheLoop = computed(() => {
232
260
  const tools: FrontendTool[] = [];
233
261
  const renderToolCalls: VueToolCallRenderer<unknown>[] = [];
234
262
 
235
263
  for (const tool of props.humanInTheLoop) {
236
264
  tools.push({
265
+ type: "human-in-the-loop",
237
266
  name: tool.name,
238
267
  description: tool.description,
239
268
  parameters: tool.parameters,
240
269
  followUp: tool.followUp,
241
270
  ...(tool.agentId && { agentId: tool.agentId }),
242
- handler: async () => {
243
- console.warn(
244
- `Human-in-the-loop tool '${tool.name}' called but no interactive handler is set up.`,
245
- );
246
- return undefined;
271
+ // Keep the tool call pending until the render calls `respond`, matching
272
+ // the `useHumanInTheLoop` composable. Resolving immediately made the HITL
273
+ // UI flash and disappear without ever waiting for the user.
274
+ handler: async (_args, context) => {
275
+ const signal = context?.signal;
276
+ const key = context?.toolCall?.id ?? tool.name;
277
+
278
+ return new Promise((resolve, reject) => {
279
+ // Already aborted before the handler ran — reject so core records an
280
+ // explicit error tool result rather than silently resolving empty.
281
+ if (signal?.aborted) {
282
+ reject(new Error(HUMAN_IN_THE_LOOP_ABORTED_MESSAGE));
283
+ return;
284
+ }
285
+
286
+ const pending: PendingHumanInTheLoop = { resolve };
287
+ pendingHumanInTheLoop.set(key, pending);
288
+
289
+ if (signal) {
290
+ const onAbort = () => {
291
+ pendingHumanInTheLoop.delete(key);
292
+ reject(new Error(HUMAN_IN_THE_LOOP_ABORTED_MESSAGE));
293
+ };
294
+ signal.addEventListener("abort", onAbort, { once: true });
295
+ pending.detachAbort = () => {
296
+ signal.removeEventListener("abort", onAbort);
297
+ };
298
+ }
299
+ });
247
300
  },
248
301
  });
249
302
  if (tool.render) {
303
+ const ToolComponent = tool.render;
304
+ const render: VueToolCallRenderer<unknown>["render"] = (
305
+ renderProps: VueToolCallRendererRenderProps<unknown>,
306
+ ) => {
307
+ const key = renderProps.toolCallId ?? tool.name;
308
+ return h(ToolComponent as Parameters<typeof h>[0], {
309
+ ...renderProps,
310
+ // `renderProps.name` is the tool that was actually invoked. It equals
311
+ // `tool.name` for a named registration; for a wildcard (`"*"`) it is
312
+ // the only place the real name exists.
313
+ name: tool.name === "*" ? renderProps.name : tool.name,
314
+ description: tool.description || "",
315
+ agentId: tool.agentId,
316
+ // `respond` is live only while the tool is executing — the one phase
317
+ // with a promise waiting on the user.
318
+ respond:
319
+ renderProps.status === ToolCallStatus.Executing
320
+ ? async (result: unknown) => {
321
+ takePendingHumanInTheLoop(key)?.resolve(result);
322
+ }
323
+ : undefined,
324
+ });
325
+ };
326
+
250
327
  renderToolCalls.push({
251
328
  name: tool.name,
252
329
  args: tool.parameters ?? z.any(),
253
- render: tool.render,
330
+ render,
254
331
  ...(tool.agentId && { agentId: tool.agentId }),
255
332
  } as VueToolCallRenderer<unknown>);
256
333
  }
@@ -407,6 +484,7 @@ const createCopilotKit = () => {
407
484
  : "auto",
408
485
  headers: mergedHeaders.value,
409
486
  credentials: props.credentials,
487
+ messageFilter: props.messageFilter,
410
488
  properties: resolvedProperties.value,
411
489
  agents__unsafe_dev_only: mergedAgents.value,
412
490
  tools: allTools.value,
@@ -525,6 +603,7 @@ function syncRuntimeConfig() {
525
603
  );
526
604
  copilotkit.value.setHeaders(mergedHeaders.value);
527
605
  copilotkit.value.setCredentials(props.credentials);
606
+ copilotkit.value.setMessageFilter(props.messageFilter);
528
607
  copilotkit.value.setProperties(resolvedProperties.value);
529
608
  copilotkit.value.setAgents__unsafe_dev_only(mergedAgents.value);
530
609
  copilotkit.value.setDebug(props.debug);
@@ -536,6 +615,7 @@ watch(
536
615
  () => chatApiEndpoint.value,
537
616
  () => mergedHeaders.value,
538
617
  () => props.credentials,
618
+ () => props.messageFilter,
539
619
  () => resolvedProperties.value,
540
620
  () => mergedAgents.value,
541
621
  () => props.useSingleEndpoint,