fix(frontend): recover clipped Codex terminal via onRender convergence re-fit#312
Merged
Merged
Conversation
…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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-terminalblack, and the user can't type or scroll.Root cause (per the triage RCA on #280)
FitAddonderives the row count by dividing the pane height by the renderer's measured cell box. That box is measured asynchronously: the WebGL renderer loads afterterm.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 aResizeObserveron the host, and the host'sheight: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:
onRenderconvergence 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()returnsundefineduntil 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.windowresize listener — OS-window and monitor/DPR changes move the true cell box without touching the host'sheight:100%box, so the existingResizeObservermisses them; observewindowdirectly as a session-long recovery path.Both are torn down on unmount. The non-integer
lineHeight: 1.35and the post-openrenderer 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 typecheckclean.npm test— 140/140 pass.