Skip to content

fix(tui): smoother streaming, no preamble ghosting, clearer ReadFile cards, ANSI-safe diffs - #162

Merged
elkaix merged 8 commits into
mainfrom
fix/tui-design
Jun 17, 2026
Merged

fix(tui): smoother streaming, no preamble ghosting, clearer ReadFile cards, ANSI-safe diffs#162
elkaix merged 8 commits into
mainfrom
fix/tui-design

Conversation

@elkaix

@elkaix elkaix commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

A set of surgical TUI rendering fixes. All changes are display-only — no tool execution, wire events, agent specs, or LLM-facing payloads change.

  1. Fallback live renderer no longer scrolls the viewport on every wrapped row. DiffLive grew the live frame with a bare "\n" per appended row, scrolling the terminal even when the new row still fit the visible region (the "text jumps up" symptom). Now uses cursor-down when the next row provably fits (frame origin + target row vs height) and falls back to newline scroll only on genuine overflow. Adds PYTHINKER_DIFF_LIVE_LOG tracing.

  2. No more stacked Vibing… (Ns) ghost rows during streaming. Interactive scrollback handoffs (every tool transition / turn end) tear down and redraw the prompt app around run_in_terminal; the height-varying transient preamble (agent stream body + elapsed verb spinner + tips) re-rendered in that window was left behind when the teardown erase miscounted. Handoffs now suppress all transient preamble via _suppress_transient_preamble, and a terminal resize is treated as a hard invalidation boundary that briefly hides multi-row tips while prompt_toolkit settles.

  3. Collapsed ReadFile cards show what happened. Replaced the generic Read 1 file with a line-count-aware summary + file basename (e.g. Read 140 lines from console.py) plus a short, cell-width-capped, ANSI-sanitized preview of the leading lines. Stays truthful when the count is unknowable (Read file content) or empty (Read 0 lines); expanded mode and the LLM-facing result are unchanged.

  4. Diff cards strip terminal control sequences. render_diff rendered untrusted file content and model-supplied edit text verbatim, so a crafted edit could smuggle ANSI/cursor escapes into the terminal through Update/Write cards and the approval/pager diffs that share the renderer. Sanitized at the single render boundary; visible text preserved.

Testing

  • make check-pythinker-code — green (ruff + format + pyright + ty)
  • tests/ui_and_conv/ + diff suites — 2250 passed
  • New tests: DiffLive scroll geometry; handoff suppression sampled from inside the real _run_scrollback_handoff emit window; collapsed ReadFile count/preview/empty/narrow-width/ANSI cases; render_diff ANSI stripping

Notes

  • Terminal-resize erase hardening beyond the handoff window is intentionally scoped as a separate follow-up (needs real-terminal verification).
  • CHANGELOG.md updated under ## Unreleased for all four user-facing changes.

Summary by CodeRabbit

  • Bug Fixes

    • Diff content now sanitizes terminal control sequences to prevent rendering artifacts
    • Terminal resize and scrollback operations eliminate transient UI glitches
    • DiffLive streaming prevents mid-viewport cursor overwrites
  • New Features

    • ReadFile cards now display line count, filename, and preview excerpt instead of generic summary
  • Tests

    • Added comprehensive test coverage for terminal geometry and resize stability

elkaix added 5 commits June 17, 2026 16:52
DiffLive grew the live frame with a bare "\n" on every appended row, which
scrolls the terminal viewport up even when the new row still fits the visible
region — the "text jumps up" symptom during non-interactive streaming. Use
cursor-down when the next row provably fits (frame origin + target row vs
height) and fall back to newline scroll only on genuine overflow. Adds
PYTHINKER_DIFF_LIVE_LOG tracing and scroll-geometry tests.
…doff

Stacked "Vibing… (Ns)" rows and duplicate tips fossilized into permanent
scrollback during streaming. Root cause: scrollback handoffs tear down and
redraw the prompt app around run_in_terminal on every tool transition / turn
end, and the height-varying transient preamble (agent stream body + elapsed
verb spinner + tips) re-rendered in that window is left behind when the
teardown erase miscounts.

Suppress all transient preamble (body, spinner, tips) for the duration of a
scrollback handoff via _suppress_transient_preamble, and treat a terminal
resize as a hard invalidation boundary that briefly hides multi-row tips while
prompt_toolkit settles at the new geometry. Committed prose is emitted by the
handoff itself; the live tail returns once the handoff completes.

Tests sample the preamble renderers from inside the real handoff emit window
to prove suppression is live, not just a getter contract.
Collapsed reads said "Read 1 file (ctrl+o to expand)", hiding the line count
and content so a successful read looked like it returned nothing. Render a
line-count-aware summary with the file basename (e.g. "Read 140 lines from
console.py") plus a short, cell-width-capped preview of the leading lines.

Line count prefers the tool message, falls back to counting the body, and
stays truthful when unknowable ("Read file content") or empty ("Read 0
lines"). Preview is ANSI-sanitized, capped to a few visual lines, width-capped
per line, and skipped on narrow terminals. Expanded mode and the LLM-facing
tool result are unchanged.
render_diff rendered untrusted file content and model-supplied edit text
verbatim, so a crafted edit could smuggle ANSI escapes (cursor movement,
color) into the terminal through Update/Write diff cards and the approval/
pager diffs that share the renderer. Sanitize diff_text once at the top of
render_diff; sanitize_ansi keeps newlines/tabs so +/- prefix and line-number
parsing are unaffected, and visible text is preserved.
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@elkaix, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b1455de5-9355-485f-9d07-3a05e4647b23

📥 Commits

Reviewing files that changed from the base of the PR and between 4cc080e and 78a1521.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • src/pythinker_code/llm.py
  • src/pythinker_code/ui/shell/visualize/_diff_live.py
  • src/pythinker_code/ui/shell/visualize/_interactive.py
  • tests/core/test_create_llm.py
  • tests/ui_and_conv/test_diff_live_scroll.py
  • tests/ui_and_conv/test_visualize_running_prompt.py
📝 Walkthrough

Walkthrough

Four TUI rendering bug fixes: render_diff now strips ANSI/control sequences from diff text before parsing; ReadFile collapsed cards show a line-count/filename summary with a width-capped ANSI-sanitized preview; DiffLive selects cursor-down vs scroll based on provable row fit with optional diagnostic tracing; _PromptLiveView adds resize-recovery suppression and hardened scrollback handoff with queue retention on failure.

Changes

TUI Rendering Fixes

Layer / File(s) Summary
ANSI sanitization in diff card rendering
src/pythinker_code/ui/shell/components/diff.py, tests/ui_and_conv/test_tui_card_tool_renderers.py
render_diff imports and applies sanitize_ansi to diff_text before line splitting; test confirms visible text survives while escape sequences are stripped.
ReadFile card: line-count summary, width-capped preview, empty-file truthfulness
src/pythinker_code/ui/shell/tool_renderers/read.py, tests/ui_and_conv/test_tui_card_tool_renderers.py, tests/ui_and_conv/test_tui_transcript_enhancements.py
Adds _basename, _line_count, _summary_text, and _preview helpers plus message-parsing constants. Rewrites _render_result to prefer details["output"], treat empty string as empty file, and show summary-only or summary+preview in collapsed mode. Tests cover count-aware strings, preview capping, narrow-terminal suppression, ANSI sanitization, empty-file correctness, and expanded rendering.
DiffLive geometry-safe cursor/scroll transitions and diagnostic tracing
src/pythinker_code/ui/shell/visualize/_diff_live.py, tests/ui_and_conv/test_diff_live_scroll.py
Adds _frame_origin_row/_last_terminal_height state, _diff_live_trace logging, and four cursor-geometry helpers. Updates refresh(), _write_initial, _write_appended_lines, _append_diff_row_transition, and _rewrite_growing_last_line to choose CUD vs scroll based on fit. Full test suite covers all transition branches, tracing, origin exhaustion, and resize invalidation.
Interactive resize recovery, scrollback handoff suppression, pending flush deferral
src/pythinker_code/ui/shell/visualize/_interactive.py, src/pythinker_code/ui/shell/visualize/_live_view.py, tests/ui_and_conv/test_visualize_running_prompt.py
Adds _RESIZE_RECOVERY_FRAMES and six new _PromptLiveView methods for resize-recovery countdown and safe invalidation. Hardens _run_scrollback_handoff with error trapping and guaranteed depth decrement. Refactors _flush_pending_scrollback with deferral and failure retention. Suppresses transient content in render_agent_status and render_pinned_status_tail during handoff; adds hide_tips to _working_indicator. Tests validate all suppression windows, deferral, failure queue retention, and invalid geometry guard.
CHANGELOG
CHANGELOG.md
Adds four Unreleased bullet points describing the fixes above.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with 'fix(tui):' prefix and clearly summarizes all four user-facing changes in the PR.
Description check ✅ Passed Description comprehensively covers all changes with technical depth, includes testing details and changelog confirmation, and follows the repository template structure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tui-design

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/pythinker_code/ui/shell/visualize/_diff_live.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pythinker_code/ui/shell/visualize/_interactive.py`:
- Around line 208-214: The _safe_prompt_invalidate method is catching exceptions
and only logging them via _handoff_trace, which is environment-gated and may not
log in default runs, causing failures to be silently swallowed. Add proper
logging (using a standard logger) before the _handoff_trace call to ensure
failures are always logged to an actionable diagnostic, while maintaining the
existing recovery behavior that prevents exceptions from aborting handoff
cleanup. This addresses both the missing diagnostic logging in default runs and
the coding guideline violation for bare exception catches without proper
logging.
- Around line 160-163: The `_flush_pending_scrollback()` method is called
concurrently from multiple locations, causing batch duplication and loss of
queued entries because the copy and delete operations are not atomic. Add a lock
to serialize access to `_pending_scrollback` and make the batch copy and clear
operation atomic within the critical section. Additionally, modify
`_flush_pending_scrollback()` to accept a parameter indicating shutdown or
interrupt flushes, and when this flag is set, bypass the resize deferral logic
to ensure final scrollback is flushed immediately without deferred retry. Update
all callers at lines 347, 517, 521, 528, 545, and 557 to pass the appropriate
shutdown flag when calling `_flush_pending_scrollback()`.

In `@tests/ui_and_conv/test_diff_live_scroll.py`:
- Around line 63-94: The test function test_append_row_transition_geometry has
an unused parameter base_row that is declared in both the parametrize decorator
and the function signature, but is never referenced in the test body. Remove
base_row from the parametrize decorator's parameter tuple (after target_row),
remove the base_row values from each parametrize test case tuple, and remove the
base_row: int parameter from the function signature. This will eliminate the
dead code flagged by Ruff ARG001.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 20d2bbcf-f385-4314-b0d1-44c0422ce329

📥 Commits

Reviewing files that changed from the base of the PR and between 2313fb1 and 4cc080e.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • src/pythinker_code/ui/shell/components/diff.py
  • src/pythinker_code/ui/shell/tool_renderers/read.py
  • src/pythinker_code/ui/shell/visualize/_diff_live.py
  • src/pythinker_code/ui/shell/visualize/_interactive.py
  • src/pythinker_code/ui/shell/visualize/_live_view.py
  • tests/ui_and_conv/test_diff_live_scroll.py
  • tests/ui_and_conv/test_tui_card_tool_renderers.py
  • tests/ui_and_conv/test_tui_transcript_enhancements.py
  • tests/ui_and_conv/test_visualize_running_prompt.py

Comment thread src/pythinker_code/ui/shell/visualize/_interactive.py
Comment thread src/pythinker_code/ui/shell/visualize/_interactive.py
Comment thread tests/ui_and_conv/test_diff_live_scroll.py
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

elkaix added 3 commits June 17, 2026 17:13
Resize recovery could defer pending scrollback indefinitely when prompt
geometry was unavailable, leaving PTY sessions stuck on Finalizing without
emitting completed response text. Force flush on outermost turn end and
always tick recovery down each status refresh frame.
Serialize pending scrollback flush with a lock, force flush on shutdown,
and log handoff failures. Flatten tool results for non-native Anthropic/OpenAI
hosts so compatibility proxies do not drop multi-part payloads.
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.

1 participant