Symptom
When a long tool result (e.g. a todo_write echoing back its full JSON payload) renders in the transcript, character fragments of the content (heli, hel, h`li, ent, etc.) appear as a vertical column of garbage inside the right sidebar, in the gutter regions between the Plan / Todos / Tasks / Agents widget borders.
Where a sidebar widget is drawn, the leak is hidden (the widget overdraws). Where there is no widget on a row (the seams between widgets, or rows where a widget has scrolled past its content), the fragments leak through.
Reproduce: open the TUI, run a turn that emits a long todo_write tool result whose content field is something like a long string (e.g. "hello world hello world ..."). The raw result: line is then long enough that wrapped continuations end up rendering past the chat pane's right edge.
Suspect
Layout itself is correct — crates/tui/src/tui/ui.rs:2761-2768 splits chunks[1] horizontally into chat_area + sidebar_area, and ChatWidget::render (crates/tui/src/tui/widgets/mod.rs:136-160) renders a Paragraph to self.content_area. ratatui clips a Paragraph to the area it's given, so the bleed should not be possible from that path alone.
That leaves three plausible paths:
- Direct
buf.set_string writes outside content_area. The streaming / live-update path may write cells using absolute coordinates without re-clipping to chat_area. Worth auditing crates/tui/src/tui/streaming.rs, crates/tui/src/tui/transcript.rs, and crates/tui/src/tui/markdown_render.rs for any cell writes that take a Buffer and an x derived from something other than area.x.
- Transcript cache wrapped to the wrong width.
app.transcript_cache.ensure(..., content_area.width.max(1), ...) (mod.rs:65) wraps lines to the chat width — but a single wide tool-result chunk that's pre-wrapped by the renderer (e.g. JSON pretty-printed at a fixed width) might not get re-wrapped. The Paragraph would then truncate, but if anything else writes those overflowed graphemes (a hover/selection layer, scrollbar, status overlay), they'd land in the sidebar.
- Stale buffer cells from a prior frame. Less likely under normal ratatui usage (full redraw each frame), but worth confirming the frame is being fully cleared and that no
app.view_stack overlay paints partial rows that get torn when chat content shrinks.
Repro environment
deepseek-tui 0.5.0
deepseek-v4-pro, YOLO mode, max reasoning effort
- Terminal width ~165 columns
Screenshot
(attached in the issue thread that filed this — long line result: Todo list updated (2 items, 0% complete){ "items": [ { "id": 1, "content": " hel... followed by a vertical column of heli / hel / h`li fragments inside the sidebar gutter)
Acceptance
- A long single-line tool result no longer produces character fragments inside the sidebar area at any terminal width.
- A test that renders
ChatWidget with a 5,000-char single-line tool result + a sidebar split asserts the sidebar's Buffer cells are untouched outside the widget regions.
Symptom
When a long tool result (e.g. a
todo_writeechoing back its full JSON payload) renders in the transcript, character fragments of the content (heli,hel,h`li,ent, etc.) appear as a vertical column of garbage inside the right sidebar, in the gutter regions between thePlan/Todos/Tasks/Agentswidget borders.Where a sidebar widget is drawn, the leak is hidden (the widget overdraws). Where there is no widget on a row (the seams between widgets, or rows where a widget has scrolled past its content), the fragments leak through.
Reproduce: open the TUI, run a turn that emits a long
todo_writetool result whosecontentfield is something like a long string (e.g."hello world hello world ..."). The rawresult:line is then long enough that wrapped continuations end up rendering past the chat pane's right edge.Suspect
Layout itself is correct —
crates/tui/src/tui/ui.rs:2761-2768splitschunks[1]horizontally intochat_area+sidebar_area, andChatWidget::render(crates/tui/src/tui/widgets/mod.rs:136-160) renders aParagraphtoself.content_area. ratatui clips a Paragraph to the area it's given, so the bleed should not be possible from that path alone.That leaves three plausible paths:
buf.set_stringwrites outsidecontent_area. The streaming / live-update path may write cells using absolute coordinates without re-clipping tochat_area. Worth auditingcrates/tui/src/tui/streaming.rs,crates/tui/src/tui/transcript.rs, andcrates/tui/src/tui/markdown_render.rsfor any cell writes that take aBufferand anxderived from something other thanarea.x.app.transcript_cache.ensure(..., content_area.width.max(1), ...)(mod.rs:65) wraps lines to the chat width — but a single wide tool-result chunk that's pre-wrapped by the renderer (e.g. JSON pretty-printed at a fixed width) might not get re-wrapped. The Paragraph would then truncate, but if anything else writes those overflowed graphemes (a hover/selection layer, scrollbar, status overlay), they'd land in the sidebar.app.view_stackoverlay paints partial rows that get torn when chat content shrinks.Repro environment
deepseek-tui0.5.0deepseek-v4-pro, YOLO mode,maxreasoning effortScreenshot
(attached in the issue thread that filed this — long line
result: Todo list updated (2 items, 0% complete){ "items": [ { "id": 1, "content": " hel...followed by a vertical column ofheli/hel/h`lifragments inside the sidebar gutter)Acceptance
ChatWidgetwith a 5,000-char single-line tool result + a sidebar split asserts the sidebar'sBuffercells are untouched outside the widget regions.