Commit Graph

5 Commits

Author SHA1 Message Date
程序员阿江(Relakkes) 6ace7f865b fix(computer-use): restore background macOS automation 2026-08-26 20:38:47 +08:00
程序员阿江(Relakkes) f3a6e02903 fix(computer-use): refuse to act through a window that cannot receive input
Three failures shared one root cause: the engine reported success for input it
had no way to deliver or verify.

A fully covered Chromium/CEF window stops drawing, so every screenshot returns
the last frame it painted while each action still answers "Action completed".
One session read a frozen image for three and a half minutes, pressed play four
times, and reported a song playing that the final capture showed paused.
Mutating commands now fail closed with `window_occluded` instead.

The physical-input monitor counted `.mouseMoved`, so moving the cursor anywhere
on screen aborted background automation — the one thing the feature exists to
allow. Movement is not an interaction with any app; presses, drags, keys and
scroll still are.

Focus notifications went out on NSEvent type 13 alone. The target accepted them,
ignored them, and reported nothing: 24 actions, 1 effective. Key-focus subtypes
travel on type 21, and `keyFocusReturned` (0x8000) needs a sign-preserving
truncation or it collapses to subtype 0 — a notification the target accepts and
discards.

The stale-capture notice claimed input does not depend on visibility. Measured
behaviour contradicts it, so it now says actions are refused while covered, and
says it without asking the model to consult the user about window management —
an earlier wording did, and the model handed back a three-step task after one
action.

Claude-Session: https://claude.ai/code/session_015j1yxxaoonyAS2iZ7qGnTS
2026-08-23 18:24:47 +08:00
程序员阿江(Relakkes) 9e211624cb fix(computer-use): make a synthesized single click register as a click
A coordinate click on a list row or a button did nothing. It moved the pointer
(the row lit up on hover), the events reached the window, the tool reported
"Action completed" — and the control never activated. Clicking a text field
worked. Clicking the same row twice, as one `click_count: 2`, worked.

Measured on one real session against an Electron target: single clicks 1/7,
double clicks 3/3. The cleanest pair is two clicks 2px apart on the same
suggestion row, with the window active both times — the only variable was the
repeat count.

Two fields were wrong, both since the engine's first commit:

**Movement claimed to be part of a click.** The leading `mouseMoved` carried
`clickCount` = the repeat count (1 for a single click, 2 for a double) instead
of 0, so the target was told "a pointer belonging to click N arrived" before any
button went down. The June comment that introduced it explains the 1…N cadence
of the press/release pairs and says nothing about why the move takes it — a
field reused, not a decision. August then widened it: the same value started
being passed to `NSEvent.mouseEvent(clickCount:)` as well, under a `max(1, …)`
that made 0 unreachable.

**A press and its release were not tied together.** Each event took a fresh
`eventNumber`, and AppKit uses that number to read two events as one click.
A text field focuses on the press alone, so it worked; anything needing a
complete click did not.

Both now follow the reference implementation, read out of its disassembly: its
drag-movement events carry `clickCount = 0` while the surrounding down/up carry
1, and its press/release pairs share one event number (a drag's down and up
share one, the dragged steps in between share another).

Verified against the same task that failed: single click on the suggestion row
lands first try, 7 tool calls and 55s where the failing run took 31 calls and
402s, no retries and no double click.

The rule is now stated once in `mouseClickState(for:click:)` instead of spelled
out per event, and nine tests cover it — there were none. `pressure` and
`mouseEventClickState` appeared nowhere in the suite, and no test had ever
called `clickPoint`, which is how these values survived from the first commit
through several rounds of "fix the clicks".
2026-08-06 04:51:34 +08:00
程序员阿江(Relakkes) a54760552b refactor(computer-use): drop the capture glow, keep the virtual cursor
The glowing border framed the app being driven. It was purely decorative and
the user asked for it to go — the animated cursor already shows what Claude is
acting on.

It was not, as suspected, interfering with clicks: the overlay is
`ignoresMouseEvents`, sits at `CGShieldingWindowLevel()`, and every window
enumeration in `WindowGeometry` filters to `layer == 0`, so it could not enter
any hit-test or occlusion decision. It went because it earns nothing, not
because it broke anything.

`WindowFrameTracker` goes with it: its own header says it exists to keep the
glow glued to a moving window, and nothing else referenced it. That also
resolves the "30Hz CGWindowList polling" entry on the redesign doc's cut list —
the polling was the tracker's fallback path, so it is gone rather than reduced.

`NonAnimatedGradientLayer` moved into `VirtualCursor.swift`. It lived in the
glow file but the cursor's orb needs it too (without it Core Animation's
implicit 0.25s actions smear every frame change), and the compiler caught the
dangling reference.

`overlay_show` now only reveals the cursor, so `playSound` and
`stopOverlaySession(animated:)` lost their only consumers. The TypeScript side
had eleven comments describing a border that no longer exists; they now describe
what the code does.
2026-08-06 04:51:14 +08:00
程序员阿江(Relakkes) b8a90626ce feat(computer-use): native macOS engine, on top of main and nothing else
Rebuilt against main so the branch carries the Computer Use work and no other
divergence. Three unrelated efforts had been sitting uncommitted in this
worktree and were swept into an earlier commit; they are preserved on
cu-worktree-full-backup and belong on their own branches — adapter control
credentials, Electron asar sealing, and the sidecar code-loading audit. Every
file outside Computer Use now matches main exactly.

The engine
  A Swift helper drives apps through the accessibility tree, with coordinate
  actuation for the Chromium and Electron apps whose tree is a bare window
  frame. Ten primitives matching the shape Codex uses, so an app's guidance and
  the model's habits transfer.

  Coordinate actions resolve their target window once and refuse when none can
  be named. The unbound event they used to fall back to is discarded by custom
  renderers, so a minimized target produced a whole session of "Action
  completed" with nothing behind it.

  Input acceptance is established for typing and key presses as well as clicks:
  each MCP call is seconds apart, so the keyboard cannot inherit the focus a
  click established. The synthetic focus notification is gated on the target
  not already being active — sent unconditionally it names window 0 at an app
  that already owns a key window, and nine window-bound clicks were discarded
  with the traffic lights fully lit.

State the model can trust
  An off-screen target says so, and says which tools still reach it: element
  actions need no on-screen geometry, so an app with a real tree can still be
  driven from the Dock. A fully covered window is recovered once, then left
  alone — burying it again is the user wanting their screen back. A repeated
  capture is reported with the cause that actually applies rather than both,
  because coverage is something we compute.

Signing
  The helper is signed under a stable identity before electron-builder sees it,
  and excluded from re-signing: macOS ties Accessibility and Screen Recording
  grants to the signing identity, so rotating it drops both on every update.

Discoverability
  The desktop slash menu falls back to a directory scan while a session's CLI
  has not started, which is when the menu is first opened. Built-ins and
  bundled skills live in the binary, so /computer-use was absent until after
  the first message.
2026-08-05 22:06:23 +08:00