Skip to content

spec: prevent overshoot scrolling code diff to top (#9808)#10228

Open
lonexreb wants to merge 5 commits intowarpdotdev:masterfrom
lonexreb:spec/9808-diff-scroll-overshoot
Open

spec: prevent overshoot scrolling code diff to top (#9808)#10228
lonexreb wants to merge 5 commits intowarpdotdev:masterfrom
lonexreb:spec/9808-diff-scroll-overshoot

Conversation

@lonexreb
Copy link
Copy Markdown
Contributor

@lonexreb lonexreb commented May 6, 2026

Spec for #9808. Clamp in-progress scroll gestures to the diff container; only new gestures (after a 200ms settling window) bubble up to the parent blocklist. Applies to all blocklist-embedded scroll containers, not just diff.

Closes (spec-only) #9808.

@cla-bot cla-bot Bot added the cla-signed label May 6, 2026
@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label May 6, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 6, 2026

@lonexreb

I'm starting a first review of this spec-only pull request.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This spec defines a gesture-window clamp for nested scroll containers to prevent in-progress overscroll from moving the parent blocklist.

Concerns

  • The behavior contract does not define ownership after a new gesture starts at an inner-scroll boundary, so implementers could bubble only the first delta and then consume the rest of the same gesture.
  • The scope expands from code diff to all blocklist-embedded scroll containers, but acceptance criteria, implementation pointers, and tests remain diff-only.
  • The scrollbar click acceptance criterion should identify which scrollbar target bypasses the gesture clamp.

Verdict

Found: 0 critical, 2 important, 1 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH9808/SPEC.md Outdated
scroll-event-stream timing: events within ≤200ms of each other
belong to the same gesture (matches macOS/Windows trackpad
inertia conventions).
- B3. After a 200ms gap, the next scroll arriving at the diff
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] B3 conflicts with B1 for a new gesture that begins while the inner container is already at its boundary: after the first delta bubbles, specify whether the remaining deltas in that same ≤200ms gesture keep bubbling to the parent or are consumed by the child.

Comment thread specs/GH9808/SPEC.md Outdated
- B4. Same behavior at top AND bottom edges.
- B5. Click-to-jump (clicking the scrollbar) bypasses this rule
— that's a discrete event, not part of a gesture.
- B6. The new behavior applies to **all** scroll containers
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] B6 expands the requirement to every blocklist-embedded scroll container, but the acceptance criteria, implementation pointers, and tests only cover diffs; either limit the scope to diffs or add explicit coverage for code review snippets and embedded markdown plus the shared hook that will enforce it.

Comment thread specs/GH9808/SPEC.md Outdated
top remains visible.
- A2. Stop scrolling for >200ms, scroll up again — blocklist
scrolls normally.
- A3. Click the scrollbar above the diff to jump up — works as
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [SUGGESTION] Clarify whether this means the inner diff scrollbar, the parent blocklist scrollbar, or both, because the bypass behavior depends on which scrollable receives the click.

Co-Authored-By: Warp <agent@warp.dev>
@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 7, 2026

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 7, 2026

@lonexreb

I'm re-reviewing this spec-only pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This spec defines gesture ownership for wheel/trackpad scrolling inside embedded code-diff containers, including boundary clamping, the 200ms gesture window, acceptance criteria, and tests.

Concerns

  • The proposed V1 scope conflicts with the PR description: the description says the behavior applies to all blocklist-embedded scroll containers, while the spec limits the behavior to code-diff containers and excludes other embedded containers. Resolve the scope before implementation so reviewers know which surfaces must be changed and tested.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH9808/SPEC.md Outdated
Clicking or dragging the parent blocklist scrollbar affects only
the parent blocklist scroll position. Neither scrollbar click
changes gesture ownership for later wheel/trackpad events.
- B6. V1 applies only to code-diff scroll containers embedded in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] The PR description says this applies to all blocklist-embedded scroll containers, but this limits V1 to code-diff containers and excludes other embedded containers; align the scope or explain why the broader surfaces are deferred.

@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 8, 2026

Round-1 fixes for oz-for-oss review: (1) added Gesture Ownership at Boundaries subsection — boundary-state at gesture start is sole binding decision point; no first-delta bubble then consume; (2) reconciled V1 scope to ALL blocklist-embedded scrollable containers (diff, tree, table, log) with enumerated component identifiers and acceptance/tests covering ≥2 container types; (3) defined scrollbar bypass precisely via role="scrollbar" and ::-webkit-scrollbar* selectors with explicit click vs wheel handling.

@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 8, 2026

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 8, 2026

@lonexreb

I'm re-reviewing this spec-only pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This spec defines a gesture-ownership model to keep in-progress wheel/trackpad scrolling inside blocklist-embedded scroll containers until a 200ms idle gap, with scrollbar pointer actions bypassing the clamp.

Concerns

  • The V1 scope says all blocklist-embedded scrollable containers are covered, but the behavior and validation only define vertical top/bottom scrolling and representative two-container coverage.
  • The scrollbar bypass contract relies on ARIA roles and CSS pseudo-elements, which do not match Warp's custom-rendered scrollbar hit-testing model.
  • The implementation pointers promise concrete identifiers but still leave implementers to discover the actual component wrappers.

Verdict

Found: 0 critical, 2 important, 1 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH9808/SPEC.md

## Scope (V1)

V1 applies to ALL blocklist-embedded scrollable containers, not
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] V1 claims all blocklist-embedded scrollable containers, but the contract and tests only define vertical top/bottom behavior and require two representative container types. Specify horizontal/dual-axis ownership semantics and either require coverage for each in-scope container class or narrow the scope.

Comment thread specs/GH9808/SPEC.md Outdated
- Wheel/trackpad input that happens to occur over the scrollbar
area still respects the gesture-window contract above; only
actual click/drag pointer input bypasses.
- B5a. **"Scrollbar target" defined.** A scrollbar target is any
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] B5a defines scrollbar targets using ARIA and CSS pseudo-elements, but Warp's scrollbars are custom-rendered Rust elements with track/thumb hit-test bounds. Rewrite this around the scrollable component's rendered scrollbar bounds so implementers can actually detect pointer bypass.

Comment thread specs/GH9808/SPEC.md Outdated

- A "blocklist-embedded scrollable container" is any descendant
scroll container of the blocklist whose root element matches
one of the following identifiers (extend this list as
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 [SUGGESTION] Replace the placeholder discovery guidance with the actual component or wrapper names for each in-scope container; otherwise different implementers can opt in different subsets despite the all-containers scope.

@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 8, 2026

Round-2 fixes: (1) generalized clamp to both vertical AND horizontal axes — per-axis boundary check, axis-lock for diagonal gestures (>=70% threshold then first-boundary-collision tiebreaker for ambiguous diagonals); (2) replaced ARIA/CSS-pseudo scrollbar contract with Warp's actual scrollbar::hit_test() -> ScrollbarTarget API in crates/warpui_core/src/elements/shared_scrollbar.rs — Track/Thumb click bypasses clamp, wheel over same area still respects clamp; (3) Implementation Pointers now enumerate every blocklist-embedded scroll container found in the codebase with file path and component identifier (code_diff_view.rs, diff_viewer.rs, file_tree/view.rs, gfm_table.rs, block_list_viewport.rs); tests cover each container type plus horizontal/diagonal axis cases (T_diff_clamp, T_filetree_clamp, T_table_clamp, T_log_clamp, T_horizontal_axis_clamp, T_diagonal_axis_lock, T_wheel_over_scrollbar).

@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 8, 2026

/oz-review

@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 8, 2026

@lonexreb

I'm re-reviewing this spec-only pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This spec defines gesture ownership for blocklist-embedded scroll containers and includes acceptance criteria, implementation pointers, and a test plan for wheel/trackpad and scrollbar behavior.

Concerns

  • Ambiguous diagonal gestures can still resolve ownership after a mid-gesture boundary collision, which conflicts with the binding rule that ownership is decided only at gesture start.
  • The 200ms gesture boundary is described inconsistently as <=200ms, >=200ms, and >200ms across the behavior contract, acceptance criteria, and implementation notes.
  • Some implementation pointers need correction before implementation: standalone diff scope is inconsistent with blocklist-only scope, the scrollbar hit-test API is referenced as existing but is not present in the current shared scrollbar module, and the suggested owner enum cannot represent mixed per-axis ownership.

Verdict

Found: 0 critical, 5 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH9808/SPEC.md Outdated
first boundary collision**: the first axis whose boundary the
inner container is already at decides ownership for that axis;
if neither is at a boundary, the gesture is provisionally
inner-owned on both axes and the lock resolves to whichever
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Resolving the lock when the inner container hits a boundary mid-gesture conflicts with B-OWN's rule that boundary state at gesture start is the sole binding decision point; define ambiguous diagonal ownership entirely at gesture start or add an explicit exception.

Comment thread specs/GH9808/SPEC.md Outdated

- B-OWN. **Boundary state at gesture start is the sole binding
decision point.** When a new wheel/trackpad gesture starts (the
first event after ≥200ms of input idleness), ownership is
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] The threshold is inconsistent: B2 keeps events within ≤200ms in the same gesture, A2/T2 use a >200ms gap, but this line says ≥200ms of idleness starts a new gesture; choose one boundary condition and use it everywhere.

Comment thread specs/GH9808/SPEC.md Outdated
| Container type | File path | Component identifier |
| ----------------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
| Code diff (inline action) | `app/src/ai/blocklist/inline_action/code_diff_view.rs` | The diff view's outer scrollable wrapper (constructed via the shared scrollable element from `warpui_core`) |
| Code diff (standalone) | `app/src/code/diff_viewer.rs` | `diff_viewer` scrollable region |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Code diff (standalone) conflicts with the stated blocklist-embedded-only scope and the out-of-scope exclusion for top-level app panels; either limit this row to embedded/inline display modes or explain why the standalone full-pane diff is included in V1.

Comment thread specs/GH9808/SPEC.md Outdated

The 200ms gesture-window state is implementable as
`last_scroll_time: Option<Instant>` plus a current gesture
owner (`InnerVertical`, `InnerHorizontal`, `Parent`, or `None`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This owner enum cannot represent the specified mixed-axis cases where the parent owns the primary axis while the inner container still consumes the non-primary axis; specify per-axis owner state instead of a single Parent/InnerVertical/InnerHorizontal value.

Comment thread specs/GH9808/SPEC.md
`last_scroll_time` is treated as a new gesture and re-evaluates
ownership from boundary state per B-OWN.

Scrollbar pointer hit-testing resolves via the
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] The spec refers to a ScrollbarTarget hit-test API as the routing mechanism, but the current shared_scrollbar.rs exposes geometry helpers without that API; state that implementation must add this API or point to the existing hit-test path to avoid an unimplementable pointer.

@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 8, 2026

Round-3 review fixes (commit e8be75e):

  • Diagonal ambiguous-gesture binding-rule fix: ambiguous diagonals now resolve the primary axis at gesture start (not by mid-gesture boundary collision). New deterministic priority: (1) at-boundary axis if exactly one, (2) vertical tie-break if both, (3) vertical default if neither. No mid-gesture ownership transfer.
  • 200ms boundary canonicalized: B2 now defines a single rule — same gesture iff gap < 200ms; gap >= 200ms ends the gesture. All references (B3, B-OWN, A2) updated; the earlier <=200ms / >=200ms / >200ms mix is replaced by the single canonical rule.
  • Standalone diff scope: app/src/code/diff_viewer.rs is rendered in the Code panel, not the blocklist viewport — explicitly removed from the impl-pointer table and called out in Scope and Out of scope as out of scope for V1.
  • Scrollbar hit-test API: verified shared_scrollbar.rs does NOT expose ScrollbarTarget / hit_test() today (only ScrollbarGeometry { track_bounds, thumb_bounds, .. }, compute_scrollbar_geometry, scroll_delta_for_pointer_movement). Spec now declares the helper as NEW in V1 (pure function over the existing geometry fields, no new state) with reference pseudocode.
  • Per-axis ownership representation: replaced the flat InnerVertical | InnerHorizontal | Parent | None enum with a GestureOwnership { vertical: AxisOwner, horizontal: AxisOwner, last_event: Option<Instant> } struct so "primary parent-owned, non-primary inner-owned" is representable. T_diagonal_axis_lock updated to cover the new ambiguous sub-cases.

@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 8, 2026

/oz-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant