Skip to content

refactor(terminal): size only the focused session to full-window#312

Merged
forketyfork merged 4 commits into
mainfrom
refactor/focused-only-full-size
May 18, 2026
Merged

refactor(terminal): size only the focused session to full-window#312
forketyfork merged 4 commits into
mainfrom
refactor/focused-only-full-size

Conversation

@forketyfork
Copy link
Copy Markdown
Owner

@forketyfork forketyfork commented May 18, 2026

Solution

applyTerminalResize used 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. applyTerminalResize now takes a Sizes struct (grid and full) and a FullSet ({ primary, secondary } indices) and picks the target per session. The runtime computes the FullSet from anim_state via fullSetForMode:

  • Grid / GridResizing → empty set
  • Full / Expanding / Collapsing{ primary = focused }
  • PanningLeft / PanningRight / PanningUp / PanningDown{ primary = focused, secondary = previous }

The renderer reads each session's own terminal.cols/rows at the grid-tile, full, panning, expanding, collapsing, and grid-resizing render callsites via a small sessionTermDims helper. Initial spawn seeds at grid-cell size; the first frame's applyTerminalLayoutIfSizeChanged promotes the focused session to full if the initial mode is Full. DEC mode 2048 in-band size reports continue to fire from inside applyTerminalResize and therefore land only on sessions whose PTY actually changes.

Stable grid dimensions across view-mode toggles. calculateTerminalSizes now takes both grid_window_height and full_window_height explicitly. 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 in src/termio/Termio.zig.

Builds on #311 — that PR adds the DEC 2048 in-band size report emission this PR relies on. Stacked.

Test plan

  • Open Codex (or Claude Code) in one cell and shells in others. Focus the agent, toggle grid → full → grid repeatedly. Non-focused cells stay visually frozen across toggles; no top-to-bottom rescroll on the agent itself either — brief pause then a single snap to the reflowed layout, like ghostty.
  • Repeat with the agent in a non-focused cell and a shell focused. Toggle grid ↔ full focused on the shell. The agent stays frozen.
  • In Full on the agent, Cmd+number to jump to a shell, Cmd+number back. The agent's chat history has not visibly rescrolled on the return.
  • Open nvim in the focused cell, grid ↔ full. nvim still reflows immediately (DEC 2048 fires for that session only).
  • Drag the Architect window border. All cells reflow to the new dimensions; the focused one to the new full size, the rest to the new grid-cell size.
  • Change the grid layout (add a column). Every cell reflows to the new grid-cell size; no cell jumps to full.

@forketyfork forketyfork marked this pull request as ready for review May 18, 2026 14:20
@forketyfork forketyfork requested a review from Copilot May 18, 2026 14:23
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread src/render/renderer.zig Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment thread src/render/renderer.zig Outdated
Comment thread src/app/layout.zig Outdated
Comment thread docs/ARCHITECTURE.md Outdated
Base automatically changed from fix/agent-scoped-resize-holds to main May 18, 2026 14:46
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.
@forketyfork forketyfork force-pushed the refactor/focused-only-full-size branch from fe7f51f to 6f60d17 Compare May 18, 2026 14:49
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.
@forketyfork forketyfork merged commit 94cf4e9 into main May 18, 2026
4 checks passed
@forketyfork forketyfork deleted the refactor/focused-only-full-size branch May 18, 2026 15:03
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