@copilotkit/vue 1.72.1-canary.sp-longthread-perf → 1.73.0

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 (61) hide show
  1. package/PARITY.md +15 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.mjs +2 -2
  4. package/dist/use-render-activity-message-C45MYgwB.cjs +85 -0
  5. package/dist/use-render-activity-message-C45MYgwB.cjs.map +1 -0
  6. package/dist/use-render-activity-message-DI4kGqHF.js +9758 -0
  7. package/dist/use-render-activity-message-DI4kGqHF.js.map +1 -0
  8. package/dist/v2/components/LicenseWarningBanner.vue.d.ts +3 -5
  9. package/dist/v2/components/LicenseWarningBanner.vue.d.ts.map +1 -1
  10. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  11. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +4 -0
  12. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  13. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +7 -0
  14. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  15. package/dist/v2/components/chat/CopilotChatUserMessage.vue.d.ts +2 -0
  16. package/dist/v2/components/chat/CopilotChatUserMessage.vue.d.ts.map +1 -1
  17. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +8 -2
  18. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  19. package/dist/v2/components/chat/CopilotPopup.vue.d.ts.map +1 -1
  20. package/dist/v2/components/chat/CopilotPopupView.vue.d.ts.map +1 -1
  21. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  22. package/dist/v2/components/chat/CopilotPopupWelcomeScreen.vue.d.ts +7 -4
  23. package/dist/v2/components/chat/CopilotPopupWelcomeScreen.vue.d.ts.map +1 -1
  24. package/dist/v2/components/chat/CopilotSidebar.vue.d.ts.map +1 -1
  25. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts.map +1 -1
  26. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  27. package/dist/v2/components/chat/CopilotSidebarWelcomeScreen.vue.d.ts +8 -5
  28. package/dist/v2/components/chat/CopilotSidebarWelcomeScreen.vue.d.ts.map +1 -1
  29. package/dist/v2/components/chat/index.d.ts +50 -26
  30. package/dist/v2/components/chat/index.d.ts.map +1 -1
  31. package/dist/v2/components/chat/types.d.ts +44 -17
  32. package/dist/v2/components/chat/types.d.ts.map +1 -1
  33. package/dist/v2/index.cjs +1 -1
  34. package/dist/v2/index.mjs +1 -1
  35. package/package.json +6 -6
  36. package/src/v2/components/LicenseWarningBanner.vue +5 -17
  37. package/src/v2/components/__tests__/LicenseWarningBanner.test.ts +23 -0
  38. package/src/v2/components/chat/CopilotChat.vue +21 -14
  39. package/src/v2/components/chat/CopilotChatAssistantMessage.vue +12 -21
  40. package/src/v2/components/chat/CopilotChatInput.vue +32 -26
  41. package/src/v2/components/chat/CopilotChatUserMessage.vue +19 -19
  42. package/src/v2/components/chat/CopilotChatView.vue +31 -39
  43. package/src/v2/components/chat/CopilotPopup.vue +8 -10
  44. package/src/v2/components/chat/CopilotPopupView.vue +8 -18
  45. package/src/v2/components/chat/CopilotPopupViewInternal.vue +5 -19
  46. package/src/v2/components/chat/CopilotPopupWelcomeScreen.vue +36 -6
  47. package/src/v2/components/chat/CopilotSidebar.vue +8 -10
  48. package/src/v2/components/chat/CopilotSidebarView.vue +8 -18
  49. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +5 -19
  50. package/src/v2/components/chat/CopilotSidebarWelcomeScreen.vue +36 -6
  51. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.ts +156 -5
  52. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +34 -0
  53. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.test.ts +46 -0
  54. package/src/v2/components/chat/__tests__/CopilotChatView.test.ts +118 -0
  55. package/src/v2/components/chat/__tests__/CopilotPopupView.test.ts +79 -0
  56. package/src/v2/components/chat/__tests__/CopilotSidebarView.test.ts +93 -0
  57. package/src/v2/components/chat/types.ts +73 -18
  58. package/dist/use-render-activity-message-Ciee03Rx.js +0 -9702
  59. package/dist/use-render-activity-message-Ciee03Rx.js.map +0 -1
  60. package/dist/use-render-activity-message-DXJDZGkW.cjs +0 -85
  61. 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: {
@@ -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
  });
@@ -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
  });