Skip to content

Commit 3a84ef4

Browse files
authored
🤖 fix: disable incomplete markdown parsing for completed content (#909)
## Summary The remend library (used by streamdown for `parseIncompleteMarkdown`) has a bug where content like `$__timeFilter` causes trailing `__` to be added to the output. This happens because remend counts all `__` in the entire string without respecting code block boundaries. ## Fix Only enable `parseIncompleteMarkdown` during actual streaming, not for completed/historical content. This prevents the bug while still providing the streaming UX benefit. ## Root Cause `remend@1.0.1` counts markdown emphasis markers (`__`) globally rather than excluding fenced code blocks, causing false 'repairs'. Example buggy input: ```markdown ```sql SELECT * WHERE $__timeFilter(x) ``` ``` Would incorrectly render as: ```markdown ```sql SELECT * WHERE $__timeFilter(x) ```__ ``` ## Testing - Added storybook story to reproduce the bug - Verified fix works by disabling parseIncompleteMarkdown for completed content _Generated with `mux`_
1 parent e28e6fe commit 3a84ef4

File tree

5 files changed

+74
-23
lines changed

5 files changed

+74
-23
lines changed

bun.lock

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"rehype-harden": "^1.1.5",
5555
"shescape": "^2.1.6",
5656
"source-map-support": "^0.5.21",
57-
"streamdown": "^1.4.0",
57+
"streamdown": "1.6.10",
5858
"trpc-cli": "^0.12.1",
5959
"turndown": "^7.2.2",
6060
"undici": "^7.16.0",
@@ -3144,6 +3144,8 @@
31443144

31453145
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
31463146

3147+
"remend": ["remend@1.0.1", "", {}, "sha512-152puVH0qMoRJQFnaMG+rVDdf01Jq/CaED+MBuXExurJgdbkLp0c3TIe4R12o28Klx8uyGsjvFNG05aFG69G9w=="],
3148+
31473149
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
31483150

31493151
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
@@ -3296,7 +3298,7 @@
32963298

32973299
"storybook": ["storybook@10.1.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "recast": "^0.23.5", "semver": "^7.6.2", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-FrBjm8I8O+pYEOPHcdW9xWwgXSZxte7lza9q2lN3jFN4vuW79m5j0OnTQeR8z9MmIbBTvkIpp3yMBebl53Yt5Q=="],
32983300

3299-
"streamdown": ["streamdown@1.6.9", "", { "dependencies": { "clsx": "^2.1.1", "hast": "^1.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "katex": "^0.16.22", "lucide-react": "^0.542.0", "marked": "^16.2.1", "mermaid": "^11.11.0", "rehype-harden": "^1.1.6", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "shiki": "^3.12.2", "tailwind-merge": "^3.3.1", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-rtUZcRvDYNEgduq1OxNJzuYYmchZVXq+1Pw3T445RrYwrT+SGNK1drtt1eaqC4HaD8YYIscdtMSlZFaNM+yYGA=="],
3301+
"streamdown": ["streamdown@1.6.10", "", { "dependencies": { "clsx": "^2.1.1", "hast": "^1.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "katex": "^0.16.22", "lucide-react": "^0.542.0", "marked": "^16.2.1", "mermaid": "^11.11.0", "rehype-harden": "^1.1.6", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.0.1", "shiki": "^3.12.2", "tailwind-merge": "^3.3.1", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-B4Y3Z/qiXl1Dc+LzAB5c52Cd1QGRiFjaDwP+ERoj1JtCykdRDM8X6HwQnn3YkpkSk0x3R7S/6LrGe1nQiElHQQ=="],
33003302

33013303
"string-length": ["string-length@6.0.0", "", { "dependencies": { "strip-ansi": "^7.1.0" } }, "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg=="],
33023304

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
"rehype-harden": "^1.1.5",
9696
"shescape": "^2.1.6",
9797
"source-map-support": "^0.5.21",
98-
"streamdown": "^1.4.0",
98+
"streamdown": "1.6.10",
9999
"trpc-cli": "^0.12.1",
100100
"turndown": "^7.2.2",
101101
"undici": "^7.16.0",

src/browser/components/Messages/MarkdownCore.tsx

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ import { markdownComponents } from "./MarkdownComponents";
1313
interface MarkdownCoreProps {
1414
content: string;
1515
children?: React.ReactNode; // For cursor or other additions
16+
/**
17+
* Enable incomplete markdown parsing for streaming content.
18+
* When true, the remend library will attempt to "repair" unclosed markdown
19+
* syntax (e.g., adding closing ** for bold). This is useful during streaming
20+
* but can cause bugs with content like $__variable (adds trailing __).
21+
* Default: false for completed content, true during streaming.
22+
*/
23+
parseIncompleteMarkdown?: boolean;
1624
}
1725

1826
// Plugin arrays are defined at module scope to maintain stable references.
@@ -42,25 +50,27 @@ const REHYPE_PLUGINS: Pluggable[] = [
4250
*
4351
* Memoized to prevent expensive re-parsing when content hasn't changed.
4452
*/
45-
export const MarkdownCore = React.memo<MarkdownCoreProps>(({ content, children }) => {
46-
// Memoize the normalized content to avoid recalculating on every render
47-
const normalizedContent = useMemo(() => normalizeMarkdown(content), [content]);
53+
export const MarkdownCore = React.memo<MarkdownCoreProps>(
54+
({ content, children, parseIncompleteMarkdown = false }) => {
55+
// Memoize the normalized content to avoid recalculating on every render
56+
const normalizedContent = useMemo(() => normalizeMarkdown(content), [content]);
4857

49-
return (
50-
<>
51-
<Streamdown
52-
components={markdownComponents}
53-
remarkPlugins={REMARK_PLUGINS}
54-
rehypePlugins={REHYPE_PLUGINS}
55-
parseIncompleteMarkdown={true}
56-
className="space-y-2" // Reduce from default space-y-4 (16px) to space-y-2 (8px)
57-
controls={{ table: false, code: true, mermaid: true }} // Disable table copy/download, keep code/mermaid controls
58-
>
59-
{normalizedContent}
60-
</Streamdown>
61-
{children}
62-
</>
63-
);
64-
});
58+
return (
59+
<>
60+
<Streamdown
61+
components={markdownComponents}
62+
remarkPlugins={REMARK_PLUGINS}
63+
rehypePlugins={REHYPE_PLUGINS}
64+
parseIncompleteMarkdown={parseIncompleteMarkdown}
65+
className="space-y-2" // Reduce from default space-y-4 (16px) to space-y-2 (8px)
66+
controls={{ table: false, code: true, mermaid: true }} // Disable table copy/download, keep code/mermaid controls
67+
>
68+
{normalizedContent}
69+
</Streamdown>
70+
{children}
71+
</>
72+
);
73+
}
74+
);
6575

6676
MarkdownCore.displayName = "MarkdownCore";

src/browser/components/Messages/TypewriterMarkdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const TypewriterMarkdown = React.memo<TypewriterMarkdownProps>(function T
2626
return (
2727
<StreamingContext.Provider value={streamingContextValue}>
2828
<div className={cn("markdown-content", className)}>
29-
<MarkdownCore content={content} />
29+
<MarkdownCore content={content} parseIncompleteMarkdown={isStreaming} />
3030
</div>
3131
</StreamingContext.Provider>
3232
);

src/browser/stories/App.markdown.stories.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ const TABLE_CONTENT = `Here are various markdown table examples:
4545
| \`api.timeout\` | 30000 | Timeout ms | \`API_TIMEOUT\` |
4646
| \`cache.enabled\` | true | Enable cache | \`CACHE_ENABLED\` |`;
4747

48+
// Bug repro: SQL with $__timeFilter causes "__" to appear at end of code block
49+
const SQL_WITH_DOUBLE_UNDERSCORE = `👍 Glad it's working. For reference, the final query:
50+
51+
\`\`\`sql
52+
SELECT
53+
TIMESTAMP_TRUNC(timestamp, DAY) as time,
54+
COUNT(DISTINCT distinct_id) as dau
55+
FROM \`mux-telemetry.posthog.events\`
56+
WHERE
57+
event NOT LIKE "$%"
58+
AND $__timeFilter(timestamp)
59+
GROUP BY time
60+
ORDER BY time
61+
\`\`\`
62+
`;
63+
4864
const CODE_CONTENT = `Here's the implementation:
4965
5066
\`\`\`typescript
@@ -100,6 +116,29 @@ export const Tables: AppStory = {
100116
),
101117
};
102118

119+
/** SQL with double underscores in code block - tests for bug where __ leaks to end */
120+
export const SqlWithDoubleUnderscore: AppStory = {
121+
render: () => (
122+
<AppWithMocks
123+
setup={() =>
124+
setupSimpleChatStory({
125+
workspaceId: "ws-sql-underscore",
126+
messages: [
127+
createUserMessage("msg-1", "Show me the SQL query", {
128+
historySequence: 1,
129+
timestamp: STABLE_TIMESTAMP - 100000,
130+
}),
131+
createAssistantMessage("msg-2", SQL_WITH_DOUBLE_UNDERSCORE, {
132+
historySequence: 2,
133+
timestamp: STABLE_TIMESTAMP - 90000,
134+
}),
135+
],
136+
})
137+
}
138+
/>
139+
),
140+
};
141+
103142
/** Code blocks with syntax highlighting */
104143
export const CodeBlocks: AppStory = {
105144
render: () => (

0 commit comments

Comments
 (0)