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.
This commit is contained in:
@@ -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<ClientPart, { type: "tool" }>
|
||||
@@ -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 {
|
||||
<Switch>
|
||||
<Match when={partType() === "text"}>
|
||||
<Show when={!shouldHideTextPart() && partHasRenderableText(props.part)}>
|
||||
<div class={textContainerClass()}>
|
||||
<Show
|
||||
when={isAssistantMessage()}
|
||||
fallback={<span class="text-primary">{plainTextContent()}</span>}
|
||||
>
|
||||
<Markdown
|
||||
part={createTextPartForMarkdown()}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
isDark={isDark()}
|
||||
size={isAssistantMessage() ? "tight" : "base"}
|
||||
onRendered={props.onRendered}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
<div class={canRenderMarkdown() ? markdownContainerClass() : textContainerClass()} data-role={textContainerRole()}>
|
||||
<Show when={canRenderMarkdown()} fallback={<span class="text-primary">{plainTextContent()}</span>}>
|
||||
<Markdown
|
||||
part={createTextPartForMarkdown()}
|
||||
instanceId={props.instanceId}
|
||||
sessionId={props.sessionId}
|
||||
isDark={isDark()}
|
||||
size={isAssistantMessage() ? "tight" : "base"}
|
||||
onRendered={props.onRendered}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Match>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user