@mastra/playground-ui 49.0.0-alpha.4 → 49.0.0-alpha.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @mastra/playground-ui
2
2
 
3
+ ## 49.0.0-alpha.5
4
+
5
+ ### Minor Changes
6
+
7
+ - Streamed markdown now renders block by block. A growing reply re-parses only the block still being written instead of the whole message on every chunk, so a long reply no longer costs more per chunk as it gets longer, and a reply that finishes streaming keeps every element it already put on screen instead of remounting. ([#21473](https://github.com/mastra-ai/mastra/pull/21473))
8
+
9
+ Half-written markdown also renders as what it is about to become: `**bold` reads as bold while its closing marker is still in flight, and a link shows its text until the URL lands, instead of flashing raw syntax on screen.
10
+
11
+ One caveat: splitting a text into blocks is deliberately conservative rather than a second full parse. Anything it cannot decide — an unclosed fence, an indented continuation, a link reference or footnote definition — is kept whole, so those replies render exactly as before and simply do not get the speedup.
12
+
13
+ - Added a `streaming` prop to `MarkdownRenderer`. A reply marked as still being written fades each word in as it arrives, instead of snapping whole chunks of text into place. ([#21417](https://github.com/mastra-ai/mastra/pull/21417))
14
+
15
+ ```tsx
16
+ <MarkdownRenderer streaming={part.state === 'streaming'}>{part.text}</MarkdownRenderer>
17
+ ```
18
+
19
+ The fade is CSS on words as they land, so the text already on screen stays put while the reply grows. A word fades in once it is whole rather than one character at a time — the word still being typed is held back until its boundary arrives, so the visible text trails the stream by at most one word. One caveat: when a growing block changes shape, a paragraph turning into a list item as the next character lands, that block fades again.
20
+
21
+ Leave the prop off — the default — for text that is already settled: it renders as plain prose, with no extra markup. The animation is disabled under `prefers-reduced-motion`.
22
+
23
+ Markdown no longer sets `text-wrap: pretty`. It re-broke the last lines of a block to avoid orphans, which reran on every chunk of a streaming reply and jumped words that were already on screen onto another line.
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [[`59d8898`](https://github.com/mastra-ai/mastra/commit/59d8898c8cb48b342fe5bcb5eee803cc8cc95060), [`a40f915`](https://github.com/mastra-ai/mastra/commit/a40f9157690d89ef13ce825cc88e30be581de5d4)]:
28
+ - @mastra/core@1.59.0-alpha.5
29
+ - @mastra/client-js@1.40.0-alpha.5
30
+ - @mastra/react@1.4.3-alpha.5
31
+
3
32
  ## 49.0.0-alpha.4
4
33
 
5
34
  ### Patch Changes
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
4
 
5
- const markdownRenderer = require('../markdown-renderer-DvJfqb-D.cjs');
5
+ const markdownRenderer = require('../markdown-renderer-WHy5Cyu2.cjs');
6
6
 
7
7
 
8
8
 
@@ -1,2 +1,2 @@
1
- export { M as MarkdownRenderer } from '../markdown-renderer-CB2WMnTG.js';
1
+ export { M as MarkdownRenderer } from '../markdown-renderer-BDe_1JLH.js';
2
2
  //# sourceMappingURL=MarkdownRenderer.es.js.map
@@ -7,7 +7,7 @@ const lucideReact = require('lucide-react');
7
7
  const React = require('react');
8
8
  const Badge = require('../../Badge-BlJpI3fY.cjs');
9
9
  const Button = require('../../Button-B4Ixj2aW.cjs');
10
- const markdownRenderer = require('../../markdown-renderer-DvJfqb-D.cjs');
10
+ const markdownRenderer = require('../../markdown-renderer-WHy5Cyu2.cjs');
11
11
  const Txt = require('../../Txt-I5lK8zy7.cjs');
12
12
  const icons_Icon = require('../../icons/Icon.cjs.js');
13
13
  const hooks_useCopyToClipboard = require('../../hooks/use-copy-to-clipboard.cjs.js');
@@ -3,7 +3,7 @@ import { CheckIcon, CopyIcon, Minimize2, Maximize2, ClipboardList } from 'lucide
3
3
  import { useState, createContext, useContext } from 'react';
4
4
  import { B as Badge } from '../../Badge-woV6OD8k.js';
5
5
  import { B as Button } from '../../Button-C-9Pzy-Z.js';
6
- import { M as MarkdownRenderer } from '../../markdown-renderer-CB2WMnTG.js';
6
+ import { M as MarkdownRenderer } from '../../markdown-renderer-BDe_1JLH.js';
7
7
  import { T as Txt } from '../../Txt-Df42XG5p.js';
8
8
  import { Icon } from '../../icons/Icon.es.js';
9
9
  import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard.es.js';
@@ -0,0 +1,210 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { memo } from 'react';
3
+ import Markdown from 'react-markdown';
4
+ import remarkGfm from 'remark-gfm';
5
+ import remend from 'remend';
6
+ import { C as CodeBlock } from './code-block-V4wpmbud.js';
7
+ import { c as cn } from './cn-BakJzhuv.js';
8
+
9
+ import './markdown-renderer.css';const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
10
+ const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
11
+ const LIST_ITEM = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:[ \t]|$)/;
12
+ const INDENTED = /^(?:\t| {2,})/;
13
+ const DEFINITION = /^ {0,3}\[[^\]]+\]:/;
14
+ function fenceOpenedBy(line) {
15
+ return FENCE_OPEN.exec(line)?.[1];
16
+ }
17
+ function closesFence(line, marker) {
18
+ return FENCE_CLOSE.exec(line)?.[1]?.startsWith(marker) ?? false;
19
+ }
20
+ function startsBlock(line, inList) {
21
+ if (INDENTED.test(line)) return false;
22
+ return !(inList && LIST_ITEM.test(line));
23
+ }
24
+ function splitBlocks(markdown) {
25
+ const lines = markdown.split("\n");
26
+ const starts = [0];
27
+ let fence;
28
+ let started = false;
29
+ let broken = false;
30
+ let inList = false;
31
+ for (const [index, line] of lines.entries()) {
32
+ if (fence) {
33
+ if (closesFence(line, fence)) fence = void 0;
34
+ continue;
35
+ }
36
+ if (line.trim() === "") {
37
+ broken = started;
38
+ continue;
39
+ }
40
+ if (DEFINITION.test(line)) return [markdown];
41
+ if (broken && startsBlock(line, inList)) {
42
+ starts.push(index);
43
+ started = false;
44
+ }
45
+ if (!started) inList = LIST_ITEM.test(line);
46
+ started = true;
47
+ broken = false;
48
+ fence = fenceOpenedBy(line);
49
+ }
50
+ return starts.map((start, position) => {
51
+ const end = starts[position + 1];
52
+ const block = lines.slice(start, end ?? lines.length).join("\n");
53
+ return end === void 0 ? block : `${block}
54
+ `;
55
+ });
56
+ }
57
+
58
+ const OPAQUE_TAGS = /* @__PURE__ */ new Set(["code", "pre"]);
59
+ const ANIMATED_TAIL_CHARS = 120;
60
+ function textOf(nodes) {
61
+ let text = "";
62
+ for (const node of nodes) {
63
+ if (node.type === "text") text += node.value;
64
+ else if (node.type === "element") text += textOf(node.children);
65
+ }
66
+ return text;
67
+ }
68
+ function rehypeWordSpans({ complete = false } = {}) {
69
+ return (tree) => {
70
+ const reply = textOf(tree.children);
71
+ const total = reply.length;
72
+ const animateFrom = total - ANIMATED_TAIL_CHARS;
73
+ const pendingFrom = complete ? Number.POSITIVE_INFINITY : total - (/\S*$/.exec(reply)?.[0].length ?? 0);
74
+ let offset = 0;
75
+ const wordProperties = (startsAt, endsAt) => {
76
+ if (startsAt >= pendingFrom) return { className: ["mastra-markdown-word-pending"] };
77
+ if (endsAt > animateFrom) return { className: ["mastra-markdown-word"] };
78
+ return {};
79
+ };
80
+ const wordSpans = (value) => value.split(/(\s+)/).flatMap((chunk) => {
81
+ if (!chunk) return [];
82
+ const startsAt = offset;
83
+ offset += chunk.length;
84
+ if (/^\s+$/.test(chunk)) return [{ type: "text", value: chunk }];
85
+ return [
86
+ {
87
+ type: "element",
88
+ tagName: "span",
89
+ properties: wordProperties(startsAt, offset),
90
+ children: [{ type: "text", value: chunk }]
91
+ }
92
+ ];
93
+ });
94
+ const wrapNodes = (nodes) => nodes.flatMap((node) => {
95
+ if (node.type === "text") return wordSpans(node.value);
96
+ if (node.type === "element") {
97
+ if (OPAQUE_TAGS.has(node.tagName)) offset += textOf(node.children).length;
98
+ else node.children = wrapNodes(node.children);
99
+ }
100
+ return [node];
101
+ });
102
+ tree.children = wrapNodes(tree.children);
103
+ };
104
+ }
105
+
106
+ const MarkdownRenderer = memo(function MarkdownRenderer2({
107
+ children,
108
+ className,
109
+ externalLinkTarget = "tab",
110
+ streaming = false
111
+ }) {
112
+ const blocks = splitBlocks(decodeEscapedNewlines(children));
113
+ const last = blocks.length - 1;
114
+ const components = externalLinkTarget === "window" ? WINDOW_COMPONENTS : COMPONENTS;
115
+ return /* @__PURE__ */ jsx("div", { className: cn("mastra-markdown", streaming && "mastra-markdown-streaming", className), children: blocks.map((block, index) => {
116
+ const growing = streaming && index === last;
117
+ return /* @__PURE__ */ jsx(
118
+ MarkdownBlock,
119
+ {
120
+ content: growing ? remend(block, REMEND_OPTIONS) : block,
121
+ rehypePlugins: wordSpansFor(streaming, growing),
122
+ components
123
+ },
124
+ index
125
+ );
126
+ }) });
127
+ });
128
+ const MarkdownBlock = memo(function MarkdownBlock2({
129
+ components,
130
+ content,
131
+ rehypePlugins
132
+ }) {
133
+ return /* @__PURE__ */ jsx(Markdown, { remarkPlugins: REMARK_PLUGINS, rehypePlugins, components, children: content });
134
+ });
135
+ const NO_WORD_SPANS = [];
136
+ const GROWING_WORD_SPANS = [[rehypeWordSpans, { complete: false }]];
137
+ const SETTLED_WORD_SPANS = [[rehypeWordSpans, { complete: true }]];
138
+ function wordSpansFor(streaming, growing) {
139
+ if (!streaming) return NO_WORD_SPANS;
140
+ return growing ? GROWING_WORD_SPANS : SETTLED_WORD_SPANS;
141
+ }
142
+ function decodeEscapedNewlines(text) {
143
+ return text.includes("\n") ? text : text.replace(/\\n/g, "\n");
144
+ }
145
+ function languageOf(node) {
146
+ const classNames = node.properties.className;
147
+ if (!Array.isArray(classNames)) return void 0;
148
+ const language = classNames.find((entry) => typeof entry === "string" && entry.startsWith("language-"));
149
+ return typeof language === "string" ? language.slice("language-".length) : void 0;
150
+ }
151
+ function fencedCode(node) {
152
+ const child = node?.children.find((entry) => entry.type === "element" && entry.tagName === "code");
153
+ if (child?.type !== "element") return void 0;
154
+ const code = child.children.map((entry) => entry.type === "text" ? entry.value : "").join("");
155
+ return { code: code.replace(/\n$/, ""), language: languageOf(child) };
156
+ }
157
+ function MarkdownCodeBlock({ node, children }) {
158
+ const fenced = fencedCode(node);
159
+ if (!fenced) return /* @__PURE__ */ jsx("pre", { children });
160
+ return /* @__PURE__ */ jsx(
161
+ CodeBlock,
162
+ {
163
+ code: fenced.code,
164
+ lang: fenced.language,
165
+ overflow: "scroll",
166
+ className: "bg-surface1 my-3",
167
+ copyMessage: "Copied code to clipboard"
168
+ }
169
+ );
170
+ }
171
+ const POPUP_WINDOW_FEATURES = "popup=yes,width=720,height=800,resizable=yes,scrollbars=yes";
172
+ function isPlainLeftClick(event) {
173
+ return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
174
+ }
175
+ function markdownLink(externalLinkTarget) {
176
+ return function MarkdownLink({ href, title, children }) {
177
+ const isExternal = /^https?:\/\//i.test(href ?? "");
178
+ const openInPopup = (event) => {
179
+ if (!isExternal || externalLinkTarget !== "window" || !isPlainLeftClick(event)) return;
180
+ const popup = window.open(href, "_blank", POPUP_WINDOW_FEATURES);
181
+ if (!popup) return;
182
+ popup.opener = null;
183
+ event.preventDefault();
184
+ };
185
+ return /* @__PURE__ */ jsx(
186
+ "a",
187
+ {
188
+ href,
189
+ title,
190
+ onClick: openInPopup,
191
+ target: isExternal ? "_blank" : void 0,
192
+ rel: isExternal ? "noopener noreferrer" : void 0,
193
+ children
194
+ }
195
+ );
196
+ };
197
+ }
198
+ const REMARK_PLUGINS = [remarkGfm];
199
+ const REMEND_OPTIONS = { katex: false, linkMode: "text-only" };
200
+ const COMPONENTS = {
201
+ pre: MarkdownCodeBlock,
202
+ a: markdownLink("tab")
203
+ };
204
+ const WINDOW_COMPONENTS = {
205
+ ...COMPONENTS,
206
+ a: markdownLink("window")
207
+ };
208
+
209
+ export { MarkdownRenderer as M };
210
+ //# sourceMappingURL=markdown-renderer-BDe_1JLH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-renderer-BDe_1JLH.js","sources":["../src/ds/components/MarkdownRenderer/blocks.ts","../src/ds/components/MarkdownRenderer/word-spans.ts","../src/ds/components/MarkdownRenderer/markdown-renderer.tsx"],"sourcesContent":["const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;\nconst FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \\t]*$/;\nconst LIST_ITEM = /^ {0,3}(?:[-*+]|\\d{1,9}[.)])(?:[ \\t]|$)/;\nconst INDENTED = /^(?:\\t| {2,})/;\nconst DEFINITION = /^ {0,3}\\[[^\\]]+\\]:/;\n\nfunction fenceOpenedBy(line: string): string | undefined {\n return FENCE_OPEN.exec(line)?.[1];\n}\n\n// Both are runs of one character, so `startsWith` is \"same marker, at least as long\".\nfunction closesFence(line: string, marker: string): boolean {\n return FENCE_CLOSE.exec(line)?.[1]?.startsWith(marker) ?? false;\n}\n\nfunction startsBlock(line: string, inList: boolean): boolean {\n if (INDENTED.test(line)) return false;\n\n return !(inList && LIST_ITEM.test(line));\n}\n\n/**\n * Cuts markdown into top-level blocks so a growing reply re-parses its last\n * block instead of all of them. Rejoining the result returns the input, and\n * every block renders as it would have inside the whole.\n *\n * Each rule only ever merges: over-merging costs a re-parse, while cutting a\n * construct in two changes what it renders as. Link reference and footnote\n * definitions resolve across blank lines, which no line-level rule can see, so\n * reaching one outside a fence abandons the split for the whole text.\n */\nexport function splitBlocks(markdown: string): string[] {\n const lines = markdown.split('\\n');\n const starts = [0];\n let fence: string | undefined;\n let started = false;\n let broken = false;\n let inList = false;\n\n for (const [index, line] of lines.entries()) {\n if (fence) {\n if (closesFence(line, fence)) fence = undefined;\n continue;\n }\n\n if (line.trim() === '') {\n broken = started;\n continue;\n }\n\n if (DEFINITION.test(line)) return [markdown];\n\n if (broken && startsBlock(line, inList)) {\n starts.push(index);\n started = false;\n }\n\n if (!started) inList = LIST_ITEM.test(line);\n\n started = true;\n broken = false;\n fence = fenceOpenedBy(line);\n }\n\n return starts.map((start, position) => {\n const end = starts[position + 1];\n const block = lines.slice(start, end ?? lines.length).join('\\n');\n\n return end === undefined ? block : `${block}\\n`;\n });\n}\n","import type { ExtraProps } from 'react-markdown';\n\ntype MarkdownElement = NonNullable<ExtraProps['node']>;\ntype MarkdownChild = MarkdownElement['children'][number];\n\n/** Load-bearing: `MarkdownCodeBlock` reads a fence's source from its direct text children. */\nconst OPAQUE_TAGS = new Set(['code', 'pre']);\n\n/** Further than this from the end of the reply, a word was already on screen. */\nconst ANIMATED_TAIL_CHARS = 120;\n\nfunction textOf(nodes: MarkdownChild[]): string {\n let text = '';\n\n for (const node of nodes) {\n if (node.type === 'text') text += node.value;\n else if (node.type === 'element') text += textOf(node.children);\n }\n\n return text;\n}\n\n/**\n * One span per word so the fade stays pure CSS: an animation runs when an\n * element mounts, and again when a class hands it an animation-name it did not\n * have. Settled words keep their span — dropping the wrapper would reshape the\n * child list and re-key every sibling after it.\n *\n * The word still being typed is held invisible instead of faded: React reuses\n * its span as characters land, so a mount animation would never cover them.\n * Marking it complete swaps the class, which starts the fade on that same span.\n * Emphasis splits such a word across nodes (`Hel**lo**`), so its boundary comes\n * from the whole text, not from each text node. Only the block still growing has\n * one at all — `complete` lets the settled ones reveal their last word.\n *\n * Only the tail is marked. A restructure — a paragraph becoming a list item\n * once the next character lands — remounts the subtree, so the window bounds\n * that re-fade rather than removing it.\n */\nexport function rehypeWordSpans({ complete = false }: { complete?: boolean } = {}) {\n return (tree: { children: MarkdownChild[] }) => {\n const reply = textOf(tree.children);\n const total = reply.length;\n const animateFrom = total - ANIMATED_TAIL_CHARS;\n const pendingFrom = complete ? Number.POSITIVE_INFINITY : total - (/\\S*$/.exec(reply)?.[0].length ?? 0);\n let offset = 0;\n\n const wordProperties = (startsAt: number, endsAt: number) => {\n if (startsAt >= pendingFrom) return { className: ['mastra-markdown-word-pending'] };\n if (endsAt > animateFrom) return { className: ['mastra-markdown-word'] };\n return {};\n };\n\n const wordSpans = (value: string): MarkdownChild[] =>\n value.split(/(\\s+)/).flatMap<MarkdownChild>(chunk => {\n if (!chunk) return [];\n\n const startsAt = offset;\n offset += chunk.length;\n if (/^\\s+$/.test(chunk)) return [{ type: 'text', value: chunk }];\n\n return [\n {\n type: 'element',\n tagName: 'span',\n properties: wordProperties(startsAt, offset),\n children: [{ type: 'text', value: chunk }],\n },\n ];\n });\n\n const wrapNodes = (nodes: MarkdownChild[]): MarkdownChild[] =>\n nodes.flatMap<MarkdownChild>(node => {\n if (node.type === 'text') return wordSpans(node.value);\n\n if (node.type === 'element') {\n if (OPAQUE_TAGS.has(node.tagName)) offset += textOf(node.children).length;\n else node.children = wrapNodes(node.children);\n }\n\n return [node];\n });\n\n tree.children = wrapNodes(tree.children);\n };\n}\n","import { memo } from 'react';\nimport type { MouseEvent, MouseEventHandler, ReactNode } from 'react';\nimport Markdown from 'react-markdown';\nimport type { Components, ExtraProps, Options } from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport remend from 'remend';\n\nimport { splitBlocks } from './blocks';\nimport { rehypeWordSpans } from './word-spans';\nimport { CodeBlock } from '@/ds/components/CodeBlock';\nimport { cn } from '@/lib/utils';\n\nimport './markdown-renderer.css';\n\nexport type MarkdownExternalLinkTarget = 'tab' | 'window';\n\nexport interface MarkdownRendererProps {\n children: string;\n className?: string;\n externalLinkTarget?: MarkdownExternalLinkTarget;\n /** The text is still being written: fade each word in as it lands. */\n streaming?: boolean;\n}\n\n/**\n * Renders a markdown string. Agent output can carry attacker-influenced text\n * (file contents, tool output, web pages): react-markdown escapes raw HTML and\n * drops dangerous link schemes, so nothing here reaches the DOM as markup.\n *\n * react-markdown re-parses on every render, and a streaming reply re-renders\n * its whole transcript on every delta. Memoizing spares the settled messages;\n * rendering block by block — streaming or not — spares every block of the live\n * one but the last, and lets a reply settle without remounting what is already\n * on screen.\n */\nexport const MarkdownRenderer = memo(function MarkdownRenderer({\n children,\n className,\n externalLinkTarget = 'tab',\n streaming = false,\n}: MarkdownRendererProps) {\n const blocks = splitBlocks(decodeEscapedNewlines(children));\n const last = blocks.length - 1;\n const components = externalLinkTarget === 'window' ? WINDOW_COMPONENTS : COMPONENTS;\n\n return (\n <div className={cn('mastra-markdown', streaming && 'mastra-markdown-streaming', className)}>\n {blocks.map((block, index) => {\n const growing = streaming && index === last;\n\n return (\n <MarkdownBlock\n key={index}\n content={growing ? remend(block, REMEND_OPTIONS) : block}\n rehypePlugins={wordSpansFor(streaming, growing)}\n components={components}\n />\n );\n })}\n </div>\n );\n});\n\n/** Keyed by position at the call site: a content key remounts on every character. */\nconst MarkdownBlock = memo(function MarkdownBlock({\n components,\n content,\n rehypePlugins,\n}: {\n components: Components;\n content: string;\n rehypePlugins: Options['rehypePlugins'];\n}) {\n return (\n <Markdown remarkPlugins={REMARK_PLUGINS} rehypePlugins={rehypePlugins} components={components}>\n {content}\n </Markdown>\n );\n});\n\nconst NO_WORD_SPANS: Options['rehypePlugins'] = [];\nconst GROWING_WORD_SPANS: Options['rehypePlugins'] = [[rehypeWordSpans, { complete: false }]];\nconst SETTLED_WORD_SPANS: Options['rehypePlugins'] = [[rehypeWordSpans, { complete: true }]];\n\n/** Module constants, so a block that keeps its plugins keeps its memo too. */\nfunction wordSpansFor(streaming: boolean, growing: boolean): Options['rehypePlugins'] {\n if (!streaming) return NO_WORD_SPANS;\n\n return growing ? GROWING_WORD_SPANS : SETTLED_WORD_SPANS;\n}\n\n// Agent networks emit their text with literal `\\n`. Only unescape when the text\n// has no real newline, otherwise a `\"a\\nb\"` inside a code fence gets shredded.\nfunction decodeEscapedNewlines(text: string): string {\n return text.includes('\\n') ? text : text.replace(/\\\\n/g, '\\n');\n}\n\ntype MarkdownNode = NonNullable<ExtraProps['node']>;\n\nfunction languageOf(node: MarkdownNode): string | undefined {\n const classNames = node.properties.className;\n if (!Array.isArray(classNames)) return undefined;\n\n const language = classNames.find(entry => typeof entry === 'string' && entry.startsWith('language-'));\n return typeof language === 'string' ? language.slice('language-'.length) : undefined;\n}\n\nfunction fencedCode(node: MarkdownNode | undefined): { code: string; language?: string } | undefined {\n const child = node?.children.find(entry => entry.type === 'element' && entry.tagName === 'code');\n if (child?.type !== 'element') return undefined;\n\n const code = child.children.map(entry => (entry.type === 'text' ? entry.value : '')).join('');\n return { code: code.replace(/\\n$/, ''), language: languageOf(child) };\n}\n\nfunction MarkdownCodeBlock({ node, children }: { node?: MarkdownNode; children?: ReactNode }) {\n const fenced = fencedCode(node);\n if (!fenced) return <pre>{children}</pre>;\n\n return (\n <CodeBlock\n code={fenced.code}\n lang={fenced.language}\n overflow=\"scroll\"\n className=\"bg-surface1 my-3\"\n copyMessage=\"Copied code to clipboard\"\n />\n );\n}\n\nconst POPUP_WINDOW_FEATURES = 'popup=yes,width=720,height=800,resizable=yes,scrollbars=yes';\n\nfunction isPlainLeftClick(event: MouseEvent): boolean {\n return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;\n}\n\nfunction markdownLink(externalLinkTarget: MarkdownExternalLinkTarget): NonNullable<Components['a']> {\n return function MarkdownLink({ href, title, children }) {\n const isExternal = /^https?:\\/\\//i.test(href ?? '');\n\n const openInPopup: MouseEventHandler<HTMLAnchorElement> = event => {\n if (!isExternal || externalLinkTarget !== 'window' || !isPlainLeftClick(event)) return;\n\n const popup = window.open(href, '_blank', POPUP_WINDOW_FEATURES);\n if (!popup) return;\n\n popup.opener = null;\n event.preventDefault();\n };\n\n return (\n <a\n href={href}\n title={title}\n onClick={openInPopup}\n target={isExternal ? '_blank' : undefined}\n rel={isExternal ? 'noopener noreferrer' : undefined}\n >\n {children}\n </a>\n );\n };\n}\n\nconst REMARK_PLUGINS = [remarkGfm];\n\n// Links stay text until their URL lands: remend's placeholder href would render\n// a live anchor to nowhere. No math is rendered here, so pairing `$$` would only\n// turn one literal into another.\nconst REMEND_OPTIONS = { katex: false, linkMode: 'text-only' } as const;\n\n// Elements are listed one by one: react-markdown also passes its `node`, which\n// React would forward to the DOM as a stray attribute. Everything else is\n// styled from markdown-renderer.css.\nconst COMPONENTS: Components = {\n pre: MarkdownCodeBlock,\n a: markdownLink('tab'),\n};\n\nconst WINDOW_COMPONENTS: Components = {\n ...COMPONENTS,\n a: markdownLink('window'),\n};\n"],"names":["MarkdownRenderer","MarkdownBlock"],"mappings":"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"}
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ require('./markdown-renderer.css');const jsxRuntime = require('react/jsx-runtime');
4
+ const React = require('react');
5
+ const Markdown = require('react-markdown');
6
+ const remarkGfm = require('remark-gfm');
7
+ const remend = require('remend');
8
+ const codeBlock = require('./code-block-BZUY8Fj9.cjs');
9
+ const utils_cn = require('./cn-vA5sWo9W.cjs');
10
+
11
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
12
+ const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
13
+ const LIST_ITEM = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:[ \t]|$)/;
14
+ const INDENTED = /^(?:\t| {2,})/;
15
+ const DEFINITION = /^ {0,3}\[[^\]]+\]:/;
16
+ function fenceOpenedBy(line) {
17
+ return FENCE_OPEN.exec(line)?.[1];
18
+ }
19
+ function closesFence(line, marker) {
20
+ return FENCE_CLOSE.exec(line)?.[1]?.startsWith(marker) ?? false;
21
+ }
22
+ function startsBlock(line, inList) {
23
+ if (INDENTED.test(line)) return false;
24
+ return !(inList && LIST_ITEM.test(line));
25
+ }
26
+ function splitBlocks(markdown) {
27
+ const lines = markdown.split("\n");
28
+ const starts = [0];
29
+ let fence;
30
+ let started = false;
31
+ let broken = false;
32
+ let inList = false;
33
+ for (const [index, line] of lines.entries()) {
34
+ if (fence) {
35
+ if (closesFence(line, fence)) fence = void 0;
36
+ continue;
37
+ }
38
+ if (line.trim() === "") {
39
+ broken = started;
40
+ continue;
41
+ }
42
+ if (DEFINITION.test(line)) return [markdown];
43
+ if (broken && startsBlock(line, inList)) {
44
+ starts.push(index);
45
+ started = false;
46
+ }
47
+ if (!started) inList = LIST_ITEM.test(line);
48
+ started = true;
49
+ broken = false;
50
+ fence = fenceOpenedBy(line);
51
+ }
52
+ return starts.map((start, position) => {
53
+ const end = starts[position + 1];
54
+ const block = lines.slice(start, end ?? lines.length).join("\n");
55
+ return end === void 0 ? block : `${block}
56
+ `;
57
+ });
58
+ }
59
+
60
+ const OPAQUE_TAGS = /* @__PURE__ */ new Set(["code", "pre"]);
61
+ const ANIMATED_TAIL_CHARS = 120;
62
+ function textOf(nodes) {
63
+ let text = "";
64
+ for (const node of nodes) {
65
+ if (node.type === "text") text += node.value;
66
+ else if (node.type === "element") text += textOf(node.children);
67
+ }
68
+ return text;
69
+ }
70
+ function rehypeWordSpans({ complete = false } = {}) {
71
+ return (tree) => {
72
+ const reply = textOf(tree.children);
73
+ const total = reply.length;
74
+ const animateFrom = total - ANIMATED_TAIL_CHARS;
75
+ const pendingFrom = complete ? Number.POSITIVE_INFINITY : total - (/\S*$/.exec(reply)?.[0].length ?? 0);
76
+ let offset = 0;
77
+ const wordProperties = (startsAt, endsAt) => {
78
+ if (startsAt >= pendingFrom) return { className: ["mastra-markdown-word-pending"] };
79
+ if (endsAt > animateFrom) return { className: ["mastra-markdown-word"] };
80
+ return {};
81
+ };
82
+ const wordSpans = (value) => value.split(/(\s+)/).flatMap((chunk) => {
83
+ if (!chunk) return [];
84
+ const startsAt = offset;
85
+ offset += chunk.length;
86
+ if (/^\s+$/.test(chunk)) return [{ type: "text", value: chunk }];
87
+ return [
88
+ {
89
+ type: "element",
90
+ tagName: "span",
91
+ properties: wordProperties(startsAt, offset),
92
+ children: [{ type: "text", value: chunk }]
93
+ }
94
+ ];
95
+ });
96
+ const wrapNodes = (nodes) => nodes.flatMap((node) => {
97
+ if (node.type === "text") return wordSpans(node.value);
98
+ if (node.type === "element") {
99
+ if (OPAQUE_TAGS.has(node.tagName)) offset += textOf(node.children).length;
100
+ else node.children = wrapNodes(node.children);
101
+ }
102
+ return [node];
103
+ });
104
+ tree.children = wrapNodes(tree.children);
105
+ };
106
+ }
107
+
108
+ const MarkdownRenderer = React.memo(function MarkdownRenderer2({
109
+ children,
110
+ className,
111
+ externalLinkTarget = "tab",
112
+ streaming = false
113
+ }) {
114
+ const blocks = splitBlocks(decodeEscapedNewlines(children));
115
+ const last = blocks.length - 1;
116
+ const components = externalLinkTarget === "window" ? WINDOW_COMPONENTS : COMPONENTS;
117
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: utils_cn.cn("mastra-markdown", streaming && "mastra-markdown-streaming", className), children: blocks.map((block, index) => {
118
+ const growing = streaming && index === last;
119
+ return /* @__PURE__ */ jsxRuntime.jsx(
120
+ MarkdownBlock,
121
+ {
122
+ content: growing ? remend(block, REMEND_OPTIONS) : block,
123
+ rehypePlugins: wordSpansFor(streaming, growing),
124
+ components
125
+ },
126
+ index
127
+ );
128
+ }) });
129
+ });
130
+ const MarkdownBlock = React.memo(function MarkdownBlock2({
131
+ components,
132
+ content,
133
+ rehypePlugins
134
+ }) {
135
+ return /* @__PURE__ */ jsxRuntime.jsx(Markdown, { remarkPlugins: REMARK_PLUGINS, rehypePlugins, components, children: content });
136
+ });
137
+ const NO_WORD_SPANS = [];
138
+ const GROWING_WORD_SPANS = [[rehypeWordSpans, { complete: false }]];
139
+ const SETTLED_WORD_SPANS = [[rehypeWordSpans, { complete: true }]];
140
+ function wordSpansFor(streaming, growing) {
141
+ if (!streaming) return NO_WORD_SPANS;
142
+ return growing ? GROWING_WORD_SPANS : SETTLED_WORD_SPANS;
143
+ }
144
+ function decodeEscapedNewlines(text) {
145
+ return text.includes("\n") ? text : text.replace(/\\n/g, "\n");
146
+ }
147
+ function languageOf(node) {
148
+ const classNames = node.properties.className;
149
+ if (!Array.isArray(classNames)) return void 0;
150
+ const language = classNames.find((entry) => typeof entry === "string" && entry.startsWith("language-"));
151
+ return typeof language === "string" ? language.slice("language-".length) : void 0;
152
+ }
153
+ function fencedCode(node) {
154
+ const child = node?.children.find((entry) => entry.type === "element" && entry.tagName === "code");
155
+ if (child?.type !== "element") return void 0;
156
+ const code = child.children.map((entry) => entry.type === "text" ? entry.value : "").join("");
157
+ return { code: code.replace(/\n$/, ""), language: languageOf(child) };
158
+ }
159
+ function MarkdownCodeBlock({ node, children }) {
160
+ const fenced = fencedCode(node);
161
+ if (!fenced) return /* @__PURE__ */ jsxRuntime.jsx("pre", { children });
162
+ return /* @__PURE__ */ jsxRuntime.jsx(
163
+ codeBlock.CodeBlock,
164
+ {
165
+ code: fenced.code,
166
+ lang: fenced.language,
167
+ overflow: "scroll",
168
+ className: "bg-surface1 my-3",
169
+ copyMessage: "Copied code to clipboard"
170
+ }
171
+ );
172
+ }
173
+ const POPUP_WINDOW_FEATURES = "popup=yes,width=720,height=800,resizable=yes,scrollbars=yes";
174
+ function isPlainLeftClick(event) {
175
+ return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
176
+ }
177
+ function markdownLink(externalLinkTarget) {
178
+ return function MarkdownLink({ href, title, children }) {
179
+ const isExternal = /^https?:\/\//i.test(href ?? "");
180
+ const openInPopup = (event) => {
181
+ if (!isExternal || externalLinkTarget !== "window" || !isPlainLeftClick(event)) return;
182
+ const popup = window.open(href, "_blank", POPUP_WINDOW_FEATURES);
183
+ if (!popup) return;
184
+ popup.opener = null;
185
+ event.preventDefault();
186
+ };
187
+ return /* @__PURE__ */ jsxRuntime.jsx(
188
+ "a",
189
+ {
190
+ href,
191
+ title,
192
+ onClick: openInPopup,
193
+ target: isExternal ? "_blank" : void 0,
194
+ rel: isExternal ? "noopener noreferrer" : void 0,
195
+ children
196
+ }
197
+ );
198
+ };
199
+ }
200
+ const REMARK_PLUGINS = [remarkGfm];
201
+ const REMEND_OPTIONS = { katex: false, linkMode: "text-only" };
202
+ const COMPONENTS = {
203
+ pre: MarkdownCodeBlock,
204
+ a: markdownLink("tab")
205
+ };
206
+ const WINDOW_COMPONENTS = {
207
+ ...COMPONENTS,
208
+ a: markdownLink("window")
209
+ };
210
+
211
+ exports.MarkdownRenderer = MarkdownRenderer;
212
+ //# sourceMappingURL=markdown-renderer-WHy5Cyu2.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-renderer-WHy5Cyu2.cjs","sources":["../src/ds/components/MarkdownRenderer/blocks.ts","../src/ds/components/MarkdownRenderer/word-spans.ts","../src/ds/components/MarkdownRenderer/markdown-renderer.tsx"],"sourcesContent":["const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;\nconst FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \\t]*$/;\nconst LIST_ITEM = /^ {0,3}(?:[-*+]|\\d{1,9}[.)])(?:[ \\t]|$)/;\nconst INDENTED = /^(?:\\t| {2,})/;\nconst DEFINITION = /^ {0,3}\\[[^\\]]+\\]:/;\n\nfunction fenceOpenedBy(line: string): string | undefined {\n return FENCE_OPEN.exec(line)?.[1];\n}\n\n// Both are runs of one character, so `startsWith` is \"same marker, at least as long\".\nfunction closesFence(line: string, marker: string): boolean {\n return FENCE_CLOSE.exec(line)?.[1]?.startsWith(marker) ?? false;\n}\n\nfunction startsBlock(line: string, inList: boolean): boolean {\n if (INDENTED.test(line)) return false;\n\n return !(inList && LIST_ITEM.test(line));\n}\n\n/**\n * Cuts markdown into top-level blocks so a growing reply re-parses its last\n * block instead of all of them. Rejoining the result returns the input, and\n * every block renders as it would have inside the whole.\n *\n * Each rule only ever merges: over-merging costs a re-parse, while cutting a\n * construct in two changes what it renders as. Link reference and footnote\n * definitions resolve across blank lines, which no line-level rule can see, so\n * reaching one outside a fence abandons the split for the whole text.\n */\nexport function splitBlocks(markdown: string): string[] {\n const lines = markdown.split('\\n');\n const starts = [0];\n let fence: string | undefined;\n let started = false;\n let broken = false;\n let inList = false;\n\n for (const [index, line] of lines.entries()) {\n if (fence) {\n if (closesFence(line, fence)) fence = undefined;\n continue;\n }\n\n if (line.trim() === '') {\n broken = started;\n continue;\n }\n\n if (DEFINITION.test(line)) return [markdown];\n\n if (broken && startsBlock(line, inList)) {\n starts.push(index);\n started = false;\n }\n\n if (!started) inList = LIST_ITEM.test(line);\n\n started = true;\n broken = false;\n fence = fenceOpenedBy(line);\n }\n\n return starts.map((start, position) => {\n const end = starts[position + 1];\n const block = lines.slice(start, end ?? lines.length).join('\\n');\n\n return end === undefined ? block : `${block}\\n`;\n });\n}\n","import type { ExtraProps } from 'react-markdown';\n\ntype MarkdownElement = NonNullable<ExtraProps['node']>;\ntype MarkdownChild = MarkdownElement['children'][number];\n\n/** Load-bearing: `MarkdownCodeBlock` reads a fence's source from its direct text children. */\nconst OPAQUE_TAGS = new Set(['code', 'pre']);\n\n/** Further than this from the end of the reply, a word was already on screen. */\nconst ANIMATED_TAIL_CHARS = 120;\n\nfunction textOf(nodes: MarkdownChild[]): string {\n let text = '';\n\n for (const node of nodes) {\n if (node.type === 'text') text += node.value;\n else if (node.type === 'element') text += textOf(node.children);\n }\n\n return text;\n}\n\n/**\n * One span per word so the fade stays pure CSS: an animation runs when an\n * element mounts, and again when a class hands it an animation-name it did not\n * have. Settled words keep their span — dropping the wrapper would reshape the\n * child list and re-key every sibling after it.\n *\n * The word still being typed is held invisible instead of faded: React reuses\n * its span as characters land, so a mount animation would never cover them.\n * Marking it complete swaps the class, which starts the fade on that same span.\n * Emphasis splits such a word across nodes (`Hel**lo**`), so its boundary comes\n * from the whole text, not from each text node. Only the block still growing has\n * one at all — `complete` lets the settled ones reveal their last word.\n *\n * Only the tail is marked. A restructure — a paragraph becoming a list item\n * once the next character lands — remounts the subtree, so the window bounds\n * that re-fade rather than removing it.\n */\nexport function rehypeWordSpans({ complete = false }: { complete?: boolean } = {}) {\n return (tree: { children: MarkdownChild[] }) => {\n const reply = textOf(tree.children);\n const total = reply.length;\n const animateFrom = total - ANIMATED_TAIL_CHARS;\n const pendingFrom = complete ? Number.POSITIVE_INFINITY : total - (/\\S*$/.exec(reply)?.[0].length ?? 0);\n let offset = 0;\n\n const wordProperties = (startsAt: number, endsAt: number) => {\n if (startsAt >= pendingFrom) return { className: ['mastra-markdown-word-pending'] };\n if (endsAt > animateFrom) return { className: ['mastra-markdown-word'] };\n return {};\n };\n\n const wordSpans = (value: string): MarkdownChild[] =>\n value.split(/(\\s+)/).flatMap<MarkdownChild>(chunk => {\n if (!chunk) return [];\n\n const startsAt = offset;\n offset += chunk.length;\n if (/^\\s+$/.test(chunk)) return [{ type: 'text', value: chunk }];\n\n return [\n {\n type: 'element',\n tagName: 'span',\n properties: wordProperties(startsAt, offset),\n children: [{ type: 'text', value: chunk }],\n },\n ];\n });\n\n const wrapNodes = (nodes: MarkdownChild[]): MarkdownChild[] =>\n nodes.flatMap<MarkdownChild>(node => {\n if (node.type === 'text') return wordSpans(node.value);\n\n if (node.type === 'element') {\n if (OPAQUE_TAGS.has(node.tagName)) offset += textOf(node.children).length;\n else node.children = wrapNodes(node.children);\n }\n\n return [node];\n });\n\n tree.children = wrapNodes(tree.children);\n };\n}\n","import { memo } from 'react';\nimport type { MouseEvent, MouseEventHandler, ReactNode } from 'react';\nimport Markdown from 'react-markdown';\nimport type { Components, ExtraProps, Options } from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport remend from 'remend';\n\nimport { splitBlocks } from './blocks';\nimport { rehypeWordSpans } from './word-spans';\nimport { CodeBlock } from '@/ds/components/CodeBlock';\nimport { cn } from '@/lib/utils';\n\nimport './markdown-renderer.css';\n\nexport type MarkdownExternalLinkTarget = 'tab' | 'window';\n\nexport interface MarkdownRendererProps {\n children: string;\n className?: string;\n externalLinkTarget?: MarkdownExternalLinkTarget;\n /** The text is still being written: fade each word in as it lands. */\n streaming?: boolean;\n}\n\n/**\n * Renders a markdown string. Agent output can carry attacker-influenced text\n * (file contents, tool output, web pages): react-markdown escapes raw HTML and\n * drops dangerous link schemes, so nothing here reaches the DOM as markup.\n *\n * react-markdown re-parses on every render, and a streaming reply re-renders\n * its whole transcript on every delta. Memoizing spares the settled messages;\n * rendering block by block — streaming or not — spares every block of the live\n * one but the last, and lets a reply settle without remounting what is already\n * on screen.\n */\nexport const MarkdownRenderer = memo(function MarkdownRenderer({\n children,\n className,\n externalLinkTarget = 'tab',\n streaming = false,\n}: MarkdownRendererProps) {\n const blocks = splitBlocks(decodeEscapedNewlines(children));\n const last = blocks.length - 1;\n const components = externalLinkTarget === 'window' ? WINDOW_COMPONENTS : COMPONENTS;\n\n return (\n <div className={cn('mastra-markdown', streaming && 'mastra-markdown-streaming', className)}>\n {blocks.map((block, index) => {\n const growing = streaming && index === last;\n\n return (\n <MarkdownBlock\n key={index}\n content={growing ? remend(block, REMEND_OPTIONS) : block}\n rehypePlugins={wordSpansFor(streaming, growing)}\n components={components}\n />\n );\n })}\n </div>\n );\n});\n\n/** Keyed by position at the call site: a content key remounts on every character. */\nconst MarkdownBlock = memo(function MarkdownBlock({\n components,\n content,\n rehypePlugins,\n}: {\n components: Components;\n content: string;\n rehypePlugins: Options['rehypePlugins'];\n}) {\n return (\n <Markdown remarkPlugins={REMARK_PLUGINS} rehypePlugins={rehypePlugins} components={components}>\n {content}\n </Markdown>\n );\n});\n\nconst NO_WORD_SPANS: Options['rehypePlugins'] = [];\nconst GROWING_WORD_SPANS: Options['rehypePlugins'] = [[rehypeWordSpans, { complete: false }]];\nconst SETTLED_WORD_SPANS: Options['rehypePlugins'] = [[rehypeWordSpans, { complete: true }]];\n\n/** Module constants, so a block that keeps its plugins keeps its memo too. */\nfunction wordSpansFor(streaming: boolean, growing: boolean): Options['rehypePlugins'] {\n if (!streaming) return NO_WORD_SPANS;\n\n return growing ? GROWING_WORD_SPANS : SETTLED_WORD_SPANS;\n}\n\n// Agent networks emit their text with literal `\\n`. Only unescape when the text\n// has no real newline, otherwise a `\"a\\nb\"` inside a code fence gets shredded.\nfunction decodeEscapedNewlines(text: string): string {\n return text.includes('\\n') ? text : text.replace(/\\\\n/g, '\\n');\n}\n\ntype MarkdownNode = NonNullable<ExtraProps['node']>;\n\nfunction languageOf(node: MarkdownNode): string | undefined {\n const classNames = node.properties.className;\n if (!Array.isArray(classNames)) return undefined;\n\n const language = classNames.find(entry => typeof entry === 'string' && entry.startsWith('language-'));\n return typeof language === 'string' ? language.slice('language-'.length) : undefined;\n}\n\nfunction fencedCode(node: MarkdownNode | undefined): { code: string; language?: string } | undefined {\n const child = node?.children.find(entry => entry.type === 'element' && entry.tagName === 'code');\n if (child?.type !== 'element') return undefined;\n\n const code = child.children.map(entry => (entry.type === 'text' ? entry.value : '')).join('');\n return { code: code.replace(/\\n$/, ''), language: languageOf(child) };\n}\n\nfunction MarkdownCodeBlock({ node, children }: { node?: MarkdownNode; children?: ReactNode }) {\n const fenced = fencedCode(node);\n if (!fenced) return <pre>{children}</pre>;\n\n return (\n <CodeBlock\n code={fenced.code}\n lang={fenced.language}\n overflow=\"scroll\"\n className=\"bg-surface1 my-3\"\n copyMessage=\"Copied code to clipboard\"\n />\n );\n}\n\nconst POPUP_WINDOW_FEATURES = 'popup=yes,width=720,height=800,resizable=yes,scrollbars=yes';\n\nfunction isPlainLeftClick(event: MouseEvent): boolean {\n return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;\n}\n\nfunction markdownLink(externalLinkTarget: MarkdownExternalLinkTarget): NonNullable<Components['a']> {\n return function MarkdownLink({ href, title, children }) {\n const isExternal = /^https?:\\/\\//i.test(href ?? '');\n\n const openInPopup: MouseEventHandler<HTMLAnchorElement> = event => {\n if (!isExternal || externalLinkTarget !== 'window' || !isPlainLeftClick(event)) return;\n\n const popup = window.open(href, '_blank', POPUP_WINDOW_FEATURES);\n if (!popup) return;\n\n popup.opener = null;\n event.preventDefault();\n };\n\n return (\n <a\n href={href}\n title={title}\n onClick={openInPopup}\n target={isExternal ? '_blank' : undefined}\n rel={isExternal ? 'noopener noreferrer' : undefined}\n >\n {children}\n </a>\n );\n };\n}\n\nconst REMARK_PLUGINS = [remarkGfm];\n\n// Links stay text until their URL lands: remend's placeholder href would render\n// a live anchor to nowhere. No math is rendered here, so pairing `$$` would only\n// turn one literal into another.\nconst REMEND_OPTIONS = { katex: false, linkMode: 'text-only' } as const;\n\n// Elements are listed one by one: react-markdown also passes its `node`, which\n// React would forward to the DOM as a stray attribute. Everything else is\n// styled from markdown-renderer.css.\nconst COMPONENTS: Components = {\n pre: MarkdownCodeBlock,\n a: markdownLink('tab'),\n};\n\nconst WINDOW_COMPONENTS: Components = {\n ...COMPONENTS,\n a: markdownLink('window'),\n};\n"],"names":["memo","MarkdownRenderer","jsx","cn","MarkdownBlock","CodeBlock"],"mappings":";;;;;;;;;;AAAA,MAAM,UAAA,GAAa,sBAAA;AACnB,MAAM,WAAA,GAAc,6BAAA;AACpB,MAAM,SAAA,GAAY,yCAAA;AAClB,MAAM,QAAA,GAAW,eAAA;AACjB,MAAM,UAAA,GAAa,oBAAA;AAEnB,SAAS,cAAc,IAAA,EAAkC;AACvD,EAAA,OAAO,UAAA,CAAW,IAAA,CAAK,IAAI,CAAA,GAAI,CAAC,CAAA;AAClC;AAGA,SAAS,WAAA,CAAY,MAAc,MAAA,EAAyB;AAC1D,EAAA,OAAO,WAAA,CAAY,KAAK,IAAI,CAAA,GAAI,CAAC,CAAA,EAAG,UAAA,CAAW,MAAM,CAAA,IAAK,KAAA;AAC5D;AAEA,SAAS,WAAA,CAAY,MAAc,MAAA,EAA0B;AAC3D,EAAA,IAAI,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,EAAG,OAAO,KAAA;AAEhC,EAAA,OAAO,EAAE,MAAA,IAAU,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA,CAAA;AACxC;AAYO,SAAS,YAAY,QAAA,EAA4B;AACtD,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA;AACjC,EAAA,MAAM,MAAA,GAAS,CAAC,CAAC,CAAA;AACjB,EAAA,IAAI,KAAA;AACJ,EAAA,IAAI,OAAA,GAAU,KAAA;AACd,EAAA,IAAI,MAAA,GAAS,KAAA;AACb,EAAA,IAAI,MAAA,GAAS,KAAA;AAEb,EAAA,KAAA,MAAW,CAAC,KAAA,EAAO,IAAI,CAAA,IAAK,KAAA,CAAM,SAAQ,EAAG;AAC3C,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,IAAI,WAAA,CAAY,IAAA,EAAM,KAAK,CAAA,EAAG,KAAA,GAAQ,MAAA;AACtC,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,IAAA,CAAK,IAAA,EAAK,KAAM,EAAA,EAAI;AACtB,MAAA,MAAA,GAAS,OAAA;AACT,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,WAAW,IAAA,CAAK,IAAI,CAAA,EAAG,OAAO,CAAC,QAAQ,CAAA;AAE3C,IAAA,IAAI,MAAA,IAAU,WAAA,CAAY,IAAA,EAAM,MAAM,CAAA,EAAG;AACvC,MAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AACjB,MAAA,OAAA,GAAU,KAAA;AAAA,IACZ;AAEA,IAAA,IAAI,CAAC,OAAA,EAAS,MAAA,GAAS,SAAA,CAAU,KAAK,IAAI,CAAA;AAE1C,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,MAAA,GAAS,KAAA;AACT,IAAA,KAAA,GAAQ,cAAc,IAAI,CAAA;AAAA,EAC5B;AAEA,EAAA,OAAO,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,EAAO,QAAA,KAAa;AACrC,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,QAAA,GAAW,CAAC,CAAA;AAC/B,IAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,CAAM,KAAA,EAAO,OAAO,KAAA,CAAM,MAAM,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AAE/D,IAAA,OAAO,GAAA,KAAQ,MAAA,GAAY,KAAA,GAAQ,CAAA,EAAG,KAAK;AAAA,CAAA;AAAA,EAC7C,CAAC,CAAA;AACH;;AChEA,MAAM,8BAAc,IAAI,GAAA,CAAI,CAAC,MAAA,EAAQ,KAAK,CAAC,CAAA;AAG3C,MAAM,mBAAA,GAAsB,GAAA;AAE5B,SAAS,OAAO,KAAA,EAAgC;AAC9C,EAAA,IAAI,IAAA,GAAO,EAAA;AAEX,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,IAAA,CAAK,IAAA,KAAS,MAAA,EAAQ,IAAA,IAAQ,IAAA,CAAK,KAAA;AAAA,SAAA,IAC9B,KAAK,IAAA,KAAS,SAAA,EAAW,IAAA,IAAQ,MAAA,CAAO,KAAK,QAAQ,CAAA;AAAA,EAChE;AAEA,EAAA,OAAO,IAAA;AACT;AAmBO,SAAS,gBAAgB,EAAE,QAAA,GAAW,KAAA,EAAM,GAA4B,EAAC,EAAG;AACjF,EAAA,OAAO,CAAC,IAAA,KAAwC;AAC9C,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA;AAClC,IAAA,MAAM,QAAQ,KAAA,CAAM,MAAA;AACpB,IAAA,MAAM,cAAc,KAAA,GAAQ,mBAAA;AAC5B,IAAA,MAAM,WAAA,GAAc,QAAA,GAAW,MAAA,CAAO,iBAAA,GAAoB,KAAA,IAAS,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,GAAI,CAAC,CAAA,CAAE,MAAA,IAAU,CAAA,CAAA;AACrG,IAAA,IAAI,MAAA,GAAS,CAAA;AAEb,IAAA,MAAM,cAAA,GAAiB,CAAC,QAAA,EAAkB,MAAA,KAAmB;AAC3D,MAAA,IAAI,YAAY,WAAA,EAAa,OAAO,EAAE,SAAA,EAAW,CAAC,8BAA8B,CAAA,EAAE;AAClF,MAAA,IAAI,SAAS,WAAA,EAAa,OAAO,EAAE,SAAA,EAAW,CAAC,sBAAsB,CAAA,EAAE;AACvE,MAAA,OAAO,EAAC;AAAA,IACV,CAAA;AAEA,IAAA,MAAM,SAAA,GAAY,CAAC,KAAA,KACjB,KAAA,CAAM,MAAM,OAAO,CAAA,CAAE,QAAuB,CAAA,KAAA,KAAS;AACnD,MAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAC;AAEpB,MAAA,MAAM,QAAA,GAAW,MAAA;AACjB,MAAA,MAAA,IAAU,KAAA,CAAM,MAAA;AAChB,MAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA,EAAG,OAAO,CAAC,EAAE,IAAA,EAAM,MAAA,EAAQ,KAAA,EAAO,KAAA,EAAO,CAAA;AAE/D,MAAA,OAAO;AAAA,QACL;AAAA,UACE,IAAA,EAAM,SAAA;AAAA,UACN,OAAA,EAAS,MAAA;AAAA,UACT,UAAA,EAAY,cAAA,CAAe,QAAA,EAAU,MAAM,CAAA;AAAA,UAC3C,UAAU,CAAC,EAAE,MAAM,MAAA,EAAQ,KAAA,EAAO,OAAO;AAAA;AAC3C,OACF;AAAA,IACF,CAAC,CAAA;AAEH,IAAA,MAAM,SAAA,GAAY,CAAC,KAAA,KACjB,KAAA,CAAM,QAAuB,CAAA,IAAA,KAAQ;AACnC,MAAA,IAAI,KAAK,IAAA,KAAS,MAAA,EAAQ,OAAO,SAAA,CAAU,KAAK,KAAK,CAAA;AAErD,MAAA,IAAI,IAAA,CAAK,SAAS,SAAA,EAAW;AAC3B,QAAA,IAAI,WAAA,CAAY,IAAI,IAAA,CAAK,OAAO,GAAG,MAAA,IAAU,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA,CAAE,MAAA;AAAA,aAC9D,IAAA,CAAK,QAAA,GAAW,SAAA,CAAU,IAAA,CAAK,QAAQ,CAAA;AAAA,MAC9C;AAEA,MAAA,OAAO,CAAC,IAAI,CAAA;AAAA,IACd,CAAC,CAAA;AAEH,IAAA,IAAA,CAAK,QAAA,GAAW,SAAA,CAAU,IAAA,CAAK,QAAQ,CAAA;AAAA,EACzC,CAAA;AACF;;AClDO,MAAM,gBAAA,GAAmBA,UAAA,CAAK,SAASC,iBAAAA,CAAiB;AAAA,EAC7D,QAAA;AAAA,EACA,SAAA;AAAA,EACA,kBAAA,GAAqB,KAAA;AAAA,EACrB,SAAA,GAAY;AACd,CAAA,EAA0B;AACxB,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,qBAAA,CAAsB,QAAQ,CAAC,CAAA;AAC1D,EAAA,MAAM,IAAA,GAAO,OAAO,MAAA,GAAS,CAAA;AAC7B,EAAA,MAAM,UAAA,GAAa,kBAAA,KAAuB,QAAA,GAAW,iBAAA,GAAoB,UAAA;AAEzE,EAAA,uBACEC,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAWC,WAAA,CAAG,iBAAA,EAAmB,SAAA,IAAa,2BAAA,EAA6B,SAAS,CAAA,EACtF,QAAA,EAAA,MAAA,CAAO,GAAA,CAAI,CAAC,OAAO,KAAA,KAAU;AAC5B,IAAA,MAAM,OAAA,GAAU,aAAa,KAAA,KAAU,IAAA;AAEvC,IAAA,uBACED,cAAA;AAAA,MAAC,aAAA;AAAA,MAAA;AAAA,QAEC,OAAA,EAAS,OAAA,GAAU,MAAA,CAAO,KAAA,EAAO,cAAc,CAAA,GAAI,KAAA;AAAA,QACnD,aAAA,EAAe,YAAA,CAAa,SAAA,EAAW,OAAO,CAAA;AAAA,QAC9C;AAAA,OAAA;AAAA,MAHK;AAAA,KAIP;AAAA,EAEJ,CAAC,CAAA,EACH,CAAA;AAEJ,CAAC;AAGD,MAAM,aAAA,GAAgBF,UAAA,CAAK,SAASI,cAAAA,CAAc;AAAA,EAChD,UAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAIG;AACD,EAAA,sCACG,QAAA,EAAA,EAAS,aAAA,EAAe,cAAA,EAAgB,aAAA,EAA8B,YACpE,QAAA,EAAA,OAAA,EACH,CAAA;AAEJ,CAAC,CAAA;AAED,MAAM,gBAA0C,EAAC;AACjD,MAAM,kBAAA,GAA+C,CAAC,CAAC,eAAA,EAAiB,EAAE,QAAA,EAAU,KAAA,EAAO,CAAC,CAAA;AAC5F,MAAM,kBAAA,GAA+C,CAAC,CAAC,eAAA,EAAiB,EAAE,QAAA,EAAU,IAAA,EAAM,CAAC,CAAA;AAG3F,SAAS,YAAA,CAAa,WAAoB,OAAA,EAA4C;AACpF,EAAA,IAAI,CAAC,WAAW,OAAO,aAAA;AAEvB,EAAA,OAAO,UAAU,kBAAA,GAAqB,kBAAA;AACxC;AAIA,SAAS,sBAAsB,IAAA,EAAsB;AACnD,EAAA,OAAO,IAAA,CAAK,SAAS,IAAI,CAAA,GAAI,OAAO,IAAA,CAAK,OAAA,CAAQ,QAAQ,IAAI,CAAA;AAC/D;AAIA,SAAS,WAAW,IAAA,EAAwC;AAC1D,EAAA,MAAM,UAAA,GAAa,KAAK,UAAA,CAAW,SAAA;AACnC,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,UAAU,GAAG,OAAO,MAAA;AAEvC,EAAA,MAAM,QAAA,GAAW,UAAA,CAAW,IAAA,CAAK,CAAA,KAAA,KAAS,OAAO,UAAU,QAAA,IAAY,KAAA,CAAM,UAAA,CAAW,WAAW,CAAC,CAAA;AACpG,EAAA,OAAO,OAAO,QAAA,KAAa,QAAA,GAAW,SAAS,KAAA,CAAM,WAAA,CAAY,MAAM,CAAA,GAAI,MAAA;AAC7E;AAEA,SAAS,WAAW,IAAA,EAAiF;AACnG,EAAA,MAAM,KAAA,GAAQ,IAAA,EAAM,QAAA,CAAS,IAAA,CAAK,CAAA,KAAA,KAAS,MAAM,IAAA,KAAS,SAAA,IAAa,KAAA,CAAM,OAAA,KAAY,MAAM,CAAA;AAC/F,EAAA,IAAI,KAAA,EAAO,IAAA,KAAS,SAAA,EAAW,OAAO,MAAA;AAEtC,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,QAAA,CAAS,GAAA,CAAI,CAAA,KAAA,KAAU,KAAA,CAAM,IAAA,KAAS,MAAA,GAAS,KAAA,CAAM,KAAA,GAAQ,EAAG,CAAA,CAAE,KAAK,EAAE,CAAA;AAC5F,EAAA,OAAO,EAAE,IAAA,EAAM,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,EAAG,QAAA,EAAU,UAAA,CAAW,KAAK,CAAA,EAAE;AACtE;AAEA,SAAS,iBAAA,CAAkB,EAAE,IAAA,EAAM,QAAA,EAAS,EAAkD;AAC5F,EAAA,MAAM,MAAA,GAAS,WAAW,IAAI,CAAA;AAC9B,EAAA,IAAI,CAAC,MAAA,EAAQ,uBAAOF,cAAA,CAAC,SAAK,QAAA,EAAS,CAAA;AAEnC,EAAA,uBACEA,cAAA;AAAA,IAACG,mBAAA;AAAA,IAAA;AAAA,MACC,MAAM,MAAA,CAAO,IAAA;AAAA,MACb,MAAM,MAAA,CAAO,QAAA;AAAA,MACb,QAAA,EAAS,QAAA;AAAA,MACT,SAAA,EAAU,kBAAA;AAAA,MACV,WAAA,EAAY;AAAA;AAAA,GACd;AAEJ;AAEA,MAAM,qBAAA,GAAwB,6DAAA;AAE9B,SAAS,iBAAiB,KAAA,EAA4B;AACpD,EAAA,OAAO,KAAA,CAAM,MAAA,KAAW,CAAA,IAAK,CAAC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,IAAY,CAAC,KAAA,CAAM,MAAA;AAC7F;AAEA,SAAS,aAAa,kBAAA,EAA8E;AAClG,EAAA,OAAO,SAAS,YAAA,CAAa,EAAE,IAAA,EAAM,KAAA,EAAO,UAAS,EAAG;AACtD,IAAA,MAAM,UAAA,GAAa,eAAA,CAAgB,IAAA,CAAK,IAAA,IAAQ,EAAE,CAAA;AAElD,IAAA,MAAM,cAAoD,CAAA,KAAA,KAAS;AACjE,MAAA,IAAI,CAAC,UAAA,IAAc,kBAAA,KAAuB,YAAY,CAAC,gBAAA,CAAiB,KAAK,CAAA,EAAG;AAEhF,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,IAAA,CAAK,IAAA,EAAM,UAAU,qBAAqB,CAAA;AAC/D,MAAA,IAAI,CAAC,KAAA,EAAO;AAEZ,MAAA,KAAA,CAAM,MAAA,GAAS,IAAA;AACf,MAAA,KAAA,CAAM,cAAA,EAAe;AAAA,IACvB,CAAA;AAEA,IAAA,uBACEH,cAAA;AAAA,MAAC,GAAA;AAAA,MAAA;AAAA,QACC,IAAA;AAAA,QACA,KAAA;AAAA,QACA,OAAA,EAAS,WAAA;AAAA,QACT,MAAA,EAAQ,aAAa,QAAA,GAAW,MAAA;AAAA,QAChC,GAAA,EAAK,aAAa,qBAAA,GAAwB,MAAA;AAAA,QAEzC;AAAA;AAAA,KACH;AAAA,EAEJ,CAAA;AACF;AAEA,MAAM,cAAA,GAAiB,CAAC,SAAS,CAAA;AAKjC,MAAM,cAAA,GAAiB,EAAE,KAAA,EAAO,KAAA,EAAO,UAAU,WAAA,EAAY;AAK7D,MAAM,UAAA,GAAyB;AAAA,EAC7B,GAAA,EAAK,iBAAA;AAAA,EACL,CAAA,EAAG,aAAa,KAAK;AACvB,CAAA;AAEA,MAAM,iBAAA,GAAgC;AAAA,EACpC,GAAG,UAAA;AAAA,EACH,CAAA,EAAG,aAAa,QAAQ;AAC1B,CAAA;;;;"}
@@ -14,7 +14,6 @@
14
14
  font-size: var(--text-ui-md);
15
15
  line-height: 1.7;
16
16
  overflow-wrap: break-word;
17
- text-wrap: pretty;
18
17
  }
19
18
 
20
19
  .mastra-markdown > :first-child {
@@ -179,4 +178,36 @@
179
178
  max-width: 100%;
180
179
  border-radius: 8px;
181
180
  }
181
+
182
+ /* Streamed words. The animation runs when a word lands, so re-parsing the
183
+ * growing text lights up only what is new. No transform: it is ignored on
184
+ * inline elements. `backwards` so no `blur(0)` survives — a word left on the
185
+ * filtered raster path antialiases unlike the settled text replacing it. */
186
+ .mastra-markdown-streaming .mastra-markdown-word {
187
+ animation: mastra-markdown-word-in 180ms cubic-bezier(0.2, 0, 0, 1) backwards;
188
+ }
189
+
190
+ /* The word still being typed. It holds its width so revealing it shifts
191
+ * nothing; gaining the class above is what starts its fade. */
192
+ .mastra-markdown-streaming .mastra-markdown-word-pending {
193
+ opacity: 0;
194
+ }
195
+
196
+ @keyframes mastra-markdown-word-in {
197
+ from {
198
+ opacity: 0;
199
+ filter: blur(3px);
200
+ }
201
+
202
+ to {
203
+ opacity: 1;
204
+ filter: blur(0);
205
+ }
206
+ }
207
+
208
+ @media (prefers-reduced-motion: reduce) {
209
+ .mastra-markdown-streaming .mastra-markdown-word {
210
+ animation: none;
211
+ }
212
+ }
182
213
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Cuts markdown into top-level blocks so a growing reply re-parses its last
3
+ * block instead of all of them. Rejoining the result returns the input, and
4
+ * every block renders as it would have inside the whole.
5
+ *
6
+ * Each rule only ever merges: over-merging costs a re-parse, while cutting a
7
+ * construct in two changes what it renders as. Link reference and footnote
8
+ * definitions resolve across blank lines, which no line-level rule can see, so
9
+ * reaching one outside a fence abandons the split for the whole text.
10
+ */
11
+ export declare function splitBlocks(markdown: string): string[];