Skip to content

feat(tui): fullscreen renderer — sticky composer + in-app scroll, now the default - #319

Merged
shankha98 merged 7 commits into
mainfrom
feat/fullscreen-renderer
Aug 10, 2026
Merged

shankha98 merged 7 commits into
mainfrom
feat/fullscreen-renderer

Conversation

@404-ammar-not-found

Copy link
Copy Markdown
Collaborator

What

Brings back the fullscreen renderer as a first-class mode — alt-screen frame, line-viewport transcript, composer glued to the bottom row, wheel/PgUp/PgDn history scroll with a ↑ scrolled up · N new messages status row, End re-pins to newest — and makes it the default (user decision, recorded as ADR amendments in docs/BigPlan/decision-inline-renderer.md). Inline remains fully supported one command away: --inline, /fullscreen (persisted per project, tri-state pref), or MINIMA_TUI_INLINE=1.

Context: Claude Code CLI v2.1.89 shipped the equivalent opt-in fullscreen mode, clearing the ADR §5 re-introduction bar the 2026-07-16 amendment set.

How

  • Stage 1 — restored the MP3-deleted pure modules from git: viewport.ts verbatim (LineIndex prefix sums, binary-search windowing, null=follow scroll state) and lines.ts re-synced to today's MessageRow via the shared classifyMarkdownLines classifier. Drift alarm: tests pin renderMessageToLines(msg, cols).length === computeMsgHeight(msg, cols) on a marker-free corpus — computeMsgHeight is already lockstepped to MessageRow by the inline anchor ledger, so the identity chains lines.ts to the JSX renderer executably. The stream cache carries fence state across flushes.
  • Stage 2 — the mode: tri-state --fullscreen/--inline flags + env mirrors + per-project persisted /fullscreen choice; alt-screen lifecycle owned by the new altscreen.ts (main.ts stays free of the ?1049 literal — the render-buffer guard keeps that pin); viewport over staticItems with the full streaming reply in-scroll; constant-height status row (pinned↔scrolled crossings never move the composer); PgUp/PgDn + a consumable TextInput onEnd; mid-session toggle both ways (exit reuses the /clear-class reseat + gen bump so <Static> reprints the whole transcript into native scrollback); anchor ledger cleanly bypassed while fullscreen; suspendToShell re-arms on SIGCONT; D3b panels render in the same slot.
  • Stage 3 — wheel capture ON by default in fullscreen (wired the decoded-but-unconsumed input-filter.ts seams); click under capture → 4s selection-hint (Option/Shift-drag); /mouse frees the wheel for native selection.
  • Stage 4 — five fs-* PTY scenarios in tui_verify.sh (scroll mid-stream, counter, toggle round-trip with reprint proof, pref persistence across restart, suspend/resume re-arm, permission overlay in-frame), raw streams deliberately excluded from the inline no-mouse sweep; inline scenarios now pin --inline (BASE_ARGV vs INLINE_ARGV).
  • Stripe fix — Ink's Text transform closes a combined ESC[37;48;2;…m with 39m only, leaving the truecolor background open past EOL → BCE flooded rows with the bubble color. paint()/emitStyledRow now emit one SGR code per sequence with its specific closer; tests/lines-bce.test.tsx pins both the strings and Ink's actual post-transform output.

Verification

  • Unit suite green (2441 tests incl. restored viewport tests, the parity drift-alarm, and the BCE guard); tsc, biome, terminology guard clean.
  • All five fs-* PTY scenarios pass against both bun run and the compiled binary; bare boot arms the alt screen (default proof) and an explicit inline choice survives restarts.
  • Inline path: echo-budget byte-clean under --inline; a 14-stream sweep shows inline never emits a capture/alt-screen byte.
  • Known pre-existing (NOT from this branch; reproduced identically on clean main in a worktree): the fresh-boot bottom-anchor slack-0/1 checks, plan-exit-gate, and acceptance are timing-broken on at least one machine — the suite's own header says "slack 2 on fresh"; follow-up candidate.

🤖 Generated with Claude Code

404-ammar-not-found and others added 7 commits July 31, 2026 11:01
viewport.ts + its test restored verbatim from 0a3a871^ (MP3 deletion).
lines.ts restored and re-synced to today's MessageRow: markdownToLines now
consumes the shared classifyMarkdownLines classifier and wraps with layout.ts
wrapLineToWidth (the computeMsgHeight ruler), new branches for banner,
guardKind harness/deny, and the todowrite gate-block header, and the stream
cache carries fence state across flushes so mid-fence chunks classify as code.

Drift alarm: tests pin renderMessageToLines(msg, cols).length ===
computeMsgHeight(msg, cols) on a marker-free corpus at 40/80/100 cols —
computeMsgHeight is already lockstepped to MessageRow by the inline anchor
ledger, so the identity chains lines.ts to the JSX renderer executably.

No runtime wiring; nothing imports these yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ser, /fullscreen toggle (Stage 2)

The mode (ADR decision-inline-renderer.md, 2026-07-31 amendment — CC v2.1.89
shipped the equivalent opt-in, clearing the §5 bar):

- --fullscreen / --inline / --no-fullscreen (tri-state) + MINIMA_TUI_FULLSCREEN /
  MINIMA_TUI_INLINE env mirrors + per-project persisted /fullscreen pref
  (mode_prefs.ts suffix-key pattern). Inline stays the default.
- Alt-screen lifecycle owned by the new altscreen.ts (idempotent enter/exit) —
  main.ts never carries the ?1049 literal (render-buffer guard retitled, kept).
- Line viewport over staticItems via the restored viewport.ts/lines.ts: banner
  scrolls away with the transcript, the FULL streaming reply + reasoning peek
  live at the virtual tail, one permanent status row (pinned<->scrolled crossings
  never move the composer) with an N-new-messages counter latched on leaving
  follow.
- Keys: PgUp/PgDn page the viewport (mid-run allowed); End jumps to newest via a
  consumable TextInput onEnd first-look (readline default preserved otherwise);
  submitting and every transcriptGen bump re-pin.
- /fullscreen toggles mid-session both ways: enter paints the next commit into
  the alt screen; exit restores the main buffer and reuses the /clear-class
  reseat + gen bump so <Static> reprints the whole transcript into native
  scrollback (fullscreen-period messages included).
- Anchor ledger bypassed while fullscreen (frozen refs; the exit gen bump takes
  the reset path). suspendToShell regains SuspendOptions and re-arms the alt
  screen on SIGCONT. D3b panels render in the same slot via a shared panelNode.

Guards: cli.test.ts asserts the tri-state flags; kill-switches declares the env
mirrors; reseat count 2 -> 3. Full unit suite + the whole inline PTY verify
suite (tui_verify.sh) green — inline output unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n hint (Stage 3)

Wires the input-filter seams that have sat decoded-but-unconsumed since MP3:
setMouseScrollCallback (33ms-coalesced notches -> scrollLinesBy, 3 lines/notch)
and setClickCallback (a click under capture is a doomed drag-select — the status
row shows the Option/Shift-drag + /mouse + Ctrl+Y escape hatches for 4s).

Capture defaults ON in fullscreen (CC parity — the OLD selection-first OFF
default is what earned "feels broken"); wheel-only, never ?1002/?1003. /mouse
toggles it (inline gets an explanatory note); suspend re-arms capture on
SIGCONT; /copy's selection advice branches on the capture state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inal (Stage 4)

Five scenarios, byte + frame asserted, raw streams deliberately excluded from
the inline no-mouse-capture sweep:

- fs-basic: wheel storm mid-stream detaches the viewport (status row flips,
  composer stays bottom-anchored), the SLOW reply commits while scrolled and
  the N-new-messages counter appears without yanking the view, a click arms
  the selection hint, End re-pins to the newest content, exit leaves the alt
  screen.
- fs-toggle: /fullscreen on mid-session enters the alt screen; off leaves it,
  replays the reseat clear, and one frame shows the OFF notice together with
  the pre-toggle transcript (the <Static> reprint proof, timing-jitter-proof).
- fs-persist: a second session on the same prefs dir boots fullscreen from the
  persisted pref alone, no flag.
- fs-suspend: Ctrl+Z drops capture + alt screen before SIGTSTP; SIGCONT
  re-arms both, ordered by raw byte offsets.
- fs-perm: the bash permission overlay renders and answers inside the
  height=rows frame.

Full tui_verify.sh green end to end — all inline scenarios byte-clean with the
fs scenarios in the same run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndment)

Resolution: --fullscreen/--inline flag > MINIMA_TUI_FULLSCREEN/INLINE env > the
per-project persisted /fullscreen choice > fullscreen. The pref goes tri-state
(on/off/unset) so an explicit inline choice persists and a future default flip
never fights a stale stored value. Inline stays fully supported one command
away; the banner tagline and help text now describe the fullscreen default.

tui_verify.sh: BASE_ARGV (no renderer flag) vs INLINE_ARGV (pins --inline) so
every inline scenario stays on the main-buffer path it asserts; fs-persist now
proves the bare-boot fullscreen default AND that a persisted inline choice
survives a restart. Validated: unit suite green; echo-budget stays inline under
--inline; all five fs-* scenarios pass with the new default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c closers

Ink's Text transform (wrap-ansi) re-encodes embedded ANSI and closes a combined
ESC[37;48;2;...m open with 39m only — the truecolor background stayed active
past end-of-line and background-color-erase flooded whole terminal rows with
the user-bubble color (reported from a live session; reproduced in the PTY:
every bubble row left the app with an unclosed bg).

lines.ts paint()/emitStyledRow now emit ONE code per CSI with its specific
closer (1/2->22, 3->23, 48;2->49, fg->39) and never rely on 0m, which the
transform also rewrites. An empirical probe showed separate single-code
sequences pass through the transform verbatim.

tests/lines-bce.test.tsx pins both layers: the strings (no combined CSIs;
every bg open paired with a 49m) and Ink's actual emitted output through the
real truncate transform. PTY repro confirms 0 rows with active bg at EOL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The branch reinstates what MP3 (MUB-146, 0a3a871) deleted, per the ADR's two
2026-07-31 amendments; main has since been through the merge wave, including
#316's extraction of app.tsx's effects into hooks. Resolutions:

- app.tsx COMMANDS -> allCommands(): main's memoized builder wins, with the
  `fullscreen` and `mouse` entries re-added inside it, and the `/fullscreen`
  line restored to /help (which now interpolates keyHelp()).
- The resize effect and the beforeToolCall wiring are dropped from the branch's
  side: #316 owns them now (useTerminalSize, useToolCallHooks). Only the
  renderer-specific effects — live-reply cache reset, wheel/click seams, SGR
  mouse reporting — stay inline.
- persistMode moved to use_session_boot.ts, so app.tsx imports only
  loadTaskPanelHidden/persistFullscreenPref/persistTaskPanelHidden.
- footer.ts was removed by #341 in favour of context_meter.ts; every
  footerStatsFromMessages call site had already become ctxFor, so only the dead
  import needed dropping.
- render-buffer.test.ts: the reseat now has FOUR sites, not three — the branch
  and main each added a different third one (/fullscreen exit and the $EDITOR
  return). Count and paired-count both go to 4. The branch's replacement for
  "no renderer selection remains" stands: main.ts may name the renderer, it
  just may not write ?1049 itself.
- editor-wiring.test.ts pinned the whole Ctrl+Z statement; suspendToShell now
  takes renderer state, so the pin is the prefix. What it asserts is order.
- lines-bce.test.tsx needed an `override` for #315's check:tests bar.

3244 pass, 0 fail.
@shankha98
shankha98 merged commit a2cb652 into main Aug 10, 2026
4 checks passed
@shankha98
shankha98 deleted the feat/fullscreen-renderer branch August 10, 2026 08:55
shankha98 added a commit that referenced this pull request Aug 10, 2026
Two import unions, plus one substantive follow-through: #319's fullscreen
renderer landed after this branch's token pass, so its scroll/select hint was
the one colour prop in the tree still hard-coding "gray". Themed it — otherwise
picking a colorscheme would restyle every surface except the new default
renderer. Zero `color="..."` literals remain in src/tui. 3250 pass, 0 fail.
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