Skip to content

fix(frontend): recover clipped Codex terminal via onRender convergence re-fit#312

Merged
harshitsinghbhandari merged 1 commit into
mainfrom
fix/280-codex-terminal-clipped
Jun 18, 2026
Merged

fix(frontend): recover clipped Codex terminal via onRender convergence re-fit#312
harshitsinghbhandari merged 1 commit into
mainfrom
fix/280-codex-terminal-clipped

Conversation

@harshitsinghbhandari

Copy link
Copy Markdown
Collaborator

Problem

Closes #280. The Codex terminal renders only in the top ~half of the pane; the cursor sits mid-pane, everything below is empty bg-terminal black, and the user can't type or scroll.

Root cause (per the triage RCA on #280)

FitAddon derives the row count by dividing the pane height by the renderer's measured cell box. That box is measured asynchronously: the WebGL renderer loads after term.open(), and the monospace font's real metrics resolve a frame or more later. The early fits (rAF + two timeouts + fonts.ready) can all run while the cell box is still too tall, so xterm under-counts rows and sends that short grid to zellij over the mux. The grid then freezes: the only remaining fit trigger is a ResizeObserver on the host, and the host's height:100% box never changes when only the row count is wrong, so the bad grid sticks for the session's lifetime.

Fix

Add a recovery re-fit that is not gated on the host box changing size:

  • onRender convergence loop — each renderer repaint re-proposes dimensions from the current measured cell box and re-fits when they differ, converging the grid to the true row count once the cell metrics settle. proposeDimensions() returns undefined until the cell box is non-zero, so a fit is never accepted from an unmeasured cell. The listener detaches once the proposal holds for a few frames (or a hard re-fit cap is hit), so steady-state content renders cost nothing.
  • window resize listener — OS-window and monitor/DPR changes move the true cell box without touching the host's height:100% box, so the existing ResizeObserver misses them; observe window directly as a session-long recovery path.

Both are torn down on unmount. The non-integer lineHeight: 1.35 and the post-open renderer load order were the suspected aggravators of the over-measure; this fix is renderer- and lineHeight-agnostic because it converges on whatever cell box the renderer ultimately settles on, rather than trusting the first measurement.

Verification

  • npm run typecheck clean.
  • npm test — 140/140 pass.
  • The terminal-fills-the-pane behavior depends on the live WebGL/canvas renderer measuring real cell metrics, which jsdom can't exercise, so it isn't covered by an automated test. Recommend a manual smoke check (open a Codex session, confirm the grid fills the full pane and survives an OS window resize) before merge.

…e re-fit

The Codex terminal rendered only in the top half of the pane: FitAddon
divided the pane height by a too-tall cell box (measured before the
post-open WebGL renderer and the monospace font's real metrics resolved),
under-counted rows, and sent that short grid to zellij. It never recovered
because every remaining fit trigger after the settle window was the host
ResizeObserver, and the host's height:100% box never changes when only the
row count is wrong.

Add an onRender convergence loop: each renderer repaint re-proposes
dimensions from the current measured cell box and re-fits when they differ,
converging the grid to the true row count once metrics settle, then detaches
once stable (bounded by a re-fit cap). proposeDimensions returns undefined
until the cell box is non-zero, so a fit is never accepted from an unmeasured
cell. Also listen on window resize for OS-window / DPR changes that move the
true cell box without touching the host box.

Fixes #280

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@harshitsinghbhandari harshitsinghbhandari merged commit 63d488b into main Jun 18, 2026
3 checks passed
harshitsinghbhandari added a commit that referenced this pull request Jun 18, 2026
…ements (#313) (#316)

The onRender convergence loop added in #312 recovered the under-counted
rows from #280, but on a HiDPI display it could lock the Codex
orchestrator terminal at half size with a ghosted composer (#313).

FitAddon derives the grid by dividing the pane box by the renderer's
measured css cell box. During the WebGL atlas warm-up that cell box can
emit a one-frame transient (a doubled box on a 2x display), which halves
the proposed cols/rows. The loop committed that single frame's proposal,
resized the grid to half, then detached after a few "stable" frames — so
nothing re-fired the PTY resize that makes zellij repaint, leaving the
grid stuck at half width and the stale composer un-cleared.

Require a differing proposal to repeat identically across two consecutive
renders before applying it, so a one-frame transient only updates the
pending value and is never committed. Add 600ms/1200ms settle fits as a
session-bounded backstop: by then the atlas and font metrics are warm, so
even if the loop detached at a briefly-stable wrong measurement, a late
re-measure corrects the grid and fires the resize zellij needs to repaint
cleanly. fit() is idempotent, so a correct terminal never reflows.

Fixes #313

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

Codex Terminal is sort of clipped. can't write, can't scroll.

1 participant