@copilotkit/vue 1.83.0 → 2.0.0-rc.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.
- package/AGENTS.md +1 -1
- package/dist/index.cjs +76 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +14290 -287
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
- package/dist/v2/components/a2ui/catalog.d.ts +1 -1
- package/dist/v2/components/a2ui/catalog.d.ts.map +1 -1
- package/dist/v2/components/chat/BasicMarkdown.vue.d.ts +9 -0
- package/dist/v2/components/chat/BasicMarkdown.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts +10 -0
- package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/index.d.ts +2 -0
- package/dist/v2/components/chat/index.d.ts.map +1 -1
- package/dist/v2/hooks/index.d.ts +0 -1
- package/dist/v2/hooks/index.d.ts.map +1 -1
- package/dist/v2/hooks/use-pin-to-send.d.ts +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.mjs +195 -188
- package/dist/v2/providers/CopilotKitProvider.types.d.ts +10 -0
- package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
- package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
- package/dist/v2/providers/index.d.ts +1 -0
- package/dist/v2/providers/index.d.ts.map +1 -1
- package/dist/v2/providers/markdown-renderer.d.ts +23 -0
- package/dist/v2/providers/markdown-renderer.d.ts.map +1 -0
- package/package.json +8 -9
- package/src/styles/globals.css +0 -97
- package/src/v2/components/A2UIMessageRenderer.ts +3 -2
- package/src/v2/components/a2ui/catalog.ts +4 -2
- package/src/v2/components/chat/BasicMarkdown.vue +158 -0
- package/src/v2/components/chat/CopilotChatAssistantMessage.vue +27 -536
- package/src/v2/components/chat/CopilotChatReasoningMessage.vue +7 -2
- package/src/v2/components/chat/StreamingMarkdownDefault.vue +57 -0
- package/src/v2/components/chat/__tests__/BasicMarkdown.test.ts +69 -0
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.ts +53 -16
- package/src/v2/components/chat/__tests__/CopilotChat.licenseWarning.test.ts +5 -7
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.markdown.test.ts +54 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.ts +0 -41
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.ts +18 -4
- package/src/v2/components/chat/__tests__/markdown-renderer-config.e2e.test.ts +59 -0
- package/src/v2/components/chat/index.ts +2 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.ts +7 -3
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +7 -3
- package/src/v2/hooks/index.ts +0 -1
- package/src/v2/hooks/use-pin-to-send.ts +1 -1
- package/src/v2/providers/CopilotKitProvider.types.ts +10 -0
- package/src/v2/providers/CopilotKitProvider.vue +2 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.ts +1 -1
- package/src/v2/providers/__tests__/markdown-renderer.test.ts +40 -0
- package/src/v2/providers/index.ts +1 -0
- package/src/v2/providers/markdown-renderer.ts +39 -0
- package/vite.config.ts +1 -3
- package/dist/use-render-activity-message-B7sDGUl4.cjs +0 -70
- package/dist/use-render-activity-message-B7sDGUl4.cjs.map +0 -1
- package/dist/use-render-activity-message-mQvByEYF.js +0 -12544
- package/dist/use-render-activity-message-mQvByEYF.js.map +0 -1
- package/dist/v2/hooks/use-katex-styles.d.ts +0 -22
- package/dist/v2/hooks/use-katex-styles.d.ts.map +0 -1
- package/src/v2/hooks/__tests__/use-katex-styles.test.ts +0 -69
- 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: ")" },
|
|
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: "" },
|
|
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: "" },
|
|
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
|
|
97
|
-
|
|
97
|
+
const el = screen.queryAllByText((_t, element) =>
|
|
98
|
+
(element?.textContent ?? "").includes(
|
|
99
|
+
"Hello! How can I help you today?",
|
|
100
|
+
),
|
|
98
101
|
);
|
|
99
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
654
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
1054
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
20
|
-
* packages/react-core/src/
|
|
21
|
-
* packages/react-core/src/
|
|
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: `\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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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());
|
package/src/v2/hooks/index.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
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
|
+
});
|
|
@@ -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
|
+
}
|