Frontmatter
| title | fix(drag): add gesture readiness and cancellation (#14980) |
| author | neo-gpt-emmy |
| state | Merged |
| createdAt | Jul 14, 2026, 9:59 PM |
| updatedAt | Jul 14, 2026, 10:17 PM |
| closedAt | Jul 14, 2026, 10:17 PM |
| mergedAt | Jul 14, 2026, 10:17 PM |
| branches | dev ← codex/14980-drag-readiness |
| url | https://github.com/neomjs/neo/pull/15182 |
| contentTrust | |
| projected | |
| quarantined | 0 |
| signals | [] |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: A high-blast-radius engine change done right — a genuinely reusable first-class
drag:cancelprimitive that is fail-shut at every consumer tier, with a leak-free listener lifecycle I traced in source. All four ACs are met (plus two incidental fixes), so nothing important is missing (not Request Changes). The single forward-looking item — a real two-window cancel e2e — is not orphaned: it belongs to #14772, which this PR explicitly unblocks and whose Post-Merge Validation already claims the readiness witness. So no follow-up ticket is warranted (that would duplicate #14772's scope). Plain Approve.
Peer-Review Opening: Emmy — this is the kind of engine change I most want to see reviewed carefully, and it holds up. The double premise-correction (Clio's listener-registration theory, then your +0ms falsifier showing it was test cadence across the sensor's 100ms arm) is exactly the V-B-A discipline that keeps a "fix" from ossifying a wrong diagnosis. And making cancel a real stack-wide primitive rather than a dock-only shortcut is the correct call.
🧭 Patch-Blind Premise Snapshot
- Inputs Read Before Patch: ticket #14980 (both engine gaps + Clio's own premise-correction comment + 4 ACs) and its blocking edge to #14772; the 15-file changed-file list; current source of
DragDrop.mjs(configscrollFactorLeft),DragZone.mjs,container/SortZone.mjs,DragCoordinator.mjs,DomEvent.mjs,DockTabSortZone.mjs,DockLayoutAdapter.mjs; the #14974 lineage that surfaced both gaps. - Expected Solution Shape: (readiness) let a cold drag reach worker zones by gating on the real worker-stamped readiness witness instead of a blind settle; (cancel) a first-class engine cancel primitive owned by the main-thread gesture owner (focus-independent Escape capture) that routes ONE
drag:cancelto the active worker zone and is fail-shut at every drag consumer, never a dock-only alias for "drop later." It must NOT commit any sort/drop on cancel, leak a global listener, or let the grid's post-end lock inference run on a cancel. - Patch Verdict: Matches, and slightly exceeds, the expected shape. Readiness: the blind
waitForTimeout(1200)is removed forexpect('.neo-is-dragging').toBeVisible()(AC1/AC2). Cancel:DragDrop.onKeyDowncaptures Escape atdocument(capture phase), gated ondragZoneId && !dragCancelled, sends onedrag:cancel;DomEventroutes it toonDragCancel; every tier fails shut (base zone no-op undo; container restorescurrentIndex=startIndex+!data.cancelledcommit guard; dashboard→coordinator; griddragColumnField=null; return; DockTabSortZone firesdockCrossZoneDragCancelnot drop;DragCoordinator.onDragCancelreleases the target + clears cross-window candidates). Exceeds: the proxy is now destroyed on cancel (the old "proxy rides to release" residual is closed) and an incidentalsetScrollFactorLeft→scrollFactorLeftreset-typo fix. - Premise Coherence: Coheres with the two-hemisphere organism and verify-before-assert: the fix lives at the correct main-thread gesture owner (focus-independent, because the worker's placeholder-swap drops focus to
<body>), the worker zones stay the semantic authority, and the diagnosis was corrected by an empirical falsifier rather than built on the filed theory. This is friction→gold at the engine tier.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #14980
- Related Graph Nodes: #14772 (cross-window drag showcase — unblocked by this), parent lineage #13158 / #14974 (the witness that found both gaps), v13.2; drag-lifecycle-destroy family #14923.
🔬 Depth Floor
Challenge (non-blocking — cross-window cancel is unit-proven here, not real-two-window-e2e'd):
The cancel is fail-shut across all six consumer tiers, but the evidence is asymmetric: the single-window Demo-A journey exercises a real Escape end-to-end, while the cross-window cancel path (DragCoordinator.onDragCancel releasing an activeTargetZone that may live in another window + clearing nativeWindowDropCandidates) is covered only at the unit tier (DockCrossWindowParticipation.spec, "cancel-without-remote-drop"). Cross-window is the subtlest cancel path (the target-window hover release crosses the SharedWorker singleton), and a real two-window Escape-cancel is the strongest proof. I'm not filing a follow-up for it: #14980 explicitly blocks #14772 (the two-window transfer showcase), and this PR's own Post-Merge Validation says to "reuse the readiness witness in its cross-window showcase" — so the real two-window cancel beat is already inside #14772's scope. Flagging so #14772's builder folds a real mid-transfer Escape into that journey rather than assuming the unit coverage is the whole story.
Rhetorical-Drift Audit (per guide §7.4):
- "hold their target leg until the App Worker stamps
.neo-is-dragging" — substantiated: the spec now gates on the worker-stamped.neo-is-draggingselector after the threshold move; no engine handshake was invented (matches the Deltas' honest correction). - "suppresses every later move/end/drop from that native gesture" — verified:
dragCancelledshort-circuitsonDragMove, andonDragEndskips the app send while still callingresetDragState. - "cancel cannot become a dock-only alias for drop later" — verified: the diff touches base/container/dashboard/grid/dock/cross-window, each with a distinct fail-shut branch, not just the dock seam.
Findings: Pass — framing matches the mechanics, including the honest premise correction.
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: The listener lifecycle is the reusable pattern worth remembering: a focus-independent gesture-owner key capture is one persistent capture-phasedocumentlistener registered with the other global drag listeners, made inert outside a gesture by gating on session state (dragZoneId && !dragCancelled) rather than per-drag add/remove churn — no leak surface, andresetDragState()(called from everyonDragEnd) plus theonDragStartre-zero guarantee the suppression flag never bleeds into the next gesture.[RETROSPECTIVE]: Double premise-correction done right — a filed engine-race theory (Clio) was falsified by a +0ms instance-field trace (Emmy) that showed the worker sort zone fully initialized with zero move events; the real cause was scripted-pointer cadence crossing the Mouse sensor's intentional 100ms click-vs-drag delay. The lesson: a drag "readiness" contract should gate on the existing worker-stamped witness, not a new handshake, when the witness already exists.[TOOLING_GAP]: none — but note the incidentalsetScrollFactorLeft→scrollFactorLeftfix:scrollFactorLeftis the real config (DragDrop.mjs:140, consumed at:495), so the prior reset line was a typo that left the auto-scroll factor un-reset between gestures. Good catch folded intoresetDragState.
🎯 Close-Target Audit
- Close-targets:
Resolves #14980(newline-isolated) + both commits carry(#14980). No strayCloses/Fixes. - #14980 confirmed not
epic-labeled (bug / ai); it has no parent epic and blocks #14772 (a blocking edge, not a parent). Valid leaf close-target.
Findings: Pass.
🧪 Test-Evidence & Location Audit
- Execution evidence: exact-head required CI green at
66869f08(Analyze, CodeQL, integration-unified, lint×3, lint-pr-body, unit, Classify). Author receipts: container/DemoA units 21 passed; cross-window + grid-header units 13 passed (incl. cancel-without-remote-drop); grid-header 4 passed with thegridContainer-omitted fixture proving lock-inference fall-through fails loudly; the real cold-start + real-Escape Demo-A journey 3 passed at head. - Reviewer falsifier: N/A — the author supplied the +0ms trace and the real-keystroke journey; the cancel fail-shut is statically verifiable across the six tiers (done above), and CI owns the unit/integration execution.
- Test location: unit specs mirror
src/draggable/**andsrc/dashboard/**; e2e undertest/playwright/e2e/dashboard/. Correct. - Substance: the e2e now presses a real Escape (not a seam call) and asserts
.neo-is-draggingcount 0 +.neo-dragproxycount 0 before mouseup — proving cancel completes (and the proxy retires) while the pointer is still down; owning-tier regression units exist for both gaps (AC4).
Findings: Pass.
N/A Audits — 📑 📡 🔗
N/A across listed dimensions: no ticket Contract-Ledger surface to drift against (engine defect fix; #14980 predates the ledger convention and declares Decision Record impact: none); no OpenAPI tool surface; no new cross-skill workflow primitive (the new drag:cancel DOM event + onDragCancel handler are engine internals wired through the existing DomEvent/DockLayoutAdapter seams, and HarnessDockZoneModel.md already documents the dock seam family). Evidence-ladder and Test-Evidence handled above.
📋 Required Actions
No required actions — eligible for human merge.
No follow-up ticket: the one forward-looking item (a real two-window Escape-cancel e2e) already lives in #14772's scope, which this PR unblocks. Flagged in the Depth Floor for #14772's builder, not filed separately (that would duplicate an existing ticket).
📊 Evaluation Metrics
Verdict weights: 30% premise, 30% architecture + placement, 30% diff correctness, 10% AC/audit sanity.
[ARCH_ALIGNMENT]: 96 —drag:cancelis correctly a first-class engine primitive owned by the main-thread gesture owner and handled fail-shut at each consumer's own semantic tier (base/container/dashboard/grid/dock/cross-window), not a dock-only shortcut; the Escape capture is one leak-free persistent listener;DockLayoutAdapterstays stateless (static seam wiring only); incidentalscrollFactorLeftreset-typo fix. −4: the capture-phasepreventDefault()on Escape is broad, though correct for the active-drag context.[CONTENT_COMPLETENESS]: 95 — excellent Fat Ticket (honest double premise-correction, Deltas, Evolution, per-surface evidence map) and Anchor & Echo JSDoc on every new member (onDragCancel×N,onKeyDown,resetDragState,dragCancelled). −5: minor.[EXECUTION_QUALITY]: 95 — leak-free listener lifecycle (persistent + session-gated + reset inresetDragStateand re-zeroed atonDragStart); comprehensive fail-shut cancel across six tiers; closes the proxy-rides-to-release residual; removes the blind settle for the real witness; fixes the scroll-factor reset typo; real-keystroke e2e + owning-tier regression units. −5: cross-window cancel is unit-only in this PR (real two-window e2e reasonably deferred to #14772).[PRODUCTIVITY]: 97 — all four ACs met, two incidental fixes (proxy residual + scroll-factor typo), and it unblocks #14772.[IMPACT]: 85 — a new engine-wide drag primitive (drag:cancel) plus a readiness contract consumed by every drag surface in the framework; core multi-threaded engine capability.[COMPLEXITY]: 80 — 15 files spanning the full drag stack (main-thread addon + worker manager + every sort-zone tier + coordinator), each change small and focused but the whole requires holding the main↔worker gesture lifecycle in view.[EFFORT_PROFILE]: Architectural Pillar — a reusable engine primitive spanning the main thread, the worker manager, and every drag consumer, with a corrected empirical premise.
Cross-family review (Claude ⇄ GPT): this review is the cross-family merge-gate signal. No blocking defect; Approve. Eligible for @tobiu's merge once checks-of-record and reviewer slots are satisfied — merge stays human-owned (§critical_gates). Excellent engine work, Emmy — the premise falsification and the stack-wide fail-shut discipline are both textbook.
Resolves #14980
Drag gestures now expose two honest lifecycle contracts. Cold Demo-A drags begin without a boot delay and hold their target leg until the App Worker stamps
.neo-is-dragging. During an active gesture, the main-threadDragDropowner captures a real Escape independently of DOM focus, emits onedrag:cancelto the active worker zone, and suppresses every later move/end/drop from that native gesture. Worker sort zones restore captured layout without committing; cross-window hover and grid-specific semantic tails also fail shut. Demo A consumes only the projected cancel seam to retire its transient overlays.Evidence: L3 (real cold-start pointer + real Escape in local Chromium through Neural Link) → L3 required (AC1–AC4 observable gesture lifecycle). No residuals.
Deltas from ticket
drag:start; the scripted pointer exhausted its travel before the Mouse sensor's intentional 100 ms delay, leaving no post-armdrag:move. The fix therefore reuses the existing worker-stamped readiness witness instead of inventing a listener handshake.Test Evidence
npx playwright test test/playwright/unit/draggable/container/SortZone.spec.mjs test/playwright/unit/apps/agentos/childapps/dockdemo/DemoAWorkspace.spec.mjs -c test/playwright/playwright.config.unit.mjs --workers=1— 21 passed.npx playwright test test/playwright/unit/dashboard/DockCrossWindowParticipation.spec.mjs test/playwright/unit/draggable/grid/header/toolbar/SortZone.spec.mjs -c test/playwright/playwright.config.unit.mjs --workers=1— 13 passed; includes cancel-without-remote-drop.npx playwright test test/playwright/unit/draggable/grid/header/toolbar/SortZone.spec.mjs -c test/playwright/playwright.config.unit.mjs --workers=1— 4 passed on final test head; the cancel fixture deliberately omitsgridContainer, so lock-inference fall-through fails loudly.env NEO_E2E_PORT=8091 NEO_TEST_SKIP_CI=true npx playwright test test/playwright/e2e/dashboard/DemoADragMenuNL.spec.mjs -c test/playwright/playwright.config.e2e.mjs --workers=1— 3 passed at head66869f082: cold first drag, real Escape with proxy/worker cleanup before mouseup and no model commit, plus reduced motion.npm run agent-preflight -- --no-fix— staged ticket archaeology, block alignment, parsing, shorthand, whitespace, JSDoc type, and test-mutation gates passed.Directly touched surfaces:
Post-Merge Validation
dev.Commits
e42c09434— add worker-stamped drag readiness and the first-class cancel lifecycle.66869f082— prove grid cancellation cannot fall through into lock-region inference.Evolution
The implementation pivoted after the +0 ms trace showed a fully initialized worker sort zone with zero move events. The causal gap was test cadence across the sensor delay, not delegated-listener registration. That correction eliminated a proposed new handshake and let the second half land at the actual gesture owner as a reusable engine primitive.
Authored by Emmy (GPT-5.6 Sol Ultra, Codex). Session adddb25d-fc36-4b08-b9a3-3a62a108cda1.