refactor: migrate the gesture family to the request-bound device runtime - #1952
refactor: migrate the gesture family to the request-bound device runtime#1952thymikee wants to merge 2 commits into
Conversation
Migrates `gesture`, `scroll` and `swipe` onto request-bound platform runtimes (ADR 0019), retiring the last daemon-owned gesture admission table. `requireGestureSupported` was intent-dependent platform policy living inside the daemon. It becomes four owner-stated execution tiers — one-contact plans, direction-authored flings, two-contact synthesis, and target-authored drag — each declared per exact device cell by the owner that can meet it. A gesture input selects exactly one tier, and its handler binds once (§9), before any plan is built. `scroll` retires its capability bucket and its dispatch leaf together: an ordinary scroll requires `scrollDirection`, and `scroll top`/`scroll bottom` additionally require `captureSnapshot`, so the edge refusal happens at admission instead of mid-command. `gestureViewport` is declared `preferred`, not `required`: Linux advertises no frame read and derives its coordinate frame from a capture today, so requiring it would refuse a cell that works.
Size Report
npm unpacked components
Startup median (7 runs, lower is better):
Top changed chunks:
Top changed packed files
|
|
Not ready at |
|
Addressed at 1. Apple 2. Live coverage of the claimed denominator. Ran the four owners I can reach locally. Full
That is six owners with live coverage. Three genuinely could not be covered here:
Your call, and I'll do either: authorize the two provider cells and I'll run them for real, or I 3. Size/body metadata. Body now carries the exact CI numbers — +12.0 kB unpacked, +2.5 kB
|
|
The Apple extraction and local-owner evidence address the earlier points. One blocker remains: this head still changes the direct WebDriver and Limrun runtime cells, while its evidence expressly omits live runs for both. ADR 0019’s unit denominator includes direct provider runtimes, so we cannot narrow the claim in prose while those migrations ship. Please either run one representative changed command/refusal cell through each provider (record exact head and result), or remove those provider migrations from this unit; then re-request review. |
|
Code review is clean at Residual blocker to merge-ready status: this unit changes direct |
|
New blocker after main moved: this head is now 12 commits behind and conflicts across runtime/registry/dispatch surfaces. Rebase must preserve #1969’s granular contracts architecture: the PR’s new value imports from wide Also remove the type workaround in All green CI and +12 kB size evidence is against the old base. After rebase, rerun the full exact-head suite and remeasure/rejustify size. |
Wave 5 unit 3 of the ADR 0019 migration (tracker #1739). Unit record posted before any code:
#1739 (comment)
Precondition checked: the iOS scroll churn has settled — #1905, #1906, #1901 and #1913 are all
merged and no open PR touches
*scroll*/*gesture*.Unit record recap
Three public descriptors:
gesture(R42),scroll(R43),swipe(R44). All three shared onecapability bucket — apple
{simulator, device}, android{emulator, device, unknown}, linux{device}— plus overlays (gesture/swipeharmonyos;scrollharmonyos + web). No platformplugin carried a
supportsByDefault/unsupportedHintByDefaultclosure for any of them, so thewhole non-bucket admission was one function:
requireGestureSupported.That function was intent-dependent, which is what makes this an action-selected unit (§9). Its
tiers become owner-stated operations:
performGesturePlan(one contact; everyswipe)performDirectionalFlingPlan(gesture fling --direction)performMultiTouchGesturePlan(pinch/rotate/transform, 2-pointer pan)performTargetAuthoredDrag(gesture drag)gestureViewportpreferredscrollDirectioncaptureSnapshot(already owned)scroll top/scroll bottomWhat changed
gesture-runtime.tsandscroll-runtime.ts, both riding the sharedInteractorseam throughlocalInteractorSource/providerInteractorSource— no newlocal/provider binder pair. The four gesture tiers share one plan executor and are separate keys
because their cells differ, not their mechanics (the shape
SnapshotRuntimeOperationsalready uses for its three capture keys).
gestureclassifies its input into one tier and binds once, beforeany plan is built — a drag binds before its targets resolve.
swipebinds once for the whole--count Nseries.scrollbindsscrollDirection, orscrollDirection + captureSnapshotforan edge. Enforced by regression assertions on
inspectFacts× 1 andbindDevice× 1.gestureViewportispreferred, and that is parity rather than taste. The client-sideresolveGestureViewportoptional-chains the backend member and falls back to a capture-derivedframe; Linux implements no
gestureViewport, so Linux gestures run on that fallback today.Declaring it
requiredwould refuse a working cell. ADR §9 measurement, from the iOS runner log:gestureViewportmedian 56 ms (n=14) vssnapshotmedian 112 ms (n=37) — the fallback costs~2× on the runner alone, before the daemon's presentation work.
(tier, device)inunavailableResponse— the precedented seamsession-state.tsalready uses — soRuntimeOperationFactdoes not change shape. All eight retired strings reduce togesture ${intent} is not supported on ${subject}, with the three owner-specific subjects theold code already special-cased. The refusal is built with
normalizeError(new AppError(...))because the retired admission threw; going through
errorResponsewould have dropped thecode's default hint and
retriableclassification from the wire shape.Deliberately left in place
augmentScrollVisualizationResult— runs infinalizeGenericCommandon the result, afterexecution. Daemon presentation policy.
post-gesture-stability.ts/deferred-interaction-outcome.tsand the selector cache'sbypassForPostGestureStabilization— session-state and capture policy; they never touch aninteractor.
gesture-no-effect.ts— capture-annotation wording.dispatchGestureViewport— deliberately NOT claimed as retired:session-replay-maestro-runtime.tsis a second consumer owned by
replay/test(Wave 6). ADR §6 keeps a shared mechanic in placeuntil its last consumer can move, so the R-rows claim only what genuinely dies.
Deleted
requireGestureSupported+ its five private helpers (all ofcapabilities.tsbelowsupportedPlatformsForCommand),handleScrollCommandand its scroll-only helpers, theDISPATCH_HANDLERS.scrollarm,dispatchGesturePlan, three capability buckets, and the overlaymemberships in
HARMONYOS_SUPPORTED_COMMANDS/WEB_INTERACTION_COMMANDS.Parity artifact
src/daemon/__tests__/gesture-admission-parity.test.tsis the retired suite's exact devicematrix, re-pointed at the real composed runtime gateway and the real daemon admission through
createRequestRuntimeBindings— no stubs, no casts. It caught two genuine drifts while I waswriting it: Vega and Web were reporting their own generic hint for
gesture dragwhere the retiredadmission reported the target-authored-drag hint. Both fixed.
Planted mutants
Each mutant applied alone; suite re-run; restored byte-identical (
git diff --quietclean).gesture-runtime.tsbinderinteractor.performGesture(...)gesture-runtime.tsbinderperformGesturescroll-runtime.tsbinderinteractor.scroll(...)scroll-runtime.tsbinder(direction, options)bindGestureTierbindGestureTierperformGesturePlanexecuteBoundScrollexecuteBoundScrolldirection/optionsin the inputLive evidence — local devices only
Every effect verified as a committed change (tree node, measured rect, or pixel diff), never
command success. Refusal cells verified by exact message and hint.
Apple — iPhone 17 Pro (iOS 26.2 sim) and the macOS host
scroll downApple Account/Ready…/General/Accessibility→General/Accessibility/Action Button/Apple Intelligence & Siriscroll top(edge tier)Camerarect y 21 → 588.3swipeGeneral/Accessibility/Action Button→Apple Intelligence & Siri/Camera/Home Screen & App Librarygesture panCamerarect y 588.3 → 253.3gesture pinch(multi-touch)gesture(ok=1) after a boundgestureViewport; pixels unchanged — see notegesture pinch is not supported on macOS+ "macOS automation has no multi-touch input…"gesture drag is not supported on macos+ "…source hold, timed movement, and destination hold…"gesture panproceeded to start the runner — admission is per-tier, not per-commandThe two macOS refusals are worth reading together: multi-touch reports the display name
(
macOS), drag reports the rawappleOs(macos), because the retiredassertAppleMultiTouchSupportedandgesturePlatformMessagediffered exactly that way.Android —
Pixel_7_reviewscroll downNotes From Poland, 1d→BBC, 20hscroll bottom(edge tier)swipeBBC, 20h→Visual Capitalist, 1dgesture panVisual Capitalistrect y 549 → 136gesture pinch(multi-touch)IME restored to
com.google.android.inputmethod.latin/...LatinIME; AVD shut down (adb devicesempty, no
qemu-systemleft).HarmonyOS —
nova 14(physical)scroll downbluetooth_entry/mobile_network_entry/satellite_network_entry/theme_settings→display_settings/volume_settings/systemui_notification_settings/intelligent_scene_entry/system_and_updatesswipedisplay_settings/volume_settings/...gesture panabout_device/wifi_entry/bluetooth_entry/...gesture pinch is not supported on harmonyos, no fact hint (falls through to the code's default) — matching the retired non-Apple/non-Android branch, which threw with no hintgesture drag is not supported on harmonyos+ the target-authored-drag hintWeb — managed backend, local fixture
AGENT_DEVICE_WEB_E2E=1platform smoke passes (23.4 s)scroll downgesture pinchgesture pinch is not supported on webswipegesture fling is not supported on web— swipe reports its normalized intent, exactly as the retired admission didVega — local VVD
All four cells refused, and this is the clearest demonstration of the per-tier hint split:
scrollscroll is not supported on this deviceswipe(fling tier)gesture fling is not supported on vegagesture pinch(multi-touch)gesture pinch is not supported on vegagesture draggesture drag is not supported on vegaVVD stopped afterwards.
Not covered live, and why
platform-linuxplatform-linux/src/runtime.test.ts, and its bound execution is exercised end-to-end inrequest-handler-chain.test.ts, which drives a real Linux swipe through the composed gateway into the realcreateLocalLinuxToolProviderdrag primitive and asserts the exact values[10, 20, 110, 20, 100]. The CI Linux lane covers the runtime path.provider-webdriverplatform-runtime.test.tscells plus the packaged Cloud WebDriver provider scenario — which caught a real defect in this unit (facts advertisedscrollDirectionwith no bound operation).provider-limrunapp-log-runtime.test.tscells, including the deliberate iOS-session behavior change below.I did not spend on cloud devices. Tell me if you want the two provider cells run for real and I will
do it, or I will narrow the unit's claims to the six owners with live coverage instead — your call
on which of those two you prefer.
iOS pinch note. The plan reached the device — the runner log shows
gestureViewportcompleted,then
gestureaccepted and completedok=1— but the screen did not change. That is the recordediOS 26.2 simulator multi-touch synthesis issue on this machine (session records, 2026-08-20), not
this migration: the same bound
performMultiTouchGesturePlanmoves 97% of the pixels on Android.Size
Exact figures from the CI Bundle Size job on this PR (authoritative; my local measurement was
+10.4 kB because tsdown chunking differs slightly from the CI build):
71 files changed. +12.0 kB unpacked across three migrated commands is ~4.0 kB each —
inside the per-command R40 band (+4.5 kB) this unit was budgeted at in its record.
Itemized, from the local per-file diff:
src/dispatch.js−3,178 B — the retiredhandleScrollCommandleaf, its scroll-only helpers,the
DISPATCH_HANDLERS.scrollarm, anddispatchGesturePlan.src/validation.js−3,383 B —requireGestureSupportedand its five private helpers, i.e.the whole daemon-owned gesture admission table.
gesture-runtime.ts,scroll-runtime.ts, ~290 linesincluding both binder pairs and the facts factories) plus gesture/scroll cell tables in all eight
owners. tsdown rebalances chunk boundaries, so the remaining per-file deltas do not map 1:1 to
source edits.
Smaller designs considered and rejected. One
performGestureoperation with the tier passed asdata would have saved roughly 40% of the contract surface, but it puts the cell table back inside
the caller: an owner could no longer declare that it does one-contact gestures and not two, which
is precisely the admission this unit exists to move out of the daemon. Collapsing
gestureViewportinto the plan operation would have saved a key, but it ispreferredand theothers are
required— folding them would force Linux to refuse a working cell. Both rejectionsare why the growth is what it is.
Gates
pnpm check:affected --rungreen (re-run after rebasing ontod2f2dae79). Full unit suite999 files / 7499 tests pass.
check:layeringnames gesture, scroll and swipe among the commandskeeping exactly one platform-execution path.
Two findings the gates produced that are worth flagging:
provider-webdriverdeclared the gesture andscroll cells but never bound the operations — the cloud WebDriver scenario failed with
"Runtime owner advertised scrollDirection without an implementation". That is the fail-closed
contract check doing its job; fixed by binding them.
androidGestureOperations/linuxGestureOperationswrappers. Rather than suppress, the owners now use the same inline shapeapple/harmonyos already used, and apple's local
whenAdmittedhelper was promoted to contractsso android and linux stop hand-rolling the branch (which also cleared their complexity findings).
Behavior changes, stated rather than hidden
wording ("Limrun iOS direct sessions do not expose portable gesture execution yet.") is
preserved verbatim as the fact hint. ADR §6 asks for exactly this classification.
dispatchGesturePlanwith "Gesture execution is unavailable". Vega never carried a gesture bucket.
scrollon a watchOS Apple leaf is declared available, deliberately:scrollhad nowatchOS gate, and it still fails where it fails today — when the Apple interactor refuses to
construct, not at admission.
Tracker comment after merge is thymikee's call.