fix(tui): single layout authority for viewport rows#124
Merged
Conversation
Streaming cards and approval modals each read stdout.rows and
computed their own row budget independently — the bottom-anchor
modal could claim rows the streaming card had already counted on
and vice versa, producing visible jitter when both rendered the
same frame (lamyc's video).
Replaces the scattered RESERVED_CHROME_ROWS / MODAL_OVERHEAD_ROWS
constants with a priority-greedy allocator. Each component declares
{ min, max } rows for its zone via useReserveRows; the provider
runs allocation and feeds back actual budgets so consumers stay
within the viewport.
Migrated zones, in priority order:
modal (EditConfirm, ShellConfirm, PlanConfirm,
PlanCheckpointConfirm, ChoiceConfirm)
status — left for follow-up; already conditionally hidden
input (PromptInput multi-line)
stream (StreamingCard)
safety — reserved margin
Skipped this pass: PlanCard anchor, plan-card zone, status rows.
Those don't trigger the lamyc bug and can plug into the same API
when their behavior is reviewed.
The hook had `ctx` in its effect deps, but the provider's context value identity changes after every dispatch (the memo depends on `allocations` which recomputes whenever `claims` changes). So claim → state update → new ctx identity → effect re-fires (release + claim) → infinite loop. Depend on `dispatch` (stable from useReducer) and the spec primitives instead. Effect now fires once on mount, again only when min/max actually change, and once on unmount.
This was referenced May 2, 2026
ChasLui
pushed a commit
to ChasLui/DeepSeek-Reasonix
that referenced
this pull request
May 23, 2026
* fix(tui): single layout authority for viewport rows (esengine#120) Streaming cards and approval modals each read stdout.rows and computed their own row budget independently — the bottom-anchor modal could claim rows the streaming card had already counted on and vice versa, producing visible jitter when both rendered the same frame (lamyc's video). Replaces the scattered RESERVED_CHROME_ROWS / MODAL_OVERHEAD_ROWS constants with a priority-greedy allocator. Each component declares { min, max } rows for its zone via useReserveRows; the provider runs allocation and feeds back actual budgets so consumers stay within the viewport. Migrated zones, in priority order: modal (EditConfirm, ShellConfirm, PlanConfirm, PlanCheckpointConfirm, ChoiceConfirm) status — left for follow-up; already conditionally hidden input (PromptInput multi-line) stream (StreamingCard) safety — reserved margin Skipped this pass: PlanCard anchor, plan-card zone, status rows. Those don't trigger the lamyc bug and can plug into the same API when their behavior is reviewed. * fix(viewport-budget): break infinite useEffect loop in useReserveRows The hook had `ctx` in its effect deps, but the provider's context value identity changes after every dispatch (the memo depends on `allocations` which recomputes whenever `claims` changes). So claim → state update → new ctx identity → effect re-fires (release + claim) → infinite loop. Depend on `dispatch` (stable from useReducer) and the spec primitives instead. Effect now fires once on mount, again only when min/max actually change, and once on unmount.
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.
Closes #120.
Why
StreamingCard,EditConfirm,ShellConfirm, and friends each readstdout.rowsand computed their own row budget independently. Two scattered constants encoded the assumption:RESERVED_CHROME_ROWS = 14(StreamingCard) andMODAL_OVERHEAD_ROWS = 18(EditConfirm). Neither knew about the other.Result: when an approval modal mounted while a tool card was still streaming (lamyc's video — write_file streaming + plan checkpoint approval), both layers claimed rows that together exceeded the viewport. Ink reflowed every frame, producing visible vertical jitter.
The class-of-bug part: every new bottom-anchored component added another independent calculation. A targeted patch would land another magic constant; the next bug just moves to a new component.
What
src/cli/ui/layout/viewport-budget.tsx—ViewportBudgetProvider+useReserveRows(zone, { min, max })hook + pureallocateRows()allocator. Priority-greedy:modal > plan-card > status > input > stream > safety.StreamingCard,EditConfirm,ShellConfirm,PlanCheckpointConfirm,PlanConfirm,ChoiceConfirm,PromptInput. Each declares its zone claim; the provider runs allocation and feeds back actual budgets.RESERVED_CHROME_ROWSand the standaloneMODAL_OVERHEAD_ROWSreads are gone.useTotalRows()/useIsModalActive()replace directuseStdoutreads in those paths.Out of scope (follow-up)
PlanCardanchored render in App.tsx — small height, doesn't trigger this bug, can plug in next pass.OngoingToolRow,ThinkingRow, etc.) — already conditionally hidden during modals, don't compete.SessionPicker,McpBrowser,PlanReviseEditor) — independently take the viewport, not a competing layer.Verification
npm run verify— typecheck, lint, build, 1791 tests all greentests/viewport-budget.test.ts) covering single/multi claim, priority order, undersize-terminal fallback, and the lamyc scenario specifically