@copilotkit/vue 1.79.2-canary.1792172552 → 1.81.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 (44) hide show
  1. package/PARITY.md +1 -0
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.mjs +3 -2
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/styles.css +1 -1
  7. package/dist/{use-render-activity-message-DlRZfpDh.js → use-render-activity-message-5H6AfuJB.js} +3069 -3043
  8. package/dist/use-render-activity-message-5H6AfuJB.js.map +1 -0
  9. package/dist/use-render-activity-message-DxOdM2Qt.cjs +70 -0
  10. package/dist/use-render-activity-message-DxOdM2Qt.cjs.map +1 -0
  11. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
  12. package/dist/v2/components/MCPAppsActivityRenderer.d.ts +21 -0
  13. package/dist/v2/components/MCPAppsActivityRenderer.d.ts.map +1 -1
  14. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +30 -0
  15. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  16. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  17. package/dist/v2/index.cjs +1 -1
  18. package/dist/v2/index.mjs +1 -1
  19. package/dist/v2/providers/CopilotKitProvider.types.d.ts +12 -0
  20. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  21. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +5 -1
  22. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  23. package/dist/v2/providers/types.d.ts +5 -0
  24. package/dist/v2/providers/types.d.ts.map +1 -1
  25. package/package.json +9 -9
  26. package/src/styles/globals.css +9 -0
  27. package/src/v2/components/A2UISurfaceActivityRenderer.vue +17 -12
  28. package/src/v2/components/MCPAppsActivityRenderer.ts +14 -0
  29. package/src/v2/components/__tests__/MCPAppsActivityRenderer.e2e.test.ts +74 -2
  30. package/src/v2/components/chat/CopilotChatInput.vue +5 -1
  31. package/src/v2/components/chat/CopilotChatUserMessage.vue +4 -4
  32. package/src/v2/components/chat/CopilotChatView.vue +2 -1
  33. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +39 -0
  34. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.ts +2 -1
  35. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.test.ts +42 -1
  36. package/src/v2/components/chat/__tests__/CopilotChatView.rootWidth.test.ts +69 -0
  37. package/src/v2/components/chat/__tests__/CopilotChatView.test.ts +56 -0
  38. package/src/v2/providers/CopilotKitProvider.types.ts +12 -0
  39. package/src/v2/providers/CopilotKitProvider.vue +14 -2
  40. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxReplay.test.ts +157 -0
  41. package/src/v2/providers/types.ts +5 -0
  42. package/dist/use-render-activity-message-CGgJhO9J.cjs +0 -70
  43. package/dist/use-render-activity-message-CGgJhO9J.cjs.map +0 -1
  44. package/dist/use-render-activity-message-DlRZfpDh.js.map +0 -1
@@ -290,8 +290,8 @@ onBeforeUnmount(() => {
290
290
  :class="toolbarButtonClass"
291
291
  class="cpk:h-6 cpk:w-6 cpk:p-0"
292
292
  :disabled="!canGoPrev"
293
- aria-label="Previous branch"
294
- title="Previous branch"
293
+ :aria-label="labels.userMessageToolbarPreviousBranchLabel"
294
+ :title="labels.userMessageToolbarPreviousBranchLabel"
295
295
  @click="goPrev"
296
296
  >
297
297
  <IconChevronLeft class="cpk:size-[20px]" />
@@ -306,8 +306,8 @@ onBeforeUnmount(() => {
306
306
  :class="toolbarButtonClass"
307
307
  class="cpk:h-6 cpk:w-6 cpk:p-0"
308
308
  :disabled="!canGoNext"
309
- aria-label="Next branch"
310
- title="Next branch"
309
+ :aria-label="labels.userMessageToolbarNextBranchLabel"
310
+ :title="labels.userMessageToolbarNextBranchLabel"
311
311
  @click="goNext"
312
312
  >
313
313
  <IconChevronRight class="cpk:size-[20px]" />
@@ -441,7 +441,7 @@ onBeforeUnmount(() => {
441
441
  <template>
442
442
  <div
443
443
  data-copilotkit
444
- class="cpk:@container cpk:relative cpk:h-full"
444
+ class="copilotKitChat cpk:@container cpk:relative cpk:h-full"
445
445
  data-testid="copilot-chat-view"
446
446
  v-bind="$attrs"
447
447
  @dragover="handleDragOver"
@@ -660,6 +660,7 @@ onBeforeUnmount(() => {
660
660
  <button
661
661
  type="button"
662
662
  data-testid="copilot-chat-view-scroll-to-bottom"
663
+ :aria-label="labels.chatScrollToBottomLabel"
663
664
  class="cpk:rounded-full cpk:w-10 cpk:h-10 cpk:p-0 cpk:pointer-events-auto cpk:bg-white cpk:dark:bg-gray-900 cpk:shadow-lg cpk:border cpk:border-gray-200 cpk:dark:border-gray-700 cpk:hover:bg-gray-50 cpk:dark:hover:bg-gray-800 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer"
664
665
  @click="scrollToBottom()"
665
666
  >
@@ -535,6 +535,45 @@ describe("CopilotChatInput", () => {
535
535
  });
536
536
  });
537
537
 
538
+ // The send button shows only an icon, so its accessible name comes from the
539
+ // labels config; that is also what lets an app translate it.
540
+ it("names the send button with the default label", () => {
541
+ renderWithProvider({
542
+ props: { modelValue: "" },
543
+ listeners: { onSubmitMessage: mockOnSubmitMessage },
544
+ });
545
+
546
+ expect(screen.getByRole("button", { name: "Send message" })).toBe(
547
+ getSendButton(),
548
+ );
549
+ });
550
+
551
+ it("names the send button Stop while it stops the run", () => {
552
+ renderWithProvider({
553
+ props: { modelValue: "", isRunning: true },
554
+ listeners: { onSubmitMessage: mockOnSubmitMessage, onStop: vi.fn() },
555
+ });
556
+
557
+ expect(screen.getByRole("button", { name: "Stop" })).toBe(getSendButton());
558
+ });
559
+
560
+ it("names the send button with the configured labels", () => {
561
+ renderWithProvider({
562
+ template: `
563
+ <CopilotChatConfigurationProvider
564
+ :thread-id="TEST_THREAD_ID"
565
+ :labels="{ chatInputToolbarSendButtonLabel: 'Nachricht senden' }"
566
+ >
567
+ <CopilotChatInput model-value="" />
568
+ </CopilotChatConfigurationProvider>
569
+ `,
570
+ });
571
+
572
+ expect(screen.getByRole("button", { name: "Nachricht senden" })).toBe(
573
+ getSendButton(),
574
+ );
575
+ });
576
+
538
577
  it("shows cancel and finish buttons in transcribe mode", () => {
539
578
  renderWithProvider({
540
579
  props: {
@@ -774,7 +774,7 @@ describe("Partial Args Accumulation", () => {
774
774
 
775
775
  await waitFor(() => {
776
776
  const tool = screen.getByTestId("complex-tool");
777
- expect(tool.textContent).toContain("Age: 30");
777
+ expect(tool.textContent).toContain("Age: pending");
778
778
  expect(tool.textContent).toContain("City: pending");
779
779
  });
780
780
 
@@ -787,6 +787,7 @@ describe("Partial Args Accumulation", () => {
787
787
 
788
788
  await waitFor(() => {
789
789
  const tool = screen.getByTestId("complex-tool");
790
+ expect(tool.textContent).toContain("Age: 30");
790
791
  expect(tool.textContent).toContain("City: Paris");
791
792
  expect(tool.textContent).toMatch(/Status: (complete|inProgress)/i);
792
793
  });
@@ -1,9 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { h, nextTick } from "vue";
2
+ import { defineComponent, h, nextTick } from "vue";
3
+ import { render, screen } from "@testing-library/vue";
3
4
  import { mount } from "@vue/test-utils";
4
5
  import type { UserMessage } from "@ag-ui/core";
5
6
  import { CopilotChatDefaultLabels } from "../../../providers/types";
6
7
  import CopilotChatUserMessage from "../CopilotChatUserMessage.vue";
8
+ import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
7
9
 
8
10
  describe("CopilotChatUserMessage", () => {
9
11
  let originalClipboard: Clipboard | undefined;
@@ -101,6 +103,45 @@ describe("CopilotChatUserMessage", () => {
101
103
  ).toBe(false);
102
104
  });
103
105
 
106
+ it("names the branch buttons with the configured labels", () => {
107
+ const Host = defineComponent({
108
+ components: { CopilotChatConfigurationProvider, CopilotChatUserMessage },
109
+ setup() {
110
+ return {
111
+ message: {
112
+ id: "user-5",
113
+ role: "user",
114
+ timestamp: new Date(),
115
+ content: "Branch message",
116
+ } as UserMessage,
117
+ labels: {
118
+ userMessageToolbarPreviousBranchLabel: "Vorherige Antwort",
119
+ userMessageToolbarNextBranchLabel: "Nächste Antwort",
120
+ },
121
+ onSwitchToBranch: vi.fn(),
122
+ };
123
+ },
124
+ template: `
125
+ <CopilotChatConfigurationProvider thread-id="thread-1" :labels="labels">
126
+ <CopilotChatUserMessage
127
+ :message="message"
128
+ :branch-index="1"
129
+ :number-of-branches="3"
130
+ :on-switch-to-branch="onSwitchToBranch"
131
+ />
132
+ </CopilotChatConfigurationProvider>
133
+ `,
134
+ });
135
+ render(Host);
136
+
137
+ expect(
138
+ screen.getByRole("button", { name: "Vorherige Antwort" }),
139
+ ).toBeDefined();
140
+ expect(
141
+ screen.getByRole("button", { name: "Nächste Antwort" }),
142
+ ).toBeDefined();
143
+ });
144
+
104
145
  it("renders branch navigation and emits switch payload exactly once", async () => {
105
146
  const onSwitchToBranch = vi.fn();
106
147
  const message: UserMessage = {
@@ -0,0 +1,69 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+ import { h } from "vue";
6
+ import { mount } from "@vue/test-utils";
7
+ import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
8
+ import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
9
+ import CopilotChatView from "../CopilotChatView.vue";
10
+
11
+ const CHAT_ROOT_SELECTOR = ".copilotKitChat[data-copilotkit]";
12
+
13
+ // Resolve from this file, so the read does not depend on the working directory.
14
+ // (A `new URL(...)` object fails here: under jsdom it is not Node's URL.)
15
+ const globalsCss = readFileSync(
16
+ resolve(
17
+ dirname(fileURLToPath(import.meta.url)),
18
+ "../../../../styles/globals.css",
19
+ ),
20
+ { encoding: "utf8" },
21
+ );
22
+
23
+ /** Returns the body of the first `@layer <name> { ... }` block. */
24
+ function layerBody(css: string, name: string): string {
25
+ const start = css.indexOf(`@layer ${name} {`);
26
+ if (start === -1) return "";
27
+ let depth = 0;
28
+ for (let i = css.indexOf("{", start); i < css.length; i++) {
29
+ if (css[i] === "{") depth++;
30
+ if (css[i] === "}" && --depth === 0) {
31
+ return css.slice(css.indexOf("{", start) + 1, i);
32
+ }
33
+ }
34
+ return "";
35
+ }
36
+
37
+ function mountChatView(props: Record<string, unknown>) {
38
+ return mount(CopilotKitProvider, {
39
+ props: { runtimeUrl: "/api/copilotkit" },
40
+ slots: {
41
+ default: () =>
42
+ h(
43
+ CopilotChatConfigurationProvider,
44
+ { threadId: "t1" },
45
+ { default: () => h(CopilotChatView, props) },
46
+ ),
47
+ },
48
+ });
49
+ }
50
+
51
+ // PE-1053, the Vue side of PE-788: the chat root is an inline-size container
52
+ // (`cpk:@container`), so its content gives it no intrinsic width. As a flex
53
+ // item in a row its auto width collapsed to a narrow column. jsdom does no
54
+ // layout, so these tests pin the two halves of the fix: the base-layer width
55
+ // rule, and a chat root that the rule's selector matches.
56
+ describe("CopilotChatView root width (PE-1053)", () => {
57
+ it("fills its container from the base layer, so app widths still win", () => {
58
+ const base = layerBody(globalsCss, "base").replace(/\s+/g, " ");
59
+ expect(base).toContain(`${CHAT_ROOT_SELECTOR} { width: 100%; }`);
60
+ });
61
+
62
+ // The welcome screen and the message view share this one root element.
63
+ it("the chat root matches the width rule", () => {
64
+ const wrapper = mountChatView({ messages: [] });
65
+ const root = wrapper.find("[data-testid='copilot-chat-view']");
66
+ expect(root.exists()).toBe(true);
67
+ expect(root.element.matches(CHAT_ROOT_SELECTOR)).toBe(true);
68
+ });
69
+ });
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { defineComponent, h, nextTick, ref } from "vue";
3
3
  import { mount } from "@vue/test-utils";
4
+ import { render, screen } from "@testing-library/vue";
4
5
  import type { AssistantMessage, Message, UserMessage } from "@ag-ui/core";
5
6
  import type { Suggestion } from "@copilotkit/core";
6
7
  import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
@@ -404,6 +405,61 @@ describe("CopilotChatView", () => {
404
405
  .trigger("click");
405
406
  });
406
407
 
408
+ it("names the scroll-to-bottom button with the configured label", async () => {
409
+ const Host = defineComponent({
410
+ components: {
411
+ CopilotKitProvider,
412
+ CopilotChatConfigurationProvider,
413
+ CopilotChatView,
414
+ },
415
+ setup() {
416
+ return {
417
+ messages: createChatMessages(12),
418
+ labels: { chatScrollToBottomLabel: "Nach unten scrollen" },
419
+ };
420
+ },
421
+ template: `
422
+ <CopilotKitProvider runtime-url="/api/copilotkit">
423
+ <CopilotChatConfigurationProvider
424
+ thread-id="thread-1"
425
+ agent-id="default"
426
+ :labels="labels"
427
+ >
428
+ <CopilotChatView :messages="messages" />
429
+ </CopilotChatConfigurationProvider>
430
+ </CopilotKitProvider>
431
+ `,
432
+ });
433
+ render(Host);
434
+
435
+ // Let the initial auto-scroll settle, then scroll away from the bottom so
436
+ // the button appears.
437
+ const scrollElement = await screen.findByTestId("copilot-chat-view-scroll");
438
+ await new Promise((resolve) => setTimeout(resolve, 0));
439
+ let scrollTop = 0;
440
+ Object.defineProperty(scrollElement, "scrollHeight", {
441
+ configurable: true,
442
+ get: () => 1000,
443
+ });
444
+ Object.defineProperty(scrollElement, "clientHeight", {
445
+ configurable: true,
446
+ get: () => 300,
447
+ });
448
+ Object.defineProperty(scrollElement, "scrollTop", {
449
+ configurable: true,
450
+ get: () => scrollTop,
451
+ set: (value: number) => {
452
+ scrollTop = value;
453
+ },
454
+ });
455
+ scrollTop = 120;
456
+ scrollElement.dispatchEvent(new Event("scroll"));
457
+
458
+ expect(
459
+ await screen.findByRole("button", { name: "Nach unten scrollen" }),
460
+ ).toBeDefined();
461
+ });
462
+
407
463
  it("preserves scroll position when new messages arrive and the user is scrolled up", async () => {
408
464
  const messages = ref<Message[]>(createChatMessages(12));
409
465
 
@@ -92,6 +92,18 @@ export interface CopilotKitProviderProps {
92
92
  loadingComponent?: Component;
93
93
  includeSchema?: boolean;
94
94
  };
95
+ /** Configuration for the built-in MCP Apps renderer. */
96
+ mcpApps?: {
97
+ /**
98
+ * Restore `allow-same-origin` on the MCP App sandbox. Default `false`.
99
+ *
100
+ * **Unsafe.** The widget then runs with your page's origin: it can read
101
+ * your cookies, storage and DOM, and call your APIs as the signed-in user.
102
+ * Only set it for widgets you wrote and trust that need `localStorage`,
103
+ * `IndexedDB` or cookies. By default widgets get an opaque origin.
104
+ */
105
+ allowSameOriginFallback?: boolean;
106
+ };
95
107
  /**
96
108
  * Enable debug logging for the client-side event pipeline.
97
109
  *
@@ -10,6 +10,7 @@ import {
10
10
  triggerRef,
11
11
  watch,
12
12
  } from "vue";
13
+ import type { Component, FunctionalComponent } from "vue";
13
14
  import { z } from "zod";
14
15
  import type { AbstractAgent } from "@ag-ui/client";
15
16
  import { ToolCallStatus } from "@copilotkit/core";
@@ -91,6 +92,13 @@ const DEFAULT_DESIGN_SKILL = `When generating UI with generateSandboxedUi, follo
91
92
  - Minimal transitions (150ms) for hover/focus states only. No decorative animations.
92
93
  - Keep the UI focused and dense — avoid excessive padding. Use compact spacing (8–12px gaps, 10–14px padding in controls).`;
93
94
 
95
+ /** The MCP Apps renderer with `mcpApps.allowSameOriginFallback` applied. */
96
+ const SameOriginMCPAppsActivityRenderer: FunctionalComponent = (_, { attrs }) =>
97
+ h(MCPAppsActivityRenderer as Component, {
98
+ ...attrs,
99
+ allowSameOriginFallback: true,
100
+ });
101
+
94
102
  const props = withDefaults(defineProps<CopilotKitProviderProps>(), {
95
103
  headers: () => ({}),
96
104
  properties: () => ({}),
@@ -103,6 +111,7 @@ const props = withDefaults(defineProps<CopilotKitProviderProps>(), {
103
111
  openGenerativeUI: undefined,
104
112
  useSingleEndpoint: undefined,
105
113
  a2ui: undefined,
114
+ mcpApps: undefined,
106
115
  enableInspector: undefined,
107
116
  });
108
117
 
@@ -424,6 +433,8 @@ const builtInFrontendTools = computed<VueFrontendTool[]>(() => {
424
433
  description: GENERATE_SANDBOXED_UI_DESCRIPTION,
425
434
  parameters: GenerateSandboxedUiArgsSchema,
426
435
  handler: async () => "UI generated",
436
+ // This acknowledgement has no side effects and is safe to reconstruct.
437
+ reconnectBehavior: "resume-pending",
427
438
  followUp: true,
428
439
  render:
429
440
  OpenGenerativeUIToolRenderer as unknown as VueFrontendTool["render"],
@@ -438,8 +449,9 @@ const builtInActivityRenderers = computed<
438
449
  {
439
450
  activityType: MCPAppsActivityType,
440
451
  content: MCPAppsActivityContentSchema as unknown as z.ZodSchema<unknown>,
441
- render:
442
- MCPAppsActivityRenderer as unknown as VueActivityMessageRenderer<unknown>["render"],
452
+ render: (props.mcpApps?.allowSameOriginFallback
453
+ ? SameOriginMCPAppsActivityRenderer
454
+ : MCPAppsActivityRenderer) as unknown as VueActivityMessageRenderer<unknown>["render"],
443
455
  },
444
456
  ];
445
457
 
@@ -0,0 +1,157 @@
1
+ import { cleanup, render, waitFor } from "@testing-library/vue";
2
+ import { defineComponent } from "vue";
3
+ import CopilotKitProvider from "../CopilotKitProvider.vue";
4
+ import { useCopilotKit } from "../useCopilotKit";
5
+ import { HttpAgent } from "@ag-ui/client";
6
+ import type { Message } from "@ag-ui/client";
7
+ import type { CopilotKitCore } from "@copilotkit/core";
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+
10
+ /** Capture the real provider core after its built-in sandbox tool is registered. */
11
+ async function mountProvider() {
12
+ let core: CopilotKitCore | undefined;
13
+ const Capture = defineComponent({
14
+ /** Expose the injected core without retaining Vue's reactive wrapper. */
15
+ setup() {
16
+ core = useCopilotKit().copilotkit.value;
17
+ return () => null;
18
+ },
19
+ });
20
+ render(
21
+ defineComponent({
22
+ components: { CopilotKitProvider, Capture },
23
+ template:
24
+ '<CopilotKitProvider :open-generative-u-i="{}"><Capture /></CopilotKitProvider>',
25
+ }),
26
+ );
27
+ await waitFor(() =>
28
+ expect(core?.getTool({ toolName: "generateSandboxedUi" })).toBeDefined(),
29
+ );
30
+ if (!core) throw new Error("Missing core");
31
+ return core;
32
+ }
33
+
34
+ /** Hydrate imported tool calls with controlled approval and side-effect handlers. */
35
+ async function setupReplay(completed = false) {
36
+ const core = await mountProvider();
37
+ const agent = new HttpAgent({
38
+ url: "https://runtime.invalid",
39
+ agentId: "default",
40
+ threadId: "imported-thread",
41
+ });
42
+ const saved: Message[] = [
43
+ {
44
+ id: "imported-message",
45
+ role: "assistant",
46
+ toolCalls: [
47
+ {
48
+ id: "sandbox-id",
49
+ type: "function",
50
+ function: {
51
+ name: "generateSandboxedUi",
52
+ arguments: JSON.stringify({ html: "<button>Calculator</button>" }),
53
+ },
54
+ },
55
+ {
56
+ id: "approval-id",
57
+ type: "function",
58
+ function: { name: "meeting", arguments: "{}" },
59
+ },
60
+ {
61
+ id: "side-effect-id",
62
+ type: "function",
63
+ function: { name: "sendEmail", arguments: "{}" },
64
+ },
65
+ ],
66
+ },
67
+ ];
68
+ if (completed)
69
+ saved.push(
70
+ {
71
+ id: "saved-sandbox",
72
+ role: "tool",
73
+ toolCallId: "sandbox-id",
74
+ content: "UI generated",
75
+ },
76
+ {
77
+ id: "saved-approval",
78
+ role: "tool",
79
+ toolCallId: "approval-id",
80
+ content: "approved",
81
+ },
82
+ );
83
+ vi.spyOn(agent, "connectAgent").mockImplementation(async () => {
84
+ agent.setMessages(structuredClone(saved));
85
+ return { result: undefined, newMessages: structuredClone(saved) };
86
+ });
87
+ const submissions: Message[][] = [];
88
+ const run = vi.spyOn(agent, "runAgent").mockImplementation(async () => {
89
+ submissions.push(structuredClone(agent.messages));
90
+ return { result: undefined, newMessages: [] };
91
+ });
92
+ let answer!: (value: string) => void;
93
+ const pending = new Promise<string>((resolve) => {
94
+ answer = resolve;
95
+ });
96
+ const approval = vi.fn(() => pending);
97
+ const sideEffect = vi.fn(async () => "sent");
98
+ core.addTool({
99
+ name: "meeting",
100
+ type: "human-in-the-loop",
101
+ handler: approval,
102
+ });
103
+ core.addTool({ name: "sendEmail", handler: sideEffect });
104
+ return { core, agent, saved, run, submissions, answer, approval, sideEffect };
105
+ }
106
+
107
+ describe("CopilotKitProvider — sandboxed UI replay", () => {
108
+ afterEach(() => {
109
+ cleanup();
110
+ vi.restoreAllMocks();
111
+ });
112
+
113
+ it("restores the original sandbox result before the pending approval continues", async () => {
114
+ const { core, agent, run, submissions, answer, approval, sideEffect } =
115
+ await setupReplay();
116
+ await core.connectAgent({ agent });
117
+ await waitFor(() => expect(approval).toHaveBeenCalledTimes(1));
118
+ expect(agent.messages).toContainEqual(
119
+ expect.objectContaining({
120
+ role: "tool",
121
+ toolCallId: "sandbox-id",
122
+ content: "UI generated",
123
+ }),
124
+ );
125
+ expect(run).not.toHaveBeenCalled();
126
+ answer("approved");
127
+ await waitFor(() => expect(run).toHaveBeenCalledTimes(1));
128
+ expect(
129
+ submissions[0]?.filter((message) => message.role === "tool"),
130
+ ).toEqual([
131
+ expect.objectContaining({
132
+ toolCallId: "sandbox-id",
133
+ content: "UI generated",
134
+ }),
135
+ expect.objectContaining({
136
+ toolCallId: "approval-id",
137
+ content: "approved",
138
+ }),
139
+ ]);
140
+ expect(sideEffect).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it("leaves saved sandbox and approval results completed across repeated reconnects", async () => {
144
+ const { core, agent, saved, run, approval, sideEffect } =
145
+ await setupReplay(true);
146
+ const tool = core.getTool({ toolName: "generateSandboxedUi" });
147
+ if (!tool?.handler) throw new Error("Missing sandbox handler");
148
+ const handler = vi.spyOn(tool, "handler");
149
+ await core.connectAgent({ agent });
150
+ await core.connectAgent({ agent });
151
+ expect(agent.messages).toEqual(saved);
152
+ expect(handler).not.toHaveBeenCalled();
153
+ expect(approval).not.toHaveBeenCalled();
154
+ expect(sideEffect).not.toHaveBeenCalled();
155
+ expect(run).not.toHaveBeenCalled();
156
+ });
157
+ });
@@ -5,6 +5,8 @@ export const CopilotChatDefaultLabels = {
5
5
  chatInputToolbarFinishTranscribeButtonLabel: "Finish",
6
6
  chatInputToolbarAddButtonLabel: "Add photos or files",
7
7
  chatInputToolbarToolsButtonLabel: "Tools",
8
+ chatInputToolbarSendButtonLabel: "Send message",
9
+ chatInputToolbarStopButtonLabel: "Stop",
8
10
  assistantMessageToolbarCopyCodeLabel: "Copy",
9
11
  assistantMessageToolbarCopyCodeCopiedLabel: "Copied",
10
12
  assistantMessageToolbarCopyMessageLabel: "Copy",
@@ -14,10 +16,13 @@ export const CopilotChatDefaultLabels = {
14
16
  assistantMessageToolbarRegenerateLabel: "Regenerate",
15
17
  userMessageToolbarCopyMessageLabel: "Copy",
16
18
  userMessageToolbarEditMessageLabel: "Edit",
19
+ userMessageToolbarPreviousBranchLabel: "Previous branch",
20
+ userMessageToolbarNextBranchLabel: "Next branch",
17
21
  chatDisclaimerText:
18
22
  "AI can make mistakes. Please verify important information.",
19
23
  chatToggleOpenLabel: "Open chat",
20
24
  chatToggleCloseLabel: "Close chat",
25
+ chatScrollToBottomLabel: "Scroll to bottom",
21
26
  modalHeaderTitle: "CopilotKit Chat",
22
27
  welcomeMessageText: "How can I help you today?",
23
28
  } as const;