Skip to content

fix(messages): ease wheel notches and brake only prepend corrections on Mac WebKit - #695

Draft
ngoodger-block wants to merge 7 commits into
block:mainfrom
ngoodger-block:fix/mac-webkit-brake-prepend-only
Draft

ngoodger-block wants to merge 7 commits into
block:mainfrom
ngoodger-block:fix/mac-webkit-brake-prepend-only

Conversation

@ngoodger-block

@ngoodger-block ngoodger-block commented Oct 7, 2026 •

Copy link
Copy Markdown

Why?

The desktop (WKWebView) timeline does not scroll like Slack's, for two reasons that only show on macOS WebKit. First, a mouse-wheel notch moves the content instantly: Chromium eases every notch with an ease-in and ease-out, WebKit jumps, and WebKit has no smooth-wheel setting to turn on. Second, flinging with a trackpad into history hitched and died early, because the patched Virtua driver braked trackpad momentum (one task of overflow-y: hidden) at every automatic scroll correction, including the row-measurement corrections that happen five or six times per fling, while the brake only exists for prepend corrections, where WebKit can drop the correction and displace the reader by a page.

What?

A useWheelSmoothing hook on both conversation scrollers recognises a notch by the only shape WebKit exposes, an isolated wheel event (none in the previous 30 ms) carrying at least half a WebKit line, consumes it, and eases the scroller to the accumulated target with a critically damped spring (about 400 ms, no overshoot, further notches retarget). Dense streams stay native, so trackpads are untouched; an interrupted ease lands its remaining distance so no input is lost; a notch over a code block or diff that can still scroll that way is left to it. It is active only on Mac WebKit and follows Reduce Motion live. The Virtua brake now keys on whether the flushed jump belongs to a prepend shift, including the case where the batch measuring the last prepended row ends shift mode before the flush. patches/README.md documents the narrowed scope; dev/wkprobe/wkprobe.swift with tests/fixtures/long-scroll.html is the native WKWebView probe that produced the measurements and verifies both behaviours with real notches and real momentum phases.

Evidence, same input in a system WKWebView: a plain list travels 663 px per fling; the timeline travelled 457 to 503 px with every correction braked and 543 to 603 px with only prepend corrections braked, with no blank band in any of 30 on-screen frames either way. A real notch reaches the page as one deltaY: -40 event; a trackpad as a stream of −3, −7, −12 at 17 ms spacing. In Playwright WebKit, one 120 px notch now passes through at least six offsets, starts with a step under 20% of the distance, never overshoots and lands exactly 120 px away; Chromium is left to its native easing.

Verification: driver tests 39 pass, hook tests 11 pass, typecheck and biome clean, browser suites scroll, channel-opening (both measurement projects), wheel-smoothing, history-loading, image-scroll, initial-position, navigation-scroll-intent, thread-window, timeline-scrollbar-gutter in Chromium and WebKit pass. Ten local vitest failures in MessageTimestamp and membership-rows are the local en-AU timezone and list locale; they pass under UTC and en-US. The local-only WebKit cursor-paging case now runs about 1.8 minutes instead of 1.0, because its large synthetic wheel gestures are eased like notches; it stays inside its 2-minute budget but is worth knowing. Not addressed here: the geometry cache skips signatures over 256 KB, so long channels always start cold and pay the measurement corrections on every fling into history.

🤖 Generated with Claude Code

ngoodger-block and others added 3 commits October 7, 2026 16:23
The patched Virtua driver set overflow-y: hidden for one task at every
automatic scroll correction on Mac WebKit, so a dropped prepend correction
could not leave the reader displaced by a page. Row-measurement corrections
during a coast were braked as well. Flinging into cold history therefore
hitched five or six times per fling and lost about a quarter of its travel,
which is the stutter the desktop timeline has against Slack.

In a system WKWebView with synthetic trackpad flings carrying real phase and
momentum fields, identical input travelled 663px on a plain list, 457-503px
on the timeline with every correction braked, and 543-603px with only shift
corrections braked, with no blank band in any of 30 on-screen frames either
way. The brake now keys on the store's shift flag, which is the case the
documented WebKit failure is about. The installed-driver tests cover both
sides, and dev/wkprobe/wkprobe.swift plus tests/fixtures/long-scroll.html
reproduce the native measurement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
…uring batch

A prepend's rows can report their heights after the 150ms scroll-end has
already asked the store to leave shift mode. The batch that measures the
last of them clears shift mode before the driver flushes the correction, so
keying the brake on the flush-time mode alone let that correction, which
can be page-scale, run unbraked. A pending jump now remembers a shift
contribution until it is flushed. The native probe drops its anchor-jump
metric: a successful compensation moves scrollTop while the anchor stays
put on screen, which that metric counted as a jump.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
Chromium eases every wheel notch; WebKit moves the content instantly per
notch and has no smooth-wheel setting, so a notched mouse feels jumpy next
to Slack. WebKit exposes no device or phase information, so a notch is
recognised by shape: an isolated wheel event, none in the previous 30ms,
carrying at least half a WebKit line. Both conversation scrollers consume
such events and ease to the accumulated target with a critically damped
spring; dense streams stay native so trackpads are untouched, and an
interrupted ease lands its remaining distance rather than losing it. Mac
WebKit only, and off under reduced motion.

Recorded in a system WKWebView: a real notch arrives as one deltaY -40
event; a trackpad as -3, -7, -12 at 17ms spacing. The native probe gains a
NOTCHES mode to verify the easing with real line-unit wheel events.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
@ngoodger-block ngoodger-block changed the title fix(messages): brake Mac WebKit momentum only for prepend corrections fix(messages): ease wheel notches and brake only prepend corrections on Mac WebKit Oct 7, 2026
ngoodger-block and others added 4 commits October 7, 2026 17:34
…Motion live

A notch over a code block or diff that can still scroll in that direction
belongs to it, not the feed. Reduce Motion can change while the app is
open, so the hook subscribes to the media query and attaches or detaches
the easing as it flips.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
…wheel ease

A notch the reader already gave must not be dropped when a nested
scrollport takes the next one mid-ease, or when the ease detaches because
Reduce Motion was switched on or the timeline unmounted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
…n persistence

Landing an ease now absorbs any correction that moved the offset since the
last frame, and the hook is a layout effect declared before the timeline's
own, so a pending notch lands before unmount records the reading position.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
The timeline records its reading position from scroll events, which the
browser queues, so a channel switch mid-ease persisted the offset before
the landed notch. Landing now dispatches the event synchronously.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant