Repository navigation
Conversation
…lved scrollable A directional scroll centred its swipe on the window, so on an inset scrollable the drag could start on a header above the list and the command failed with scroll_no_progress. The daemon now resolves the scroll container from the current stored tree before the gesture and hands its rect to the owner. The shared planner keeps both endpoints inside the container's intersection with the padded frame: a span that fits is unchanged, one that leaves the band moves into it, and one longer than the band shrinks to it and reports `requestedPixels` beside the dispatched `pixels`. Android applies it after the keyboard clip, so the two compose. iOS plans in the runner and follows separately. Refs callstack#2714
|
Live evidence: Pixel_7_API_36, Android Settings. The list is inset at y=480 under the search bar. Steps: Comparison strip, left to right: main before, main after (no movement,
main (0149438): base-scroll-up-0.8.mp4branch: head-scroll-up-0.8.mp4 |
There was a problem hiding this comment.
5 issues found across 8 files
You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/daemon/src/execution/scroll-movement.ts">
<violation number="1" location="packages/daemon/src/execution/scroll-movement.ts:194">
P3: `readScrollTargetContainer` is gated only by `isMobilePlatform`, which includes iOS. iOS does not clamp yet, so this computes an edge analysis of the whole tree on every iOS directional scroll and discards the result. Gate it to Android so the iOS path stays as it was.</violation>
</file>
<file name="packages/contracts/src/scroll-gesture.ts">
<violation number="1" location="packages/contracts/src/scroll-gesture.ts:157">
P3: For odd travel distances, a shortened swipe can report `pixels` equal to `requestedPixels`, which contradicts the documented contract that `pixels` is smaller when shortened. Compare against the unclamped lane length, or derive `requestedPixels` from it.</violation>
<violation number="2" location="packages/contracts/src/scroll-gesture.ts:182">
P2: When the container does not intersect the padded band, this returns the unclamped lane outside the container, so the swipe misses the target scrollable. Either surface that case to the caller or update the JSDoc, which currently promises the endpoints stay inside the intersection.</violation>
<violation number="3" location="packages/contracts/src/scroll-gesture.ts:186">
P2: The cross-axis fallback can place a container-clamped swipe outside the 10% safe bounds. Intersect the container’s cross-axis range with the corresponding padded frame range before choosing its center (or handle an empty intersection explicitly).</violation>
</file>
<file name="packages/contracts/src/scroll-gesture-container.test.ts">
<violation number="1" location="packages/contracts/src/scroll-gesture-container.test.ts:45">
P3: No test covers a left or right swipe that is actually shortened or moved by a container. The `['up', 'down', 'left', 'right']` case only checks an already-fitting container, so the horizontal axis mapping is unverified. Add a horizontal case with a narrow container, such as `{ x: 100, y: 0, width: 200, height: 874 }`, asserting the x endpoints and `pixels`/`requestedPixels`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
| const containerSize = vertical ? container.height : container.width; | ||
| const bandMin = Math.max(padded.min, Math.round(containerMin)); | ||
| const bandMax = Math.min(padded.max, Math.round(containerMin + containerSize)); | ||
| if (bandMax <= bandMin) return lane; |
There was a problem hiding this comment.
P2: When the container does not intersect the padded band, this returns the unclamped lane outside the container, so the swipe misses the target scrollable. Either surface that case to the caller or update the JSDoc, which currently promises the endpoints stay inside the intersection.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/contracts/src/scroll-gesture.ts, line 182:
<comment>When the container does not intersect the padded band, this returns the unclamped lane outside the container, so the swipe misses the target scrollable. Either surface that case to the caller or update the JSDoc, which currently promises the endpoints stay inside the intersection.</comment>
<file context>
@@ -125,16 +134,65 @@ export function buildScrollGesturePlan(options: ScrollGestureOptions): ScrollGes
+ const containerSize = vertical ? container.height : container.width;
+ const bandMin = Math.max(padded.min, Math.round(containerMin));
+ const bandMax = Math.min(padded.max, Math.round(containerMin + containerSize));
+ if (bandMax <= bandMin) return lane;
+
+ const crossMin = vertical ? container.x : container.y;
</file context>
|
|
||
| const crossMin = vertical ? container.x : container.y; | ||
| const crossSize = vertical ? container.width : container.height; | ||
| const cross = |
There was a problem hiding this comment.
P2: The cross-axis fallback can place a container-clamped swipe outside the 10% safe bounds. Intersect the container’s cross-axis range with the corresponding padded frame range before choosing its center (or handle an empty intersection explicitly).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/contracts/src/scroll-gesture.ts, line 186:
<comment>The cross-axis fallback can place a container-clamped swipe outside the 10% safe bounds. Intersect the container’s cross-axis range with the corresponding padded frame range before choosing its center (or handle an empty intersection explicitly).</comment>
<file context>
@@ -125,16 +134,65 @@ export function buildScrollGesturePlan(options: ScrollGestureOptions): ScrollGes
+
+ const crossMin = vertical ? container.x : container.y;
+ const crossSize = vertical ? container.width : container.height;
+ const cross =
+ lane.cross >= crossMin && lane.cross <= crossMin + crossSize
+ ? lane.cross
</file context>
| session: SessionState, | ||
| direction: ScrollDirection, | ||
| ): Promise<Rect | undefined> { | ||
| if (!isMobilePlatform(device)) return undefined; |
There was a problem hiding this comment.
P3: readScrollTargetContainer is gated only by isMobilePlatform, which includes iOS. iOS does not clamp yet, so this computes an edge analysis of the whole tree on every iOS directional scroll and discards the result. Gate it to Android so the iOS path stays as it was.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon/src/execution/scroll-movement.ts, line 194:
<comment>`readScrollTargetContainer` is gated only by `isMobilePlatform`, which includes iOS. iOS does not clamp yet, so this computes an edge analysis of the whole tree on every iOS directional scroll and discards the result. Gate it to Android so the iOS path stays as it was.</comment>
<file context>
@@ -164,17 +164,40 @@ export function readScrollSurfaceBaseline(
+ session: SessionState,
+ direction: ScrollDirection,
+): Promise<Rect | undefined> {
+ if (!isMobilePlatform(device)) return undefined;
+ const edge = verticalEdgeFor(direction);
+ const nodes = session.snapshot?.nodes;
</file context>
| if (!isMobilePlatform(device)) return undefined; | |
| if (device.platform !== 'android') return undefined; |
| vertical ? lane.axisEnd : lane.cross, | ||
| ); | ||
| const placed = shortened | ||
| ? { ...plan, pixels: lane.axisEnd - lane.axisStart, requestedPixels: travelPixels } |
There was a problem hiding this comment.
P3: For odd travel distances, a shortened swipe can report pixels equal to requestedPixels, which contradicts the documented contract that pixels is smaller when shortened. Compare against the unclamped lane length, or derive requestedPixels from it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/contracts/src/scroll-gesture.ts, line 157:
<comment>For odd travel distances, a shortened swipe can report `pixels` equal to `requestedPixels`, which contradicts the documented contract that `pixels` is smaller when shortened. Compare against the unclamped lane length, or derive `requestedPixels` from it.</comment>
<file context>
@@ -125,16 +134,65 @@ export function buildScrollGesturePlan(options: ScrollGestureOptions): ScrollGes
+ vertical ? lane.axisEnd : lane.cross,
+ );
+ const placed = shortened
+ ? { ...plan, pixels: lane.axisEnd - lane.axisStart, requestedPixels: travelPixels }
+ : plan;
+ // `up`/`left` drag from the low end toward the high end; `down`/`right` drag the other way.
</file context>
| }, | ||
| ); | ||
|
|
||
| test.each<ScrollDirection>(['up', 'down', 'left', 'right'])( |
There was a problem hiding this comment.
P3: No test covers a left or right swipe that is actually shortened or moved by a container. The ['up', 'down', 'left', 'right'] case only checks an already-fitting container, so the horizontal axis mapping is unverified. Add a horizontal case with a narrow container, such as { x: 100, y: 0, width: 200, height: 874 }, asserting the x endpoints and pixels/requestedPixels.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/contracts/src/scroll-gesture-container.test.ts, line 45:
<comment>No test covers a left or right swipe that is actually shortened or moved by a container. The `['up', 'down', 'left', 'right']` case only checks an already-fitting container, so the horizontal axis mapping is unverified. Add a horizontal case with a narrow container, such as `{ x: 100, y: 0, width: 200, height: 874 }`, asserting the x endpoints and `pixels`/`requestedPixels`.</comment>
<file context>
@@ -0,0 +1,94 @@
+ },
+);
+
+test.each<ScrollDirection>(['up', 'down', 'left', 'right'])(
+ 'a %s swipe already inside its container keeps today’s endpoints and distance',
+ (direction) => {
</file context>

Summary
Clamps Android directional
scrollendpoints to the resolved scrollable (#2714), following the rule agreed with #1821: endpoints stay inside the intersection of the container and the safe bounds (still the 10% padding until #1821), and a shortened swipe reportspixels(dispatched) besiderequestedPixels.buildScrollGesturePlantakes an optionalcontainer. A span that fits is unchanged.8 files. The planner vectors are TS-only, because the Swift twin doesn't clamp yet.
Not fixed yet: iOS, including the Oct 7 keyboard-clip repro. I'd like that as a stacked follow-up (Swift port, wire field, parity vectors). @thymikee, does that split work for you?
Validation
check:affected --run --head 95ba7e823(exactCommit): format, lint, typecheck, layering, fallow and build pass. vitest-related has 856/857 files passing. The one failure is anew URL(import.meta.url).pathnametest that breaks on this host's space-containing path, and it fails identically on main. Run directly after it: unit has 14,565 passing and 6 failures that also fail on main; provider-integration andeager-closure-budgetspass.Live on Pixel_7_API_36 (tree on 0149438, before rebasing over #3736's top/bottom change). Settings list inset at y=480,
scroll up 0.8:scroll_no_progresspixels2328,requestedPixels2496,movedScreenshots and recordings are in the first comment.