feat(cli): scroll the transcript three lines per wheel notch - #1274
feat(cli): scroll the transcript three lines per wheel notch#1274kavish-19 wants to merge 2 commits into
Conversation
Closes CodebuffAI#1268. OpenTUI's ScrollBox multiplies each wheel event's notch delta by whatever its ScrollAcceleration returns, and defaults to LinearScrollAccel, whose tick() returns 1. A terminal reports one notch as a delta of 1, so the transcript moves a single line per notch -- far slower than the three lines terminals and desktop apps use. ScrollBox already accepts a scrollAcceleration option, so this needs no upstream change: the React reconciler spreads JSX props straight into the renderable's constructor, which assigns the field the wheel handler reads. Neither shipped accelerator gives a flat multiplier -- LinearScrollAccel is fixed at 1, MacOSScrollAccel ramps with scroll velocity -- so this adds a small stateless one. Being stateless, a single shared instance is enough, which also keeps the prop's identity stable across renders. Scoped to the chat transcript on purpose. The prompt editor's scrollbox is a few rows tall, where three lines a notch would skip most of its content, so it keeps the one-line default; a test pins that split. Claude-Session: https://claude.ai/code/session_018vPhyqaaoKa8cgs7GEnyq5
|
Nice work — this is a well-scoped, well-documented fix. Root-causing to OpenTUI's A couple of things worth double-checking before porting:
Overall: correct instinct, right layer, small diff, decent test coverage, and unusually transparent about its own verification limits. Recommend porting after a manual scroll-wheel smoke test. |
The review on CodebuffAI#1274 flagged the one gap in that PR: the delta -> multiplier -> scrollTop path was checked by reading OpenTUI's source, not by an actual wheel event, so nothing proved the scrollAcceleration JSX prop reaches the constructor field the wheel handler reads. This renders a scrollbox through @opentui/react's reconciler and scrolls it with the mock mouse from @opentui/core/testing, which emits the same SGR sequence a terminal does. One notch moves three lines; the same scrollbox without the prop still moves one, so the assertion cannot pass for any reason other than the accelerator. The line counts are written out rather than read from WHEEL_SCROLL_LINES -- a test that reads the constant it pins follows it anywhere. Verified red with the constant set to 1 (3 of 4 fail, the no-prop control correctly unaffected) and green at 3. Uses flushSync rather than @opentui/react's testRender helper: that helper wraps the render in React's act(), which is stripped from React's production build, and the suite runs under NODE_ENV=production. Claude-Session: https://claude.ai/code/session_01QNL5SiuLLyRHZcFgtUN5Yp
|
Thanks — both flagged points are now closed in 1. The verification gap. You're right that this was the one real hole: nothing proved the Two things I got wrong first time and corrected, since they'd otherwise be worth your review time:
A manual scroll is still worth doing before you port — this covers the parser and reconciler, not a physical wheel — but it's no longer the only thing standing behind the change. 2. Platform variance in notch deltas. I don't think this one bites. Both of OpenTUI's mouse decoders hardcode the delta: // index-54s7pk0d.js:6592 (SGR) and :6631 (basic mode)
scrollInfo = { direction: scrollDirection, delta: 1 }A terminal that scrolls "faster" sends more escape sequences, not a larger delta, so One thing for you to decide. I named the new file
|
Closes #1268.
The cause
The CLI never handles wheel events itself — OpenTUI's
ScrollBoxRenderabledoes:and the constructor defaults to
LinearScrollAccel:A terminal reports one wheel notch as a delta of 1, so
1 × 1moves the transcript a single line. That is the reported behaviour, and it's a default rather than a bug.The change
ScrollBoxalready accepts ascrollAccelerationoption (and exposes a setter), so nothing upstream needs changing — it's a prop on the existing<scrollbox>inchat.tsx. The React reconciler spreads JSX props straight into the renderable's constructor (createInstancein@opentui/react), which assigns the field the wheel handler reads.Neither shipped accelerator gives a flat multiplier:
LinearScrollAccelis fixed at 1, andMacOSScrollAccelramps with scroll velocity, which would make a fast flick jump much further than the three lines the issue asks for. So this adds a small statelessConstantScrollAccel. Because it's stateless there is nothing to accumulate or reset, so one shared instance serves it — which also keeps the prop's identity stable across renders instead of allocating per render.Two decisions worth flagging
Scoped to the chat transcript. There are other scrollboxes — the prompt editor (
multiline-input.tsx) and the landing screen. The editor is a few rows tall, where three lines a notch would skip most of its content, so it keeps the one-line default. A test pins that split so it isn't "fixed" later by accident.Hardcoded, not configurable. The issue asks for 3 to match standard terminal and desktop behaviour, so that's what this does. Happy to put it behind a setting if you'd rather, but that seemed like unrequested surface area.
Verification
mainbaseline — no new ones.tsc --noEmitclean on both touched files.chat.tsxstill failsprettier --check, but only at lines 279 and 931, both of which fail on unmodifiedmaintoo. My lines (100 and 1668) are clean, so I've left it rather than bury the diff in an unrelated reformat.One thing I could not verify locally: I checked the delta→multiplier→
scrollToppath by reading OpenTUI's source rather than by driving a real wheel event, sinceonMouseEventlives in the dependency. Worth a quick manual scroll on your side before porting.