refactor(terminal): size only the focused session to full-window#312
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe7f51fc60
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR refactors terminal resizing so sessions can keep independent grid/full cell dimensions, reducing unnecessary redraw/reflow work for hidden sessions during grid↔full transitions.
Changes:
- Adds per-session grid/full terminal sizing via
Sizes,FullSet, and runtime mode mapping. - Updates rendering to use each session’s current VT dimensions.
- Adds synchronized-output cache holding and updates architecture documentation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/render/renderer.zig |
Uses session-specific terminal dimensions and adds synchronized-output cache hold logic. |
src/app/runtime.zig |
Computes grid/full sizes and selects which sessions receive full-window dimensions. |
src/app/layout.zig |
Replaces single resize target with per-session grid/full resize dispatch. |
docs/ARCHITECTURE.md |
Documents the updated terminal resize model. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Issue: applyTerminalResize sized every session to the same dimensions — full-window cell count in Full mode, grid-cell count in Grid mode. In Full mode only the focused session is rendered; the eight invisible sessions still got the resize and were forced to redraw at the new width. For agent TUIs that meant a full top-to-bottom rescroll of chat history on every grid/full toggle, even though only one session was on screen. Solution: Make sizing per session. applyTerminalResize now takes Sizes (grid + full) and a FullSet (primary, secondary indices) and picks the target per session. fullSetForMode promotes the focused session in Full/Expanding/Collapsing, and the previous session as well during panning. Everyone else stays at grid-cell size, so a grid/full toggle reflows exactly one session. The renderer reads each session's own terminal cols/rows at the grid-tile, full, panning, expanding, collapsing, and grid-resizing callsites via a small sessionTermDims helper. Initial spawn seeds at grid-cell size; the first frame's applyTerminalLayoutIfSizeChanged promotes the focused session to full when the startup mode is Full. DEC mode 2048 in-band reports still fire from inside applyTerminalResize and therefore land only on sessions whose PTY actually changes.
Issue: After the per-session sizing refactor, unfocused sessions still got resized on every grid/full toggle. adjustedRenderHeightForMode reserves CWD-bar height only in Grid mode and returns the raw render height in every other mode. Because the same height fed into both the grid_size and full_size computations, grid_size itself shifted on every view-mode change, and unfocused sessions saw pty_size != target and got the resize they were meant to be insulated from. Codex's chat scrolled top-to-bottom every time. Solution: calculateTerminalSizes now takes two heights — grid uses the Grid-mode CWD-reserved height (constant across view modes), full uses the raw render height. runtime.zig wraps this in computeTerminalSizes and passes the raw render_height to applyTerminalResize, so unfocused sessions stay at the same grid dims whenever the actual grid layout is unchanged.
Issue: When the focused session toggled grid<->full, Codex's redraw of its chat history was visible top-to-bottom — line by line — instead of appearing as one atomic update like in Ghostty. Codex brackets the redraw with `\e[?2026h` ... `\e[?2026l` to ask the terminal to suppress intermediate frames; we were rendering every intermediate state from the in-progress vt model. Solution: In the cached render path, treat synchronized-output mode as a signal to reuse the last rendered texture instead of refreshing from the vt model. The hold is skipped when the cache is empty (initial render must run) or when the cached render mode doesn't match the requested one (grid-sized cache can't fill a full-window rect). When the app sends `\e[?2026l` the next frame refreshes once and snaps to the final state. Matches ghostty's behavior of pausing briefly while the agent reflows.
fe7f51f to
6f60d17
Compare
Issue: Four unresolved review threads on PR #312 flagged real edge cases in the synchronized-output cache hold and the per-session size plumbing: the render-mode mismatch guard dropped the hold across a grid/full toggle that happened mid-sync (so reflow frames became visible after the transition); the predicate did not consider cache_composition (so a wave/overlay started mid-sync would be skipped); grid-tile sessions reported the full window's pixel dimensions in their winsize and DEC 2048 reports; and ARCHITECTURE.md still said there was no output-hold machinery. Solution: synchronizedOutputHoldsCache now ignores render-mode mismatches (SDL stretches the cached texture for the brief sync window) and instead drops the hold on composition mismatch so a wave or overlay forces a fresh frame. TerminalSize gains width_px/height_px computed from cols * cell_w and rows * cell_h, so grid_size carries the grid tile's pixel dimensions and full_size carries the full-window pixels. applyTerminalResize reads pixel dims from the per-target Sizes entry and no longer needs the render-area arguments. ARCHITECTURE.md describes the synchronized-output cache hold.
Solution
applyTerminalResizeused to give every session the same dimensions: the full-window cell count in Full mode and the grid-cell count in Grid mode. In Full mode only the focused session is rendered, but the eight invisible sessions still got the resize and were forced to redraw at the new width. For agent TUIs like Codex and Claude Code that meant a full top-to-bottom rescroll of chat history on every grid↔full toggle, even though only one session was on screen. Additionally, Codex's own reflow when toggling its own grid↔full was visible line-by-line instead of appearing as one atomic update like in Ghostty.This PR addresses both:
Per-session sizing.
applyTerminalResizenow takes aSizesstruct (gridandfull) and aFullSet({ primary, secondary }indices) and picks the target per session. The runtime computes theFullSetfromanim_stateviafullSetForMode:Grid/GridResizing→ empty setFull/Expanding/Collapsing→{ primary = focused }PanningLeft/PanningRight/PanningUp/PanningDown→{ primary = focused, secondary = previous }The renderer reads each session's own
terminal.cols/rowsat the grid-tile, full, panning, expanding, collapsing, and grid-resizing render callsites via a smallsessionTermDimshelper. Initial spawn seeds at grid-cell size; the first frame'sapplyTerminalLayoutIfSizeChangedpromotes the focused session to full if the initial mode is Full. DEC mode 2048 in-band size reports continue to fire from insideapplyTerminalResizeand therefore land only on sessions whose PTY actually changes.Stable grid dimensions across view-mode toggles.
calculateTerminalSizesnow takes bothgrid_window_heightandfull_window_heightexplicitly. The grid height always uses the Grid-mode CWD-bar reservation regardless of current mode, so unfocused sessions see the same target every frame and the per-session resize is a no-op for them. Without this, the CWD-bar reservation flipped on and off across toggles and the unfocused sessions saw a one-row delta every cycle.Synchronized-output cache hold. When a session has DEC mode 2026 (
\e[?2026h) active, the renderer reuses its last cached texture instead of refreshing from the in-progress vt model. The hold is skipped when the cache is empty (initial render) or when the cached render mode doesn't match the requested one (so we don't stretch a grid-sized texture across the full window). When the app sends the closing\e[?2026l, the next frame refreshes once and snaps to the final state. Matches ghostty's behavior insrc/termio/Termio.zig.Builds on #311 — that PR adds the DEC 2048 in-band size report emission this PR relies on. Stacked.
Test plan