Skip to content

feat(gesture): hold a single-pointer pan at its origin with --hold-ms - #3907

Draft
zijinhuang24 wants to merge 1 commit into
callstack:mainfrom
zijinhuang24:feat/pan-hold-ms
Draft

zijinhuang24 wants to merge 1 commit into
callstack:mainfrom
zijinhuang24:feat/pan-hold-ms

Conversation

@zijinhuang24

@zijinhuang24 zijinhuang24 commented Oct 22, 2026 •

Copy link
Copy Markdown

Summary

Single-pointer gesture pan takes --hold-ms: the pointer holds still at the origin, then moves over durationMs. Long-press-then-drag UIs (reorderable lists, web pages with a long-press timer) can then be driven by coordinates; gesture drag already holds, but only between selector or ref endpoints. The plan reuses drag's stationary segment (withContactHolds), so every backend that plays plan samples holds; endpoint-only backends (tvOS remote, HarmonyOS) do not, as the docs say.

  • CLI: agent-device gesture pan 200 420 0 240 600 --hold-ms 600
  • Node: client.interactions.pan({ x: 200, y: 420, dx: 0, dy: 240, durationMs: 600, holdMs: 600 })
  • MCP: gesture with { kind: "pan", origin, delta, durationMs, holdMs }

holdMs + durationMs is capped at 10000ms; --pointer-count 2 with --hold-ms is refused. .ad lines carry it as a recorded flag. 19 files changed, 218 insertions(+), 32 deletions(-).

Context: zijinmedia/device-harness#149 (the harness patches 0.21.15's dist until this ships).

Validation

Tested SHA db04d90. pnpm check:affected --run --head: format, lint, typecheck, layering, fallow, build passed; vitest-related failed only in help-conformance-sample-outputs, daemon-runtime-composition and daemon-client-startup-race, which fail the same way on base 72c051d on this host (macOS, Node 22.22). New unit tests cover input, normalization, plan samples, CLI, client and MCP schema.

Draft: device-facing and not yet run live. iOS Simulator evidence will be added here.

View guided diff

Long-press-then-drag UIs (reorderable lists, web pages with a
long-press timer) lift an item only after the finger has held still.
gesture pan moved from its first instant, and gesture drag needs
selector or ref endpoints, so there was no way to drive such a UI by
coordinates. gesture pan now takes --hold-ms (holdMs on the Node
client and MCP) and plans the same stationary segment drag uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member

I reviewed db04d90 and found two defects and a missing live run. No checks are reported on this fork head yet, so CI is pending and likely waits for maintainer approval. There are no conflicts.

holdMs is a project-config flag, and config and env defaults are merged into flags before the gesture reader runs (gesture.ts:25). The "does not read" guard in args.ts only checks provided flags. Before this PR the pan branch ignored flags.holdMs. Now it forwards it. An agent-device.json with "holdMs": 300 (documented for press and gesture tap) would make every coordinate pan hold first. With --pointer-count 2 it would fail with "requires a single pointer". With "holdMs": 0 every pan would fail the minimum check. The MCP gesture tool does the same through resolveMcpConfigDefaults. I read this from the code and did not run it with a project config. The rule should be that a pan hold is applied only when the caller asked for it on that gesture, never from a config default. The gesture command's explicit-only flag handling in args.ts and the matching MCP default filter enumerate where that applies. Please add a CLI test with a default holdMs plus gesture pan ... --pointer-count 2 that succeeds and plans no hold.

gestureRuntimeTier classifies a held pan as tier plan (platform-runtime-operations.ts:379). So owners that only use endpoints plus plan.durationMs accept it, including macOS desktop, Linux, tvOS and HarmonyOS. On those the hold is dropped and the move is stretched, or it becomes a remote swipe. gesture pan ... --hold-ms 600 would report success without a long press. The existing performTargetAuthoredDrag fact already means "preserves source hold, timed movement, and destination hold", and it is unavailable on exactly those owners. The rule should be that a gesture whose plan contains a contact hold is admitted only where the owner declares a hold-preserving adapter. Please classify intent === 'pan' && holdMs !== undefined into that tier, adjust the hint wording if you rename it, drop the docs caveat in commands.md, and add a tier-classification test.

This is a device-facing change to the pan plan on every gesture backend, and the PR body says iOS Simulator evidence is still to come. Please run on an iOS simulator and an Android device or emulator against a long-press-to-reorder list. agent-device gesture pan <x> <y> 0 <dy> 600 --hold-ms 600 should reorder the item, shown by snapshot order before and after. The same pan without --hold-ms should not reorder. The --json output should show durationMs equal to hold plus move. Once the tier fix lands, please also show the typed refusal on a macOS or Linux session.

Not blocking, take or leave: pan holdMs minimum is 1 in gesture-input.ts:197 and buildPanPlan, while tap and the MCP field use GESTURE_DURATION_MIN_MS, so holdMs: 5 passes Node and CLI but fails the MCP schema; the single-pointer and total-cap checks are written twice with identical messages, and normalizePositiveDuration(input.holdMs, input.holdMs, ...) passes the value as its own fallback (gesture-plan.ts:411); the new pan holdMs branch in gestureReplayFlags (interaction-gesture.ts:443) has no test, so deleting that line keeps every test green; the docs and changeset say "web pages with a long-press timer" although the web backend refuses all gestures; and the PR body links zijinmedia/device-harness#149, which resolves to the unrelated #149, so a full URL would be safer.

Could this be simpler while keeping the withContactHolds reuse? Classify a held pan into the existing hold-preserving tier, which refuses endpoint-only owners through their existing facts and removes the docs caveat. Keep the hold and single-pointer rules only in buildPanPlan and let readPanGesturePayload just read an integer. Read pan holdMs only when typed, so config defaults stay tap and press only. Nothing needs to change first, except possibly renaming the target-authored-drag tier if it should cover held pans.

Before merge, the held pan needs to route through the hold-preserving tier, config holdMs defaults must stop reaching pan, and the live iOS and Android evidence needs to be attached. The checks need to run and pass as well.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants