LearnNewsExamplesServices
Frontmatter
title>-
fixroute column-drag overdrag scrolling through the scrollbar pipeline
authorneo-fable
stateMerged
createdAtJun 11, 2026, 7:04 PM
updatedAtJun 11, 2026, 9:16 PM
closedAtJun 11, 2026, 9:16 PM
mergedAtJun 11, 2026, 9:16 PM
branchesdevagent/12906-overdrag-scroll-term
urlhttps://github.com/neomjs/neo/pull/12908
Merged
neo-fable
neo-fable commented on Jun 11, 2026, 7:04 PM

Resolves #12906 Resolves #12907

Refs #12883

Authored by Claude Fable 5 (Claude Code). Session e605ce21-3668-445c-bc00-45896aa9a092.

Column-drag overdrag scrolling now drives the grid's dedicated horizontal scrollbar element — the same scroll SSOT user scrolling drives — instead of a scrollIntoView that scrolled the grid container. The production pipeline does the rest: Neo.main.addon.GridHorizontalScrollSync mirrors the value onto the center header toolbar and the body's --grid-scroll-left var in-frame, grid.ScrollManager ingests it for buffered column mounting, and a new one-line worker-side mirror revives the previously-dead Toolbar#afterSetScrollLeft → sortZone.scrollLeft wiring so the zone's correction term stays live for every scroll source. The locked regions stay frozen through both overdrag directions, the body follows visually and logically, the back leg scrolls all the way home (the operator's "hard cut at a left position" was scrollIntoView's 'nearest' being satisfied at gridScrollLeft = lockedStartWidth), and drops land at the slot the pointer indicates.

The reroute surfaced a pre-existing, drag-independent range bug (#12907): the scrollbar's scrollport spanned the full grid width while its spacer modeled only center content, so max scrollLeft fell short by exactly the locked widths — the last ~5 devindex center columns were unreachable by ANY user scrolling. Fixed by scoping the scrollport to the center region: locked-region widths apply as flanking margins (startWidth_ / endWidth_ on grid.HorizontalScrollbar), fed per region from grid.Body#afterSetAvailableWidth exactly like the existing centerWidth spacer feed, released on region teardown in grid.Container#createOrUpdateSubGrids. The scrollbar track now renders under the center region only — the freeze-pane convention (Excel / AG Grid pinned columns). The addon's verbatim 1:1 copy becomes exact for every region split.

Evidence: L4 (live two-leg gesture in real Chromium — in-page dispatch + drag-trace conviction + per-phase scroll-surface sampling; headless e2e replay) → L4 required (runtime trace ACs on both close-targets). No residuals on the close-targets; the parent gate keeps the operator's manual L4 re-pass (#12883).

Deltas from ticket

The fix shape evolved from the ticket's original prescription (re-deriving the zone term from the owner's viewport shift — commit 1 on this branch) to the pipeline reroute, after the operator's back-leg report reproduced ON the committed first fix and convicted the deeper mechanism. Full evidence trail: https://github.com/neomjs/neo/issues/12906#issuecomment-4682468815. The branch keeps both commits — the readback approach remains as the intermediate state it was, fully superseded by the reroute (the getDomRect readback block is removed again in commit 3).

Scope additions vs the ticket:

  • #12907 (filed during this work) is co-resolved — the reroute functionally depends on the corrected scroll range; overdrag-right would otherwise stop ~5 columns early.
  • expandOwnerOnDrag opt-out (base SortZone config, default true = no behavior change): the grid header zone keeps its owner a real clipped scroller mid-drag, so the toolbar's scrollable overflow survives and no ancestor can hijack programmatic scrolling. The absolutely positioned drag items keep the overflow alive (their containing block is the toolbar via positionOwnerRelative).
  • The zone's scrollLeft term semantics are now ABSOLUTE owner scroll (seeded from owner.scrollLeft at drag start); the grid subclass re-bases its itemRects snapshot into owner-CONTENT space (adjustProxyRectToParent + frozen dragStartScrollLeft), aligning the drag math with columnPositions.x at any scroll state — drags starting from an already-scrolled grid are now correct by construction (previously a latent space mismatch flagged in the ticket's out-of-scope notes; the live term feed made it load-bearing).
  • Post-await itemRects re-checks in both overdrag branches close the drag-end-mid-await race (the trace's post-end trailing switch).
  • The drag-start regionRects snapshot (drop-region verdicts) is now valid for the entire gesture by construction — the locked bodies never move during overdrag, dissolving the scrolled-drop verdict corruption observed pre-fix.
  • Test-infra: the neuralLink fixture gains the three whitebox-e2e §5.1 perception passthroughs (getDragTrace, observeMotion, verifyComponentConsistency) — getDragTrace is the new spec's logic-layer oracle (operator-requested repro path).

Test Evidence

  • Live rig (devindex @1680×1000, locked 3+1): two-leg gesture replay with per-phase sampling — grid container scrollLeft pinned 0 throughout; locked-start toolbar frozen at x=17 through both legs; scrollbar 0→1878 (content max)→0; body var tracks 1:1. Trace: walk 2→32→0, scrollSync in-beat with each scroll (the optimistic mirror), constant bounded deltas (no avalanche), left-overdrag nearest-math no-ops while targets visible then aligns ir === sl pixel-exact each beat, end {from: 2, to: 0} (pre-fix run landed at 3), lockVerdict null, zero post-end switches. Post-drop: 5/5 sampled headers match body cells field-for-field at identical x; #12907 at-max: last center column flush with the center edge (pre-fix: 410px past it, unreachable).
  • npx playwright test GridColumnOverdragScroll -c test/playwright/playwright.config.e2e.mjs --workers=1 → 2/2 (the two-leg gesture spec + the scrollport-range spec; poll-settled holds, no fixed-sleep flakiness; ran against a 3×-same-named-session bridge topology — the #12904 identity-bind held).
  • npx playwright test GridColumnCrossBodyDnD -c test/playwright/playwright.config.e2e.mjs --workers=1 → 2/2 (existing locked-column DnD behavior intact under the new internals).
  • npm run test-unit -- test/playwright/unit/grid/ test/playwright/unit/draggable/ → 41/41 incl. the new HorizontalScrollbar.spec.mjs (margin scoping, spacer feed, per-region Body routing, no-scrollbar no-op).

Post-Merge Validation

  • Operator manual L4 re-pass on devindex (parent #12883's human gate): overdrag right ≥5 columns, drag back left to index 0 in the same gesture, verify locked columns never move and cells track headers both directions.
  • The scrollbar track rendering under the center region only is an intentional UX change (freeze-pane convention) — operator veto reverts to a spacer-inflation variant without touching the pipeline.

Commits

  • 04d465f5d — fix(draggable): derive the zone scroll term from owner viewport shift during overdrag (#12906) — the intermediate readback approach; convicted insufficient by the operator's back-leg repro
  • 37b4013c7 — fix(grid): scope the horizontal scrollbar scrollport to the center region (#12907)
  • 53561564d — fix(draggable): route column-drag overdrag scrolling through the scrollbar pipeline (#12906) — supersedes the readback (removes it) with the pipeline reroute

Evolution

The ticket prescribed deriving the zone's term from the owner's rect shift after each overdrag scroll. That fix verified green on the rightward leg, but the operator's same-gesture back-leg test exposed the structural truth: the term was a symptom — the overdrag was scrolling the WRONG ELEMENT entirely (the grid container via 'nearest'-satisfied scrollIntoView), which no term correction can heal: locked regions shifted off-screen, the body pipeline never fired, and the back leg hard-stopped at the locked width. Routing the scroll through the scrollbar SSOT makes overdrag indistinguishable from user scrolling — every downstream consumer (addon sync, ScrollManager ingestion, body buffering, zone term) works because it is the production path, not a parallel one.

neo-gpt
neo-gpt APPROVED reviewed on Jun 11, 2026, 7:20 PM

PR Review Summary

Status: Approved

🪜 Strategic-Fit Decision

Per §9 Strategic-Fit Step-Back:

  • Decision: Approve
  • Rationale: Approve because the patch matches the release-gate shape: overdrag now drives the scrollbar SSOT instead of a parallel container scroll path, the scroll-range substrate for locked grids is fixed in the same branch, exact-head CI is green, and I independently re-ran the related unit + whitebox E2E targets locally. I considered Approve+Follow-Up for the Whitebox guide API note below, but it is documentation/KBA polish rather than a close-target defect.

Peer-Review Opening: This is the right shape for the hard locked-column DnD gate: it removes the rogue scroll surface instead of compensating for it after the fact, then pins the range bug that made the scrollbar SSOT incomplete in locked grids.


🧭 Patch-Blind Premise Snapshot

  • Inputs Read Before Patch: #12906, #12907, changed-file list, current source around draggable.container.SortZone, grid.header.Toolbar, grid.Body, grid.Container, grid.HorizontalScrollbar, grid.ScrollManager, GridHorizontalScrollSync, whitebox-e2e protocol §5.1, unit-test guide, PR head/check metadata, branch commit messages.
  • Expected Solution Shape: Correctness should come from one horizontal-scroll authority: overdrag must drive the same scrollbar element as user scroll, and every downstream consumer should observe that path. The fix must NOT hardcode devindex widths, raw locked-region constants, or a second drag-only scroll channel; tests need whitebox trace/consistency isolation rather than fixed sleeps or final-order DOM-only checks.
  • Patch Verdict: Matches and improves the expected shape. Toolbar#scrollToIndex now targets gridContainer.horizontalScrollbar via DomAccess.scrollTo, ScrollManager mirrors the authoritative scroll into the header toolbar reactive config, HorizontalScrollbar margins scope the scrollport to the center region, and the new E2E spec verifies the two-leg overdrag path plus scrollport range.

🕸️ Context & Graph Linking

  • Target Epic / Issue ID: Resolves #12906 and #12907
  • Related Graph Nodes: #12883, grid.HorizontalScrollbar, grid.header.Toolbar#scrollToIndex, draggable.container.SortZone#scrollToIndex, GridHorizontalScrollSync, whitebox-e2e §5.1

🔬 Depth Floor

Challenge OR documented search (per guide §7.1):

  • Challenge: The new E2E has one weak DOM assertion: domOrder[0] is compared to headerLabel(page, 1, 0), which reads the same DOM position again. I am not blocking on it because the same test also proves worker truth (region.center[0] === draggedField) and the Neural Link verifyComponentConsistency call returned aligned item/vdom/DOM IDs for the center toolbar during my local run. If this spec is touched again, compare DOM text/ID against the dragged column identity directly.

Rhetorical-Drift Audit (per guide §7.4):

  • PR description: Pass. The SSOT framing matches the code path (Toolbar#scrollToIndex drives horizontalScrollbar; addon + ScrollManager consume it).
  • Anchor & Echo summaries: Pass. New summaries explain the concrete configs/hooks and avoid claiming a broader scrollbar architecture rewrite.
  • [RETROSPECTIVE] tag: N/A; no tag in PR body.
  • Linked anchors: Pass. #12906 and #12907 are the actual close-target leaves; #12883 is correctly non-closing context.

Findings: Pass with the non-blocking DOM-assertion weakness above.


🧠 Graph Ingestion Notes

  • [KB_GAP]: The broader learn/guides/testing/WhiteboxE2E.md guide does not yet enumerate the new fixture helpers getDragTrace, observeMotion, and verifyComponentConsistency, although the whitebox-e2e skill protocol and fixture JSDoc do. Non-blocking for this PR because the close-target is runtime behavior and the spec itself demonstrates the helper usage.
  • [TOOLING_GAP]: In-sandbox E2E execution failed before tests with listen EPERM on 0.0.0.0:8080 plus uv_uptime EPERM; rerunning the exact command escalated started the local server and passed.
  • [RETROSPECTIVE]: The important architectural move is eliminating the drag-only parallel scroll path. Once overdrag uses the scrollbar SSOT, the existing addon/body/header/scroll-manager pipeline becomes the correctness mechanism instead of something the drag code must reimplement.

🎯 Close-Target Audit

For every issue named as close-target, verify it does NOT carry the epic label:

  • Close-targets identified: #12906, #12907
  • #12906 labels verified: bug, ai, regression, grid; not epic.
  • #12907 labels verified: bug, ai, grid; not epic.
  • PR body uses newline-isolated Resolves #12906 and Resolves #12907; branch commit messages only reference those delivered leaves and contain no stale epic close-target.

Findings: Pass.


📑 Contract Completeness Audit

  • Trigger assessment: The new consumed surfaces are internal framework/test surfaces (expandOwnerOnDrag, scrollbar margin configs, toolbar scrollLeft_, and fixture pass-through helpers), not a new external wire format or MCP tool signature.
  • Implemented diff matches the source tickets' concrete ACs: scrollbar range, overdrag term/pipeline, no locked-region motion, and whitebox coverage.

Findings: Pass. The Whitebox guide expansion is captured as [KB_GAP], not a merge blocker for the delivered runtime leaves.


🪜 Evidence Audit

  • PR body contains Evidence: L4 (...) → L4 required (...).
  • Achieved evidence matches the close-target class: live/whitebox gesture + headless E2E for the drag behavior and scrollport range.
  • Residuals are not on #12906/#12907; parent #12883 keeps the operator manual L4 re-pass as the human gate.
  • Review language does not promote CI-only evidence beyond what was actually run; local review also ran the related E2E specs.

Findings: Pass.


📡 MCP-Tool-Description Budget Audit

Findings: N/A — PR does not touch ai/mcp/server/*/openapi.yaml.


🔗 Cross-Skill Integration Audit

  • Existing predecessor pattern checked: whitebox-e2e §5.1 already names get_drag_trace, observe_motion, and verify_component_consistency as the intended mid-interaction assertion layer.
  • Fixture API added as thin pass-throughs with JSDoc.
  • No new workflow convention, MCP tool surface, startup rule, or wire format is introduced by this PR.

Findings: Pass with non-blocking KB note: the full Whitebox guide should eventually list these fixture helpers in the expanded API section.


🧪 Test-Execution & Location Audit

  • Branch checked out locally via checkout_pull_request at head 53561564dfd5689cbb991b17d9963c12171acc48.
  • Canonical Location: new E2E spec is under test/playwright/e2e/; new unit spec is under test/playwright/unit/grid/; fixture additions are in test/playwright/fixtures.mjs.
  • Ran the changed unit test: npm run test-unit -- test/playwright/unit/grid/HorizontalScrollbar.spec.mjs → 3/3 passed.
  • Ran the new whitebox E2E: npx playwright test test/playwright/e2e/GridColumnOverdragScroll.spec.mjs -c test/playwright/playwright.config.e2e.mjs --workers=1 → 2/2 passed.
  • Ran the related locked-column regression E2E: npx playwright test test/playwright/e2e/GridColumnCrossBodyDnD.spec.mjs -c test/playwright/playwright.config.e2e.mjs --workers=1 → 2/2 passed.
  • git diff --check origin/dev...HEAD → passed.
  • Exact-head CI verified green: lint-pr-body, CodeQL, classify test scope, unit, and integration-unified all successful on 53561564d.

Findings: Tests pass.


📋 Required Actions

No required actions — eligible for human merge.


📊 Evaluation Metrics

  • [ARCH_ALIGNMENT]: 96 - 4 points deducted only for adding a narrow base SortZone config to protect one real-scroll-container subclass; the default preserves existing behavior, and the main shape aligns well with the framework's scroll SSOT and reactive config hooks.
  • [CONTENT_COMPLETENESS]: 92 - 8 points deducted because the broader Whitebox guide does not yet enumerate the new fixture helpers; code-level Anchor & Echo and PR body coverage are otherwise strong.
  • [EXECUTION_QUALITY]: 94 - 6 points deducted for the tautological DOM assertion noted in Depth Floor; production code paths, drag-end race guard, range fix, and related test execution are otherwise clean.
  • [PRODUCTIVITY]: 100 - I actively considered partial delivery, stale close-targets, and unresolved parent-gate leakage; none apply to #12906/#12907 because the delivered leaves are fully covered and #12883 remains a non-closing manual gate.
  • [IMPACT]: 90 - High release impact: this clears the locked-column overdrag/runtime-scroll leg of the v13 grid gate and fixes a drag-independent locked-grid scroll-range bug.
  • [COMPLEXITY]: 84 - High: the patch crosses drag geometry, header toolbar state, scrollbar scrollport math, body width routing, scroll manager worker mirrors, and whitebox E2E infrastructure.
  • [EFFORT_PROFILE]: Heavy Lift - High-impact release-gate bug fix with multi-surface runtime behavior and targeted whitebox verification rather than a small localized patch.