fix(macos-helper): hold synthetic clicks long enough to be delivered - #2716
Conversation
|
Size Report
Startup median (7 runs, lower is better):
|
|
Reviewed at 929cdfc. The fix is right in direction but two gaps stop this from being safe to merge.
The schedule duration is now Not blocking: the Swift tests only exercise mouseClickSteps/mouseClickHoldMs and miss the clickState policy in postMouseClick, TouchPressExtras.holdMs silently means something different on macOS than elsewhere with no doc note, the "Build macOS helper" workflow step name no longer matches the "macOS helper build and tests" gate label, and Is the schedule type doing more than it needs to? A Swift executableTarget can't be imported by a test target, so pulling the schedule into a library target to make it testable seems justified. But The PR body's 15/15 activation and clickCount results, and the Bluetooth-pane test, come from the author and were not reproduced here; the stuck-button scenario is reasoned from the fixed timeout and the uninterruptible sleep, not from killing a live helper mid-hold. CI is green across 21 checks, including the macos-helper gate this PR changes, and the diff stays confined to macos-helper and the platform-apple press path those checks exercise. No conflicts. The two open questions are the click-count rule (independent clicks for |
929cdfc to
5a46d70
Compare
Live evidence at 5a46d70 (rebased on main with #2715)Rebased this branch onto Built CLI on the macOS host, Both presses activated their sidebar row on the first click through the dwell-held synthetic click. The recording is the host screen while the sequence ran. One note for anyone reproducing: macos-press.mov |
The helper posted mouseMoved, leftMouseDown, and leftMouseUp back to back with no delay. AppKit keeps a mouse-down under track long enough to separate a click from a drag, so the release was dropped: on an NSButton probe a zero-dwell click delivered 0 of 15 mouse-ups and activated 0 actions, 20ms activated 14 of 15, and 40ms and above activated 15 of 15. Every `press` on a macOS surface that has no runner was therefore a visible no-op. Click posting now lives in AgentDeviceMacOSInput as a pure schedule plus a poster, so the dwell and the rising click state are unit-testable without a window server. The default hold is 60ms with a 40ms floor, and --hold-ms, --clicks, and --interval-ms are honoured, which makes --double-tap and --count reach the frontmost-app, desktop, and menubar surfaces as real repeated clicks instead of being dropped.
…gate The macos-helper gate only built the helper, so the new AgentDeviceMacOSInput schedule had no gate behind it. Add test:macos-helper and point the gate at check:macos-helper, which builds and tests, keeping the CI step id unchanged.
… to the click schedule, and release the button on termination `--count N` posted N presses with a rising click state, so `--count 2` read as a double-click and `--count 3` as a triple-click, unlike every other platform where the count is N independent presses. The rising state is now an explicit `doubleClick` field on the request, set only from `--double-tap`, and it rises inside each press: `--count 3 --double-tap` is three double-clicks. The step array is gone; the schedule is one pure function over presses (click state and delay), which the Swift tests pin, and the poster is one loop over it. The helper ran under a fixed 30s timeout while a schedule could last `hold*clicks + interval*(clicks-1)`, so `--hold-ms 10000 --count 4` was killed inside the third hold with the system button down. The bridge now derives the timeout from the same schedule sum plus the usual margin, forwards the request's abort signal, and the helper releases a held button from a termination handler before it exits, so no path can end between a mouse-down and its mouse-up. Also: an explicit `--interval-ms 0` is carried rather than read as unset, the docs say what `--hold-ms`, `--count` and `--double-tap` mean on these surfaces (and that jitter is not applied), and the workflow step carries the gate's name. Verified on an AppKit probe that logs every mouse-down's clickCount and every activation: `--count 3` → three downs at clickCount 1, three activations; `--double-tap` → clickCount 1 then 2; `--double-tap --count 2` → two such pairs; `--hold-ms 10000 --count 4` completed in 40s with four activations, and a plain press afterwards activated normally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5a46d70 to
0c6a78f
Compare
|
Addressed at 0c6a78f (rebased onto Click count. The rising click state is no longer derived from the repeat count. Timeout and the stuck button. Live, on an AppKit probe that logs every mouse-down's Non-blocking, taken: an explicit
|
|
Reviewed at 0c6a78f. Both earlier points are fixed in code. The new release handler does not run on the route that stops the helper. The live run at 5a46d70 shows single presses only, and this change alters the click schedule on the host. Please add a run at the new head on the macOS host: The Smoke Tests failure is an |
…eld mouse button is released The helper's release handler traps SIGTERM, SIGINT and SIGHUP, but the host stopped the helper only through `killProcessTree`, which sends SIGKILL on both the deadline and a cancelled request. Now that the request signal reaches the surface press, a cancelled `press --hold-ms 10000` or a dropped client killed the helper between the mouse-down and its mouse-up and the button stayed down. `ExecOptions` gains a `kill` policy: the first signal to send and a grace after which SIGKILL follows. `killProcessTree` sends the policy's signal, arms an unref'd escalation timer, and clears it on the child's exit; the group path and the reaped-pid guard are shared by both signals. `runMacOsHelper` asks for SIGTERM with a one-second grace on every helper run, so the release handler runs on every stop the host applies. The helper also posts the mouse-up before it clears the held-button record, so a signal landing between the two can no longer exit with the button down. Two exec tests pin the route with a trap script standing in for the helper: a cancelled command with a kill policy sees the signal and records its release, and a child that ignores the first signal is still ended once the grace passes. The helper test asserts the policy the press hands the provider. The docs note named the `desktop` surface among those that post through the helper; that surface inspects only, and the note now says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
c3a0642 to
14d1006
Compare
|
Addressed at 14d1006. Every host stop now reaches the helper as SIGTERM first. Pinned by two exec tests that use a Live run at 14d1006 on the macOS host (session Probe. An AppKit window that logs every mouse down/up with the Finder list row "Opens On Double-Click". Cancelled hold.
Without the fix the same run left the button down until the helper's own 3 s hold ended; a Recordings: 🤖 Generated with Claude Code macos-press-count-double-tap-cancel.movmacos-press-hold-cancelled.mov |
|
Reviewed at 14d1006. The earlier finding is fixed. Every host stop now reaches the helper as SIGTERM first, with SIGKILL one second later, and callers that set no Small notes, not blocking: the new CHANGELOG entry still lists Smoke Tests fails at "wait for Agent Device Tester" after the daemon became unreachable in the iOS simulator run. That looks unrelated: no iOS or simctl caller sets the new There are no conflicts, and I added |







Summary
presson a macOS session whose surface has no runner (frontmost-app,desktop,menubar) never activated anything. The helper postedmouseMoved,leftMouseDown,leftMouseUpback to back, and AppKit drops a mouse-up posted in the same event tick as its down: on anNSButtonprobe a zero-dwell click delivered 0 of 15 mouse-ups and activated 0 actions; 20 ms activated 14 of 15; 40 ms and above activated 15 of 15.Click posting now lives in a new
AgentDeviceMacOSInputSwift target as a pure schedule plus a poster, so the dwell and the rising click state are unit-testable without a window server. Default hold is 60 ms with a 40 ms floor.--count Nis N independent clicks at click state 1, as on every other platform;--double-tapposts each click as a double-click pair with a rising click state, so the two compose into N double-clicks. The helper's process timeout is derived from the click schedule plus margin and the request's abort signal is forwarded. Every stop the host applies — the deadline, a cancelled request, a dropped client — reaches the helper as SIGTERM first (a newkillpolicy onExecOptions, SIGKILL only after a one-second grace), so the helper's release handler runs and a button held mid-stop comes back up. 17 files, no scope beyond the macOS press path and the exec kill seam it needed.agent-device open --surface frontmost-app --platform macos agent-device press "label=Bluetooth" --platform macos agent-device press @e7 --double-tap --platform macosValidation
Head
14d1006f7(rebased ontomainwith #2715 and #2718; review rounds one and two addressed in the comment thread).pnpm check:affected --runon that head: all runnable checks passed. Alsopnpm test:macos-helper(6 Swift tests),pnpm test:integration:macos-coverage,pnpm test:integration:progress:check,test/integration/provider-scenarios/macos-desktop.test.ts,pnpm typecheck,pnpm lint,pnpm check:fallow --base origin/main.Live host evidence,
agent-deviceagainst an AppKit probe and System Settings:pressactivated 15/15 (0/15 before);--count 3gave three activations atclickCount1;--double-tapgaveclickCount1 then 2, and--double-tap --count 2two such pairs;--hold-ms 10000 --count 4completed in 40 s with four activations and a normal click afterwards;press "label=Bluetooth"on afrontmost-appsession opened the Bluetooth pane, proven bywait "windowtitle=Bluetooth"(screenshots and recording in the evidence comment). Second live run at14d1006f7on a Finderfrontmost-appsession:--count 2reads as twoclickCount1 clicks on the AppKit probe and leaves a Finder row unopened at--interval-ms 700(Finder itself derives a double-click from timing, so the default 120 ms interval still opens it);--double-tapopens the folder; a--hold-ms 3000press stopped one second in by Ctrl-C and by killing the client leaves the button up within 0.4 s with the helper gone, where before the button stayed down for the rest of the hold.Two gaps stay open and are not fixed here:
longpresson those surfaces is still a silent no-op (longPressPointis declared runner-only inpackages/platform-apple/src/runner-demand.ts), and the replayopengrammar accepts no--surface, so no live.adscenario can reach the helper route.pnpm test:replay:macosalso fails on this host at the pre-existing About-pane wait in01-system-settings.ad, identically on unmodifiedorigin/main.