From c8ff858565531ba468ad4a2324610d1b2333418f Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 17 Feb 2026 22:44:30 +0000 Subject: [PATCH] fix(ui): render user message text as markdown User text parts now use the same Markdown renderer + cache path as assistant messages, while keeping role-specific heading and accent colors. --- packages/ui/src/components/message-part.tsx | 62 +++++++++++-------- packages/ui/src/styles/markdown.css | 7 ++- .../ui/src/styles/messaging/message-base.css | 7 +++ 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 4f2ff2aa..7178eb93 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -3,7 +3,6 @@ import ToolCall from "./tool-call" import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state" import { Markdown } from "./markdown" import { useTheme } from "../lib/theme" -import { useConfig } from "../stores/preferences" import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/message" type ToolCallPart = Extract @@ -17,16 +16,18 @@ interface MessagePartProps { // Other synthetic text parts (tool traces, read outputs, etc.) should be hidden. primaryUserTextPartId?: string | null onRendered?: () => void - } - export default function MessagePart(props: MessagePartProps) { +} + +export default function MessagePart(props: MessagePartProps) { const { isDark } = useTheme() - const { preferences } = useConfig() const partType = () => props.part?.type || "" const reasoningId = () => `reasoning-${props.part?.id || ""}` const isReasoningExpanded = () => isItemExpanded(reasoningId()) const isAssistantMessage = () => props.messageType === "assistant" const textContainerClass = () => (isAssistantMessage() ? "message-text message-text-assistant" : "message-text") + const markdownContainerClass = () => "message-text message-text-assistant" + const textContainerRole = () => props.messageType || "assistant" const shouldHideTextPart = () => { const part = props.part @@ -57,6 +58,11 @@ interface MessagePartProps { return "" } + const canRenderMarkdown = () => { + const id = (props.part as unknown as { id?: unknown })?.id + return typeof id === "string" && id.length > 0 + } + function reasoningSegmentHasText(segment: unknown): boolean { if (typeof segment === "string") { return segment.trim().length > 0 @@ -91,20 +97,28 @@ interface MessagePartProps { const createTextPartForMarkdown = (): TextPart => { const part = props.part - if ((part.type === "text" || part.type === "reasoning") && typeof part.text === "string") { + if (part.type === "text" && typeof part.text === "string") { + // Pass through the original part so `renderCache` updates persist. + return part as unknown as TextPart + } + + if (part.type === "reasoning" && typeof (part as any).text === "string") { + // Reasoning parts render as markdown in some views; normalize to TextPart. return { id: part.id, type: "text", - text: part.text, - synthetic: part.type === "text" ? part.synthetic : false, - version: (part as { version?: number }).version + text: (part as any).text, + synthetic: false, + version: (part as { version?: number }).version, + renderCache: (part as any).renderCache, } } + return { id: part.id, - type: "text", + type: "text", text: "", - synthetic: false + synthetic: false, } } @@ -117,22 +131,18 @@ interface MessagePartProps { -
- {plainTextContent()}} - > - - - -
+
+ {plainTextContent()}}> + + +
diff --git a/packages/ui/src/styles/markdown.css b/packages/ui/src/styles/markdown.css index 142e8e68..d8b89be7 100644 --- a/packages/ui/src/styles/markdown.css +++ b/packages/ui/src/styles/markdown.css @@ -9,6 +9,9 @@ line-height: var(--line-height-normal); font-weight: var(--font-weight-regular); color: var(--text-primary); + /* Message containers may use `whitespace-pre-wrap` for plain text. + Markdown should always match assistant rendering (normal whitespace). */ + white-space: normal; } .markdown-body p, @@ -28,7 +31,7 @@ .markdown-body h5, .markdown-body h6 { font-family: inherit; - color: inherit; + color: var(--markdown-heading-color, inherit); font-weight: var(--font-weight-semibold); line-height: 1.3; margin-top: 0.9em; @@ -71,7 +74,7 @@ .markdown-body strong { font-weight: var(--font-weight-regular); - color: var(--message-assistant-border); + color: var(--markdown-accent, var(--message-assistant-border)); } .markdown-body em { diff --git a/packages/ui/src/styles/messaging/message-base.css b/packages/ui/src/styles/messaging/message-base.css index 8064ae62..5f45939a 100644 --- a/packages/ui/src/styles/messaging/message-base.css +++ b/packages/ui/src/styles/messaging/message-base.css @@ -1,6 +1,10 @@ /* Message item base styles */ .message-item-base { @apply flex flex-col gap-2 p-3 w-full; + + /* Markdown rendering uses these to theme emphasis + headings per message role. */ + --markdown-accent: var(--message-user-border); + --markdown-heading-color: var(--message-user-border); } .message-item-header { @@ -71,6 +75,9 @@ padding: 0.6rem 0.65rem; margin-top: 0; margin-bottom: 0; + + --markdown-accent: var(--message-assistant-border); + --markdown-heading-color: var(--text-primary); } .message-item-base:not(.assistant-message) {