Skip to content

feat(tui): inline rendering — conversation in native scrollback#131

Open
Fullstop000 wants to merge 9 commits into
masterfrom
worktree-tui-inline
Open

feat(tui): inline rendering — conversation in native scrollback#131
Fullstop000 wants to merge 9 commits into
masterfrom
worktree-tui-inline

Conversation

@Fullstop000
Copy link
Copy Markdown
Owner

Moves the TUI from alternate-screen Viewport::Fullscreen (#89) to inline rendering (Viewport::Inline). Finalized conversation blocks are pushed into the terminal's real scrollback via insert_before, so native copy, native scroll, and tmux detach/reattach persistence all work by construction. A fixed-height band stays pinned at the bottom; no alt-screen, no mouse capture.

Supersedes #128 — the reattach clear() band-aid becomes redundant once history lives in native scrollback. Close #128 in favor of this.

What's in it

  • Inline core — welcome banner, user prompts, assistant/tool blocks all commit to native scrollback; band pinned at bottom; mouse capture dropped (native selection restored).
  • Streaming row committer (stream_commit.rs, TDD) — the in-progress assistant/reasoning block's stable visual rows flow into scrollback as they settle, rather than appearing whole on completion. The only retroactive markdown construct is a GFM table (code fences are append-only, setext unsupported), so the rule is commit-per-line holding a trailing run of outside-fence table rows. A byte-by-byte invariant test proves every streamed commit is an exact prefix of the final render (no row ever mutates).
  • Clean resize — ratatui 0.26's inline clear() only scrubs downward, stranding the old band above the new anchor on resize (TUI: terminal resize leaves stale input bands stacked in scrollback #77; confirmed unchanged in 0.30). We take over: full-clear + re-anchor on any size change. Visible screen stays clean (conversation preserved, single band) across grow/shrink.
  • Pickers (/model, ask_user, …) render in the grown viewport with the band pinned.
  • Net −46 lines — deletes the now-dead in-app transcript/scroll model (transcript Vec, scroll_offset/auto_follow, scroll_transcript_*, render_transcript, PgUp/PgDn handlers). Native terminal scroll replaces in-app scroll.

Verification

  • cargo clippy --workspace --all-targets -- -D warnings clean; cargo fmt --check clean.
  • All console + committer tests pass (152 + 6). Workspace-wide parallel-load flakiness in llm::protocols/hooks is pre-existing on master (passes in isolation), not introduced here.
  • Dogfooded the real binary under tmux + pty: conversation/code-fence/markdown into scrollback, /model picker, /clear, resize across 80×24 / 120×40 / 70×18.

Known minor polish (follow-ups)

  • Resize leaves one stale band fragment in deep scroll-up history (inherent to ratatui 0.26 inline; visible screen is clean).
  • ask_user renders in the grown viewport rather than strictly replacing the input slot.
  • /clear clears the logical session but not terminal scroll-up history.
  • No separate in-band "current partial line" preview (lines appear in scrollback as they settle).

🤖 Generated with Claude Code

…lback

Switch the TUI from alternate-screen Viewport::Fullscreen to Viewport::Inline:
finalized blocks are pushed into the terminal's real scrollback via
insert_before, so native copy, native scroll, and tmux detach/reattach all
work. A fixed-height band stays pinned at the bottom; no alt-screen, no mouse
capture (native selection restored).

Working + dogfooded: welcome banner, user prompts, and assistant markdown
responses all land in native scrollback; band pinned at bottom; /model and
other pickers render in the grown viewport.

WIP — remaining toward the full spec: streaming-token display (visual-row
committer), strict fixed-band invariant for pickers/multiline (currently
rebuilds the viewport on height change, the #21 mechanism), ask_user inline
input-slot, and deletion of the now-inert in-app transcript/scroll model
(leaves one dead-code warning on commit_transcript_lines).
Streaming committer (stream_commit.rs, TDD): the in-progress assistant/
reasoning block's STABLE visual rows flow into native scrollback via
insert_before as they settle, instead of appearing whole on completion. The
only retroactive markdown construct is a GFM table (render_md_block) — code
fences are append-only, setext unsupported — so the rule is commit-per-line,
holding a trailing run of outside-fence table rows. Proven by a byte-by-byte
invariant test: every streamed commit is an exact prefix of the final render.

Resize: ratatui 0.26's inline clear() only scrubs downward, stranding the old
band above the new anchor on resize (#77; unchanged in 0.30). On any terminal
size change we now full-clear + re-anchor a fresh viewport — the visible screen
stays clean (conversation preserved, single band) across grow/shrink.
Inline rendering puts the conversation in the terminal's native scrollback, so
the in-memory transcript Vec, scroll_offset/auto_follow/transcript_visible_rows
state, the scroll_transcript_* methods, render_transcript, natural_max_offset,
and the PgUp/PgDn/Ctrl+Home/End scroll-key handlers are all obsolete — removed
along with their tests. PgUp/PgDn/etc. are now left to the terminal's own
scrollback (mouse wheel, Shift+PgUp). A small committed_rows cursor on App
replaces the runner-local row counter so /clear can't desync the incremental
commit. Zero warnings, clippy clean, fmt clean.
Inline rendering only painted the band (ratatui-drawn) with the app bg; blocks
pushed into scrollback via insert_before kept the terminal's default bg,
leaving a two-tone seam (band dark, conversation not). render_block_into now
fills the insert_before buffer with BG before blitting the lines, so the
conversation scrollback matches the band. Verified with a contrasting-canvas
pty screenshot: scrollback rows render at BG, not the terminal default.
In inline mode the conversation lives in the terminal's scrollback, so a
session reset (/clear, /resume) that only cleared app.blocks left the old
output lingering when scrolling up. Add a pending_screen_clear flag (set in
the reset path); the runner wipes the visible screen + scrollback history
(Clear All + Purge) and re-anchors a fresh viewport before committing the new
session's blocks. Verified: a marker present before /clear is gone from the
full scrollback after.
While a picker is open the band collapses to status + footer (no input box /
slash / queued) — the picker is the interaction. For ask_user (inline_picker)
the viewport is sized to just picker + band, so the conversation stays visible
in native scrollback above it (CC-style 'replace the input bar'); slash pickers
(/model etc.) still take the full screen. Removes the redundant empty input box
that used to sit below an open picker.
#130 added a regression test asserting an over-wide ask_user trace stays
visible via the old transcript auto-follow render path, which inline rendering
removed. Re-target the same regression at the inline path: block_lines must
hard-break over-wide words so every row fits the width (insert_before blits
rows verbatim, so an over-width row would be truncated).
Viewport::Inline queries cursor position (ESC[6n) on startup and every rebuild;
a real terminal replies. The pty harness didn't, so ratatui errored (cursor
position unreadable) and the TUI exited before rendering — failing all six
slash-flow e2e tests. The reader thread now answers each DSR query.
prep_history_strips_* asserted via HistoryPolicy::default(), which reads the
process-global IGNIS_HISTORY_TRIM env var; a sibling test that toggles it to
'off' raced them under parallel load (flaky). Pass an explicit
HistoryPolicy { strip_think: true } so the strip-behavior tests don't depend on
global env state. Production call site keeps default().
@Fullstop000 Fullstop000 force-pushed the worktree-tui-inline branch from bcfee0b to 77795bc Compare June 6, 2026 03:42
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.88889% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.83%. Comparing base (3d79e61) to head (77795bc).

Files with missing lines Patch % Lines
ignis/src/console/runner.rs 42.37% 34 Missing ⚠️
ignis/src/console/render/stream_commit.rs 81.17% 16 Missing ⚠️
ignis/src/console/render/mod.rs 83.82% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #131      +/-   ##
==========================================
- Coverage   76.94%   76.83%   -0.12%     
==========================================
  Files          66       67       +1     
  Lines       17671    17625      -46     
==========================================
- Hits        13597    13542      -55     
- Misses       4074     4083       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

2 participants