fix(desktop): stop reasoning text flashing/re-typing on every delta

Reasoning ("Thinking") rendered through MarkdownTextContent with a smooth
typewriter reveal. TextMessagePartProvider mints a fresh part object on every
text change, and useSmooth resets its reveal to empty whenever the part
identity changes — so each reasoning delta restarted the animation from the
first character (h / hell / hello wo ...). Token-streaming reasoners
(R1/Qwen/GLM/Claude thinking) fire a delta per token and flash hard; GPT-5's
coarse reasoning summary updates too rarely to notice, which is why it looked
fine.

Drop smooth on the reasoning path so it plain-appends, exactly like the
assistant answer already does. Removes the now-dead smooth plumbing too.
This commit is contained in:
Brooklyn Nicholson 2026-07-15 14:35:15 -04:00
parent e0e7cfa673
commit a4c9b069dc

View file

@ -1,6 +1,6 @@
'use client'
import { type SmoothOptions, TextMessagePartProvider, useMessagePartText } from '@assistant-ui/react'
import { TextMessagePartProvider, useMessagePartText } from '@assistant-ui/react'
import {
parseMarkdownIntoBlocks,
type StreamdownTextComponents,
@ -342,7 +342,6 @@ interface MarkdownTextSurfaceProps {
containerClassName?: string
containerProps?: ComponentProps<'div'>
defer?: boolean
smooth?: boolean | SmoothOptions
}
// Headings shrink to chat scale rather than the prose default (h1≈xl). Kept
@ -391,7 +390,7 @@ function HugeTextFallback({ containerClassName, text }: { containerClassName?: s
)
}
function MarkdownTextSurface({ containerClassName, containerProps, defer, smooth }: MarkdownTextSurfaceProps) {
function MarkdownTextSurface({ containerClassName, containerProps, defer }: MarkdownTextSurfaceProps) {
const { status, text } = useMessagePartText()
const isStreaming = status.type === 'running'
@ -531,29 +530,25 @@ function MarkdownTextSurface({ containerClassName, containerProps, defer, smooth
parseMarkdownIntoBlocksFn={parseMarkdownIntoBlocksCached}
plugins={plugins}
preprocess={preprocessWithTailRepair}
smooth={smooth}
/>
)
}
const SMOOTH_OPTIONS: SmoothOptions = {
drainMs: 500,
maxCharsPerFrame: 30,
minCommitMs: 33
}
interface MarkdownTextContentProps extends MarkdownTextSurfaceProps {
isRunning: boolean
text: string
}
export function MarkdownTextContent({ isRunning, text, ...surfaceProps }: MarkdownTextContentProps) {
// Same path as the assistant answer. A reasoning-only smoothing wrapper used
// to sit here but stalled its char-reveal at empty (the part stays running
// the whole message), blanking the Thinking widget.
// No `smooth` on purpose — same as the assistant answer. `TextMessagePartProvider`
// mints a fresh part object on every `text` change, and useSmooth resets its
// reveal to empty whenever the part identity changes, so a smoothed reasoning
// stream re-types from the first character on every delta (the flash). Token-
// streaming reasoners (R1/Qwen/GLM/Claude thinking) hit it hardest; GPT-5's
// coarse summary updates too rarely to notice. Plain append matches the answer.
return (
<TextMessagePartProvider isRunning={isRunning} text={text}>
<MarkdownTextSurface defer smooth={SMOOTH_OPTIONS} {...surfaceProps} />
<MarkdownTextSurface defer {...surfaceProps} />
</TextMessagePartProvider>
)
}