@copilotkit/vue 1.83.0 → 2.0.0-rc.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 (64) hide show
  1. package/AGENTS.md +1 -1
  2. package/dist/index.cjs +76 -1
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.mjs +14290 -287
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/styles.css +1 -1
  7. package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
  8. package/dist/v2/components/a2ui/catalog.d.ts +1 -1
  9. package/dist/v2/components/a2ui/catalog.d.ts.map +1 -1
  10. package/dist/v2/components/chat/BasicMarkdown.vue.d.ts +9 -0
  11. package/dist/v2/components/chat/BasicMarkdown.vue.d.ts.map +1 -0
  12. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  13. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
  14. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts +10 -0
  15. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -0
  16. package/dist/v2/components/chat/index.d.ts +2 -0
  17. package/dist/v2/components/chat/index.d.ts.map +1 -1
  18. package/dist/v2/hooks/index.d.ts +0 -1
  19. package/dist/v2/hooks/index.d.ts.map +1 -1
  20. package/dist/v2/hooks/use-pin-to-send.d.ts +1 -1
  21. package/dist/v2/index.cjs +1 -1
  22. package/dist/v2/index.mjs +195 -188
  23. package/dist/v2/providers/CopilotKitProvider.types.d.ts +10 -0
  24. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  25. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  26. package/dist/v2/providers/index.d.ts +1 -0
  27. package/dist/v2/providers/index.d.ts.map +1 -1
  28. package/dist/v2/providers/markdown-renderer.d.ts +23 -0
  29. package/dist/v2/providers/markdown-renderer.d.ts.map +1 -0
  30. package/package.json +8 -9
  31. package/src/styles/globals.css +0 -97
  32. package/src/v2/components/A2UIMessageRenderer.ts +3 -2
  33. package/src/v2/components/a2ui/catalog.ts +4 -2
  34. package/src/v2/components/chat/BasicMarkdown.vue +158 -0
  35. package/src/v2/components/chat/CopilotChatAssistantMessage.vue +27 -536
  36. package/src/v2/components/chat/CopilotChatReasoningMessage.vue +7 -2
  37. package/src/v2/components/chat/StreamingMarkdownDefault.vue +57 -0
  38. package/src/v2/components/chat/__tests__/BasicMarkdown.test.ts +69 -0
  39. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.ts +53 -16
  40. package/src/v2/components/chat/__tests__/CopilotChat.licenseWarning.test.ts +5 -7
  41. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.markdown.test.ts +54 -0
  42. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.ts +0 -41
  43. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.ts +18 -4
  44. package/src/v2/components/chat/__tests__/markdown-renderer-config.e2e.test.ts +59 -0
  45. package/src/v2/components/chat/index.ts +2 -0
  46. package/src/v2/hooks/__tests__/use-agent.e2e.test.ts +7 -3
  47. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +7 -3
  48. package/src/v2/hooks/index.ts +0 -1
  49. package/src/v2/hooks/use-pin-to-send.ts +1 -1
  50. package/src/v2/providers/CopilotKitProvider.types.ts +10 -0
  51. package/src/v2/providers/CopilotKitProvider.vue +2 -0
  52. package/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.ts +1 -1
  53. package/src/v2/providers/__tests__/markdown-renderer.test.ts +40 -0
  54. package/src/v2/providers/index.ts +1 -0
  55. package/src/v2/providers/markdown-renderer.ts +39 -0
  56. package/vite.config.ts +1 -3
  57. package/dist/use-render-activity-message-B7sDGUl4.cjs +0 -70
  58. package/dist/use-render-activity-message-B7sDGUl4.cjs.map +0 -1
  59. package/dist/use-render-activity-message-mQvByEYF.js +0 -12544
  60. package/dist/use-render-activity-message-mQvByEYF.js.map +0 -1
  61. package/dist/v2/hooks/use-katex-styles.d.ts +0 -22
  62. package/dist/v2/hooks/use-katex-styles.d.ts.map +0 -1
  63. package/src/v2/hooks/__tests__/use-katex-styles.test.ts +0 -69
  64. package/src/v2/hooks/use-katex-styles.ts +0 -44
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import BasicMarkdown from "../BasicMarkdown.vue";
4
+
5
+ describe("BasicMarkdown.vue", () => {
6
+ it("renders headings", () => {
7
+ const w = mount(BasicMarkdown, { props: { content: "# Title" } });
8
+ expect(w.find("h1").text()).toBe("Title");
9
+ });
10
+
11
+ it("renders bold/italic", () => {
12
+ const w = mount(BasicMarkdown, { props: { content: "**b** *i*" } });
13
+ expect(w.find("strong").text()).toBe("b");
14
+ expect(w.find("em").text()).toBe("i");
15
+ });
16
+
17
+ it("renders code blocks", () => {
18
+ const w = mount(BasicMarkdown, { props: { content: "```\nx\n```" } });
19
+ expect(w.find("pre code").text()).toContain("x");
20
+ });
21
+
22
+ it("renders tables", () => {
23
+ const w = mount(BasicMarkdown, {
24
+ props: { content: "| a | b |\n| --- | --- |\n| 1 | 2 |" },
25
+ });
26
+ expect(w.findAll("th")).toHaveLength(2);
27
+ expect(w.findAll("tbody td")).toHaveLength(2);
28
+ });
29
+
30
+ it("renders links with safe href", () => {
31
+ const w = mount(BasicMarkdown, {
32
+ props: { content: "[CK](https://copilotkit.ai)" },
33
+ });
34
+ expect(w.find("a").attributes("href")).toBe("https://copilotkit.ai");
35
+ });
36
+
37
+ it("neutralizes javascript: URIs in links (no XSS)", () => {
38
+ const w = mount(BasicMarkdown, {
39
+ props: { content: "[x](javascript:alert(1))" },
40
+ });
41
+ expect(w.find("a").attributes("href")).toBeUndefined();
42
+ });
43
+
44
+ it("neutralizes javascript: image src", () => {
45
+ const w = mount(BasicMarkdown, {
46
+ props: { content: "![x](javascript:alert(1))" },
47
+ });
48
+ expect(w.find("img").attributes("src")).toBeUndefined();
49
+ });
50
+
51
+ it("renders nothing for empty content", () => {
52
+ const w = mount(BasicMarkdown, { props: { content: "" } });
53
+ expect(w.text()).toBe("");
54
+ });
55
+
56
+ it("rejects data:image/svg+xml image src (SVG script vector)", () => {
57
+ const w = mount(BasicMarkdown, {
58
+ props: { content: "![x](data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=)" },
59
+ });
60
+ expect(w.find("img").attributes("src")).toBeUndefined();
61
+ });
62
+
63
+ it("still allows data:image/png image src", () => {
64
+ const w = mount(BasicMarkdown, {
65
+ props: { content: "![x](data:image/png;base64,iVBORw0KGgo=)" },
66
+ });
67
+ expect(w.find("img").attributes("src")).toContain("data:image/png");
68
+ });
69
+ });
@@ -92,11 +92,14 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
92
92
  agent.emit(runFinishedEvent());
93
93
  agent.complete();
94
94
 
95
+ // The streaming renderer splits text across span segments; query by textContent instead.
95
96
  await waitFor(() => {
96
- const assistantMessage = screen.getByText(
97
- "Hello! How can I help you today?",
97
+ const el = screen.queryAllByText((_t, element) =>
98
+ (element?.textContent ?? "").includes(
99
+ "Hello! How can I help you today?",
100
+ ),
98
101
  );
99
- expect(assistantMessage).toBeDefined();
102
+ expect(el.length).toBeGreaterThan(0);
100
103
  });
101
104
  });
102
105
 
@@ -111,22 +114,32 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
111
114
 
112
115
  agent.emit(textChunkEvent(messageId, "Once upon"));
113
116
 
117
+ // The streaming renderer splits text across span segments; query by textContent instead.
114
118
  await waitFor(() => {
115
- expect(screen.getByText(/Once upon/)).toBeDefined();
119
+ const el = screen.queryAllByText((_t, element) =>
120
+ (element?.textContent ?? "").includes("Once upon"),
121
+ );
122
+ expect(el.length).toBeGreaterThan(0);
116
123
  });
117
124
 
118
125
  agent.emit(textChunkEvent(messageId, " a time"));
119
126
 
120
127
  await waitFor(() => {
121
- expect(screen.getByText(/Once upon a time/)).toBeDefined();
128
+ const el = screen.queryAllByText((_t, element) =>
129
+ (element?.textContent ?? "").includes("Once upon a time"),
130
+ );
131
+ expect(el.length).toBeGreaterThan(0);
122
132
  });
123
133
 
124
134
  agent.emit(textChunkEvent(messageId, " there was a robot."));
125
135
 
126
136
  await waitFor(() => {
127
- expect(
128
- screen.getByText(/Once upon a time there was a robot\./),
129
- ).toBeDefined();
137
+ const el = screen.queryAllByText((_t, element) =>
138
+ (element?.textContent ?? "").includes(
139
+ "Once upon a time there was a robot.",
140
+ ),
141
+ );
142
+ expect(el.length).toBeGreaterThan(0);
130
143
  });
131
144
 
132
145
  agent.emit(runFinishedEvent());
@@ -650,8 +663,11 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
650
663
  );
651
664
 
652
665
  await waitFor(() => {
653
- const allMessages = screen.getAllByText(
654
- /Here is some actual text content/,
666
+ // The streaming renderer splits text across span segments; query by textContent instead.
667
+ const allMessages = screen.queryAllByText((_t, element) =>
668
+ (element?.textContent ?? "").includes(
669
+ "Here is some actual text content",
670
+ ),
655
671
  );
656
672
  expect(allMessages.length).toBeGreaterThan(0);
657
673
 
@@ -807,8 +823,12 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
807
823
  consumerAgent.emit(runFinishedEvent());
808
824
  consumerAgent.complete();
809
825
 
826
+ // The streaming renderer splits text across span segments; query by textContent instead.
810
827
  await waitFor(() => {
811
- expect(screen.getByText(/I can help with that/)).toBeDefined();
828
+ const el = screen.queryAllByText((_t, element) =>
829
+ (element?.textContent ?? "").includes("I can help with that"),
830
+ );
831
+ expect(el.length).toBeGreaterThan(0);
812
832
  });
813
833
 
814
834
  await waitFor(
@@ -1014,8 +1034,15 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
1014
1034
  await agent.emit(reasoningMessageEndEvent(reasoningId));
1015
1035
  await agent.emit(reasoningEndEvent(reasoningId));
1016
1036
 
1037
+ // The streaming renderer splits text across span segments; query by textContent instead.
1017
1038
  await waitFor(() => {
1018
- expect(screen.getByText(/Part 1 Part 2 Part 3/)).toBeDefined();
1039
+ const el = screen.queryAllByText(
1040
+ (_t, element) =>
1041
+ (element?.textContent ?? "").includes("Part 1") &&
1042
+ (element?.textContent ?? "").includes("Part 2") &&
1043
+ (element?.textContent ?? "").includes("Part 3"),
1044
+ );
1045
+ expect(el.length).toBeGreaterThan(0);
1019
1046
  });
1020
1047
 
1021
1048
  await agent.emit(runFinishedEvent());
@@ -1041,17 +1068,23 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
1041
1068
  await agent.emit(runFinishedEvent());
1042
1069
  await agent.complete();
1043
1070
 
1071
+ // The streaming renderer splits text across span segments; query by textContent instead.
1044
1072
  await waitFor(() => {
1045
1073
  expect(screen.getByText(/Thought for/)).toBeDefined();
1046
- expect(screen.getByText("The answer is 42.")).toBeDefined();
1074
+ const el = screen.queryAllByText((_t, element) =>
1075
+ (element?.textContent ?? "").includes("The answer is 42."),
1076
+ );
1077
+ expect(el.length).toBeGreaterThan(0);
1047
1078
  });
1048
1079
 
1049
1080
  const reasoningEl = screen
1050
1081
  .getByText(/Thought for/)
1051
1082
  .closest("[data-message-id]");
1052
1083
  const textEl = screen
1053
- .getByText("The answer is 42.")
1054
- .closest("[data-message-id]");
1084
+ .queryAllByText((_t, element) =>
1085
+ (element?.textContent ?? "").includes("The answer is 42."),
1086
+ )[0]
1087
+ ?.closest("[data-message-id]");
1055
1088
 
1056
1089
  if (reasoningEl && textEl) {
1057
1090
  const position = reasoningEl.compareDocumentPosition(textEl);
@@ -1121,8 +1154,12 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
1121
1154
 
1122
1155
  await agent.emit(textChunkEvent(textId, "Starting answer..."));
1123
1156
 
1157
+ // The streaming renderer splits text across span segments; query by textContent instead.
1124
1158
  await waitFor(() => {
1125
- expect(screen.getByText(/Starting answer/)).toBeDefined();
1159
+ const el = screen.queryAllByText((_t, element) =>
1160
+ (element?.textContent ?? "").includes("Starting answer"),
1161
+ );
1162
+ expect(el.length).toBeGreaterThan(0);
1126
1163
  });
1127
1164
 
1128
1165
  await waitFor(() => {
@@ -3,10 +3,8 @@ import { defineComponent, h, provide, ref } from "vue";
3
3
  import { render, cleanup } from "@testing-library/vue";
4
4
  import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
5
5
  import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
6
- import {
7
- LicenseContextKey,
8
- type LicenseContextValue,
9
- } from "../../../providers/license-context";
6
+ import { LicenseContextKey } from "../../../providers/license-context";
7
+ import type { LicenseContextValue } from "../../../providers/license-context";
10
8
  import { StateCapturingAgent } from "../../../__tests__/utils/agents";
11
9
  import CopilotChat from "../CopilotChat.vue";
12
10
  import CopilotSidebar from "../CopilotSidebar.vue";
@@ -16,9 +14,9 @@ import CopilotPopup from "../CopilotPopup.vue";
16
14
  * Vue parity tests for the inline feature warning surfaces on
17
15
  * `CopilotChat`, `CopilotSidebar`, and `CopilotPopup`. Mirrors the React
18
16
  * inline warning + console.warn behavior at:
19
- * packages/react-core/src/v2/components/chat/CopilotChat.tsx
20
- * packages/react-core/src/v2/components/chat/CopilotSidebar.tsx
21
- * packages/react-core/src/v2/components/chat/CopilotPopup.tsx
17
+ * packages/react-core/src/components/chat/CopilotChat.tsx
18
+ * packages/react-core/src/components/chat/CopilotSidebar.tsx
19
+ * packages/react-core/src/components/chat/CopilotPopup.tsx
22
20
  *
23
21
  * The default `createLicenseContextValue(null)` is permissive
24
22
  * (`checkFeature` always returns true), so the inline warning is never
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { defineComponent, h, provide } from "vue";
3
+ import { render } from "@testing-library/vue";
4
+ import type { AssistantMessage } from "@ag-ui/core";
5
+ import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
6
+ import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
7
+ import CopilotChatAssistantMessage from "../CopilotChatAssistantMessage.vue";
8
+ import { MARKDOWN_RENDERER_KEY } from "../../../providers/markdown-renderer";
9
+
10
+ const message: AssistantMessage = {
11
+ id: "1",
12
+ role: "assistant",
13
+ content: "# Hi",
14
+ };
15
+
16
+ const Providers = (inner: object) =>
17
+ defineComponent({
18
+ components: {
19
+ CopilotKitProvider,
20
+ CopilotChatConfigurationProvider,
21
+ },
22
+ render() {
23
+ return h(CopilotKitProvider, { runtimeUrl: "/api/copilotkit" }, () => [
24
+ h(CopilotChatConfigurationProvider, { threadId: "test-thread" }, () => [
25
+ h(inner),
26
+ ]),
27
+ ]);
28
+ },
29
+ });
30
+
31
+ describe("CopilotChatAssistantMessage markdown (vue)", () => {
32
+ it("renders basic markdown by default", () => {
33
+ const Inner = defineComponent({
34
+ setup: () => () => h(CopilotChatAssistantMessage, { message }),
35
+ });
36
+ const { container } = render(Providers(Inner));
37
+ expect(container.querySelector("h1")?.textContent).toBe("Hi");
38
+ });
39
+
40
+ it("uses the provided renderer when set", () => {
41
+ const Custom = defineComponent({
42
+ props: { content: { type: String, default: "" } },
43
+ setup: (p) => () => h("div", { "data-testid": "custom" }, p.content),
44
+ });
45
+ const Inner = defineComponent({
46
+ setup() {
47
+ provide(MARKDOWN_RENDERER_KEY, Custom);
48
+ return () => h(CopilotChatAssistantMessage, { message });
49
+ },
50
+ });
51
+ const { container } = render(Providers(Inner));
52
+ expect(container.querySelector('[data-testid="custom"]')).not.toBeNull();
53
+ });
54
+ });
@@ -1064,45 +1064,4 @@ describe("CopilotChatAssistantMessage", () => {
1064
1064
  expect(screen.queryByRole("button", { name: /copy/i })).toBeNull();
1065
1065
  });
1066
1066
  });
1067
-
1068
- describe("Vue-specific semantics", () => {
1069
- it("renders markdown image and table actions", () => {
1070
- const message: AssistantMessage = {
1071
- id: "assistant-vue-extra",
1072
- role: "assistant",
1073
- content: `![Alt text](https://example.com/image.png)\n\n| Feature | Supported |\n| --- | --- |\n| Tables | yes |`,
1074
- timestamp: new Date(),
1075
- };
1076
-
1077
- const Host = defineComponent({
1078
- components: {
1079
- CopilotKitProvider,
1080
- CopilotChatConfigurationProvider,
1081
- CopilotChatAssistantMessage,
1082
- },
1083
- setup() {
1084
- return { message, TEST_THREAD_ID };
1085
- },
1086
- template: `
1087
- <CopilotKitProvider runtime-url="/api/copilotkit">
1088
- <CopilotChatConfigurationProvider :thread-id="TEST_THREAD_ID">
1089
- <CopilotChatAssistantMessage :message="message" />
1090
- </CopilotChatConfigurationProvider>
1091
- </CopilotKitProvider>
1092
- `,
1093
- });
1094
-
1095
- renderWithProvider(Host);
1096
- expect(
1097
- document.querySelector('button[title="Download image"]'),
1098
- ).toBeDefined();
1099
- expect(document.querySelector("table")).toBeDefined();
1100
- expect(
1101
- document.querySelector('button[title="Copy table"]'),
1102
- ).toBeDefined();
1103
- expect(
1104
- document.querySelector('button[title="Download table"]'),
1105
- ).toBeDefined();
1106
- });
1107
- });
1108
1067
  });
@@ -223,8 +223,12 @@ describe("Tool Call Re-render Prevention", () => {
223
223
  delta: "The weather in Paris is ",
224
224
  } as BaseEvent);
225
225
 
226
+ // The streaming renderer splits text across span segments; query by textContent instead.
226
227
  await waitFor(() => {
227
- expect(screen.getByText(/The weather in Paris is/)).toBeDefined();
228
+ const el = screen.queryAllByText((_t, element) =>
229
+ (element?.textContent ?? "").includes("The weather in Paris is"),
230
+ );
231
+ expect(el.length).toBeGreaterThan(0);
228
232
  });
229
233
 
230
234
  await agent.emit({
@@ -234,7 +238,10 @@ describe("Tool Call Re-render Prevention", () => {
234
238
  } as BaseEvent);
235
239
 
236
240
  await waitFor(() => {
237
- expect(screen.getByText(/currently sunny/)).toBeDefined();
241
+ const el = screen.queryAllByText((_t, element) =>
242
+ (element?.textContent ?? "").includes("currently sunny"),
243
+ );
244
+ expect(el.length).toBeGreaterThan(0);
238
245
  });
239
246
 
240
247
  await agent.emit({
@@ -244,7 +251,10 @@ describe("Tool Call Re-render Prevention", () => {
244
251
  } as BaseEvent);
245
252
 
246
253
  await waitFor(() => {
247
- expect(screen.getByText(/22°C/)).toBeDefined();
254
+ const el = screen.queryAllByText((_t, element) =>
255
+ (element?.textContent ?? "").includes("22°C"),
256
+ );
257
+ expect(el.length).toBeGreaterThan(0);
248
258
  });
249
259
 
250
260
  const renderCountAfterAllText = toolRenderCount;
@@ -325,8 +335,12 @@ describe("Tool Call Re-render Prevention", () => {
325
335
  delta: "Let me search for that...",
326
336
  } as BaseEvent);
327
337
 
338
+ // The streaming renderer splits text across span segments; query by textContent instead.
328
339
  await waitFor(() => {
329
- expect(screen.getByText(/Let me search for that/)).toBeDefined();
340
+ const el = screen.queryAllByText((_t, element) =>
341
+ (element?.textContent ?? "").includes("Let me search for that"),
342
+ );
343
+ expect(el.length).toBeGreaterThan(0);
330
344
  });
331
345
 
332
346
  const renderCountAfterText = toolRenderCount;
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { h, defineComponent, provide } from "vue";
3
+ import { render } from "@testing-library/vue";
4
+ import type { AssistantMessage } from "@ag-ui/core";
5
+ import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
6
+ import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
7
+ import CopilotChatAssistantMessage from "../CopilotChatAssistantMessage.vue";
8
+ import { MARKDOWN_RENDERER_KEY } from "../../../providers/markdown-renderer";
9
+
10
+ const codeMsg: AssistantMessage = {
11
+ id: "m1",
12
+ role: "assistant",
13
+ content: "```\nx\n```",
14
+ } as any;
15
+
16
+ /** Wraps the component in the required providers, optionally injecting a markdownRenderer. */
17
+ function makeWrapper(rendererValue: unknown) {
18
+ return defineComponent({
19
+ render() {
20
+ return h(CopilotKitProvider, { runtimeUrl: "/api/copilotkit" }, () => [
21
+ h(CopilotChatConfigurationProvider, { threadId: "test-thread" }, () => [
22
+ h(
23
+ defineComponent({
24
+ setup() {
25
+ provide(MARKDOWN_RENDERER_KEY, rendererValue as any);
26
+ return () =>
27
+ h(CopilotChatAssistantMessage, {
28
+ message: codeMsg,
29
+ messages: [codeMsg],
30
+ });
31
+ },
32
+ }),
33
+ ),
34
+ ]),
35
+ ]);
36
+ },
37
+ });
38
+ }
39
+
40
+ describe("Vue markdownRenderer config", () => {
41
+ it("provider config configures the built-in default", () => {
42
+ const { container } = render(
43
+ makeWrapper({
44
+ nodeRenderers: {
45
+ codeBlock: (node: any) =>
46
+ h("pre", { "data-testid": "prov-code" }, node.text),
47
+ },
48
+ }),
49
+ );
50
+ expect(container.querySelector('[data-testid="prov-code"]')).not.toBeNull();
51
+ });
52
+
53
+ it("provider component replaces the renderer (escape hatch)", () => {
54
+ const Custom = (props: { content: string }) =>
55
+ h("div", { "data-testid": "custom" }, props.content);
56
+ const { container } = render(makeWrapper(Custom));
57
+ expect(container.querySelector('[data-testid="custom"]')).not.toBeNull();
58
+ });
59
+ });
@@ -1,4 +1,6 @@
1
1
  export * from "./types";
2
+ export { default as BasicMarkdown } from "./BasicMarkdown.vue";
3
+ export { default as StreamingMarkdownDefault } from "./StreamingMarkdownDefault.vue";
2
4
  export { default as CopilotChatAssistantMessage } from "./CopilotChatAssistantMessage.vue";
3
5
  export { default as CopilotChatAudioRecorder } from "./CopilotChatAudioRecorder.vue";
4
6
  import _CopilotChat from "./CopilotChat.vue";
@@ -129,10 +129,14 @@ describe("useAgent e2e", () => {
129
129
  await agent.emit(runFinishedEvent());
130
130
  await agent.complete();
131
131
 
132
+ // The streaming renderer splits text across span segments; query by textContent instead.
132
133
  await waitFor(() => {
133
- expect(
134
- screen.getByText("Hello! I received your message."),
135
- ).toBeDefined();
134
+ const el = screen.queryAllByText((_text, element) =>
135
+ (element?.textContent ?? "").includes(
136
+ "Hello! I received your message.",
137
+ ),
138
+ );
139
+ expect(el.length).toBeGreaterThan(0);
136
140
  });
137
141
  });
138
142
  });
@@ -556,10 +556,14 @@ describe("useFrontendTool E2E - Dynamic Registration", () => {
556
556
  ),
557
557
  );
558
558
 
559
+ // The streaming renderer splits text across span segments; query by textContent instead.
559
560
  await waitFor(() => {
560
- expect(
561
- screen.getByText("This is a follow-up message after tool execution"),
562
- ).toBeDefined();
561
+ const el = screen.queryAllByText((_text, element) =>
562
+ (element?.textContent ?? "").includes(
563
+ "This is a follow-up message after tool execution",
564
+ ),
565
+ );
566
+ expect(el.length).toBeGreaterThan(0);
563
567
  });
564
568
 
565
569
  await agent.emit(runFinishedEvent());
@@ -23,7 +23,6 @@ export type {
23
23
  } from "./use-attachments";
24
24
  export { useKeyboardHeight } from "./use-keyboard-height";
25
25
  export type { KeyboardState } from "./use-keyboard-height";
26
- export { useKatexStyles } from "./use-katex-styles";
27
26
  export { useCapabilities } from "./use-capabilities";
28
27
  export { useRenderCustomMessages } from "./use-render-custom-messages";
29
28
  export { useRenderActivityMessage } from "./use-render-activity-message";
@@ -13,7 +13,7 @@ export interface UsePinToSendOptions {
13
13
  }
14
14
 
15
15
  /**
16
- * Vue counterpart of React's `usePinToSend` (`packages/react-core/src/v2/hooks/use-pin-to-send.ts`).
16
+ * Vue counterpart of React's `usePinToSend` (`packages/react-core/src/hooks/use-pin-to-send.ts`).
17
17
  *
18
18
  * Anchors the chat scroll container so the most recent user message stays
19
19
  * pinned near the top of the viewport while the assistant streams a
@@ -15,6 +15,7 @@ import type {
15
15
  VueToolCallRenderer,
16
16
  } from "../types";
17
17
  import type { Component } from "vue";
18
+ import type { MarkdownRendererValue } from "./markdown-renderer";
18
19
 
19
20
  export interface CopilotKitProviderProps {
20
21
  runtimeUrl?: string;
@@ -119,6 +120,15 @@ export interface CopilotKitProviderProps {
119
120
  */
120
121
  allowSameOriginFallback?: boolean;
121
122
  };
123
+ /**
124
+ * Global markdown renderer for assistant/reasoning messages. Either a Vue
125
+ * component (escape hatch — receives `{ content: string; isStreaming?: boolean }`)
126
+ * or a `DefaultMarkdownRendererProps` config object (e.g.
127
+ * `{ nodeRenderers: { codeBlock: ShikiBlock } }`) to configure the built-in
128
+ * streaming renderer without writing a wrapper. Overrides the built-in default;
129
+ * a per-message slot still wins.
130
+ */
131
+ markdownRenderer?: MarkdownRendererValue;
122
132
  /**
123
133
  * Enable debug logging for the client-side event pipeline.
124
134
  *
@@ -45,6 +45,7 @@ import {
45
45
  } from "../components/MCPAppsActivityRenderer";
46
46
  import { CopilotKitKey, InspectorKey, SandboxFunctionsKey } from "./keys";
47
47
  import type { VueInspectorOpenRequest } from "./keys";
48
+ import { MARKDOWN_RENDERER_KEY } from "./markdown-renderer";
48
49
  import {
49
50
  LicenseContextKey,
50
51
  createLicenseContextValue,
@@ -769,6 +770,7 @@ provide(CopilotKitKey, {
769
770
  a2uiIncludeSchema,
770
771
  });
771
772
  provide(SandboxFunctionsKey, sandboxFunctions);
773
+ provide(MARKDOWN_RENDERER_KEY, props.markdownRenderer);
772
774
 
773
775
  // License context — driven by structured and legacy Runtime authority.
774
776
  const retryableRuntimeEntitlementFailure = computed(
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * #1937: the headers builder is evaluated when a request is SENT, not during
3
3
  * render/setup. Mirrors
4
- * `react-core/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.tsx`.
4
+ * `react-core/src/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.tsx`.
5
5
  */
6
6
  import { mount } from "@vue/test-utils";
7
7
  import { defineComponent, h, nextTick } from "vue";
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { defineComponent, h, provide } from "vue";
3
+ import { mount } from "@vue/test-utils";
4
+ import {
5
+ MARKDOWN_RENDERER_KEY,
6
+ useMarkdownRenderer,
7
+ } from "../markdown-renderer";
8
+
9
+ describe("useMarkdownRenderer (vue)", () => {
10
+ it("returns undefined with no provider", () => {
11
+ let captured: unknown = "unset";
12
+ const Probe = defineComponent({
13
+ setup() {
14
+ captured = useMarkdownRenderer();
15
+ return () => h("div");
16
+ },
17
+ });
18
+ mount(Probe);
19
+ expect(captured).toBeUndefined();
20
+ });
21
+
22
+ it("returns the provided renderer", () => {
23
+ const Custom = defineComponent({ setup: () => () => h("span") });
24
+ let captured: unknown = null;
25
+ const Probe = defineComponent({
26
+ setup() {
27
+ captured = useMarkdownRenderer();
28
+ return () => h("div");
29
+ },
30
+ });
31
+ const Wrapper = defineComponent({
32
+ setup() {
33
+ provide(MARKDOWN_RENDERER_KEY, Custom);
34
+ return () => h(Probe);
35
+ },
36
+ });
37
+ mount(Wrapper);
38
+ expect(captured).toBe(Custom);
39
+ });
40
+ });
@@ -19,3 +19,4 @@ export {
19
19
  type CopilotChatConfigurationValue,
20
20
  } from "./types";
21
21
  export type { CopilotKitContextValue } from "./keys";
22
+ export * from "./markdown-renderer";
@@ -0,0 +1,39 @@
1
+ import { inject } from "vue";
2
+ import type { Component, InjectionKey } from "vue";
3
+ import type { VueStreamingMarkdownNodeRenderers } from "@copilotkit/markdown-renderer/vue";
4
+ import type { StreamingMarkdownParserOptions } from "@copilotkit/markdown-renderer";
5
+
6
+ /** Config for the built-in Vue default renderer (pass instead of a component). */
7
+ export interface DefaultMarkdownRendererProps {
8
+ nodeRenderers?: VueStreamingMarkdownNodeRenderers;
9
+ caret?: boolean;
10
+ options?: Partial<StreamingMarkdownParserOptions>;
11
+ class?: string;
12
+ }
13
+
14
+ export type MarkdownRendererValue = Component | DefaultMarkdownRendererProps;
15
+
16
+ /** Injection key for a global markdown renderer component (or `undefined`). */
17
+ export const MARKDOWN_RENDERER_KEY: InjectionKey<
18
+ MarkdownRendererValue | undefined
19
+ > = Symbol("copilotkit-markdown-renderer");
20
+
21
+ /**
22
+ * The markdown renderer provided at the provider level, or `undefined`.
23
+ * Message components fall back to the built-in `StreamingMarkdownDefault` when undefined.
24
+ * The value is either a Vue component (accepting `{ content: string; isStreaming?: boolean }`)
25
+ * or a `DefaultMarkdownRendererProps` config object for the built-in renderer.
26
+ */
27
+ export function useMarkdownRenderer(): MarkdownRendererValue | undefined {
28
+ return inject(MARKDOWN_RENDERER_KEY, undefined);
29
+ }
30
+
31
+ /** A Vue component is a function or an object carrying setup/render/__vccOpts/__name. */
32
+ export function isVueComponentRenderer(value: unknown): value is Component {
33
+ if (typeof value === "function") return true;
34
+ if (value && typeof value === "object") {
35
+ const o = value as Record<string, unknown>;
36
+ return "setup" in o || "render" in o || "__vccOpts" in o || "__name" in o;
37
+ }
38
+ return false;
39
+ }