@copilotkit/react-ui 1.80.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-ui",
3
- "version": "1.80.0",
3
+ "version": "1.81.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -49,9 +49,9 @@
49
49
  "rehype-sanitize": "^6.0.0",
50
50
  "remark-gfm": "^4.0.1",
51
51
  "remark-math": "^6.0.0",
52
- "@copilotkit/react-core": "1.80.0",
53
- "@copilotkit/runtime-client-gql": "1.80.0",
54
- "@copilotkit/shared": "1.80.0"
52
+ "@copilotkit/react-core": "1.81.0",
53
+ "@copilotkit/shared": "1.81.0",
54
+ "@copilotkit/runtime-client-gql": "1.81.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "^19.1.0",
@@ -0,0 +1,93 @@
1
+ import React from "react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { renderToStaticMarkup } from "react-dom/server";
4
+
5
+ const hoisted = vi.hoisted(() => ({
6
+ runtime: null as { transport: "tool" | "markdown" } | null,
7
+ surfacesProps: undefined as Record<string, unknown> | undefined,
8
+ }));
9
+
10
+ vi.mock("@copilotkit/react-core/v2/generative-ui", async (importOriginal) => {
11
+ const mod = await importOriginal<Record<string, unknown>>();
12
+ return {
13
+ ...mod,
14
+ useGenerativeUIRuntime: () => hoisted.runtime,
15
+ MarkdownSurfaces: (props: { content: string; messageId: string }) => {
16
+ hoisted.surfacesProps = props;
17
+ return (
18
+ <div data-testid="markdown-surfaces" data-message={props.messageId}>
19
+ {props.content}
20
+ </div>
21
+ );
22
+ },
23
+ };
24
+ });
25
+
26
+ import { Markdown } from "./Markdown";
27
+
28
+ const FENCE = '```a2ui\n{"x":1}\n```';
29
+
30
+ /**
31
+ * The v1 chat renders a2ui fences through the generative UI markdown
32
+ * transport when the v1 provider got a `generativeUI` prop with it.
33
+ */
34
+ describe("v1 Markdown with the generative UI markdown transport", () => {
35
+ it("renders the message with MarkdownSurfaces when the transport is markdown", () => {
36
+ hoisted.runtime = { transport: "markdown" };
37
+ const html = renderToStaticMarkup(
38
+ <Markdown content={FENCE} messageId="m1" />,
39
+ );
40
+ expect(html).toContain('data-testid="markdown-surfaces"');
41
+ expect(html).toContain('data-message="m1"');
42
+ expect(html).not.toContain("<pre");
43
+ });
44
+
45
+ it("gives MarkdownSurfaces the app's tag renderers and plugins for the prose", () => {
46
+ hoisted.runtime = { transport: "markdown" };
47
+ const Link = () => null;
48
+ const remarkPlugin = () => undefined;
49
+ const rehypePlugin = () => undefined;
50
+ renderToStaticMarkup(
51
+ <Markdown
52
+ content={FENCE}
53
+ messageId="m1"
54
+ components={{ a: Link }}
55
+ remarkPlugins={[remarkPlugin]}
56
+ rehypePlugins={[rehypePlugin]}
57
+ />,
58
+ );
59
+ const props = hoisted.surfacesProps as {
60
+ components: Record<string, unknown>;
61
+ remarkPlugins: unknown[];
62
+ rehypePlugins: unknown[];
63
+ };
64
+ expect(props.components.a).toBe(Link);
65
+ expect(props.remarkPlugins).toContain(remarkPlugin);
66
+ // The app's rehype plugins run before the sanitizer, as without fences.
67
+ expect(props.rehypePlugins).toContain(rehypePlugin);
68
+ expect(props.rehypePlugins.length).toBe(3);
69
+ });
70
+
71
+ it("renders a plain code block with the tool transport", () => {
72
+ hoisted.runtime = { transport: "tool" };
73
+ const html = renderToStaticMarkup(
74
+ <Markdown content={FENCE} messageId="m1" />,
75
+ );
76
+ expect(html).not.toContain("markdown-surfaces");
77
+ expect(html).toContain("a2ui");
78
+ });
79
+
80
+ it("renders a plain code block without a generative UI provider", () => {
81
+ hoisted.runtime = null;
82
+ const html = renderToStaticMarkup(
83
+ <Markdown content={FENCE} messageId="m1" />,
84
+ );
85
+ expect(html).not.toContain("markdown-surfaces");
86
+ });
87
+
88
+ it("needs a message id to find the fences of the message", () => {
89
+ hoisted.runtime = { transport: "markdown" };
90
+ const html = renderToStaticMarkup(<Markdown content={FENCE} />);
91
+ expect(html).not.toContain("markdown-surfaces");
92
+ });
93
+ });
@@ -21,6 +21,10 @@ import type { FC } from "react";
21
21
  import { memo, useMemo } from "react";
22
22
  import type { Options, Components } from "react-markdown";
23
23
  import ReactMarkdown from "react-markdown";
24
+ import {
25
+ MarkdownSurfaces,
26
+ useGenerativeUIRuntime,
27
+ } from "@copilotkit/react-core/v2/generative-ui";
24
28
  import { CodeBlock } from "./CodeBlock";
25
29
  import remarkGfm from "remark-gfm";
26
30
  import remarkMath from "remark-math";
@@ -148,6 +152,11 @@ const MemoizedReactMarkdown: FC<Options> = memo(ReactMarkdown);
148
152
 
149
153
  type MarkdownProps = Omit<Options, "children"> & {
150
154
  content: string;
155
+ /**
156
+ * The id of the message. With the generative UI markdown transport on the
157
+ * provider, the ```a2ui fences of the message render as surfaces.
158
+ */
159
+ messageId?: string;
151
160
  };
152
161
 
153
162
  export const Markdown = ({
@@ -155,13 +164,20 @@ export const Markdown = ({
155
164
  components,
156
165
  remarkPlugins,
157
166
  rehypePlugins,
167
+ messageId,
158
168
  ...rest
159
169
  }: MarkdownProps) => {
170
+ // The v1 provider sits on the v2 core, so a `generativeUI` prop on it
171
+ // reaches this hook. Without the markdown transport, a fence is a code
172
+ // block.
173
+ const generativeUI = useGenerativeUIRuntime();
174
+ const rendersSurfaces =
175
+ generativeUI?.transport === "markdown" && messageId !== undefined;
160
176
  const mergedComponents = useMemo(
161
177
  () => ({ ...defaultComponents, ...components }),
162
178
  [components],
163
179
  );
164
- const mergedRemarkPlugins = useMemo<Options["remarkPlugins"]>(
180
+ const mergedRemarkPlugins = useMemo<NonNullable<Options["remarkPlugins"]>>(
165
181
  () => [
166
182
  remarkGfm,
167
183
  [remarkMath, { singleDollarTextMath: false }],
@@ -169,10 +185,25 @@ export const Markdown = ({
169
185
  ],
170
186
  [remarkPlugins],
171
187
  );
172
- const mergedRehypePlugins = useMemo<Options["rehypePlugins"]>(
188
+ const mergedRehypePlugins = useMemo<NonNullable<Options["rehypePlugins"]>>(
173
189
  () => [rehypeRaw, ...(rehypePlugins ?? []), rehypeSanitize],
174
190
  [rehypePlugins],
175
191
  );
192
+ if (rendersSurfaces) {
193
+ return (
194
+ <div className="copilotKitMarkdown">
195
+ {/* The prose between the fences keeps the app's tag renderers and
196
+ the same plugins and sanitizing as a message without fences. */}
197
+ <MarkdownSurfaces
198
+ content={content}
199
+ messageId={messageId}
200
+ components={mergedComponents}
201
+ remarkPlugins={mergedRemarkPlugins}
202
+ rehypePlugins={mergedRehypePlugins}
203
+ />
204
+ </div>
205
+ );
206
+ }
176
207
  return (
177
208
  <div className="copilotKitMarkdown">
178
209
  <MemoizedReactMarkdown
@@ -88,7 +88,11 @@ export const AssistantMessage = (props: AssistantMessageProps) => {
88
88
  {content && (
89
89
  <div className="copilotKitMessage copilotKitAssistantMessage">
90
90
  {content && (
91
- <Markdown content={content} components={markdownTagRenderers} />
91
+ <Markdown
92
+ content={content}
93
+ components={markdownTagRenderers}
94
+ messageId={message?.id}
95
+ />
92
96
  )}
93
97
 
94
98
  {content && !isLoading && (
package/vitest.config.mjs CHANGED
@@ -4,7 +4,10 @@ export default defineConfig({
4
4
  test: {
5
5
  environment: "node",
6
6
  globals: true,
7
- include: ["src/**/*.{test,spec}.ts", "oxlint-rules/**/*.{test,spec}.mjs"],
7
+ include: [
8
+ "src/**/*.{test,spec}.{ts,tsx}",
9
+ "oxlint-rules/**/*.{test,spec}.mjs",
10
+ ],
8
11
  reporters: [["default", { summary: false }]],
9
12
  silent: true,
10
13
  },