mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* fix(cli): window the OpenTUI transcript so a long resume cannot blank the screen @opentui allocates one native TextBuffer per mounted text element, and the OpenTUI transcript mounted every history item at once. Resuming a session of a few thousand records exhausted the process, and the resulting throw was silent: it lands inside a React host-instance creation, @opentui/react's own boundary catches it, its red-text fallback calls a jsxDEV the bundle does not provide and throws too, and React unmounts the whole root, leaving an empty alternate screen with a live process still accepting input. Mount only the items near the viewport, budgeted in rows rather than items, with spacers standing in for the rest so the scroll geometry still spans the whole session. Estimates mirror the values the ink transcript already uses, real heights are measured per item id and survive unmount, and the scroll position is sampled on the renderer's frame event so an idle terminal costs nothing. Also wrap each mounted item in an error boundary whose fallback renders nothing. A failed item now costs one row instead of the whole UI, and the error reaches the OPEN_TUI_TRANSCRIPT debug log. The session picker's preview pane is clipped and does not scroll, so the view takes an initial anchor and the preview passes 'top'. * fix(cli): stop the transcript window moving the reading position Three defects in the windowing's scroll handling: The height table was cleared on a width change, which renumbered every row offset underneath a scroll position that is itself counted in rows, so a resize moved the visible turn by more than a hundred. The entries a resize actually invalidates are the mounted ones, and the next frame re-measures those. A height correction was written back through the sticky-aware scrollTop setter. No correction is owed: an item the measuring loop can see is already painted at the height just read from it, and both spacers derive from offsets a change inside the window leaves alone. Writing one moved the reading position and, at the tail, took the shell's bottom pin off for the rest of the session. The position was sampled only on the renderer's frame event, which fires after the paint, so an absolute jump — a scrollbar track click or a thumb drag, which can land anywhere in the transcript — painted one spacer-only gap first. The scroll bar emits a public change event synchronously before that paint; subscribing to it covers every scroll path. Also: the host-less session preview now follows the height prop across a resize instead of seeding its viewport rows once at mount; the item failure log carries the component stack and the error stack, matching ink; the two windowing options no caller ever set are gone, so the cap and the overscan are pinned by the tests that exercise them; and the cap's binding condition is stated correctly in the code and in both design docs. Adds a scroll-host harness so the frame-driven half is reachable from jsdom at all, and five tests over it, each mutation-proved against the defect it pins. * docs(cli): record the two windowing follow-ups the review named The bundle-level jsxDEV escalation Decision 8 compensates for at the item is still live for every other subtree, and a window-only move re-executes every mounted item's render path. Both are named in the design rather than left to be rediscovered. * fix(cli): measure transcript items by identity and settle pulled-in rows Each mounted item now registers its own node under `(kind, id)` instead of being recovered from its position among the transcript root's children, so the tool and task cards one subagent call mints no longer overwrite a single height slot, and the item key no longer collides as a React key either. The rows recorded are Yoga's computed height rather than the reported one, which is clamped to a row and would permanently charge a row to an item that paints nothing. An upward window move pulls in items whose spacer rows were charged at the estimate, so the frame that first measures them now subtracts what they turned out to cost from the scroll position — guarded to fire only strictly above the tail, where the reader's own scrolling has already released the shell's bottom pin. Measuring an item that was already mounted still owes nothing. Measuring also runs when no scroll host is found, so the session picker's preview records real heights too. * docs(cli): state the measured mutant victims in the windowing design doc * fix(cli): settle a transcript row whose recorded height went stale off-window The settlement for a row the window pulls in above the viewport only fired for an identity the height table had never seen. A row measured earlier, evicted, and then re-measured at a different height moved the offsets under a scroll position nothing re-anchored, so the page lurched on every wheel tick after a terminal resize or ctrl+O. Settle on the charge rather than on first-ever measurement, spend the charge on the measurement that answers it, and never charge an identity the previous commit already had mounted — which is what a `task-end` splice above the window would otherwise do, since it shifts the index space the charging loop walks while the identities on screen stay put. * docs(cli): keep the windowing design doc true about the stale-height settlement Decision 5 said the frame that *first* measures a pulled-in item settles it, and that the charge is always the estimate; the settlement now spends a charge on the measurement that answers it and never charges an already-mounted identity. The mutation ledger named ten tests on the scroll-host harness and a suite total that both moved. --------- Co-authored-by: Shaojin Wen <[email protected]>