@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
@@ -1,7 +1,7 @@
1
1
  import { cleanup, fireEvent, screen, waitFor } from "@testing-library/vue";
2
2
  import { computed, defineComponent, onMounted } from "vue";
3
3
  import type { PropType } from "vue";
4
- import { afterEach, describe, expect, it } from "vitest";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import { EventType } from "@ag-ui/client";
6
6
  import { z } from "zod";
7
7
  import { useConfigureSuggestions } from "../../../hooks/use-configure-suggestions";
@@ -23,6 +23,7 @@ import {
23
23
  toolCallResultEvent,
24
24
  } from "../../../__tests__/utils/test-helpers";
25
25
  import CopilotChat from "../CopilotChat.vue";
26
+ import CopilotPopup from "../CopilotPopup.vue";
26
27
 
27
28
  afterEach(() => {
28
29
  cleanup();
@@ -138,11 +139,33 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
138
139
  components: { CopilotChat },
139
140
  template: `
140
141
  <CopilotChat :welcome-screen="false">
141
- <template #chat-view="{ isRunning, onStop, onSubmitMessage }">
142
+ <template #chat-view="{
143
+ isRunning,
144
+ onStop,
145
+ onSubmitMessage,
146
+ onAddFile,
147
+ onStartTranscribe,
148
+ onCancelTranscribe,
149
+ onFinishTranscribe,
150
+ onFinishTranscribeWithAudio,
151
+ }">
142
152
  <div>
143
153
  <button data-testid="submit" @click="onSubmitMessage('trigger run')">submit</button>
144
154
  <span data-testid="running">{{ isRunning ? "running" : "idle" }}</span>
145
- <span data-testid="stop-availability">{{ onStop ? "available" : "missing" }}</span>
155
+ <span data-testid="stop-availability">
156
+ {{
157
+ [
158
+ onStop,
159
+ onAddFile,
160
+ onStartTranscribe,
161
+ onCancelTranscribe,
162
+ onFinishTranscribe,
163
+ onFinishTranscribeWithAudio,
164
+ ].every((command) => typeof command === "function")
165
+ ? "available"
166
+ : "missing"
167
+ }}
168
+ </span>
146
169
  </div>
147
170
  </template>
148
171
  </CopilotChat>
@@ -153,7 +176,7 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
153
176
 
154
177
  expect(screen.getByTestId("running").textContent).toBe("idle");
155
178
  expect(screen.getByTestId("stop-availability").textContent).toBe(
156
- "missing",
179
+ "available",
157
180
  );
158
181
 
159
182
  await fireEvent.click(screen.getByTestId("submit"));
@@ -172,7 +195,7 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
172
195
  await waitFor(() => {
173
196
  expect(screen.getByTestId("running").textContent).toBe("idle");
174
197
  expect(screen.getByTestId("stop-availability").textContent).toBe(
175
- "missing",
198
+ "available",
176
199
  );
177
200
  });
178
201
  });
@@ -1237,4 +1260,132 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
1237
1260
  });
1238
1261
  });
1239
1262
  });
1263
+
1264
+ describe("Vue-specific semantics", () => {
1265
+ it("forwards a static stop listener through a custom input slot after a run starts", async () => {
1266
+ const agent = new MockStepwiseAgent();
1267
+ const onStop = vi.fn();
1268
+ const CustomInputChat = defineComponent({
1269
+ components: { CopilotChat },
1270
+ setup() {
1271
+ return { onStop };
1272
+ },
1273
+ template: `
1274
+ <CopilotChat :welcome-screen="false" @stop="onStop">
1275
+ <template #input="{ isRunning, onSubmitMessage, onStop }">
1276
+ <button data-testid="custom-input-submit" @click="onSubmitMessage('Hello')">
1277
+ submit
1278
+ </button>
1279
+ <button v-if="isRunning" data-testid="custom-input-stop" @click="onStop">
1280
+ stop
1281
+ </button>
1282
+ </template>
1283
+ </CopilotChat>
1284
+ `,
1285
+ });
1286
+
1287
+ renderWithCopilotKit({ agent, children: CustomInputChat });
1288
+
1289
+ expect(screen.queryByTestId("custom-input-stop")).toBeNull();
1290
+
1291
+ await fireEvent.click(screen.getByTestId("custom-input-submit"));
1292
+ await waitFor(() => expect(agent.messages.length).toBeGreaterThan(0));
1293
+
1294
+ await agent.emit(runStartedEvent());
1295
+ await waitFor(() => {
1296
+ expect(screen.getByTestId("custom-input-stop")).toBeDefined();
1297
+ });
1298
+
1299
+ await fireEvent.click(screen.getByTestId("custom-input-stop"));
1300
+ expect(onStop).toHaveBeenCalledTimes(1);
1301
+
1302
+ await agent.complete();
1303
+ });
1304
+
1305
+ it("does not emit unavailable transcription commands from a custom chat-view slot", async () => {
1306
+ const agent = new MockStepwiseAgent();
1307
+ const onCancelTranscribe = vi.fn();
1308
+ const onFinishTranscribe = vi.fn();
1309
+ const CustomChatView = defineComponent({
1310
+ components: { CopilotChat },
1311
+ setup() {
1312
+ return { onCancelTranscribe, onFinishTranscribe };
1313
+ },
1314
+ template: `
1315
+ <CopilotChat
1316
+ :welcome-screen="false"
1317
+ @cancel-transcribe="onCancelTranscribe"
1318
+ @finish-transcribe="onFinishTranscribe"
1319
+ >
1320
+ <template #chat-view="{ onCancelTranscribe, onFinishTranscribe }">
1321
+ <button data-testid="custom-cancel-transcribe" @click="onCancelTranscribe">
1322
+ cancel
1323
+ </button>
1324
+ <button data-testid="custom-finish-transcribe" @click="onFinishTranscribe">
1325
+ finish
1326
+ </button>
1327
+ </template>
1328
+ </CopilotChat>
1329
+ `,
1330
+ });
1331
+
1332
+ renderWithCopilotKit({ agent, children: CustomChatView });
1333
+
1334
+ await fireEvent.click(screen.getByTestId("custom-cancel-transcribe"));
1335
+ await fireEvent.click(screen.getByTestId("custom-finish-transcribe"));
1336
+
1337
+ expect(onCancelTranscribe).not.toHaveBeenCalled();
1338
+ expect(onFinishTranscribe).not.toHaveBeenCalled();
1339
+ });
1340
+
1341
+ it("preserves stop behavior through a popup close and reopen", async () => {
1342
+ const agent = new MockStepwiseAgent();
1343
+ const onStop = vi.fn();
1344
+ const CustomPopup = defineComponent({
1345
+ components: { CopilotPopup },
1346
+ setup() {
1347
+ return { onStop };
1348
+ },
1349
+ template: `
1350
+ <CopilotPopup :welcome-screen="false" @stop="onStop">
1351
+ <template #toggle-button="{ toggle }">
1352
+ <button data-testid="popup-toggle" @click="toggle">Toggle</button>
1353
+ </template>
1354
+ <template #input="{ isRunning, onSubmitMessage, onStop }">
1355
+ <button data-testid="popup-submit" @click="onSubmitMessage('Hello')">
1356
+ submit
1357
+ </button>
1358
+ <button v-if="isRunning" data-testid="popup-stop" @click="onStop">
1359
+ stop
1360
+ </button>
1361
+ </template>
1362
+ </CopilotPopup>
1363
+ `,
1364
+ });
1365
+
1366
+ renderWithCopilotKit({ agent, children: CustomPopup });
1367
+
1368
+ await fireEvent.click(screen.getByTestId("popup-submit"));
1369
+ await waitFor(() => expect(agent.messages.length).toBeGreaterThan(0));
1370
+ await agent.emit(runStartedEvent());
1371
+ await waitFor(() => {
1372
+ expect(screen.getByTestId("popup-stop")).toBeDefined();
1373
+ });
1374
+
1375
+ await fireEvent.click(screen.getByTestId("popup-toggle"));
1376
+ await waitFor(() => {
1377
+ expect(screen.queryByTestId("popup-stop")).toBeNull();
1378
+ });
1379
+
1380
+ await fireEvent.click(screen.getByTestId("popup-toggle"));
1381
+ await waitFor(() => {
1382
+ expect(screen.getByTestId("popup-stop")).toBeDefined();
1383
+ });
1384
+
1385
+ await fireEvent.click(screen.getByTestId("popup-stop"));
1386
+ expect(onStop).toHaveBeenCalledTimes(1);
1387
+
1388
+ await agent.complete();
1389
+ });
1390
+ });
1240
1391
  });
@@ -562,6 +562,40 @@ describe("CopilotChatInput", () => {
562
562
  expect(screen.queryByTestId("copilot-chat-input-send")).toBeNull();
563
563
  });
564
564
 
565
+ it("shows finish control when only onFinishTranscribeWithAudio is provided", () => {
566
+ renderWithProvider({
567
+ props: {
568
+ mode: "transcribe",
569
+ },
570
+ listeners: {
571
+ onCancelTranscribe: () => {},
572
+ onFinishTranscribeWithAudio: () => {},
573
+ },
574
+ });
575
+
576
+ expect(
577
+ screen.getByTestId("copilot-chat-input-finish-transcribe"),
578
+ ).toBeDefined();
579
+ });
580
+
581
+ it("hides finish control when neither finish handler is provided", () => {
582
+ renderWithProvider({
583
+ props: {
584
+ mode: "transcribe",
585
+ },
586
+ listeners: {
587
+ onCancelTranscribe: () => {},
588
+ },
589
+ });
590
+
591
+ expect(
592
+ screen.queryByTestId("copilot-chat-input-finish-transcribe"),
593
+ ).toBeNull();
594
+ expect(
595
+ screen.getByTestId("copilot-chat-input-cancel-transcribe"),
596
+ ).toBeDefined();
597
+ });
598
+
565
599
  it("disables add menu button in transcribe mode", () => {
566
600
  renderWithProvider({
567
601
  props: {
@@ -0,0 +1,140 @@
1
+ import { defineComponent, ref } from "vue";
2
+ import { render, screen } from "@testing-library/vue";
3
+ import { describe, expect, it } from "vitest";
4
+ import type { Message, ToolCall } from "@ag-ui/core";
5
+ import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
6
+ import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
7
+ import CopilotChatMessageView from "../CopilotChatMessageView.vue";
8
+
9
+ const AGENT_ID = "default";
10
+ const THREAD_ID = "thread-test";
11
+
12
+ function toolCall(id: string, name = "approve"): ToolCall {
13
+ return { id, type: "function", function: { name, arguments: "{}" } };
14
+ }
15
+
16
+ function assistantMsg(id: string, content?: string, toolCalls?: ToolCall[]) {
17
+ return { id, role: "assistant" as const, content, toolCalls } as Message;
18
+ }
19
+
20
+ /**
21
+ * Renders the view over a reactive message list and returns a setter, so a test
22
+ * can swap the list on the SAME mounted instance. Row teardown-vs-reuse is only
23
+ * observable that way — comparing DOM node identity across an update.
24
+ */
25
+ function renderWithSwappableMessages(initial: Message[]) {
26
+ const messages = ref<Message[]>(initial);
27
+
28
+ const Host = defineComponent({
29
+ components: {
30
+ CopilotKitProvider,
31
+ CopilotChatConfigurationProvider,
32
+ CopilotChatMessageView,
33
+ },
34
+ setup() {
35
+ return { messages, agentId: AGENT_ID, threadId: THREAD_ID };
36
+ },
37
+ template: `
38
+ <CopilotKitProvider runtime-url="/api/copilotkit">
39
+ <CopilotChatConfigurationProvider :agent-id="agentId" :thread-id="threadId">
40
+ <CopilotChatMessageView :messages="messages" />
41
+ </CopilotChatConfigurationProvider>
42
+ </CopilotKitProvider>
43
+ `,
44
+ });
45
+
46
+ render(Host);
47
+
48
+ return {
49
+ async setMessages(next: Message[]) {
50
+ messages.value = next;
51
+ // Let Vue flush the patch (and the post-flush prune watcher).
52
+ await new Promise((resolve) => setTimeout(resolve, 0));
53
+ },
54
+ };
55
+ }
56
+
57
+ function assistantRow() {
58
+ return screen.getByTestId("copilot-assistant-message");
59
+ }
60
+
61
+ describe("CopilotChatMessageView stable row keys", () => {
62
+ it("reuses the row when the message id changes but a tool call is stable", async () => {
63
+ const { setMessages } = renderWithSwappableMessages([
64
+ assistantMsg("lc_run--1", "Working...", [toolCall("call_A")]),
65
+ ]);
66
+ const node = assistantRow();
67
+
68
+ await setMessages([assistantMsg("resp_1", "Done", [toolCall("call_A")])]);
69
+
70
+ expect(assistantRow()).toBe(node);
71
+ expect(assistantRow().textContent).toContain("Done");
72
+ });
73
+
74
+ it("does not tear down the row when a tool call arrives mid-stream", async () => {
75
+ // Regression guard: keying by the tool-call id instead of recording an
76
+ // override tears the row down here, for every provider.
77
+ const { setMessages } = renderWithSwappableMessages([
78
+ assistantMsg("lc_run--1", "Let me check that..."),
79
+ ]);
80
+ const node = assistantRow();
81
+
82
+ await setMessages([
83
+ assistantMsg("lc_run--1", "Let me check that...", [toolCall("call_A")]),
84
+ ]);
85
+
86
+ expect(assistantRow()).toBe(node);
87
+ });
88
+
89
+ it("survives the whole sequence: text, then tool call, then id re-key", async () => {
90
+ const { setMessages } = renderWithSwappableMessages([
91
+ assistantMsg("lc_run--1", "Let me check..."),
92
+ ]);
93
+ const node = assistantRow();
94
+
95
+ await setMessages([
96
+ assistantMsg("lc_run--1", "Let me check...", [toolCall("call_A")]),
97
+ ]);
98
+ expect(assistantRow()).toBe(node);
99
+
100
+ await setMessages([assistantMsg("resp_1", "Done", [toolCall("call_A")])]);
101
+ expect(assistantRow()).toBe(node);
102
+ });
103
+
104
+ it("tears down a text-only row on an id change (documented limitation)", async () => {
105
+ const { setMessages } = renderWithSwappableMessages([
106
+ assistantMsg("lc_run--1", "Working..."),
107
+ ]);
108
+ const node = assistantRow();
109
+
110
+ await setMessages([assistantMsg("resp_1", "Done")]);
111
+
112
+ expect(assistantRow()).not.toBe(node);
113
+ });
114
+
115
+ it("renders both rows when a tool-call id is shared", async () => {
116
+ renderWithSwappableMessages([
117
+ assistantMsg("a-1", "First", [toolCall("call_X")]),
118
+ assistantMsg("a-2", "Second", [toolCall("call_X")]),
119
+ ]);
120
+
121
+ const rows = screen.getAllByTestId("copilot-assistant-message");
122
+ expect(rows).toHaveLength(2);
123
+ expect(rows[0]!.textContent).toContain("First");
124
+ expect(rows[1]!.textContent).toContain("Second");
125
+ });
126
+
127
+ it("keeps the first row when the list grows around it", async () => {
128
+ const { setMessages } = renderWithSwappableMessages([
129
+ assistantMsg("lc_run--1", "First", [toolCall("call_A")]),
130
+ ]);
131
+ const node = assistantRow();
132
+
133
+ await setMessages([
134
+ assistantMsg("resp_1", "First", [toolCall("call_A")]),
135
+ assistantMsg("lc_run--2", "Second", [toolCall("call_B", "b")]),
136
+ ]);
137
+
138
+ expect(screen.getAllByTestId("copilot-assistant-message")[0]).toBe(node);
139
+ });
140
+ });
@@ -254,6 +254,52 @@ describe("CopilotChatUserMessage", () => {
254
254
  });
255
255
  });
256
256
 
257
+ it("disables custom branch commands when no switch callback is available", async () => {
258
+ const message: UserMessage = {
259
+ id: "user-no-branch-handler",
260
+ role: "user",
261
+ timestamp: new Date(),
262
+ content: "Custom branch controls without a handler",
263
+ };
264
+
265
+ const wrapper = mount(CopilotChatUserMessage, {
266
+ props: {
267
+ message,
268
+ branchIndex: 0,
269
+ numberOfBranches: 2,
270
+ },
271
+ slots: {
272
+ "branch-navigation": ({
273
+ canGoPrev,
274
+ canGoNext,
275
+ goNext,
276
+ }: {
277
+ canGoPrev: boolean;
278
+ canGoNext: boolean;
279
+ goNext: () => void;
280
+ }) =>
281
+ h("div", [
282
+ h(
283
+ "span",
284
+ { "data-testid": "branch-capabilities" },
285
+ `${canGoPrev}:${canGoNext}`,
286
+ ),
287
+ h(
288
+ "button",
289
+ { "data-testid": "branch-next", onClick: goNext },
290
+ "next",
291
+ ),
292
+ ]),
293
+ },
294
+ });
295
+
296
+ expect(wrapper.get("[data-testid='branch-capabilities']").text()).toBe(
297
+ "false:false",
298
+ );
299
+ await wrapper.get("[data-testid='branch-next']").trigger("click");
300
+ expect(wrapper.emitted("switch-to-branch")).toBeUndefined();
301
+ });
302
+
257
303
  it("supports custom layout slot with all control callbacks", async () => {
258
304
  const onEditMessage = vi.fn();
259
305
  const onSwitchToBranch = vi.fn();
@@ -121,6 +121,124 @@ describe("CopilotChatView", () => {
121
121
  ]);
122
122
  });
123
123
 
124
+ it("forwards a static stop listener through the input slot", async () => {
125
+ const onStop = vi.fn();
126
+ const StaticStopHost = defineComponent({
127
+ components: {
128
+ CopilotKitProvider,
129
+ CopilotChatConfigurationProvider,
130
+ CopilotChatView,
131
+ },
132
+ setup() {
133
+ return { chatMessages, onStop };
134
+ },
135
+ template: `
136
+ <CopilotKitProvider runtime-url="/api/copilotkit">
137
+ <CopilotChatConfigurationProvider
138
+ thread-id="thread-1"
139
+ agent-id="default"
140
+ >
141
+ <CopilotChatView :messages="chatMessages" @stop="onStop">
142
+ <template #input="{ onStop: slotOnStop }">
143
+ <button data-testid="direct-stop" @click="slotOnStop">
144
+ stop
145
+ </button>
146
+ </template>
147
+ </CopilotChatView>
148
+ </CopilotChatConfigurationProvider>
149
+ </CopilotKitProvider>
150
+ `,
151
+ });
152
+ const wrapper = mount(StaticStopHost);
153
+
154
+ await wrapper.get("[data-testid='direct-stop']").trigger("click");
155
+
156
+ expect(onStop).toHaveBeenCalledTimes(1);
157
+ });
158
+
159
+ it("exposes canStop / canAddFile / canTranscribe on the input slot", () => {
160
+ const CapabilityHost = defineComponent({
161
+ components: {
162
+ CopilotKitProvider,
163
+ CopilotChatConfigurationProvider,
164
+ CopilotChatView,
165
+ },
166
+ setup() {
167
+ return { chatMessages };
168
+ },
169
+ template: `
170
+ <CopilotKitProvider runtime-url="/api/copilotkit">
171
+ <CopilotChatConfigurationProvider
172
+ thread-id="thread-1"
173
+ agent-id="default"
174
+ >
175
+ <CopilotChatView
176
+ :messages="chatMessages"
177
+ :on-stop="() => {}"
178
+ :on-add-file="() => {}"
179
+ >
180
+ <template
181
+ #input="{ canStop, canAddFile, canTranscribe }"
182
+ >
183
+ <span data-testid="can-stop">{{ canStop }}</span>
184
+ <span data-testid="can-add-file">{{ canAddFile }}</span>
185
+ <span data-testid="can-transcribe">{{ canTranscribe }}</span>
186
+ </template>
187
+ </CopilotChatView>
188
+ </CopilotChatConfigurationProvider>
189
+ </CopilotKitProvider>
190
+ `,
191
+ });
192
+ const wrapper = mount(CapabilityHost);
193
+
194
+ expect(wrapper.get("[data-testid='can-stop']").text()).toBe("true");
195
+ expect(wrapper.get("[data-testid='can-add-file']").text()).toBe("true");
196
+ expect(wrapper.get("[data-testid='can-transcribe']").text()).toBe("false");
197
+ });
198
+
199
+ it("exposes canStop / canAddFile / canTranscribe on the welcome-screen slot", () => {
200
+ const WelcomeCapabilityHost = defineComponent({
201
+ components: {
202
+ CopilotKitProvider,
203
+ CopilotChatConfigurationProvider,
204
+ CopilotChatView,
205
+ },
206
+ template: `
207
+ <CopilotKitProvider runtime-url="/api/copilotkit">
208
+ <CopilotChatConfigurationProvider
209
+ thread-id="thread-1"
210
+ agent-id="default"
211
+ >
212
+ <CopilotChatView
213
+ :messages="[]"
214
+ :on-stop="() => {}"
215
+ :on-start-transcribe="() => {}"
216
+ >
217
+ <template
218
+ #welcome-screen="{ canStop, canAddFile, canTranscribe }"
219
+ >
220
+ <span data-testid="welcome-can-stop">{{ canStop }}</span>
221
+ <span data-testid="welcome-can-add-file">{{ canAddFile }}</span>
222
+ <span data-testid="welcome-can-transcribe">{{
223
+ canTranscribe
224
+ }}</span>
225
+ </template>
226
+ </CopilotChatView>
227
+ </CopilotChatConfigurationProvider>
228
+ </CopilotKitProvider>
229
+ `,
230
+ });
231
+ const wrapper = mount(WelcomeCapabilityHost);
232
+
233
+ expect(wrapper.get("[data-testid='welcome-can-stop']").text()).toBe("true");
234
+ expect(wrapper.get("[data-testid='welcome-can-add-file']").text()).toBe(
235
+ "false",
236
+ );
237
+ expect(wrapper.get("[data-testid='welcome-can-transcribe']").text()).toBe(
238
+ "true",
239
+ );
240
+ });
241
+
124
242
  it("renders suggestions and forwards selection callback and event", async () => {
125
243
  const onSelectSuggestion = vi.fn();
126
244
  const wrapper = mountChatView({
@@ -55,6 +55,43 @@ describe("CopilotPopupView", () => {
55
55
  );
56
56
  });
57
57
 
58
+ it("only exposes stop capability when an onStop callback is available", async () => {
59
+ const withoutStop = mountPopupView(
60
+ {},
61
+ {
62
+ "welcome-screen": ({ canStop }: { canStop: boolean }) =>
63
+ h("div", { "data-testid": "popup-can-stop" }, String(canStop)),
64
+ },
65
+ );
66
+
67
+ expect(withoutStop.get("[data-testid='popup-can-stop']").text()).toBe(
68
+ "false",
69
+ );
70
+
71
+ const onStop = vi.fn();
72
+ const withStop = mountPopupView(
73
+ { onStop },
74
+ {
75
+ "welcome-screen": ({
76
+ canStop,
77
+ onStop: stop,
78
+ }: {
79
+ canStop: boolean;
80
+ onStop: () => void;
81
+ }) =>
82
+ h(
83
+ "button",
84
+ { "data-testid": "popup-stop", onClick: stop },
85
+ String(canStop),
86
+ ),
87
+ },
88
+ );
89
+
90
+ expect(withStop.get("[data-testid='popup-stop']").text()).toBe("true");
91
+ await withStop.get("[data-testid='popup-stop']").trigger("click");
92
+ expect(onStop).toHaveBeenCalledTimes(1);
93
+ });
94
+
58
95
  it("closes on escape and outside pointerdown when enabled", async () => {
59
96
  const wrapper = mountPopupView({
60
97
  clickOutsideToClose: true,
@@ -109,4 +146,46 @@ describe("CopilotPopupView", () => {
109
146
  it("exposes the WelcomeScreen namespaced export", () => {
110
147
  expect(NamespacedPopupView.WelcomeScreen).toBeDefined();
111
148
  });
149
+
150
+ it("passes capability flags through the namespaced WelcomeScreen input slot", () => {
151
+ const wrapper = mount(NamespacedPopupView.WelcomeScreen, {
152
+ props: {
153
+ suggestions: [],
154
+ loadingIndexes: [],
155
+ modelValue: "",
156
+ isRunning: false,
157
+ inputMode: "input",
158
+ inputToolsMenu: [],
159
+ canStop: false,
160
+ canAddFile: false,
161
+ canTranscribe: false,
162
+ onUpdateModelValue: vi.fn(),
163
+ onSubmitMessage: vi.fn(),
164
+ onStop: vi.fn(),
165
+ onAddFile: vi.fn(),
166
+ onStartTranscribe: vi.fn(),
167
+ onSelectSuggestion: vi.fn(),
168
+ },
169
+ slots: {
170
+ input: ({
171
+ canStop,
172
+ canAddFile,
173
+ canTranscribe,
174
+ }: {
175
+ canStop: boolean;
176
+ canAddFile: boolean;
177
+ canTranscribe: boolean;
178
+ }) =>
179
+ h(
180
+ "div",
181
+ { "data-testid": "popup-welcome-capabilities" },
182
+ `${canStop}:${canAddFile}:${canTranscribe}`,
183
+ ),
184
+ },
185
+ });
186
+
187
+ expect(
188
+ wrapper.get("[data-testid='popup-welcome-capabilities']").text(),
189
+ ).toBe("false:false:false");
190
+ });
112
191
  });