feat(tui): persist thinking block across streaming with markdown body#31
Merged
feat(tui): persist thinking block across streaming with markdown body#31
Conversation
Thinking tokens used to evaporate the moment the model switched to prose — `append_stream_token` cleared the transient buffer before routing the first text chunk, and `commit_streaming` did the same at turn end, so a thinking-only turn (think → tool call) left nothing visible either. Flush the buffer into a committed `AssistantThinking` block instead, so the reasoning stays in the transcript for the rest of the session. Rework the block's visual language while we're here: a dim `│ ` bar on every line replaces the `◇ Thinking...` hanging indent, the header sits flush against the bar, and the body runs through `render_markdown` so inline code, emphasis, and fenced blocks keep their styling. A selective patch dims plain-text spans (prose reads muted) but leaves spans that already carry an explicit fg — inline code, links, headings, syntax-highlighted fences — at full color.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…istantThinking Codecov flagged one uncovered branch in — the fenced-code early-return only fires when the line's whole-line style carries an fg, which none of the existing thinking-block tests exercised. Add a focused test module that pins: - Empty / whitespace-only body renders the header alone (guard skips the markdown pass — relevant during the zero-delta frame between a thinking block opening and its first chunk). - Fenced code body keeps its highlight fg and still picks up the bar prefix — a regression patching thinking() onto highlighted lines would swap code colors for dim gray. - Plain spans get dimmed while explicitly-colored spans (inline code) keep their fg — the split that lets reasoning read as muted while code stays legible.
`│` (U+2502) sits centered in its column while `▎` (U+258E — the glyph tool blocks use) sits flush against the column's left edge. Even though both are one column wide, the visible vertical line lands at different horizontal positions, so the thinking block's left border was offset from adjacent tool call / result bars. Promote the `BAR` constant to the shared `blocks` module and point `THINKING_PREFIX` at the same glyph. Tool blocks now import `BAR` from `super` instead of defining their own copy, and a runtime test pins that `THINKING_PREFIX` starts with `BAR` so a future glyph change can't silently desync the two.
Comments added earlier in the PR narrated intent at length and explained alternatives; bring them in line with CLAUDE.md — why, not what; one line where possible; no "picked X over Y" rationale.
`content.trim()` ran across the whole string before splitting into lines, stripping leading whitespace off the first line only. Tools like `git diff --stat` that indent every line with a meaningful space rendered with a misaligned first row. Iterate `content.lines()` directly and drop whole blank surrounding lines instead, keeping per-line indent intact. Regression test pins the `git diff --stat` shape.
The helper rebuilt each line from spans only, silently dropping `Line.style` on the way through. Fenced code blocks set their fg on the whole-line style (not the inner spans), so assistant text with an unknown-lang fence lost its color once wrapped in the icon prefix. Preserve the input's whole-line style on the output. Also collapses `AssistantThinking::render`'s body loop to reuse the helper (same shape, less hand-rolled), renames `inner_width` → `md_width` to match `render_assistant_markdown`, moves the `thinking_prefix_shares_bar_glyph_with_tool_blocks` invariant test under its own `// ── THINKING_PREFIX ──` section, and drops the stale "used by thinking body" mention from the helper's doc plus the redundant one-line doc on `commit_thinking_buffer`. Adds a regression test pinning fenced-code fg preservation for assistant text — same scenario that the thinking block already covers post-review.
Codecov flagged the two `while` loops in `render_text_body` that drop leading / trailing blank lines — none of the existing tests feed content that survives the `trim().is_empty()` guard with blanks on either end. Pin the stripping so a regression that re-adds the old `content.trim()` behavior (swallowing per-line indent) can't land unnoticed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The thinking block evaporated the moment the model switched to prose —
append_stream_tokencleared the transient buffer before routing the first text chunk, andcommit_streamingdid the same at turn end, so a thinking-only turn (think → tool call) also left nothing behind. The reasoning is now committed into anAssistantThinkingblock so it stays in the transcript.thinking_bufferinto a committed block in bothappend_stream_tokenandcommit_streaming(no more silent clears).│bar on every line —Thinking...label flush with the bar, body flush under it (no hanging indent).render_markdownso inline code, emphasis, and fenced blocks keep their styling; a selective patch dims only spans without an explicit fg, so prose reads muted while code / links / headings / syntax-highlighted fences stay at full color.Test plan
cargo fmt --all --checkcargo buildcargo clippy --all-targets -- -D warningscargo test— 927 passedpnpm lint— 0 errors