Skip to content

fix(chat): stop leftover * and _ at the end of finished replies - #487

Merged
xintaofei merged 5 commits into
xintaofei:mainfrom
Adam-Dalloul:fix/streamdown-trailing-emphasis
Sep 1, 2026
Merged

fix(chat): stop leftover * and _ at the end of finished replies#487
xintaofei merged 5 commits into
xintaofei:mainfrom
Adam-Dalloul:fix/streamdown-trailing-emphasis

Conversation

@Adam-Dalloul

@Adam-Dalloul Adam-Dalloul commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Finished assistant replies were showing a stray * or _ at the end. Not the model. Streamdown defaults to mode="streaming", so remend ran on every completed bubble. remend 1.2.0 then appends a closer after a glob like `foo/*` or `_meta` / _blank.

Finished markdown now stays mode="static". Only the live streaming turn still remends. Reasoning and file previews are static too.

Any agent that writes those shapes could hit this. Grok just does it a lot.

Also fixes #555. That report looked like a separate code-fence parser bug: a reply ending exactly at a closing fence rendered _ ``` as a line inside the block. Same cause. The appended _ lands after the final fence, the fence stops closing, and the backticks become code content. Added a regression test for that shape (`message-final-fence.test.tsx`).

Measured truth table on the exact reply from #555, driving MessageResponse under vitest:

mode parseIncompleteMarkdown result
streaming true _ ``` inside the block
streaming false clean
static true clean
static false (this PR) clean

Both defaults have to be at the bad value to reproduce, so either half of this PR is enough; it ships both.

Independent of the other open PRs. One concern.

Streamdown defaults to mode=streaming, so remend ran on every completed
assistant bubble. remend 1.2.0 then appended a closer after globs like
`foo/*` and `_meta` / `_blank`. Default finished markdown to static and
only remend the live streaming turn.
@Adam-Dalloul
Adam-Dalloul force-pushed the fix/streamdown-trailing-emphasis branch from 73c0e98 to d2708fb Compare August 28, 2026 18:30
Finished markdown going static is right, but ReasoningContent was pinned
there unconditionally — and Reasoning auto-opens the panel the moment the
model starts thinking, so it re-renders on every delta of a block that
routinely runs to thousands of tokens. Static re-parses the whole text each
time instead of only the tail block: measured 5897ms vs 2067ms over 120
growing deltas, and the gap widens with length. Track the turn there too,
the way the reply prose already does.

Also fold the two mode/parseIncompleteMarkdown defaults into shared
constants so the impl default and the memo comparator cannot drift apart,
and record which half of the fix actually repaints — Streamdown's own memo
compares `mode` and not `parseIncompleteMarkdown`, so at the live to
finished flip only the mode change forces the remended DOM off screen.

The final-fence regression waited on a 400ms wall clock; anchor it on the
code block's content instead, which is what the stray closer would be part
of anyway.
@xintaofei

Copy link
Copy Markdown
Owner

codeg work task 55 is done — #487 (4 files, +154/-20).

Only completed-turn-content.tsx conflicted. Main's 73e96ac rewrote the
exact region this branch had patched: the single early return that used to
catch every live reply became a `foldable` predicate plus per-round fold
state. Took main's structure wholesale — this branch has no stake in the
fold logic — and re-applied its own intent on top.

That re-application is not the one-line patch it was. Under the old shape
`!completed` always took the early return, so the progress and answer
renderers below it were provably settled and needed nothing. Main's
`foldable` disjunct (`!completed || hasVisibleAnswer(...)`) drops a live
reply that has already made a tool call into the fold branch instead, so
both halves now render growing text and both need to be told so. Hence a
named `isStreaming` passed at every renderer in the component rather than
at one of them.

completed-turn-streaming-mode.test.tsx pins that: a live foldable reply
renders both halves streaming, a settled one static. Dropping the flag from
the two fold-branch sites fails it.
@xintaofei
xintaofei merged commit 93c3386 into xintaofei:main Sep 1, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Markdown renderer draws the closing code fence inside the block when the message ends exactly at the fence

2 participants