Skip to content

fix(tui): collapse fragmented reasoning parts and strip thinking echo…#32152

Open
BEEugene wants to merge 1 commit into
anomalyco:devfrom
BEEugene:reasoning-dedup
Open

fix(tui): collapse fragmented reasoning parts and strip thinking echo…#32152
BEEugene wants to merge 1 commit into
anomalyco:devfrom
BEEugene:reasoning-dedup

Conversation

@BEEugene

@BEEugene BEEugene commented Jun 13, 2026

Copy link
Copy Markdown

PR #3: reasoning-dedup (Closes #31999, probably, also #20782, #20706, #11439, and some related)

Issue for this PR

Closes #31999

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Some OpenAI-compatible reasoning providers (notably MiniMax-M3, but also DeepSeek-R1, GLM-Z1) stream the model's reasoning_content field as discrete reasoning-start / reasoning-delta / reasoning-end events (packages/opencode/src/session/processor.ts:371-425) and also echo the same text into the regular content field for back-compat. opencode correctly persists both as separate parts, but the TUI rendered them as dozens of "Thought: Xms" boxes plus a duplicate text paragraph in the same message.

Dedupe at the TUI layer in packages/tui/src/routes/session/index.tsx:

  1. Aggregate all ReasoningParts into one block at the top of the message.
  2. Strip unmatched <think>/</think> tags from text parts (opening/closing often split across the boundary, so a paired regex misses them).
  3. Dedup on a normalized fingerprint (lowercase, whitespace + Unicode punctuation stripped) and use substring includes (not prefix/suffix) so 4-part alternating streams A B A' B' collapse correctly.
  4. For text parts that begin with the merged reasoning, strip the echo prefix and keep only the tail (the actual response) using a character-level diff that skips whitespace/punctuation on both sides.

A debug logging hook (DEBUG_DEDUP_LOG = false by default) writes the part sequence and merge decisions to displayparts.log when enabled.

How did you verify your code works?

  • bun typecheck clean on packages/tui and packages/opencode.
  • Local build → binary installed, ran the original repro from Too many thought messages with MiniMax-M3 #31999: reasoning collapsed to one block, no duplicate text paragraph, actual response preserved.
  • No regression on non-reasoning responses (the "text has new content not in reasoning" branch keeps them as-is).
  • Pre-push bun turbo typecheck (29 packages) on the local fork passed for the touched packages. Two unrelated packages (@opencode-ai/stats-app and @opencode-ai/enterprise) have pre-existing typecheck failures on dev (verified by checking out clean origin/dev and running bun run typecheck in each — both exit 2 on the unmodified upstream). Unrelated to this PR; flagged here so reviewers aren't surprised.

Screenshots / recordings

TUI before fix (two near-duplicate reasoning paragraphs, no collapsed display):
[screenshot the user has, or describe: dozens of "Thought: Xms" boxes for one line of thinking, then the same text echoed in a paragraph below] PR #3: reasoning-dedup (Closes #31999)

Issue for this PR

Closes #31999

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Some OpenAI-compatible reasoning providers (notably MiniMax-M3, but also DeepSeek-R1, GLM-Z1) stream the model's reasoning_content field as discrete reasoning-start / reasoning-delta / reasoning-end events (packages/opencode/src/session/processor.ts:371-425) and also echo the same text into the regular content field for back-compat. opencode correctly persists both as separate parts, but the TUI rendered them as dozens of "Thought: Xms" boxes plus a duplicate text paragraph in the same message.

Dedupe at the TUI layer in packages/tui/src/routes/session/index.tsx:

  1. Aggregate all ReasoningParts into one block at the top of the message.
  2. Strip unmatched <think>/</think> tags from text parts (opening/closing often split across the boundary, so a paired regex misses them).
  3. Dedup on a normalized fingerprint (lowercase, whitespace + Unicode punctuation stripped) and use substring includes (not prefix/suffix) so 4-part alternating streams A B A' B' collapse correctly.
  4. For text parts that begin with the merged reasoning, strip the echo prefix and keep only the tail (the actual response) using a character-level diff that skips whitespace/punctuation on both sides.

A debug logging hook (DEBUG_DEDUP_LOG = false by default) writes the part sequence and merge decisions to displayparts.log when enabled.

How did you verify your code works?

  • bun typecheck clean on packages/tui and packages/opencode.
  • Local build → binary installed, ran the original repro from Too many thought messages with MiniMax-M3 #31999 (Russian ghbdtn and English ghbdtn rfr jyj&): reasoning collapsed to one block, no duplicate text paragraph, actual response preserved.
  • No regression on non-reasoning responses (the "text has new content not in reasoning" branch keeps them as-is).
  • Pre-push bun turbo typecheck (29 packages) on the local fork passed for the touched packages. Two unrelated packages (@opencode-ai/stats-app and @opencode-ai/enterprise) have pre-existing typecheck failures on dev (verified by checking out clean origin/dev and running bun run typecheck in each — both exit 2 on the unmodified upstream). Unrelated to this PR; flagged here so reviewers aren't surprised.

Screenshots / recordings

TUI before fix (two near-duplicate reasoning paragraphs, no collapsed display):
[screenshot the user has, or describe: dozens of "Thought: Xms" boxes for one line of thinking, then the same text echoed in a paragraph below]
image

TUI after fix (one merged reasoning block, clean response):
[screenshot: single "Thought: 1.9s" header, single reasoning paragraph, then the response "Hi! How can I help you?" on its own]
image

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

…es from text

Some models (notably MiniMax-M3 with extended thinking, but also
DeepSeek-R1, GLM-Z1, and other OpenAI-compatible reasoning providers)
fragment a single line of reasoning across many short ReasoningPart
chunks, sometimes interleaved with TextPart echoes of the same content.
The TUI rendered this as dozens of "Thought: Xms" boxes plus a duplicate
text paragraph in the assistant message.

Root cause: certain providers stream the model's `reasoning_content`
field as discrete reasoning events AND also echo the same text into
the regular `content` field for back-compat. opencode's
`SessionProcessor.handleEvent` correctly persists both as separate
parts (`processor.ts:371-425`), but the TUI then renders them as
duplicates. Dedupe at the TUI layer is the right place because not
every provider does this, and the echoed text often contains the
final response appended after the echo (we have to keep that tail).

- Aggregate ALL reasoning parts into one block at the top of the
  message (not just consecutive ones; the model can interleave
  non-reasoning parts in between).
- Strip `<think>`/`</mm:think>` tags individually from text parts. The
  opening/closing tags often end up split across the reasoning/text
  boundary, so a paired regex misses them.
- Dedup on a normalized fingerprint (lowercase, whitespace + Unicode
  punctuation stripped) and use substring `includes` (not prefix/suffix
  only) so 4-part alternating streams A B A' B' are caught.
- For text parts that begin with the merged reasoning, strip the echo
  prefix and keep only the tail (the actual response) using a
  character-level diff that skips whitespace/punctuation on both sides.

A debug logging hook (`DEBUG_DEDUP_LOG = false` by default) writes the
part sequence, fingerprints, and merge decisions to displayparts.log
when enabled, kept for future regressions.

Closes anomalyco#31999
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.

Too many thought messages with MiniMax-M3

1 participant