Repository navigation
fix(chat): keep the words under a selection held through a slow fold - #2684
Conversation
The selection hold from #2658 lost the words when frames were slow, which made chat-selection-turn-end-demo-regression.ts flaky in CI: - the list puts a row's shrink off a frame, so a fold in the held words' own row was painted uncorrected: the hold now watches that row too; - a fractional scrollTop read back a pixel apart looked like the reader leaving the end, and the hold stopped: it scrolls in whole pixels and keeps the words' aimed place; - the list's MVCP scrollBy and the browser's trim after it moved the view up with no input: under a hold that is the layout's, and the hold puts the list's adjust back (scrollAdjust, typed in the LegendList patch). The script compares unrounded readings and adds a reduced-motion fold, which fails on main every time. Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 6 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
…der's scroll Review (Codex, reproduced by omo): every move up without input under a selection was taken for the layout's, so a reader who scrolled up with no input the page sees (a screen reader, find in page) was pulled back to the end once the selection went. Only the browser's whole-pixel trim needs the excuse; anything more is the reader leaving the end again. The turn-end script gains that case, and watches each fold until it has slid shut and the list has measured it. Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
|
Review round on c9901b9 (omo: BLOCK; Codex gpt-6.1-terra xhigh: REQUEST_CHANGES, no HIGH). Fixed in 1db6fa9:
Validation on 1db6fa9: CodeRabbit is rate-limited on this PR (no review ran); the omo and Codex reviews above stand in for it. |
…t allowance Review (Codex, reproduced by omo): each move up of a pixel without input was excused under a selection however many came, so a reader who left the end a pixel at a time was pulled back once the selection went. The moves excused under one selection now add up to the layout allowance at most; past it they are the reader's, as without a selection. The reader-away case runs in 30 px steps and in 1 px steps. Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
|
Second review round on 1db6fa9 (Codex gpt-6.1-terra xhigh: one MEDIUM, no HIGH; omo: BLOCK on the same MEDIUM, reproduced). Fixed in c358899:
Validation on c358899: |
…view Review (Codex, reproduced by omo): a budget of excused pixel moves still let a reader already far from the end (the transcript grew below the selection) drift up a pixel at a time and be pulled back once the selection went. Under a selection, only a move to within a pixel of where the chat itself last put the view (a follow, or the hold's correction: onFollowed) is now the browser's trim; any further it is the reader's. The reader-away case also runs after the transcript grew below the selection, a pixel at a time. Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
… view Review (Codex): the excused trim was bounded on one side only, so after the reader scrolled down past where the chat last put the view, each move back up was excused until it passed that place again. Only a landing within a pixel above that place is the browser's trim now. The reader-away case also scrolls down into what grew below the selection after the hold has put the view back once, then up a pixel at a time. Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
|
Third (last) review round, on c358899 (omo BLOCK and Codex REQUEST_CHANGES on the same reproduced MEDIUM, no HIGH), then a Codex finding on the local fix; head is now 0830c48 (a4e9fa8 plus a merge of main after #2693):
Validation on a4e9fa8, under load (6 in parallel): whole script (now 10 cases) 24/24;
|
|
Review of 0830c48: omo BLOCK on one reproduced MEDIUM, no HIGH (its Codex Terra pass reported the same one; Codex's own pass on a4e9fa8 approved). Per the last-round rule for this PR, it is not fixed here:
CI on 0830c48 is running. |
Review (omo, reproduced): the trim excuse checked only where the view landed, so a reader's no-input scroll of any size landing a pixel above where the chat last put the view was excused, and the reader was pulled back to the end once the selection went. The scroll event's own move up must now be a pixel at most too (read before onScroll records the new position). The reader-away case adds a 151 px move landing exactly there. Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
|
Head 8181b42: the fix for the MEDIUM omo reproduced on 0830c48 (a no-input scroll of any size landing a pixel above where the chat last put the view was excused). The scroll event's own move up must now be at most a pixel too, read before Validation on 8181b42: the whole script (11 cases) 6/6 under load; omo review at 8181b42: CLEAR (with a Codex Terra pass folded in). Remaining notes, none blocking:
CodeRabbit is rate-limited on this PR; the omo and Codex reviews stand in for it. |
|
Merge note: CodeRabbit was rate-limited for these heads; merged on the omo review (CLEAR receipt at the head), Codex Terra (APPROVE; every reproduced MEDIUM across 4 rounds fixed with a failing-first case) and green required checks on GitHub-hosted runners. Remaining LOW notes are in the comments above. |
…d chat lands at its end (#2720) ## Change `scripts/chat-virtual-browser-qa.ts:81` (`settleBottom` after `page.reload()`) timed out intermittently on PR runners (#2621's rerun, twice in a row; #2644): the reopened chat ended about 54 px short of its end, and CI's failure screenshot shows Jump to latest. The test was right: a reopened chat took the reader for one who had scrolled away, with no input. Caught with a debug build that logs every follow and scroll decision (it failed the first reload in 4 of 8 runs under load): 1. On the reload the list's first landing is short of the end (its footer not measured yet), and the chat's `followMeasured` puts the view at the real end: `max(measured 120061, scrollHeight 120115) - 900 = 119215`. 2. In the same frame the view is pulled up to **119161 = the list's own measured `contentLength` (120061) - clientHeight**. LegendList's own scroll to the end is still on its way, and while one is (`state.scrollingTo`), its `onScroll` clamps any position past the end *it has measured so far* back to that end (`clampScrollOffset`). Its measure lags the transcript by 54 px there (the footer and some rows not measured yet). 3. `ChatView.onScroll` sees a move up with no input, 54 px from the end (more than the 48 px layout allowance) over a transcript that did not shrink, and concludes the reader left the end: `stick=false paging=true`, Jump to latest. Nothing follows again. When the lag is under the allowance, the chat instead stays "following" short of the end, with nothing left to correct it (the local variant). LegendList's end on the web can only lag what the browser shows (it measures what it renders, a frame or more late), so the fix is in the LegendList patch this repo already keeps (`patches/@LegendApp%2Flist@3.3.5.patch`, two hunks, plus a helper): - A helper finds the end of what is drawn: the bottom of the list content's children and of the rows inside the rows' wrapper (rows as tall as they are drawn, a header or footer not measured yet), relative to the scroll origin. It deliberately leaves out the room the list pads past its end for a frame while it holds rows in place, and a `min-height` under the content (the selection hold's room, #2684). An earlier version used the scroller's `scrollHeight`, which includes that room, and made `reply-comments-demo-regression.ts` drag selections land on other words in 2 of 7 runs. - `clampScrollOffset` never clamps below that drawn end. This is what pulled the view up in the captured failure. - `scrollToEnd` adds what the list has not measured yet (the drawn end past its measured one) to its end offset, so its own target is the real end instead of re-landing short. When the list's measure matches what is drawn (settled), both changes are no-ops; a larger measure (rows estimated taller than drawn) keeps the list's own value. No app code changes. The test: `chat-virtual-browser-qa.ts` gains the case right after the reload step that timed out. A 100 px box drawn after the footer inside the list's content, which the list never measures, makes its end lag the transcript's by more than the layout allowance (as an unmeasured footer and rows did in the captured failure), and a view a pixel shorter makes the chat follow its end and the list scroll to its own. The chat must end at the end, still following, with no Jump to latest. On main it ends 61 px short in every frame from the second on (`{"gaps":[101,100,61,61,...]}`); with this change it ends at 0. ## Validation - `chat-virtual-browser-qa.ts` with the new case: origin/main (with #2684) **fails 2/2** at the new case (61 px short in every frame from the second on); this branch **passes 4/4** (all four width/theme passes). The case lags the list's end twice: a box after the footer, and a box past the rows' wrapper (a row drawn taller than measured), which the review head 735a121 failed 2/2. - `reply-comments-demo-regression.ts` (drag selections, which the earlier `scrollHeight` version broke): 10/10 on main; on this branch 10/10, and 6/6 again on the final head 74b4389 (with turn-end 4/4, chat-history and local-send-follow passing, `check fast` ok). - The original reload timeout reproduces only on a loaded machine (about once in a few hundred reloads on this PC, 2 of 2 attempts on #2621's runner), so its before/after rests on the captured trace above and the deterministic case. - Merged with main (#2684's selection hold also leans on the list's scroll): `chat-selection-turn-end-demo-regression.ts` 6/6 on this branch. - Adjacent browser scripts on this branch, all pass: `chat-history-browser-qa.ts`, `chat-local-send-follow-demo-regression.ts`, `chat-quote-selection-end-regression.ts`, `chat-scroll-padding-regression.ts`, `chat-select-quote-demo-regression.ts`, `chat-selection-copy-regression.ts`, `chat-working-line-demo-regression.ts`, `chat-no-rewind-demo-regression.ts`, `reply-comments-demo-regression.ts`, `chat-gjc-streaming-browser-qa.ts`. Runs were capped at 4 at a time with one shared demo build per variant. - The patch applies with a clean `bun install` from an empty cache and reproduces the tested file byte for byte; #2684's `react.d.ts` hunk is kept; `bun.lock` is unchanged. - `bun run check fast`. - Not run: `bun run check full`, the Mac and Windows lanes (no platform code changed). Changelog: `changelog.d/chat-reopen-lands-at-end.md` (Fixed). --------- Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
* fix(qa): open the folded work run before claude-image clicks a row A work block's calls fold into a run line, so the row the check clicks is not drawn until the run is opened (the same step #44 added to the other image checks). * fix(qa): hide-pc waits for motion that ends, not an agent's wave An agent friend's wave never ends, so waiting for every animation to stop timed out. A looping animation is no motion to wait for. * fix(qa): terminal-autoswitch reads the lens the header shows The header's lens button flips the lens with one tap (#49) and shows the one it goes to, so a peek's header shows the grid, and one tap on it ends the peek. The check no longer expects Chat pressed, and the second way of keeping the terminal is the same tap as the first. * fix(qa): workspace-rows reads the tab strip's row surface The strip's background is on .tab-strip-row now; .tab-strip itself is transparent. * fix(qa): pane-mute fixture's roster tells each pane's last status A roster carries each pane's status as the server's does, so a page that takes a roster again (a stream reopened as the tab is shown) finds the panes as the frames left them, instead of the demo's first statuses. * fix(chat): keep the words under a selection held through a slow fold (devswha#2684) ## Change `scripts/chat-selection-turn-end-demo-regression.ts` (devswha#2658) failed intermittently in "Browser regression (other scripts)" on PRs that do not touch chat, e.g. devswha#2621 run 41725988136: `desktop-1280-fold`, one frame at 533.3 px against 572.8. It is not the test: the selection hold from devswha#2658 loses the words under the selection when frames are slow. Instrumenting every frame (rAF, every ResizeObserver callback, every `scrollTop` set and `scrollBy`, the scroll events) on a slow machine showed three races, all in the hold: 1. **A fold in the held words' own row was painted uncorrected for a frame.** LegendList puts a row's shrink of more than 1 px off to the next animation frame (`scheduleWebShrinkMeasurement`), so when the ending turn's work block folds above the words in the same row, no observer the hold listens to fires in that frame. On a slow frame the slide moves 40-50 px at once, and that frame is painted with the words moved. The hold now also watches the held words' own row (`[data-chat-row]`) with the chat's resize observer, which the chat made when it mounted, so the move is put back in the frame it happens, before the paint. 2. **The hold's own correction made the chat think the reader left the end.** `keep()` set a fractional `scrollTop` (253.5625), reported the read-back (254) through `onFollowed`, and the scroll event then read 253: a 1 px move up with no input, which `ChatView.onScroll` took for the reader leaving the end. `following` went false, `keep()` stopped holding, and the rest of the fold drifted the words 10-27 px. `keep()` now scrolls in whole pixels, and keeps the words' aimed place as its reference instead of re-reading it, so the rounding adds up with the next move instead of drifting by a pixel. 3. **The list and the browser move the view up after the hold, with no input.** A frame later LegendList measures the same fold and its MVCP `scrollBy`s again (−24, −36 px), and the browser trims the view as the transcript shrinks. `ChatView.onScroll` now takes a move up without the reader's input as the layout's while a selection holds the view (the reader's wheel, keys, touch and scrollbar still count as leaving), and the hold hears the list's adjust (`listen("scrollAdjust")`) and puts the words' whole move back, as it already did for the list's padded frame (`listEndPadded`). `scrollAdjust` is a signal LegendList's `listen` already delivers, but its public `LegendListListenerType` leaves it out; the existing `patches/@LegendApp%2Flist@3.3.5.patch` gains a one-line `react.d.ts` hunk adding it (no runtime change, `bun.lock` unchanged; checked with a clean install from an empty cache). The test: readings are compared unrounded. Rounding each to 0.1 px first made a 0.96 px step (inside the hold's own sub-pixel rule, put back the next frame) read as exactly 1 px and fail `< 1`. And a new case, `desktop-1280-fold-reduced-motion`: with reduced motion the block folds in one frame, which is what one slow frame of the slide does, so it fails on main every time instead of only on a slow runner (on main the words move to 608.8 and then 460.8 px; reduced-motion users saw that). Review round (1db6fa9): only a move up of at most one pixel without input is excused while a selection stands (the browser's whole-pixel trim), only a move of at most a pixel landing within a pixel above where the chat itself last put the view (a4e9fa8, 8181b42), so a reader who scrolls up with no input the page sees, in large steps or a pixel at a time, is still away from the end once the selection goes; the script gains `desktop-1280-reader-away` for it (fails 3/3 on c9901b9), and watches each fold until it has slid shut. Details in the review comment. No changelog fragment: `changelog.d/chat-selection-holds-still.md` from devswha#2658 is still unreleased, and this makes it true. ## Validation Before/after on this PC (load average 36-98 from other lanes and CI runners), the demo build as CI builds it (`bun scripts/demo-build.ts`), through `dori heavy`: | | origin/main 4bc043d | this PR | | --- | --- | --- | | `chat-selection-turn-end-demo-regression.ts`, whole script | 0/20 pass (old script); 0/5 with the new script, the reduced-motion case failing every time it is reached | 24/24 pass, 6 in parallel | | `desktop-1280-fold` only, looped (instrumented copy) | fails within the first 3 iterations | 150/150 (6 × 25 in parallel) | | `phone-390-fold` only, looped | | 25/25 | | `desktop-1280-fold` with reduced motion, looped | 0/5 | 10/10 | Intermediate states, for the record: with only the row watch (1), 18/20 whole-script runs passed and the remaining failures were the drift of (2) and (3); with (1) and (2) but no list-adjust signal, transitions passed 150/150 but reduced motion still failed. - `bun run check fast` (through `dori heavy`) on c9901b9 and again on 1db6fa9: workflow syntax, generated types and themes, typecheck, build, unit tests (10883 pass) all ok. - On 1db6fa9, under the same load: whole script (then 7 cases) 24/24; `desktop-1280-fold` looped 150/150, `phone-390-fold` 25/25, reduced motion 10/10. - On a4e9fa8 (last review round: only a landing a pixel above where the chat last put the view is excused), whole script (10 cases: reader-away in 30 px steps, 1 px steps, 1 px after growth below, and down-then-up) 24/24; the same loops 150/150, 25/25, 10/10; the 11 adjacent scripts pass; `check fast` ok. Each new reader-away case fails on the commit before its fix (1db6fa9, c358899, f37fab8). On the merged head 0830c48: `check fast` ok (10894 unit tests), whole script 6/6. On 8181b42 (the event's own move must be a pixel at most too; new 151 px case, which fails 2/2 on 0830c48): whole script (11 cases) 6/6, `check fast` ok. - Adjacent browser scripts on the final build, all pass: `chat-virtual-browser-qa.ts`, `chat-history-browser-qa.ts`, `chat-local-send-follow-demo-regression.ts`, `chat-quote-selection-end-regression.ts`, `chat-scroll-padding-regression.ts`, `chat-select-quote-demo-regression.ts`, `chat-selection-copy-regression.ts`, `chat-working-line-demo-regression.ts`, `chat-no-rewind-demo-regression.ts`, `reply-comments-demo-regression.ts`, `chat-gjc-streaming-browser-qa.ts`. - `chat-browser-qa.ts` could not run here: it times out waiting for `.conn-live` on this PC, the same on origin/main (no live connection in this environment); CI runs it. - `chat-virtual-browser-qa.ts`'s failure on devswha#2644 (`continuous content left the reader behind for 6 consecutive animation frames`, line 717) is a different cause: it is about following streamed content, and that run (2026-11-03 08:03 UTC) predates devswha#2658. Not addressed here. - Not run: `bun run check full`, the Mac and Windows lanes. No server, shared or install code changed. --------- Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com> (cherry picked from commit e96b369) * fix(qa): pane-mute waits for the sheet's focus and ignores a cancelled animation CI runs the script with UI_EVIDENCE_DIR set, which adds the captures. In them the phone's keyboard walk pressed ArrowDown before the opening sheet had taken focus, so the arrows landed on the toggle behind it and the focus ended on the sheet's first item: the check now waits for focus to be inside the sheet first, as the desktop branch does for the menu. capture() also awaited animation.finished bare, which rejects with an AbortError when an animation is cancelled; the other settle helpers catch that, and so does this one now. --------- Co-authored-by: Hako <devswha@gmail.com> Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
Change
scripts/chat-selection-turn-end-demo-regression.ts(#2658) failed intermittently in "Browser regression (other scripts)" on PRs that do not touch chat, e.g. #2621 run 41725988136:desktop-1280-fold, one frame at 533.3 px against 572.8. It is not the test: the selection hold from #2658 loses the words under the selection when frames are slow. Instrumenting every frame (rAF, every ResizeObserver callback, everyscrollTopset andscrollBy, the scroll events) on a slow machine showed three races, all in the hold:scheduleWebShrinkMeasurement), so when the ending turn's work block folds above the words in the same row, no observer the hold listens to fires in that frame. On a slow frame the slide moves 40-50 px at once, and that frame is painted with the words moved. The hold now also watches the held words' own row ([data-chat-row]) with the chat's resize observer, which the chat made when it mounted, so the move is put back in the frame it happens, before the paint.keep()set a fractionalscrollTop(253.5625), reported the read-back (254) throughonFollowed, and the scroll event then read 253: a 1 px move up with no input, whichChatView.onScrolltook for the reader leaving the end.followingwent false,keep()stopped holding, and the rest of the fold drifted the words 10-27 px.keep()now scrolls in whole pixels, and keeps the words' aimed place as its reference instead of re-reading it, so the rounding adds up with the next move instead of drifting by a pixel.scrollBys again (−24, −36 px), and the browser trims the view as the transcript shrinks.ChatView.onScrollnow takes a move up without the reader's input as the layout's while a selection holds the view (the reader's wheel, keys, touch and scrollbar still count as leaving), and the hold hears the list's adjust (listen("scrollAdjust")) and puts the words' whole move back, as it already did for the list's padded frame (listEndPadded).scrollAdjustis a signal LegendList'slistenalready delivers, but its publicLegendListListenerTypeleaves it out; the existingpatches/@legendapp%2Flist@3.3.5.patchgains a one-linereact.d.tshunk adding it (no runtime change,bun.lockunchanged; checked with a clean install from an empty cache).The test: readings are compared unrounded. Rounding each to 0.1 px first made a 0.96 px step (inside the hold's own sub-pixel rule, put back the next frame) read as exactly 1 px and fail
< 1. And a new case,desktop-1280-fold-reduced-motion: with reduced motion the block folds in one frame, which is what one slow frame of the slide does, so it fails on main every time instead of only on a slow runner (on main the words move to 608.8 and then 460.8 px; reduced-motion users saw that).Review round (1db6fa9): only a move up of at most one pixel without input is excused while a selection stands (the browser's whole-pixel trim), only a move of at most a pixel landing within a pixel above where the chat itself last put the view (a4e9fa8, 8181b42), so a reader who scrolls up with no input the page sees, in large steps or a pixel at a time, is still away from the end once the selection goes; the script gains
desktop-1280-reader-awayfor it (fails 3/3 on c9901b9), and watches each fold until it has slid shut. Details in the review comment.No changelog fragment:
changelog.d/chat-selection-holds-still.mdfrom #2658 is still unreleased, and this makes it true.Validation
Before/after on this PC (load average 36-98 from other lanes and CI runners), the demo build as CI builds it (
bun scripts/demo-build.ts), throughdori heavy:chat-selection-turn-end-demo-regression.ts, whole scriptdesktop-1280-foldonly, looped (instrumented copy)phone-390-foldonly, loopeddesktop-1280-foldwith reduced motion, loopedIntermediate states, for the record: with only the row watch (1), 18/20 whole-script runs passed and the remaining failures were the drift of (2) and (3); with (1) and (2) but no list-adjust signal, transitions passed 150/150 but reduced motion still failed.
bun run check fast(throughdori heavy) on c9901b9 and again on 1db6fa9: workflow syntax, generated types and themes, typecheck, build, unit tests (10883 pass) all ok.desktop-1280-foldlooped 150/150,phone-390-fold25/25, reduced motion 10/10.check fastok. Each new reader-away case fails on the commit before its fix (1db6fa9, c358899, f37fab8). On the merged head 0830c48:check fastok (10894 unit tests), whole script 6/6. On 8181b42 (the event's own move must be a pixel at most too; new 151 px case, which fails 2/2 on 0830c48): whole script (11 cases) 6/6,check fastok.chat-virtual-browser-qa.ts,chat-history-browser-qa.ts,chat-local-send-follow-demo-regression.ts,chat-quote-selection-end-regression.ts,chat-scroll-padding-regression.ts,chat-select-quote-demo-regression.ts,chat-selection-copy-regression.ts,chat-working-line-demo-regression.ts,chat-no-rewind-demo-regression.ts,reply-comments-demo-regression.ts,chat-gjc-streaming-browser-qa.ts.chat-browser-qa.tscould not run here: it times out waiting for.conn-liveon this PC, the same on origin/main (no live connection in this environment); CI runs it.chat-virtual-browser-qa.ts's failure on feat(server): support IPv6 loopback beside IPv4 #2644 (continuous content left the reader behind for 6 consecutive animation frames, line 717) is a different cause: it is about following streamed content, and that run (2026-11-03 08:03 UTC) predates fix(chat): hold the chat still while a selection is in progress (#2650) #2658. Not addressed here.bun run check full, the Mac and Windows lanes. No server, shared or install code changed.