From dda5e5493404d139733f7d26374b97278cf84478 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 01:48:31 -0400 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=A4=96=20Migrate=20markdown=20renderi?= =?UTF-8?q?ng=20to=20streamdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace react-markdown with streamdown, a drop-in replacement optimized for AI streaming - Replace rehype-sanitize with rehype-harden for improved security - Add streamdown source path to Tailwind v4 config for proper styling - Enable parseIncompleteMarkdown for better handling of streaming content - Update plugin configuration to match streamdown's API (typed Pluggable arrays) - Remove old dependencies: react-markdown, rehype-sanitize Streamdown provides the same functionality with additional features: - Better handling of incomplete/unterminated markdown blocks during streaming - Built-in security with rehype-harden - Optimized for AI-powered content streaming - Full compatibility with our existing custom components and styling --- bun.lock | 14 ++++---- package.json | 4 +-- src/components/Messages/MarkdownCore.tsx | 43 ++++++++++++------------ src/styles/globals.css | 1 + 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index 525904f5cf..8c65976937 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,9 @@ "lru-cache": "^11.2.2", "markdown-it": "^14.1.0", "minimist": "^1.2.8", + "rehype-harden": "^1.1.5", "source-map-support": "^0.5.21", + "streamdown": "^1.4.0", "undici": "^7.16.0", "write-file-atomic": "^6.0.0", "ws": "^8.18.3", @@ -91,10 +93,8 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", - "react-markdown": "^10.1.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.13.0", @@ -1718,8 +1718,6 @@ "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], @@ -2090,6 +2088,8 @@ "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + "lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], @@ -2492,12 +2492,12 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "rehype-harden": ["rehype-harden@1.1.5", "", {}, "sha512-JrtBj5BVd/5vf3H3/blyJatXJbzQfRT9pJBmjafbTaPouQCAKxHwRyCc7dle9BXQKxv4z1OzZylz/tNamoiG3A=="], + "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], - "release-zalgo": ["release-zalgo@1.0.0", "", { "dependencies": { "es6-error": "^4.0.1" } }, "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], @@ -2646,6 +2646,8 @@ "storybook": ["storybook@8.6.14", "", { "dependencies": { "@storybook/core": "8.6.14" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": { "sb": "./bin/index.cjs", "storybook": "./bin/index.cjs", "getstorybook": "./bin/index.cjs" } }, "sha512-sVKbCj/OTx67jhmauhxc2dcr1P+yOgz/x3h0krwjyMgdc5Oubvxyg4NYDZmzAw+ym36g/lzH8N0Ccp4dwtdfxw=="], + "streamdown": ["streamdown@1.4.0", "", { "dependencies": { "clsx": "^2.1.1", "katex": "^0.16.22", "lucide-react": "^0.542.0", "marked": "^16.2.1", "mermaid": "^11.11.0", "react-markdown": "^10.1.0", "rehype-harden": "^1.1.5", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.12.2", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-ylhDSQ4HpK5/nAH9v7OgIIdGJxlJB2HoYrYkJNGrO8lMpnWuKUcrz/A8xAMwA6eILA27469vIavcOTjmxctrKg=="], + "string-length": ["string-length@5.0.1", "", { "dependencies": { "char-regex": "^2.0.0", "strip-ansi": "^7.0.1" } }, "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/package.json b/package.json index c327bec835..64a4c210ad 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,9 @@ "lru-cache": "^11.2.2", "markdown-it": "^14.1.0", "minimist": "^1.2.8", + "rehype-harden": "^1.1.5", "source-map-support": "^0.5.21", + "streamdown": "^1.4.0", "undici": "^7.16.0", "write-file-atomic": "^6.0.0", "ws": "^8.18.3", @@ -131,10 +133,8 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", - "react-markdown": "^10.1.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "shiki": "^3.13.0", diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index 93f6d176c1..85bd0b8162 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -1,11 +1,11 @@ import React, { useMemo } from "react"; -import ReactMarkdown from "react-markdown"; -import type { PluggableList } from "unified"; +import { Streamdown } from "streamdown"; +import type { Pluggable } from "unified"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; import rehypeRaw from "rehype-raw"; -import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; +import { harden } from "rehype-harden"; import "katex/dist/katex.min.css"; import { normalizeMarkdown } from "./MarkdownStyles"; import { markdownComponents } from "./MarkdownComponents"; @@ -16,24 +16,24 @@ interface MarkdownCoreProps { } // Plugin arrays are defined at module scope to maintain stable references. -// ReactMarkdown treats new array references as changes requiring full re-parse. -const REMARK_PLUGINS = [remarkGfm, remarkMath]; - -// Sanitization schema: whitelist only safe HTML elements -// This prevents XSS attacks while allowing
/ toggles -const SANITIZE_SCHEMA = { - ...defaultSchema, - tagNames: [...(defaultSchema.tagNames ?? []), "details", "summary"], - attributes: { - ...defaultSchema.attributes, - details: ["open"], // Allow 'open' attribute for default-expanded state - }, -}; +// Streamdown treats new array references as changes requiring full re-parse. +const REMARK_PLUGINS: Pluggable[] = [ + [remarkGfm, {}], + [remarkMath, { singleDollarTextMath: false }], +]; -const REHYPE_PLUGINS: PluggableList = [ +const REHYPE_PLUGINS: Pluggable[] = [ + [ + harden, + { + allowedImagePrefixes: ["*"], + allowedLinkPrefixes: ["*"], + defaultOrigin: undefined, + allowDataImages: true, + }, + ], rehypeRaw, // Parse HTML elements - [rehypeSanitize, SANITIZE_SCHEMA], // Sanitize to whitelist only - rehypeKatex, // Render math (must be after sanitization) + [rehypeKatex, { errorColor: "var(--color-muted-foreground)" }], // Render math ]; /** @@ -48,13 +48,14 @@ export const MarkdownCore = React.memo(({ content, children } return ( <> - {normalizedContent} - + {children} ); diff --git a/src/styles/globals.css b/src/styles/globals.css index b037e429f5..b342d5c79a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@source "../node_modules/streamdown/dist/index.js"; @theme { /* Mode Colors */ From e080968e121502e022fd2cbc5c10774295b7709e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 01:51:13 -0400 Subject: [PATCH 2/9] Fix plugin order: sanitize after parsing raw HTML Move rehype-harden after rehypeRaw to ensure all HTML is sanitized. Previously, harden ran before rehypeRaw, allowing unsanitized HTML to be injected after the security check. --- src/components/Messages/MarkdownCore.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index 85bd0b8162..a4139cfa82 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -23,8 +23,9 @@ const REMARK_PLUGINS: Pluggable[] = [ ]; const REHYPE_PLUGINS: Pluggable[] = [ + rehypeRaw, // Parse HTML elements first [ - harden, + harden, // Sanitize after parsing raw HTML to prevent XSS { allowedImagePrefixes: ["*"], allowedLinkPrefixes: ["*"], @@ -32,7 +33,6 @@ const REHYPE_PLUGINS: Pluggable[] = [ allowDataImages: true, }, ], - rehypeRaw, // Parse HTML elements [rehypeKatex, { errorColor: "var(--color-muted-foreground)" }], // Render math ]; From 3aaba3e0d615b028af4d4fd780010c045540cd95 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 02:33:30 -0400 Subject: [PATCH 3/9] Remove streamdown's default spacing wrapper Override streamdown's default 'space-y-4' className to prevent extra vertical padding on markdown content. We handle spacing at the component level in MarkdownRenderer/TypewriterMarkdown. --- src/components/Messages/MarkdownCore.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index a4139cfa82..a1b07bd0dd 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -53,6 +53,7 @@ export const MarkdownCore = React.memo(({ content, children } remarkPlugins={REMARK_PLUGINS} rehypePlugins={REHYPE_PLUGINS} parseIncompleteMarkdown={true} + className="" // Override streamdown's default "space-y-4" spacing > {normalizedContent} From 30ed40b97599b8930a1915d9e338994658dd2435 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 02:39:17 -0400 Subject: [PATCH 4/9] Override streamdown spacing with space-y-0 Use space-y-0 instead of empty string to properly override streamdown's default space-y-4 wrapper spacing. --- src/components/Messages/MarkdownCore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index a1b07bd0dd..27a1986708 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -53,7 +53,7 @@ export const MarkdownCore = React.memo(({ content, children } remarkPlugins={REMARK_PLUGINS} rehypePlugins={REHYPE_PLUGINS} parseIncompleteMarkdown={true} - className="" // Override streamdown's default "space-y-4" spacing + className="space-y-0" // Override streamdown's default "space-y-4" to remove extra spacing > {normalizedContent} From 8644c07cc2f73fe6f0b7c4c1816673821a836a14 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 02:39:47 -0400 Subject: [PATCH 5/9] Add CSS override for streamdown wrapper spacing Explicitly set margin-top: 0 on streamdown's block children to prevent double-spacing. Our paragraph/heading margins already provide appropriate spacing between elements. --- src/styles/globals.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/styles/globals.css b/src/styles/globals.css index b342d5c79a..e06e208f8a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -439,6 +439,11 @@ code { white-space: normal; } +/* Override streamdown's space-y wrapper - we handle spacing with element margins */ +.markdown-content > div > * { + margin-top: 0 !important; +} + .markdown-content h1, .markdown-content h2, .markdown-content h3, From feff844b8d13999fc8f86149398429037e7bc7ef Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 02:47:51 -0400 Subject: [PATCH 6/9] Use streamdown's spacing and apply to user messages - Change message content padding from p-3 to px-3 (horizontal only) - Remove CSS override that was fighting streamdown's spacing - Use streamdown's default space-y-4 wrapper for proper block spacing - Apply MarkdownRenderer to user messages for consistent formatting This allows streamdown's block spacing (space-y-4) to provide vertical spacing while px-3 provides horizontal padding, eliminating the double-spacing issue. --- src/components/Messages/MarkdownCore.tsx | 1 - src/components/Messages/MessageWindow.tsx | 2 +- src/components/Messages/UserMessage.tsx | 7 ++----- src/styles/globals.css | 5 ----- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index 27a1986708..a4139cfa82 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -53,7 +53,6 @@ export const MarkdownCore = React.memo(({ content, children } remarkPlugins={REMARK_PLUGINS} rehypePlugins={REHYPE_PLUGINS} parseIncompleteMarkdown={true} - className="space-y-0" // Override streamdown's default "space-y-4" to remove extra spacing > {normalizedContent} diff --git a/src/components/Messages/MessageWindow.tsx b/src/components/Messages/MessageWindow.tsx index 85b752de3a..548c0a0042 100644 --- a/src/components/Messages/MessageWindow.tsx +++ b/src/components/Messages/MessageWindow.tsx @@ -122,7 +122,7 @@ export const MessageWindow: React.FC = ({ )} -
+
{showJson ? (
             {JSON.stringify(message, null, 2)}
diff --git a/src/components/Messages/UserMessage.tsx b/src/components/Messages/UserMessage.tsx
index 75968b3519..d24a4c1abd 100644
--- a/src/components/Messages/UserMessage.tsx
+++ b/src/components/Messages/UserMessage.tsx
@@ -5,6 +5,7 @@ import { MessageWindow } from "./MessageWindow";
 import { TerminalOutput } from "./TerminalOutput";
 import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
 import type { KebabMenuItem } from "@/components/KebabMenu";
+import { MarkdownRenderer } from "./MarkdownRenderer";
 
 interface UserMessageProps {
   message: DisplayedMessage & { type: "user" };
@@ -120,11 +121,7 @@ export const UserMessage: React.FC = ({
       kebabMenuItems={kebabMenuItems}
       className={className}
     >
-      {content && (
-        
-          {content}
-        
- )} + {content && } {message.imageParts && message.imageParts.length > 0 && (
{message.imageParts.map((img, idx) => ( diff --git a/src/styles/globals.css b/src/styles/globals.css index e06e208f8a..b342d5c79a 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -439,11 +439,6 @@ code { white-space: normal; } -/* Override streamdown's space-y wrapper - we handle spacing with element margins */ -.markdown-content > div > * { - margin-top: 0 !important; -} - .markdown-content h1, .markdown-content h2, .markdown-content h3, From e08b431abf5f6024d5f858ea6a71f3d2cda5eb9f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 02:53:48 -0400 Subject: [PATCH 7/9] Use margin instead of padding for message content - Change content wrapper from p-3/px-3 to m-3 (margin instead of padding) - Revert UserMessage to use
 tags (not markdown)

This prevents double-spacing when streamdown's space-y-4 adds
margin-top to blocks. Margins collapse, padding doesn't.
---
 src/components/Messages/MessageWindow.tsx | 2 +-
 src/components/Messages/UserMessage.tsx   | 7 +++++--
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/components/Messages/MessageWindow.tsx b/src/components/Messages/MessageWindow.tsx
index 548c0a0042..c89f9192d1 100644
--- a/src/components/Messages/MessageWindow.tsx
+++ b/src/components/Messages/MessageWindow.tsx
@@ -122,7 +122,7 @@ export const MessageWindow: React.FC = ({
           )}
         
-
+
{showJson ? (
             {JSON.stringify(message, null, 2)}
diff --git a/src/components/Messages/UserMessage.tsx b/src/components/Messages/UserMessage.tsx
index d24a4c1abd..75968b3519 100644
--- a/src/components/Messages/UserMessage.tsx
+++ b/src/components/Messages/UserMessage.tsx
@@ -5,7 +5,6 @@ import { MessageWindow } from "./MessageWindow";
 import { TerminalOutput } from "./TerminalOutput";
 import { formatKeybind, KEYBINDS } from "@/utils/ui/keybinds";
 import type { KebabMenuItem } from "@/components/KebabMenu";
-import { MarkdownRenderer } from "./MarkdownRenderer";
 
 interface UserMessageProps {
   message: DisplayedMessage & { type: "user" };
@@ -121,7 +120,11 @@ export const UserMessage: React.FC = ({
       kebabMenuItems={kebabMenuItems}
       className={className}
     >
-      {content && }
+      {content && (
+        
+          {content}
+        
+ )} {message.imageParts && message.imageParts.length > 0 && (
{message.imageParts.map((img, idx) => ( From 7aed3a040299069616a3d672f93c47989fbaf32c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 02:58:19 -0400 Subject: [PATCH 8/9] Reduce streamdown block spacing from 16px to 8px Use space-y-2 instead of the default space-y-4 to reduce excessive vertical spacing between markdown blocks. --- src/components/Messages/MarkdownCore.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index a4139cfa82..652d5f05b1 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -53,6 +53,7 @@ export const MarkdownCore = React.memo(({ content, children } remarkPlugins={REMARK_PLUGINS} rehypePlugins={REHYPE_PLUGINS} parseIncompleteMarkdown={true} + className="space-y-2" // Reduce from default space-y-4 (16px) to space-y-2 (8px) > {normalizedContent} From 05df60bcf1c98648e834057ca6375161e11a8904 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 23 Oct 2025 03:04:15 -0400 Subject: [PATCH 9/9] Remove paragraph margin, rely on streamdown spacing Remove .markdown-content p margin rule - streamdown's space-y-2 wrapper provides sufficient spacing between blocks. --- src/styles/globals.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/styles/globals.css b/src/styles/globals.css index b342d5c79a..2f08dafe29 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -467,9 +467,7 @@ code { font-size: 13px; } -.markdown-content p { - margin: 0.8em 0; -} + /* Remove default margins on first and last elements */ .markdown-content > :first-child {