Skip to content

fix(cli): gesture accepts an array of steps so drags/swipes work in one call#127

Open
barkleesanders wants to merge 1 commit into
EvanBacon:mainfrom
barkleesanders:fix/gesture-multi-step-sequence
Open

fix(cli): gesture accepts an array of steps so drags/swipes work in one call#127
barkleesanders wants to merge 1 commit into
EvanBacon:mainfrom
barkleesanders:fix/gesture-multi-step-sequence

Conversation

@barkleesanders

@barkleesanders barkleesanders commented Jul 10, 2026

Copy link
Copy Markdown

Problem

serve-sim gesture parses exactly one {type,x,y} object, sends a single 0x03 frame, and closes the socket ~50ms later (gesture() in packages/serve-sim/src/index.ts). Since a complete touch must ride one WebSocket, the CLI cannot express a drag/swipe/scroll:

  • Splitting begin/move/end across invocations registers as a long-press — in a WKWebView it triggers text selection instead of scrolling.
  • Passing a JSON array of steps parses without error and silently does nothing.
  • The bundled agent skill's drag recipe recommends the multi-call form and notes it "may produce a long-press start on some hosts" — confirmed, it does.

Fix

gesture now accepts either the existing single-object form (unchanged, back-compat) or a JSON array of steps replayed in order on one socket, waiting per-step delayMs (default 16ms ≈ one 60fps frame) between sends:

# scroll DOWN one screen = drag the finger UP
npx serve-sim gesture '[
  {"type":"begin","x":0.5,"y":0.8},
  {"type":"move","x":0.5,"y":0.6},
  {"type":"move","x":0.5,"y":0.4},
  {"type":"move","x":0.5,"y":0.35,"delayMs":80},
  {"type":"end","x":0.5,"y":0.35}
]'

The delayMs: 80 settle before end reads as a positional drag; without it you get a momentum flick. Multi-touch (x1/y1/x2/y2) and edge pass through unchanged. Invalid input now fails loudly, naming the failing step (step 2/3: "type" must be one of begin|move|end), instead of silently no-opping.

  • New gesture-steps.ts module (parse + validate, mirrors the text-to-keys.ts pattern) with bun:test coverage (12 tests).
  • Skill docs updated: drag/pinch/edge recipes now show the single-call array form.

Verification

  • Live on an iPhone 17 Pro simulator (iOS 26.5) against a WKWebView (Capacitor) app: the array form scrolls the page; the previous multi-call recipe long-pressed (text selection) and the array form did nothing.
  • bun run typecheck clean (incl. noUncheckedIndexedAccess), bun run lint 0/0, new tests 12/12.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BEtZcizxUo7x9da5MLpHqC

Summary by CodeRabbit

  • New Features

    • Added support for multi-step gestures, including taps, drags, swipes, scrolling, pinch-to-zoom, and edge gestures.
    • Gesture steps can include custom delays, with a default timing of 16 ms.
    • Existing single-step gesture commands remain supported.
    • Invalid gesture input now reports clear validation errors.
  • Documentation

    • Updated gesture examples and guidance to use one JSON array for complete gesture sequences.

…ne call

`serve-sim gesture` parsed exactly one {type,x,y} object, sent a single
0x03 frame, and closed the socket 50ms later. Since a complete touch must
ride one WebSocket, the CLI could not express a drag: splitting
begin/move/end across invocations registers as a long-press (in a webview
it triggers text selection), and passing a JSON array parsed without error
but silently did nothing. The bundled agent skill even recommended the
multi-call form and noted it "may produce a long-press on some hosts".

gesture now accepts either the existing single-object form (unchanged) or
a JSON array of steps replayed in order on one socket, waiting per-step
`delayMs` (default 16ms, ~one 60fps frame) between sends. A trailing
settle (e.g. delayMs: 80 on the last move) turns a flick into a
positional drag. Multi-touch (x1/y1/x2/y2) and `edge` pass through
unchanged; invalid input now fails loudly naming the failing step instead
of silently no-opping.

Verified live on an iPhone 17 Pro simulator (iOS 26.5) against a WKWebView
app: the array form scrolls; the old multi-call recipe long-pressed and the
array form previously did nothing. Skill docs updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BEtZcizxUo7x9da5MLpHqC
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

serve-sim gesture now accepts validated single-step or multi-step JSON input, replays steps sequentially over one WebSocket with configurable delays, and documents the single-call gesture format.

Changes

Gesture sequence replay

Layer / File(s) Summary
Gesture step contracts and validation
packages/serve-sim/src/gesture-steps.ts, packages/serve-sim/src/__tests__/gesture-steps.test.ts
Adds typed single-touch and multi-touch steps, coordinate and delay validation, payload conversion, default timing, and parsing tests.
CLI gesture replay
packages/serve-sim/src/index.ts
Parses gesture sequences, sends each payload in order on one WebSocket, and applies per-step or default delays.
Gesture usage documentation
skills/serve-sim/SKILL.md, skills/serve-sim/references/gestures.md
Updates drag, swipe, scroll, pinch, and edge gesture examples to use one JSON-array gesture invocation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main CLI change: gesture now accepts an array of steps for one-call multi-step gestures.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/serve-sim/src/gesture-steps.ts`:
- Around line 47-53: Update the coordinate validation around `single` and
`multi` to reject steps where both coordinate forms are valid. Add a guard for
`single && multi` that throws the same invalid-coordinate error before
constructing or sending `stepPayload`, ensuring mixed single-touch and
multi-touch fields cannot reach the server.

In `@packages/serve-sim/src/index.ts`:
- Around line 713-742: Import WebSocket from the package dependency "ws" at the
top of the module, and use that imported symbol in the existing WebSocket
construction within the replay function. Remove reliance on the Node global
while preserving the current connection behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 69d8a95b-205e-426f-ac63-5cb39c96ec8c

📥 Commits

Reviewing files that changed from the base of the PR and between af681b8 and 4e45b6e.

📒 Files selected for processing (5)
  • packages/serve-sim/src/__tests__/gesture-steps.test.ts
  • packages/serve-sim/src/gesture-steps.ts
  • packages/serve-sim/src/index.ts
  • skills/serve-sim/SKILL.md
  • skills/serve-sim/references/gestures.md

Comment on lines +47 to +53
const single = isNormalized(x) && isNormalized(y);
const multi = isNormalized(x1) && isNormalized(y1) && isNormalized(x2) && isNormalized(y2);
if (!single && !multi) {
throw new Error(
`Invalid ${label}: needs normalized 0..1 coords — either {"x","y"} or {"x1","y1","x2","y2"} — got ${JSON.stringify(step)}`,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject steps with both single-touch and multi-touch coordinates.

When both {x, y} and {x1, y1, x2, y2} are present and valid, validation passes and stepPayload sends all fields to the server, creating an ambiguous wire payload. Add a guard to reject mixed coordinates.

🛡️ Proposed fix
   const single = isNormalized(x) && isNormalized(y);
   const multi = isNormalized(x1) && isNormalized(y1) && isNormalized(x2) && isNormalized(y2);
+  if (single && multi) {
+    throw new Error(
+      `Invalid ${label}: provide either {"x","y"} or {"x1","y1","x2","y2"}, not both — got ${JSON.stringify(step)}`,
+    );
+  }
   if (!single && !multi) {
     throw new Error(
       `Invalid ${label}: needs normalized 0..1 coords — either {"x","y"} or {"x1","y1","x2","y2"} — got ${JSON.stringify(step)}`,
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const single = isNormalized(x) && isNormalized(y);
const multi = isNormalized(x1) && isNormalized(y1) && isNormalized(x2) && isNormalized(y2);
if (!single && !multi) {
throw new Error(
`Invalid ${label}: needs normalized 0..1 coords — either {"x","y"} or {"x1","y1","x2","y2"} — got ${JSON.stringify(step)}`,
);
}
const single = isNormalized(x) && isNormalized(y);
const multi = isNormalized(x1) && isNormalized(y1) && isNormalized(x2) && isNormalized(y2);
if (single && multi) {
throw new Error(
`Invalid ${label}: provide either {"x","y"} or {"x1","y1","x2","y2"}, not both — got ${JSON.stringify(step)}`,
);
}
if (!single && !multi) {
throw new Error(
`Invalid ${label}: needs normalized 0..1 coords — either {"x","y"} or {"x1","y1","x2","y2"} — got ${JSON.stringify(step)}`,
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/serve-sim/src/gesture-steps.ts` around lines 47 - 53, Update the
coordinate validation around `single` and `multi` to reject steps where both
coordinate forms are valid. Add a guard for `single && multi` that throws the
same invalid-coordinate error before constructing or sending `stepPayload`,
ensuring mixed single-touch and multi-touch fields cannot reach the server.

Comment on lines +713 to +742
let steps: GestureStep[];
try {
touch = JSON.parse(jsonStr);
} catch {
console.error("Invalid JSON:", jsonStr);
steps = parseGestureSteps(jsonStr);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}

return new Promise<void>((resolve, reject) => {
const ws = new WebSocket(state.wsUrl);
ws.binaryType = "arraybuffer";

ws.onopen = () => {
const send = (touch: ReturnType<typeof stepPayload>) => {
const json = new TextEncoder().encode(JSON.stringify(touch));
const msg = new Uint8Array(1 + json.length);
msg[0] = 0x03;
msg.set(json, 1);
ws.send(msg);
};
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));

// All steps replay on THIS socket: a begin→move×N→end sequence split
// across CLI invocations (one socket each) registers as a long-press.
ws.onopen = async () => {
for (const [i, step] of steps.entries()) {
send(stepPayload(step));
if (i < steps.length - 1) {
await sleep(step.delayMs ?? DEFAULT_STEP_DELAY_MS);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether WebSocket is imported from the `ws` package or used as global in index.ts.

# Search for ws-related imports at the top of the file
rg -n 'import.*WebSocket.*from.*ws' packages/serve-sim/src/index.ts

# Also check for any require('ws') patterns
rg -n "require\(['\"]ws['\"]\)" packages/serve-sim/src/index.ts

# Show all import lines to see if WebSocket is imported
sed -n '1,15p' packages/serve-sim/src/index.ts

Repository: EvanBacon/serve-sim

Length of output: 1141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every WebSocket reference in the file.
rg -n '\bWebSocket\b' packages/serve-sim/src/index.ts

# Show the surrounding code where WebSocket is used.
sed -n '700,760p' packages/serve-sim/src/index.ts

# Check whether ws is declared in package.json dependencies for this package.
sed -n '1,220p' packages/serve-sim/package.json

Repository: EvanBacon/serve-sim

Length of output: 5436


Import WebSocket from ws here
new WebSocket(state.wsUrl) relies on the Node global. Import it from ws instead so this path stays consistent with the package dependency and portable across supported runtimes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/serve-sim/src/index.ts` around lines 713 - 742, Import WebSocket
from the package dependency "ws" at the top of the module, and use that imported
symbol in the existing WebSocket construction within the replay function. Remove
reliance on the Node global while preserving the current connection behavior.

Source: Coding guidelines

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.

1 participant