You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(web): mostly vertical trackpad scrolls no longer navigate back or forward - #30173
On desktop, a mostly vertical two-finger trackpad scroll can navigate back or forward. If the first ~8px of the scroll lean sideways (e.g. 5px x / 3px y), HistorySwipeNavigation locks onto the horizontal axis. After that it ignores deltaY, so the small sideways drift of a long vertical scroll adds up to the 120px trigger, and releasing the scroll (or its momentum) navigates. Reproduced on main cefb544af3 in the desktop app: a scroll 15° off vertical navigates back, also without momentum, and the mirrored scroll navigates forward.
Change
The gesture now counts total horizontal and vertical travel. Once it has moved 30px, horizontal travel must stay more than twice the vertical travel. If it doesn't, the gesture is dropped: the indicator hides, nothing navigates, and the rest of the gesture is ignored as before. The same check applies when the axis locks, so one large diagonal event cannot lock and navigate. Below 30px the direction is still noise, so swipes whose first events lean diagonally still navigate.
vp test run apps/web/src/components/HistorySwipeNavigation.browser.test.tsx apps/web/src/components/HistorySwipeNavigation.test.ts: 17 passed. New cases:
a vertical scroll that starts slightly diagonally keeps the thread: fails on main (navigateHistory called with "back"), passes here.
a swipe that turns into a vertical scroll keeps the thread: fails on main, passes here.
a swipe whose first events lean diagonally navigates and a swipe that leans diagonally for two events navigates: pass on main and here (guard real swipes with a diagonal start).
a single diagonal event past the trigger keeps the thread: fails on main, passes here.
vp run check:changed: format, lint, knip, typecheck, tests and React compiler all pass.
Desktop app (vp run dev:desktop on Linux/Xvfb, fresh database, back history #/new → #/pull-requests). A driver over CDP replayed each gesture as one latched sequence of wheel events over the main content. CDP mouse-wheel input gets its own Chromium gesture per event, so the driver sends one CDP wheel to fire the gesture begin/end reset and then dispatches the sequence in the page. Same driver on both sides:
variant: same scroll, gesture ends when the fingers lift (no momentum)
dx=-125 dy=453 (15° off vertical)
navigated
stayed
variant: rightward drift with forward history
dx=148 dy=539 (15° off vertical)
navigated
stayed
variant: diagonal start, then perfectly vertical
dx=-5 dy=539 (1° off vertical)
stayed
stayed
swipe whose first events lean diagonally
dx=-186 dy=-3 (89° off vertical)
navigated
navigated
swipe that leans diagonally for two events
dx=-157 dy=5 (88° off vertical)
navigated
navigated
swipe that turns into a vertical scroll
dx=-153 dy=536 (16° off vertical)
navigated
stayed
swipe 18° off horizontal
dx=-180 dy=60 (72° off vertical)
navigated
navigated
swipe 34° off horizontal
dx=-180 dy=120 (56° off vertical)
navigated
stayed
control: clearly horizontal swipe
dx=-180 dy=15 (85° off vertical)
navigated
navigated
Replay driver (not committed)
// Repro driver for pingdotgg/t3code#30160 against the running desktop app (CDP on :9342).// CDP mouseWheel events each get their own Chromium gesture (begin/end around every event), so the// latched trackpad gesture is replayed as in-page wheel events over the settings content instead.// Usage: node repro.mjs <dir that resolves playwright> <output dir>import{createRequire}from"node:module";import{mkdirSync,writeFileSync}from"node:fs";constrequire=createRequire(process.argv[2]+"/");const{ chromium }=require("playwright");constoutDir=process.argv[3];mkdirSync(outDir,{recursive: true});constbrowser=awaitchromium.connectOverCDP("http://localhost:9342");constpage=browser.contexts().flatMap((c)=>c.pages()).find((p)=>p.url().startsWith("t3code-dev://app"));constlog=[];constnote=(line)=>{log.push(line);console.log(line);};// Wheel deltas as the DOM sees them (natural scrolling already applied).constvertical=(n,drift)=>Array.from({length: n},()=>[drift,15]);constmomentum=(drift)=>Array.from({length: 12},(_,k)=>[drift*0.85**k,15*0.85**k]);constscenarios=[{name: "reporter: diagonal start (5x/3y), vertical scroll with slight leftward drift, momentum",events: [[-5,3], ...vertical(30,-4), ...momentum(-4)],},{name: "variant: same scroll, gesture ends when the fingers lift (no momentum)",events: [[-5,3], ...vertical(30,-4),[0,0]],},{name: "variant: rightward drift with forward history",forward: true,events: [[5,3], ...vertical(30,4), ...momentum(4)],},{name: "variant: diagonal start, then perfectly vertical",events: [[-5,3], ...vertical(30,0), ...momentum(0)],},{name: "swipe whose first events lean diagonally",events: [[-1,-1],[-2,-1],[-3,-1], ...Array.from({length: 15},()=>[-12,0]),[0,0]],},{name: "swipe that leans diagonally for two events",events: [[-5,3],[-2,2], ...Array.from({length: 15},()=>[-10,0]),[0,0]],},{name: "swipe that turns into a vertical scroll",events: [[-10,0], ...vertical(30,-4), ...momentum(-4)],},{name: "swipe 18° off horizontal",events: [...Array.from({length: 15},()=>[-12,4]),[0,0]],},{name: "swipe 34° off horizontal",events: [...Array.from({length: 15},()=>[-12,8]),[0,0]],},{name: "control: clearly horizontal swipe",events: [...Array.from({length: 15},()=>[-12,1]),[0,0]],},];asyncfunctiongo(hash){awaitpage.evaluate((h)=>(location.hash=h),hash);awaitpage.waitForTimeout(700);}for(const[index,scenario]ofscenarios.entries()){awaitgo("#/new");awaitgo("#/pull-requests");if(scenario.forward){awaitpage.goBack();awaitpage.waitForTimeout(700);}// A real trackpad gesture starts with Chromium's gestureScrollBegin/End, which resets the// component's per-gesture state; one tiny CDP wheel does that before the latched sequence.constcdp=awaitpage.context().newCDPSession(page);awaitcdp.send("Input.dispatchMouseEvent",{type: "mouseWheel",x: 700,y: 400,deltaX: 0,deltaY: 1});awaitpage.waitForTimeout(300);constbefore=page.url();consthistory=awaitpage.evaluate(()=>({back: navigation.canGoBack,forward: navigation.canGoForward,}));awaitpage.screenshot({path: `${outDir}/${index}-before.png`});constsums=awaitpage.evaluate(async(events)=>{consttarget=document.elementFromPoint(700,400)??document.body;if(target.closest('[role="dialog"]'))thrownewError("target is inside a dialog");letx=0;lety=0;for(const[deltaX,deltaY]ofevents){x+=deltaX;y+=deltaY;target.dispatchEvent(newWheelEvent("wheel",{ deltaX, deltaY,bubbles: true,composed: true,clientX: 700,clientY: 400}),);awaitnewPromise((resolve)=>setTimeout(resolve,8));}return{x: Math.round(x),y: Math.round(y)};},scenario.events);awaitpage.waitForTimeout(800);constafter=page.url();awaitpage.screenshot({path: `${outDir}/${index}-after.png`});note(`${scenario.name}\n total dx=${sums.x} dy=${sums.y} (${Math.round((Math.atan2(Math.abs(sums.x),Math.abs(sums.y))*180)/Math.PI,)}° off vertical); history ${JSON.stringify(history)}\n ${before} -> ${after}: ${before===after ? "stayed" : "NAVIGATED"}`,);}writeFileSync(`${outDir}/result.txt`,log.join("\n")+"\n");process.exit(0);
SwipeIndicator rendering is unchanged; what changes is how long it stays up. Sampled in the same desktop app, same replay method: a swipe that starts [-10,0] and turns into a vertical scroll (no release event). Indicator circle style after each event:
Event
Before cefb544af3
After 3f9b2380e4
1 [-10,0]
opacity 1, translateX(-95.8333%)
opacity 1, translateX(-95.8333%)
2 [-4,15]
opacity 1, translateX(-94.1667%)
opacity 0, translateX(-95.8333%)
3 [-4,15]
opacity 1, translateX(-92.5%)
opacity 0, translateX(-95.8333%)
4 [-4,15]
opacity 1, translateX(-90.8333%)
opacity 0, translateX(-95.8333%)
5 [-4,15]
opacity 1, translateX(-89.1667%)
opacity 0, translateX(-95.8333%)
6 [-4,15]
opacity 1, translateX(-87.5%)
opacity 0, translateX(-95.8333%)
7 [-4,15]
opacity 1, translateX(-85.8333%)
opacity 0, translateX(-95.8333%)
8 [-4,15]
opacity 1, translateX(-84.1667%)
opacity 0, translateX(-95.8333%)
9 [-4,15]
opacity 1, translateX(-82.5%)
opacity 0, translateX(-95.8333%)
10 [-4,15]
opacity 1, translateX(-80.8333%)
opacity 0, translateX(-95.8333%)
11 [-4,15]
opacity 1, translateX(-79.1667%)
opacity 0, translateX(-95.8333%)
Before, the indicator stays visible and keeps growing through the vertical scroll; after, it hides once the gesture reaches 30px of travel and the route stays.
cannot-verify wayland: not checked with a real libinput touchpad on GNOME Wayland (the reporter's setup); the events above are synthetic.
cannot-verify macos: not checked with a real macOS trackpad and its momentum/gesture bracketing.
Model and harness: Claude Opus 5.5 in Claude Code (via T3 Code) for triage, tests and fix; reviews by GPT-6.1 sol (Codex) and Claude Opus 5.5.
… forward
A vertical scroll that started slightly diagonally could lock to the
horizontal axis on its first 8px, and once tracking, only deltaX counted,
so sideways drift reached the trigger distance and navigated on release.
Horizontal travel now has to exceed twice the vertical travel both when the
axis locks and for the rest of the gesture; when it stops doing so, the
gesture is ignored and the indicator hides without navigating.
Fixes#30160
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's axis lock stays as it was; only the running travel check while
tracking decides whether a gesture is a swipe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Macroscope's review found this PR approvable — The change rejects mostly vertical trackpad gestures instead of treating their sideways drift as history navigation. Its effects are limited to macOS desktop gesture handling, with existing navigation safeguards preserved.
You can add or adjust custom eligibility rules. Learn more.
No actionable comments were generated in the recent review.
Walkthrough
HistorySwipeNavigation now evaluates cumulative horizontal and vertical travel when locking and tracking gestures. It discards gestures that meet the vertical-scroll rule and hides the indicator. Browser tests check navigation outcomes for diagonal wheel-event sequences.
Changes: Swipe gesture tracking
Layer / File(s)
Summary
Track and cancel diagonal gestures apps/web/src/components/HistorySwipeNavigation.tsx, apps/web/src/components/HistorySwipeNavigation.browser.test.tsx
The gesture state records cumulative horizontal and vertical travel. The component ignores gestures when total travel reaches 30 pixels and horizontal travel is no more than twice vertical travel. Browser tests cover vertical-dominant and diagonal sequences.
Mostly vertical trackpad scrolls should no longer trigger back or forward navigation, and deliberate horizontal swipes should still navigate. No concrete merge-blocking risk remains. Real trackpad behavior on Linux and macOS was not tested.
Pre-merge checks: 4
Check name
Status
Explanation
Title check
The title names the main change: mostly vertical trackpad scrolls no longer trigger back or forward navigation. The raw summary and objectives both confirm this is the core fix in HistorySwipeNavigati…
Description check
The description covers the problem, the change, scope, and verification. It has a clear problem statement with reproduction details, an explanation of the 30px and 2:1 ratio fix, and a linked issue (#…
Linked Issues check
Issue #30160 requires that mostly vertical two-finger scrolls not trigger history navigation, and that navigation require a clearly horizontal swipe. The whole-PR diff against the merge base (f313d0a…
Out of Scope Changes check
The whole-PR source diff for apps/web/src/components/HistorySwipeNavigation.tsx contains only the travel-tracking, dominance constants (SWIPE_HORIZONTAL_DOMINANCE, SWIPE_DOMINANCE_DISTANCE), `is…
Full details: Title check
Explanation
The title names the main change: mostly vertical trackpad scrolls no longer trigger back or forward navigation. The raw summary and objectives both confirm this is the core fix in HistorySwipeNavigation. It follows the conventional commit form and is specific enough for a teammate scanning history.
Full details: Description check
Explanation
The description covers the problem, the change, scope, and verification. It has a clear problem statement with reproduction details, an explanation of the 30px and 2:1 ratio fix, and a linked issue (#30160). Verification is detailed with test names, a before/after table, and a replay driver. It names what could not be checked (Wayland, macOS). The template's comment about approval and scope is partly addressed by the link to the issue. The 'Scope and approval' section is brief and does not say whether a maintainer requested the PR. The model and harness line is present. Overall the description is mostly complete.
Full details: Linked Issues check
Explanation
Issue #30160 requires that mostly vertical two-finger scrolls not trigger history navigation, and that navigation require a clearly horizontal swipe. The whole-PR diff against the merge base (f313d0a) shows HistorySwipeNavigation.tsx now: (1) tracks cumulative travelX and travelY on the tracking gesture; (2) defines isVerticalScroll, which returns true when total travel is at least 30px and travelX <= 2 * travelY; (3) applies that check in lockSwipeAxis, so an axis lock cannot occur on a vertical-dominant gesture; (4) on each tracking event, adds both travel values and, when isVerticalScroll is true, sets the gesture to ignored and hides the indicator, so no navigation follows. This matches the issue's suggested fix (lock on clear horizontal intent after about 20–30px, track vertical travel, cancel when vertical movement becomes significant). Gestures below 30px keep the prior behavior, so real diagonal-start swipes can still navigate. The issue's optional trigger-distance and setting changes are not required. The PR's browser tests cover the named cases; their results were not re-run in this assessment. Real libinput and macOS trackpad behavior is device validation, not a coding requirement. Assessment is based on the whole-PR source diff; the incremental diff from the previous reviewed head could not be read, so the current code was assessed directly.
Full details: Out of Scope Changes check
Explanation
The whole-PR source diff for apps/web/src/components/HistorySwipeNavigation.tsx contains only the travel-tracking, dominance constants (SWIPE_HORIZONTAL_DOMINANCE, SWIPE_DOMINANCE_DISTANCE), isVerticalScroll, and axis-lock and tracking-branch changes that serve issue #30160. The PR summary and raw summary report that the only other changed file is HistorySwipeNavigation.browser.test.tsx, which adds tests for the same gesture behavior. No refactor, setting, trigger-distance change, or unrelated product behavior was identified. The PR comment about the automated capture pipeline and the request for Julius describe process, not code changes. The browser test file diff was not read directly in this assessment, relying on the summary for its content.
Finishing Touches
Generate unit tests (beta)
Commit to this branch
Create a new PR
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. View usage-based billing.
A swipe whose first events lean diagonally is no longer dropped before
its direction is clear, and a large diagonal first event cannot lock
and navigate without the dominance check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The automated pipeline stopped on this pull request and will not merge it:
the capture step could not show the problem on the base commit, so there is nothing to prove fixed.
Visit synthetic Thread A, then Thread B; expand the long notes and position the chat at scrollTop 600. Inject deltaX=-9, deltaY=3, followed by 34 packets of deltaX=-4, deltaY=22 at approximately 20 ms intervals, then pause for release. Use Back to establish forward history and repeat with mirrored positive horizontal deltas. Attempt deliberate horizontal navigation with single ±145 px wheel packets. The vertical sequences scrolled from 600 to 1351 without changing threads on either commit.
The automated pipeline stopped on this pull request and will not merge it:
the fix is written and reviewed, but proving it needs something no worker has:
Permanent blocker: A macOS desktop environment with two-finger trackpad input is required and unavailable on this VM. Mostly vertical scrolling in both history directions, indicator dismissal, continued content scrolling, and deliberate horizontal navigation remain unverified.
… forward (pingdotgg#30173)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5cbdf00)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
bugfixFix PR opened by an issue lead; enters the PR looppr:gate-failedA land gate failedsize:S10-29 changed lines (additions + deletions).takeover:blockedTakeover blocked; reason in the PR bodyvouch:trustedAuthor is trusted by repo permissions or the VOUCHED list.
2 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On desktop, a mostly vertical two-finger trackpad scroll can navigate back or forward. If the first ~8px of the scroll lean sideways (e.g. 5px x / 3px y),
HistorySwipeNavigationlocks onto the horizontal axis. After that it ignoresdeltaY, so the small sideways drift of a long vertical scroll adds up to the 120px trigger, and releasing the scroll (or its momentum) navigates. Reproduced on maincefb544af3in the desktop app: a scroll 15° off vertical navigates back, also without momentum, and the mirrored scroll navigates forward.Change
The gesture now counts total horizontal and vertical travel. Once it has moved 30px, horizontal travel must stay more than twice the vertical travel. If it doesn't, the gesture is dropped: the indicator hides, nothing navigates, and the rest of the gesture is ignored as before. The same check applies when the axis locks, so one large diagonal event cannot lock and navigate. Below 30px the direction is still noise, so swipes whose first events lean diagonally still navigate.
Scope and approval
Fixes #30160
Verification
vp test run apps/web/src/components/HistorySwipeNavigation.browser.test.tsx apps/web/src/components/HistorySwipeNavigation.test.ts: 17 passed. New cases:a vertical scroll that starts slightly diagonally keeps the thread: fails on main (navigateHistorycalled with"back"), passes here.a swipe that turns into a vertical scroll keeps the thread: fails on main, passes here.a swipe whose first events lean diagonally navigatesanda swipe that leans diagonally for two events navigates: pass on main and here (guard real swipes with a diagonal start).a single diagonal event past the trigger keeps the thread: fails on main, passes here.vp run check:changed: format, lint, knip, typecheck, tests and React compiler all pass.vp run dev:desktopon Linux/Xvfb, fresh database, back history#/new→#/pull-requests). A driver over CDP replayed each gesture as one latched sequence of wheel events over the main content. CDP mouse-wheel input gets its own Chromium gesture per event, so the driver sends one CDP wheel to fire the gesture begin/end reset and then dispatches the sequence in the page. Same driver on both sides:cefb544af33f9b2380e4Replay driver (not committed)
SwipeIndicatorrendering is unchanged; what changes is how long it stays up. Sampled in the same desktop app, same replay method: a swipe that starts[-10,0]and turns into a vertical scroll (no release event). Indicator circle style after each event:cefb544af33f9b2380e4Before, the indicator stays visible and keeps growing through the vertical scroll; after, it hides once the gesture reaches 30px of travel and the route stays.
cannot-verify wayland: not checked with a real libinput touchpad on GNOME Wayland (the reporter's setup); the events above are synthetic.cannot-verify macos: not checked with a real macOS trackpad and its momentum/gesture bracketing.Model and harness: Claude Opus 5.5 in Claude Code (via T3 Code) for triage, tests and fix; reviews by GPT-6.1 sol (Codex) and Claude Opus 5.5.
🤖 Generated with Claude Code