From 00f88f2adc1a373f17c977e3201904ead59ee097 Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Mon, 3 Aug 2026 08:06:17 -0700 Subject: [PATCH] fix(tui): drop the abandoned ScrollBox remount key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An earlier attempt at the resize-blanking bug keyed the transcript ScrollBox on `cols` to force a fresh Yoga node. It was abandoned once the real cause turned out to be TranscriptScrollbar stretching the row from a stale viewportHeight (fixed in #775), but it had already been swept into #774 and reached main. It is not merely redundant now, it is harmful. Remounting the ScrollBox builds a new listenersRef Set behind useImperativeHandle(..., []), while the three useSyncExternalStore subscribers (useVirtualHistory, useViewportSnapshot, useScrollbarSnapshot) have stable deps and never resubscribe — so every subscription is orphaned on the dead handle, on every resize. It also resets the handle, sending useVirtualHistory down its `vp <= 0` cold-start branch, which on a long transcript mounts only the last 30 items behind a large blank topSpacer. Its comment was also wrong about the mechanism, which is worse than no comment: it told the next reader the vendored Yoga port fails to re-measure and that marking the tree dirty is insufficient. Neither is true. Verified the resize fix still holds without it: PTY at 60 -> 110 columns, blank rows 7/30 before and after, matching #775's numbers. Suite unchanged from main's baseline at 8 failed / 1681 passed across three runs. Co-Authored-By: Claude Opus 5 --- ui-tui/src/components/appLayout.tsx | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index 650c8d792..9d5977a24 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -104,27 +104,6 @@ const TranscriptPane = memo(function TranscriptPane({ flexDirection="column" flexGrow={1} flexShrink={1} - // Remount on width change — inline mode only. - // - // Inline mode has no constrained-height root (ink lays the tree out - // with `calculateLayout(columns)`, height undefined), so the ScrollBox - // sizes to its content. Widening the terminal re-wraps every row - // shorter, but the box does NOT shrink with them: it keeps its - // pre-resize height and pads the difference with blank rows. Those - // blanks push the transcript above the terminal viewport, and the - // resize repaint's ERASE_SCROLLBACK (log-update fullReset → - // clearTerminal) wipes it from scrollback — so the whole transcript - // reads as gone, with only the composer left on screen. - // - // A fresh Yoga node has no prior layout to keep, so remounting is what - // actually re-measures. Marking the tree dirty is NOT enough (tested). - // Every transcript row already remounts on a width change (useMainApp - // keys virtualRows on `cols`), so this adds no new render cost. - // - // Fullscreen pins the root to terminalRows, so its ScrollBox can never - // drift — and keying there would throw away the user's scroll position - // on every resize. - key={INLINE_MODE ? composer.cols : undefined} onClick={(e: { cellIsBlank?: boolean }) => { if (e.cellIsBlank) { actions.clearSelection()