Skip to content

Native input engine, interaction primitives, resident daemon, MCP surface - #3

Merged
wmehanna merged 4 commits into
mainfrom
feat/native-daemon-and-verification
Aug 7, 2026
Merged

Native input engine, interaction primitives, resident daemon, MCP surface#3
wmehanna merged 4 commits into
mainfrom
feat/native-daemon-and-verification

Conversation

@wmehanna

@wmehanna wmehanna commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Rebuilds the input path around a compiled Swift engine with a resident daemon mode, adds the interaction primitives agents were missing (read/wait/verify/long-press/double-tap/keys/clipboard/back/scroll-to), fixes non-US keyboard layouts, and exposes the full surface through the MCP server.

Changes

  • Native engine (helpers/input-src/): single-process tap/swipe/scroll/drag/key/text with hardware-fidelity events; marginal chained-action cost ~26ms (was ~170ms). Range-checked CLI args so bad input is a clean no-op, never a mid-gesture trap; every exit path restores cursor + focus.
  • Resident daemon (input serve/send): warm Vision OCR, ScreenCaptureKit capture, in-process stillness detection, one input session held across command bursts (idle-closed). read/wait/screenshot/tap-text ride it automatically with process-pipeline fallback (ICTL_NO_DAEMON=1 opt-out).
  • New primitives: read (OCR + tappable coordinates - the only inspection channel over mirroring), wait text|gone|still, tap-verify (self-checking taps with retry), press, double-tap, key + clipboard, back, scroll-to, batch, drag-icon --via.
  • Keyboard layouts: characters resolve via the active layout (UCKeyTranslate) instead of a hardcoded US-QWERTY table.
  • OCR confidence gate (0.6): garbled clipped glyphs (measured 0.50) dropped at both OCR sources; legit text measures 1.0.
  • MCP server: +11 tools (tap_text, tap_verify, read_screen, wait_for, scroll, scroll_to, long_press, double_tap, press_key, go_back, drag_icon); descriptions encode the platform rules so agents inherit them.
  • Fixes: sips crop+resample split back into two calls (single call silently wrong at 2x), focus-restore waits for activation to land, screenshot.sh daemon path, pipe-safe read.sh.

Testing

  • Unit: 174 bats + 43 vitest, all passing (also verified with no prebuilt binary, simulating CI's fresh checkout)
  • Live device suite (tests/live/run-live.sh --bench 3): 42/42 across 3 iterations
  • Validated at Retina scale=2 (window moved to 2x display): 13/13, coordinates exact on daemon and fallback paths
  • shellcheck clean, SwiftLint clean, MCP server boots

Compiled Swift engine (helpers/input-src) replaces per-action bash+osascript
spawns for tap/swipe/scroll/drag/key, cutting marginal cost per chained
action from ~170ms to ~26ms. New interaction primitives: read (OCR screen
dump with coordinates), wait (text/gone/still conditions instead of guessed
sleeps), press, double-tap, back, scroll-to, key, clipboard, batch.

Fixes bundled in: key.sh and Cmd+1/2/3 now resolve characters via the Mac's
active keyboard layout (UCKeyTranslate) instead of a hardcoded US-QWERTY
table; screenshot.sh no longer merges sips crop+resample into one call
(silently wrong size at 2x scale); focus-restore now waits for activation to
land instead of racing the process exit; numeric CLI args are range-checked
before any session opens, closing a trapping-conversion crash that could
leave the mouse button down and focus stolen.

Live device suite (tests/live/) and 40 new bats tests cover the new surface.
`input serve` runs the engine as a unix-socket daemon, auto-started by
`input send`: Vision OCR model stays warm, capture goes through
ScreenCaptureKit one-shots, stillness detection diffs frames in-process,
and one input session is held across commands (idle-closed after 1.5s, so a
burst of actions pays the macOS focus-steal once; daemon self-exits after
5min unused).

read.sh, wait.sh, screenshot.sh and tap-text.sh use the daemon when
reachable and fall back to their process pipelines otherwise
(ICTL_NO_DAEMON=1 forces the fallback). Window info is re-resolved per
daemon command so a moved window never receives stale coordinates.

Live suite 39/39 after: screenshot 267->173ms, OCR find 962->368ms,
read 1329->495ms, wait-still 1097->718ms, wait-text 3786->2545ms; gesture
tests dropped ~1-1.6s each because their internal waits ride the daemon.
tap-verify taps and confirms the result (screen changed by default, or
--expect/--gone <text>), retrying once - its first live run already caught
a genuinely missed tap and recovered on the retry. Wired into the
dispatcher and the live suite (negative case: a no-op tap must be reported
as unverified, catching a lying verifier).

Both OCR producers (ocr-image helper and the daemon) now drop lines below
0.6 confidence: on phone-UI content Vision scores are bimodal - legitimate
text reads 1.0, garbled clipped glyphs ~0.5 (measured "Rottor" misread of a
clipped "Battery" at exactly 0.50) - so consumers never see junk text.
Output format unchanged.

Also validated the whole live suite at Retina scale (window moved to the
2x display, 13/13, coordinates and dimensions exact on both daemon and
fallback paths) - previously everything had only ever run at scale=1.
Adds eleven tools the MCP server was missing relative to the CLI:
tap_text, tap_verify, read_screen, wait_for, scroll, scroll_to,
long_press, double_tap, press_key, go_back and drag_icon. Most notable
gap closed: scroll - swipes do not scroll iOS lists through mirroring,
so an agent on the old tool set could not scroll at all, read the
screen, wait on conditions, or verify taps.

Tool descriptions encode the platform rules discovered in testing
(swipe vs scroll, Escape not closing context menus, one-slot icon
moves, confidence-filtered OCR) so agents inherit them for free.
@wmehanna
wmehanna merged commit 9d9f092 into main Aug 7, 2026
1 check passed
@wmehanna
wmehanna deleted the feat/native-daemon-and-verification branch August 7, 2026 19:16
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