Commit graph

27 commits

Author SHA1 Message Date
Lahfir
595ccb6cc4 feat: 10-step scroll chain, focus guards, enhanced click chain, bounds fix
- Rewrite ax_scroll with 10-step AX-first chain (scroll-to-visible,
  increment/decrement, page scroll, value shift, sub-element press,
  focus child, select rows, keyboard, arrow keys, CGEvent last resort)
- Add ensure_app_focused() guard before all CGEvent calls
- Enhance smart_activate with AXScrollToVisible pre-step,
  AXShowAlternateUI, AXCustomActions, and keyboard space fallback
- Fix bounds_hash always null in RefMap (always read bounds in builder)
- Add right-click menu capture in response data
- Add append_surface_refs for post-action menu detection
2026-02-21 15:28:28 -08:00
Lahfir
2f495ffb69 fix: address code review findings (double-free, CF leaks, injection)
P1: fix double-free in find_scroll_area (el.clone() instead of AXElement(el.0))
P1: add input validation in close_app_impl to prevent AppleScript injection
P2: fix CFRelease leaks in focus_window_impl and press_for_app_impl
P2: remove duplicated copy_element_attr/copy_bool_attr from surfaces.rs
P3: replace is_attr_settable_pub wrapper with direct pub export
P3: extract TOGGLEABLE_ROLES constant, collapse try_parent_activation loop
P3: fix fragile super::super::builder path, tighten list_windows_impl visibility
2026-02-21 14:01:51 -08:00
Lahfir
a2319623b4 fix: add menubar surface, fix press --app crash and modifier mapping
- Add --surface menubar to expose full menu bar hierarchy
- Fix use-after-free in press_for_app_impl (ManuallyDrop)
- Fix AX modifier bit mapping (Shift=1<<0, Alt=1<<1)
- Improve alert detection to search all app windows
2026-02-21 00:21:40 -08:00
Lahfir
74242f5040 fix: remove AXShowDefaultUI from activation chain, fix child walk
AXShowDefaultUI returns success on Finder sidebar rows but doesn't
navigate — it's a presentation action, not activation. Remove it so
the chain falls through to child activation which fires AXOpen on the
child AXCell, triggering actual navigation.

Also expand child activation to try AXOpen and AXShowDefaultUI on
children (not just AXPress/AXConfirm), and fire-and-forget since some
apps navigate as side effect even on error return codes.
2026-02-21 00:03:46 -08:00
Lahfir
4616c8f65f feat: smart AX-first click chain + macOS crate restructure
Restructure crates/macos/src/ from flat files into tree/, actions/,
input/, system/ subfolders. No file exceeds 400 LOC.

Add actions/activate.rs with a 10-step discovery-based activation chain:
steps 1-9 are pure AX (AXPress, AXConfirm, AXOpen, AXPick, AXSelected,
AXSelectedRows on parent, focus+retry, child walk, parent walk),
step 10 falls back to CGEvent only when every AX path fails.

Wire smart_activate() into Click/DoubleClick/RightClick/TripleClick/Toggle.
Add AXDisclosing attribute fallback for Expand/Collapse.
Rewrite README with full command reference.
2026-02-20 23:53:05 -08:00
Lahfir
9745887e12 docs: add platform crate folder structure convention to CLAUDE.md 2026-02-20 23:32:20 -08:00
Lahfir
b2c34f6ec5 Revert "docs: add platform crate folder structure convention and track plans"
This reverts commit 775892ee8e.
2026-02-20 23:31:50 -08:00
Lahfir
775892ee8e docs: add platform crate folder structure convention and track plans
Add Platform Crate Folder Structure section to CLAUDE.md enforcing
consistent subfolder layout (tree/, actions/, input/, system/) across
all platform crates. Update smart AX-first click chain plan with
new file paths reflecting the subfolder structure. Un-ignore
docs/plans/ and docs/brainstorms/ so design documents are tracked.
2026-02-20 23:26:56 -08:00
Lahfir
198d7d7d27 fix: ancestor-path cycle detection + CGEvent click fallback
Two critical fixes for macOS accessibility tree traversal:

1. Pointer reuse bug: macOS returns different logical AX elements
   sharing the same AXUIElementRef pointer value across sibling
   branches (e.g., split view panes). The global visited set
   treated these as duplicates, silently dropping entire UI
   sections (search fields, content panes, toolbars).

   Fix: changed build_subtree and find_element_recursive from
   global visited sets to ancestor-path sets. Pointers are added
   on entry and removed after children are processed, allowing
   the same pointer in different branches while still preventing
   true ancestor→descendant cycles.

   Impact: System Settings snapshot went from 45 to 121 refs,
   now exposing sidebar, search, toolbar, AND content pane.

2. CGEvent click fallback: when AXPress/AXConfirm fail (common
   for treeitems, custom SwiftUI controls), read live bounds
   and synthesize a CGEvent mouse click at the element center.

   Fallback chains: Click (AXPress→AXConfirm→CGEvent),
   DoubleClick (AXOpen→AXPress×2→CGEvent), RightClick
   (AXShowMenu→CGEvent), TripleClick, Toggle, Check/Uncheck.
2026-02-20 22:29:58 -08:00
Lahfir
48ea80c65f
Merge pull request #2 from lahfir/claude/add-performance-benchmarks-7lyog 2026-02-20 08:47:26 -08:00
Claude
ad9256d888
docs: add performance benchmarks section documenting 6s → 0.3s snapshot speedup
Documents the root cause (sequential AX IPC calls per node), the fix
(AXUIElementCopyMultipleAttributeValues batch fetching), measured wall-time
benchmarks across TextEdit/Finder/System Settings/Xcode, and the secondary
optimisations: FxHashSet cycle detection, streaming JSON serialisation, and
per-element messaging timeout.
2026-02-20 16:45:59 +00:00
Lahfir
ccbf736f9b chore: remove agentic test notes from tracking 2026-02-19 21:49:04 -08:00
Lahfir
d3f7e03c67
Merge pull request #1 from lahfir/feat/ax-first-bug-fixes-new-commands
feat: add 19 new commands, AX-first rewrites, LOC compliance
2026-02-19 21:47:56 -08:00
Lahfir
3a796023f0 style: apply cargo fmt to all files 2026-02-19 20:00:18 -08:00
Lahfir
eca04e8392 feat: add 19 new commands, AX-first rewrites, LOC compliance
- New commands: check, uncheck, triple-click, scroll-to, clear,
  clipboard-clear, hover, drag, mouse-move/click/down/up,
  resize/move/minimize/maximize/restore-window, key-down, key-up
- Rewrite input.rs to AX-first keyboard synthesis
- Add mouse synthesis (CGEvent) and window ops (AX) adapter methods
- Split dispatch.rs into dispatch + batch_dispatch for LOC compliance
- Split actions.rs → action_extras.rs (scroll/select helpers)
- Split app_ops.rs → key_dispatch.rs (press-for-app + key dispatch)
- Split cli.rs → cli_args.rs (arg structs)
- Improve is-check with applicability field, list-apps with wrapped shape
- Enhance wait with --text/--menu/--menu-closed support
2026-02-19 18:17:15 -08:00
Lahfir
2c9aee3979 fix: right-click uses AXShowMenu; context menus detected via focused element
AXShowMenu is the accessibility-native way to trigger a context menu on
an element without moving the cursor or posting CGEvents. After the
action, the resulting AXMenu appears as a child of the triggered element.

- actions.rs: RightClick tries AXShowMenu first, falls back to CGEvent
- surfaces.rs: context_menu_from_app checks AXFocusedUIElement's children
  for AXMenu, then falls back to direct app children (Electron apps)
- list_surfaces_for_pid: surfaces context menus as 'context_menu' type
  (distinct from menu-bar 'menu' entries)
2026-02-19 15:45:52 -08:00
Lahfir
7f0d6103d1 fix: detect open menus via AXMenuBarItem.AXSelected, not AXMenus attribute
AXMenus on AXApplication returns kAXErrorAttributeUnsupported (-25205)
for standard Cocoa apps. Menu bar menus are always children of their
AXMenuBarItem; the only reliable open-state indicator is AXSelected=true
on that item.

- open_menubar_menu: scan AXMenuBar.AXChildren for AXMenuBarItem with
  AXSelected=true, return its AXMenu child
- context_menu_from_app: kept as fallback for Electron-style apps that
  do expose context menus as AXMenu in AXApplication.AXChildren
- list_surfaces_for_pid: reports menu title and item_count when open
- is_menu_open: checks both paths
2026-02-19 15:40:05 -08:00
Lahfir
39178b2916 feat: surface-targeted snapshot, menu wait, list-surfaces command
Add snapshot --surface flag (menu/sheet/popover/alert/focused/window)
for direct O(1) AX attribute reads rather than full-tree traversal.
Add wait --menu/--menu-closed for polling-based context-menu gate.
Add list-surfaces command to enumerate open transient surfaces.
Remove all inline // comments from macos crate per 400-LOC/no-comment rule.
2026-02-19 15:21:07 -08:00
Lahfir
f1ed36046c refactor: Phase A quality fixes — dead code, bugs, LOC compliance
- Delete clipboard.rs (superseded by clipboard_get/set; zero callers)
- Remove batch::execute() stub (dispatch layer owns batch execution)
- Fix wait.rs: replace double get+unwrap with if-let pattern
- Fix press.rs: replace dead is_empty check + unwrap with ok_or_else
- Add doc comment to is_check.rs documenting stale-state semantics
- Trim tree.rs to 394 LOC (was 403; compress non-macos stub + remove redundant comment)
- Move probe binaries to examples/ with required-features = ["dev-tools"]
- Fix clippy::explicit_auto_deref in adapter.rs
- Fix clippy::needless_borrows_for_generic_args in screenshot.rs
- Apply prior session core/macos fixes (adapter, actions, roles, snapshot, get)

All targets pass cargo clippy -D warnings.
2026-02-19 15:04:18 -08:00
Lahfir
1d98ab828c fix: make all 30 commands work end-to-end on macOS
- tree: window_element_for() starts traversal from correct AXWindow
  element (matching by title) instead of app root, fixing mixed-window
  trees and 'disabled group' noise in Electron apps
- tree: AXUIElementCopyMultipleAttributeValues batch fetch (2.5x faster)
- tree: copy_ax_array() CFRetains each element before CFArray drops,
  fixing the dangling-pointer bug that returned kAXErrorInvalidUIElement
- roles: add AXApplication->application and AXSplitGroup->splitter
- actions: CGEvent is now primary click mechanism (AXPress is best-effort
  first try); fixes clicks in Electron/web apps that don't support kAXPressAction
- actions: DoubleClick uses MOUSE_EVENT_CLICK_STATE=2, RightClick uses
  RightMouseDown/Up, Scroll sets event.set_location(element_center)
- screenshot: rewrite using screencapture CLI + CGWindowListCopyWindowInfo
  to find the largest CGWindowID for the app PID; resolves app/window_id
  to pid before calling adapter (was producing all-zero data placeholder)
- screenshot: ScreenshotTarget::Window now carries pid (i32) instead of
  our hash string; commands/screenshot.rs resolves via list_windows
- input: add 8ms inter-keystroke delay in synthesize_text so apps can
  process events (was truncating long strings)
- snapshot: return WindowInfo in SnapshotResult; include app and window
  fields in JSON output per contract
- find: switch to snapshot::run so found refs are persisted to refmap
- dispatch: implement dispatch_batch_command covering all 29 commands
  (was stub returning 'not yet implemented' for every batch sub-command)
2026-02-19 12:51:37 -08:00
Lahfir
6dc567a4ae fix: align error codes with spec (APP_NOT_FOUND, PERM_DENIED) and add -i shorthand 2026-02-19 11:49:17 -08:00
Lahfir
fb34600448 docs: add professional README with full command reference and architecture guide 2026-02-19 11:44:28 -08:00
Lahfir
fe501a1a20 docs: mark all 47 code review todos as complete 2026-02-19 11:39:21 -08:00
Lahfir
218503a7eb fix: resolve all 47 code review findings from Phase 1 audit
Security (P1):
- Replace AppleScript app-name interpolation with PID-based scripts (001, 002)
- Switch pkill -f regex to pkill -x exact-name match (003)
- Validate launch_app id against path traversal (006)
- Remove CFRetain+mem::forget; use ManuallyDrop correctly (005, 029)
- Add cycle detection (visited set) to resolve_element (009)

Correctness (P1/P2):
- Implement read_bounds via kAXPositionAttribute+kAXSizeAttribute (023)
- Fix Scroll action to use CGEventCreateScrollWheelEvent (027)
- Propagate Expand/Collapse AX errors instead of discarding (042)
- Store AXDescription separately from AXTitle (046)
- Fix interactive_only to recurse into container children (043)
- Pass window.pid to RefEntry instead of hardcoded 0 (019)
- Add batch::parse_commands() for dispatch-layer batch execution (022)
- Fix find command to use snapshot::build() — no RefMap overwrite (030)
- Return non-interactive elements from find with ref:null (044)
- Wire permissions --request to adapter.check_permissions (031)
- Fix screenshot --app to resolve window ID (032)
- Close-app protected process check uses exact match (014)
- Wait unbounded sleep capped at 30s (012)
- Fix wait double-lookup unwrap (038)
- Fix press.rs unwrap, normalize modifier order (021, 038)
- emit_json no longer silently discards write errors (041)

Data / API:
- WindowInfo.app serializes as "app_name" per spec (045)
- Add value/states/bounds fields to RefEntry; populate from snapshot (024, 025)
- Stable window IDs via FxHasher(pid+title) (028)
- Use FxHasher for bounds_hash, replacing unstable DefaultHasher (020)
- Flush refmap temp file before rename (039)
- Add HOME fallback to USERPROFILE (015)

Architecture:
- Split click.rs → click, double_click, right_click per one-command-per-file (047)
- Split clipboard.rs → clipboard_get, clipboard_set (047)
- Consolidate RefArgs to helpers.rs (047)
- Move focus/launch/close impl to app_ops.rs; adapter.rs 389→311 LOC (001)
- Deduplicate ErrorCode::code() via ErrorCode::as_str() (034)
- Add doc comment to Response struct explaining Phase 3 intent (035)
- Add snapshot::build() for read-only tree access (030)
- Remove duplicate ABSOLUTE_MAX_DEPTH from snapshot.rs (033)
2026-02-19 11:38:17 -08:00
Lahfir
608d4aaaa1 fix: suppress dead_code lint on BatchCommand deserializer struct 2026-02-19 10:46:47 -08:00
Lahfir
29d6cbaac4 docs: check off all Phase 1 Must-Fix agent-native items 2026-02-19 10:46:04 -08:00
Lahfir
a346f242c2 feat: Phase 1 foundation — workspace scaffold, core engine, macOS adapter, 31 commands
Implements the complete agent-desktop Phase 1 specification:

- Workspace: 5-crate layout (core, macos, windows/linux stubs, binary)
- Core: AccessibilityNode, Action, ErrorCode, PlatformAdapter trait, RefMap
  with atomic writes, SnapshotEngine with depth-first ref allocation
- macOS adapter: AXUIElement tree traversal, action execution, input
  synthesis via CGEvent, screenshot via CGWindowListCreateImage, clipboard
  via pbpaste/pbcopy, window listing and app management via osascript
- 31 CLI subcommands via clap derive: snapshot, find, screenshot, get, is,
  click, double-click, right-click, type, set-value, focus, select, toggle,
  expand, collapse, scroll, press, launch, close-app, list-windows,
  list-apps, focus-window, clipboard-get/set, wait, status, permissions,
  version, batch
- Windows/Linux: not-supported stubs ready for Phase 2 implementation
- JSON output contract: {version,ok,command,data} envelope with structured
  error payloads including SCREAMING_SNAKE_CASE error codes
- Ref system: @e{N} sequential refs for interactive elements, stored at
  ~/.agent-desktop/last_refmap.json with 0o600/0o700 permissions
- CI: GitHub Actions macOS runner with dependency isolation check, clippy,
  unit tests, release build, and 15MB binary size gate
2026-02-19 10:44:38 -08:00