Commit graph

128 commits

Author SHA1 Message Date
Lahfir
00d2474498 feat(ffi): ABI surface completion — AdSnapshotSurface, focused_only, release_handle (Unit 6)
Lands the last set of ABI-shape changes before the header is published.
Any addition after publication would be a breaking change for every
consumer linked against the old layout.

Core / platform additions:
- crates/core/src/adapter.rs: new `PlatformAdapter::release_handle`
  trait method with `not_supported()` default. macOS implementations
  must `CFRelease` the underlying AXUIElementRef to balance the
  CFRetain that happened during resolve. Windows/Linux inherit the
  default unchanged.
- crates/macos/src/adapter.rs: macOS impl calls
  `core_foundation::base::CFRelease(raw)` when the raw pointer is
  non-null.

FFI surface additions:
- crates/ffi/src/types/snapshot_surface.rs: new AdSnapshotSurface enum
  (Window=0, Focused, Menu, Menubar, Sheet, Popover, Alert) mirroring
  the 7-variant core SnapshotSurface.
- crates/ffi/src/types/tree_options.rs: new `surface: AdSnapshotSurface`
  field. Previous hard-coded SnapshotSurface::Window is replaced by
  consumer-selected surface with enum-validated read at entry.
- crates/ffi/src/windows/list.rs: ad_list_windows gains `focused_only:
  bool` between app_filter and out — maps directly to
  WindowFilter::focused_only. Consumers can now call `ad_focused_window`-
  style queries without a second ABI round.
- crates/ffi/src/actions/native_handle.rs: new `ad_free_handle(adapter,
  handle)` exported. Null-tolerant on both sides; Windows/Linux
  `ActionNotSupported` is translated to `Ok` so C consumers can call
  the release path uniformly across platforms.

Validation:
- AdSnapshotSurface added to enum_validation.rs — ad_get_tree now
  rejects invalid surface discriminants with
  `AD_RESULT_ERR_INVALID_ARGS` rather than matching on UB.

Closes R5, R9, R10 from PR #22 review.
52 lib tests pass, 1 integration test passes. Clippy clean.
2026-04-16 04:03:34 -07:00
Lahfir
2c13fbefaa feat(ffi): main-thread enforcement for macOS-sensitive entrypoints (Unit 11)
macOS accessibility and Cocoa APIs must be called on the process's main
thread; off-thread use is silent undefined behavior that is hard to
diagnose from the consumer side (Python, Swift, Node workers).

Add crates/ffi/src/main_thread.rs with:
- is_main_thread(): libc::pthread_main_np() on macOS, `true` elsewhere.
- debug_assert_main_thread(): panics in debug, no-op in release. Panic
  is caught by the trap_panic boundary and surfaces as
  AD_RESULT_ERR_INTERNAL with a diagnostic last-error — violators
  get a loud signal in dev, an actionable error code in prod.

Apply the assert as the first statement inside trap_panic bodies for
the high-traffic AX paths: ad_get_tree, ad_resolve_element,
ad_execute_action, ad_get_clipboard, ad_screenshot. (Release / launch /
list / window-op / etc. remaining entrypoints can be covered in a
follow-up; these five are the hot paths agents actually hit.)

Add a crate-level //! rustdoc block on lib.rs documenting the thread-
safety model, the build-profile requirement (release-ffi, not release),
and the errno-style last-error lifetime. cbindgen propagates the
rustdoc to agent_desktop.h so C consumers read the constraints from the
header.

53 tests pass. Clippy clean.
2026-04-16 04:00:38 -07:00
Lahfir
ee2374e565 feat(ffi): build hygiene — pinned cbindgen, drift check, variant parity (Unit 12)
Closes R14, R19, R20, R23 from PR #22 review.

Build pipeline:
- crates/ffi/Cargo.toml: pin cbindgen = "= 0.27.0" exactly. No more
  silent formatting drift from patch-version bumps of cbindgen; any
  future bump is a deliberate PR that also commits the regenerated
  header.
- crates/ffi/build.rs: replace .expect()/.ok() swallowing with explicit
  panics on every failure path. Missing CARGO_MANIFEST_DIR or OUT_DIR
  now emits a cargo:warning rather than crashing rustc. cbindgen errors
  now panic loudly with the diagnostic; previously an .ok() on fs::copy
  silently emitted a stale header.

CI:
- .github/workflows/ci.yml: new "FFI cdylib build" step under the
  release-ffi profile and a "FFI header drift" step that runs
  `git diff --exit-code crates/ffi/include/agent_desktop.h` immediately
  after. Any uncommitted change to the generated header fails the build
  with a clear message telling the developer to run the local rebuild
  and commit.

Variant parity:
- crates/ffi/src/error.rs: compile-time assertion
  `const _: () = assert!(error_code_variant_count() ==
  ad_result_error_variant_count())` guards against core adding an
  ErrorCode variant without a matching AdResult entry (which would
  silently drop information at the FFI boundary). Uses stable const fn
  + explicit variant arrays, no nightly variant_count feature.

50 lib tests, 1 integration test — all passing. Clippy clean.
2026-04-16 03:58:30 -07:00
Lahfir
6a01d976d5 feat(ffi): out-param zeroing, lossy NUL handling, window validation (Unit 7)
Three defensive-hardening additions that close R6, R13, and R15 from the
PR #22 review:

1. Out-param zeroing at entry
   Every fallible FFI fn now zeroes its out-param *before* any fallible
   work, so when the fn returns an error code the caller can safely call
   the paired ad_free_* fn on an as-if-never-populated struct.
   - ad_launch_app: *out = zeroed AdWindowInfo
   - ad_resolve_element: (*out).ptr = null
   - ad_execute_action: *out = zeroed AdActionResult
   - ad_screenshot: *out = zeroed AdImageBuffer
   (ad_get_tree / ad_list_* already had this from prior work.)

2. Lossy string helper for mandatory fields
   Add convert::string::string_to_c_lossy — replaces interior NUL bytes
   with U+FFFD (3-byte UTF-8 sequence) so CString::new is always
   infallible. Applied to mandatory fields:
   - convert::window_info_to_c: id, title, app_name
   - convert::app_info_to_c: name
   - convert::surface_info_to_c: kind
   - tree::flatten: role and each state string
   - actions::action_result_to_c: action, post_state.role, state strings

   Optional fields keep opt_string_to_c (null-on-NUL is the correct
   signal for "field absent" when the header documents nullability).

3. Window identity validation
   windows::to_core::ad_window_to_core now returns
   Result<WindowInfo, AdapterError>. Null or non-UTF-8 id/title returns
   InvalidArgs with a diagnostic message rather than silently coercing
   to "" — previously a caller with a bad id could match the wrong
   window. app_name stays lenient (some Electron apps emit blank
   window owners).

   Updated callers to propagate the error: tree::get::ad_get_tree,
   windows::focus::ad_focus_window, windows::op::ad_window_op.

50 tests pass (+3 new lossy-NUL tests).
2026-04-16 03:56:44 -07:00
Lahfir
4434357bdd fix(ffi): rewrite tree flatten as iterative BFS (Unit 4)
The prior recursive DFS layout placed a child node immediately after
its parent, intermixing descendants with the parent's siblings. That
meant AdNode.child_start..child_start + child_count walked through
grandchildren instead of direct children — silent wrong data for every
consumer iterating the flat tree.

New layout: iterative level-order (BFS) traversal using a VecDeque.
Siblings of any node are always contiguous at
  nodes[n.child_start .. n.child_start + n.child_count]
so direct-child iteration is correct by construction, at every level.

- Pre-count pass sizes the Vec once with Vec::with_capacity — avoids
  repeated reallocation on deep/wide trees.
- No recursion means consumers on thin stacks (JNI ~256 KB, Python
  ctypes worker threads) no longer risk stack overflow regardless of
  tree depth.
- `parent_index` still back-references correctly.

New test coverage:
- test_flatten_breadth_first_layout (was depth_first_order): iterates
  the child ranges and asserts direct-child roles match the expected
  sequence — this is the test that would fail against the old DFS
  layout.
- test_flatten_deep_chain: 11-level nested chain, walks child_start at
  every level and confirms the single-child ranges address correctly.
- test_flatten_wide_root: 100 direct children under one root, confirms
  the whole sibling run is contiguous and every child is reachable.

Closes R1 from PR #22 review (the blocker-class child-range bug).
47 tests pass (+2 new BFS-specific tests).
2026-04-16 03:53:21 -07:00
Lahfir
0ff2993b0f feat(ffi): validate every #[repr(i32)] enum at the C boundary (Unit 2)
C callers can legally place any int32_t bit pattern into an enum-typed
field of a #[repr(C)] struct. Reading that field as the Rust enum type
and pattern-matching against its variants is undefined behavior when
the discriminant is out-of-range — Rust assumes enum values are valid.

New crates/ffi/src/enum_validation.rs provides:
- enum_raw_i32(&T) -> i32: reads the raw discriminant via pointer cast,
  never invokes the enum's validity invariant. Safe because #[repr(i32)]
  guarantees layout.
- try_from_c_enum! macro: generates <Enum>::from_c(raw: i32) -> Option<Self>
  that returns Some only for enumerated discriminants, None otherwise.
  Applied to all 8 C-exposed enums: AdActionKind (0..=20), AdDirection,
  AdModifier, AdMouseButton, AdMouseEventKind, AdWindowOpKind,
  AdScreenshotKind, AdImageFormat.

Call sites updated to read raw, validate, then match:
- actions/conversion.rs: action.kind, action.scroll.direction,
  each modifier in key_combo slice
- input/mouse.rs: ev.kind, ev.button
- windows/op.rs: op.kind
- screenshot/capture.rs: t.kind

Invalid discriminants now return AD_RESULT_ERR_INVALID_ARGS with a
diagnostic last-error string ("invalid <enum> discriminant").
Previously the code invoked UB before the call could return.

11 new enum-validation tests (45 total lib tests, +11 from 34).
Fuzz test iterates i32::MIN, i32::MAX, -1, 999, and edge values through
every validator — never panics, never UB.
2026-04-16 03:51:28 -07:00
Lahfir
b7af53e920 feat(ffi): errno-style last-error lifetime (Unit 3)
Last-error pointers returned by ad_last_error_{code,message,suggestion,
platform_detail} now survive across any number of subsequent successful
FFI calls — only the next *failing* call rotates them. This matches the
POSIX errno contract and closes R2 from the PR #22 review (use-after-free
when caller cached the message pointer and made another successful call).

Changes:
- Remove error::clear_last_error() from every Ok branch across adapter,
  tree/get, actions/{resolve,execute}, apps/{list,launch,close},
  windows/{list,focus,op}, input/{clipboard,mouse,drag},
  screenshot/capture, surfaces/list. The slot now only rotates on a new
  set_last_error().
- clear_last_error is gated behind #[cfg(test)] since no production
  caller needs it after this change.
- Add a crate-level rustdoc block on ad_last_error_code documenting the
  errno-style lifetime contract — cbindgen propagates this to
  agent_desktop.h so C consumers can read the rule from the header.
- crates/ffi/tests/error_lifetime.rs: integration test reproducing the
  review's UAF scenario — fails on ErrInvalidArgs, caches the message
  pointer, makes 10 successful ad_check_permissions calls, asserts the
  cached pointer still resolves to the same string.

Lib Cargo.toml now emits both cdylib and rlib so the integration test
can link against the public crate without duplicating symbol bindings.

35 tests pass (34 lib + 1 integration). Clippy clean.
2026-04-16 03:47:27 -07:00
Lahfir
c3856c1be4 feat(ffi): wrap every extern "C" entrypoint in a panic boundary
Add crates/ffi/src/ffi_try.rs with three helpers:
- trap_panic(|| -> AdResult)   for AdResult-returning fns
- trap_panic_ptr(|| -> *mut T) for ad_adapter_create
- trap_panic_const_ptr(|| -> *const T) for ad_last_error_*
- trap_panic_void(|| ())        for ad_*_destroy / ad_free_*

Each helper runs its body under std::panic::catch_unwind with
AssertUnwindSafe, stashes a 'static C-string message
("rust panic in FFI boundary") via a new
error::set_last_error_static(code, &'static CStr) helper that never
allocates, and returns AD_RESULT_ERR_INTERNAL / null on panic. The
panic payload is intentionally discarded — allocating inside a panic
handler risks double-panic and the host's tracing/logs hold the
diagnostic anyway.

Every #[no_mangle] pub extern "C" fn body is now wrapped:
- adapter.rs: ad_adapter_create, ad_adapter_destroy, ad_check_permissions
- error.rs: ad_last_error_{code,message,suggestion,platform_detail}
- tree/{get,free}.rs: ad_get_tree, ad_free_tree
- actions/{resolve,execute,result}.rs: ad_resolve_element,
  ad_execute_action, ad_free_action_result
- apps/{list,launch,close}.rs: ad_list_apps, ad_free_apps,
  ad_launch_app, ad_close_app
- windows/{list,free_one,focus,op}.rs: ad_list_windows, ad_free_windows,
  ad_free_window, ad_focus_window, ad_window_op
- input/{clipboard,mouse,drag}.rs: ad_get_clipboard, ad_set_clipboard,
  ad_clear_clipboard, ad_free_string, ad_mouse_event, ad_drag
- screenshot/{capture,free}.rs: ad_screenshot, ad_free_image
- surfaces/list.rs: ad_list_surfaces, ad_free_surfaces

Add crates/ffi/examples/panic_spike.rs as a permanent regression
example — `cargo run --profile release-ffi --example panic_spike`
catches the synthetic panic and exits 0. If someone flips the profile
back to panic=abort, the example SIGABRTs instead — a loud signal.

Verified:
- cargo build --profile release-ffi -p agent-desktop-ffi: ok
- cargo test --lib -p agent-desktop-ffi: 34 passed (+7 new ffi_try tests)
- cargo clippy --all-targets -p agent-desktop-ffi -- -D warnings: ok
- cbindgen header symbol set: 31 functions before / 31 after
- panic_spike under release-ffi: PANIC CAUGHT OK (exit 0)

Closes R4 from PR #22 review.
2026-04-16 03:43:35 -07:00
Lahfir
a38bdd22d9 feat(ffi): add release-ffi profile and publish guard for cdylib
Workspace Cargo.toml gains [profile.release-ffi] inheriting from
release with panic = "unwind". Cargo rejects per-package `panic`
overrides (error: panic may not be specified in a package profile —
verified experimentally), so a dedicated profile is the Cargo-supported
path to build the cdylib with unwinding while the CLI stays on
panic = "abort".

crates/ffi/Cargo.toml gains:
- publish = false — prevents accidental `cargo publish` of the cdylib
  crate until the distribution plan intentionally removes the guard.
- libc = "0.2" under the macos target — Unit 11 uses pthread_main_np().

Both profiles coexist:
- target/release/libagent_desktop_ffi.dylib       442 KB (panic=abort)
- target/release-ffi/libagent_desktop_ffi.dylib   464 KB (panic=unwind)

Size delta for unwind metadata (~22 KB, under the 500 KB risk budget).
Prerequisite for Unit 1's panic-boundary macro.
2026-04-16 03:34:28 -07:00
Lahfir
078553299a refactor(ffi): rename c_to_str to c_to_string, fix unbound-lifetime footgun
c_to_str<'a>(*const c_char) -> Option<&'a str> let the caller pick any
lifetime — including 'static — while the returned reference actually
only lives as long as the raw pointer. That is unsound: a caller could
bind the result to a 'static slot and dereference long after the
underlying buffer is gone.

Replace with c_to_string(*const c_char) -> Option<String> — always
clones into an owned String. Every caller (actions, apps, input,
windows, tree tests, convert tests) updated to pass &String where &str
was needed or .as_deref() in asserts. No caller was relying on
short-lived borrow semantics; the existing flow was already
.to_owned()-ing immediately.

Closes the Unit R soundness portion. No behavior or ABI change;
cbindgen header: 31 functions in, 31 out (ordering-only diff).
2026-04-16 03:33:34 -07:00
Lahfir
f28dd95555 refactor(ffi): audit and remove unjustified #[allow(dead_code)] annotations
Every reachable pub(crate) helper (convert/ helpers, tree/flatten::flatten_tree,
error::{set_last_error, clear_last_error, last_error_code, error_code_to_result})
now stands without the annotation — these are all called from #[no_mangle]
entrypoints and rustc sees them correctly.

Move the three test-only last_error readers (last_error_message_str,
last_error_suggestion_str, last_error_platform_detail_str) plus the
MessageSource::to_owned_string helper into #[cfg(test)] scope. They were
only ever used by tests; having them stand at module scope required the
annotation and muddied the public-surface read.

Zero #[allow(dead_code)] remains in crates/ffi/src/. Tree still compiles,
27 tests still pass, cbindgen header symbol set unchanged.
2026-04-16 03:29:37 -07:00
Lahfir
95c3da2352 refactor(ffi): extract adapter from lib.rs, use explicit re-exports
Move AdAdapter + build_adapter + ad_adapter_create / ad_adapter_destroy /
ad_check_permissions into a dedicated crates/ffi/src/adapter.rs matching
the crates/macos/src/adapter.rs single-responsibility pattern.

lib.rs is now a module-declaration + explicit re-export shell. The
`pub use types::*` wildcard is replaced with one `pub use` per type so
adding a new type requires a deliberate re-export (prevents accidental
ABI expansion). Narrow most modules to pub(crate) — only error and
types stay pub because cbindgen scans them for public types.

Every file under 400 LOC (max 225). No behavior or ABI change; symbol
set identical.

Closes the structural portion of Unit R. Remaining Unit R cleanup
(c_to_str unbound lifetime + dead_code audit) lands next.
2026-04-16 03:27:03 -07:00
Lahfir
a49c35165e refactor(ffi): split screenshot.rs and surfaces.rs into submodules
- screenshot/{capture,free}.rs: ad_screenshot + ad_free_image
- surfaces/list.rs: ad_list_surfaces + ad_free_surfaces

Drop the last unused convert::surface re-exports; convert/mod.rs now
only re-exports rect_to_c (the single consumer sits inside tree/flatten
and benefits from the shortened path).

No behavior or ABI change; symbol set identical, ordering-only cbindgen diff.
2026-04-16 03:26:01 -07:00
Lahfir
29bde8dd03 refactor(ffi): split input.rs into input/ submodules
Move input.rs into input/{clipboard,mouse,drag}.rs.
- clipboard.rs: ad_get_clipboard + ad_set_clipboard + ad_clear_clipboard + ad_free_string
- mouse.rs: mouse_button_from_c + ad_mouse_event + tests
- drag.rs: ad_drag + tests

Drop unused convert::{c_to_str, free_c_string, string_to_c} re-exports
from convert/mod.rs; all callers now hit crate::convert::string::
directly.

No behavior or ABI change.
2026-04-16 03:25:05 -07:00
Lahfir
690d275484 refactor(ffi): split windows.rs into windows/ submodules
Move windows.rs into windows/{to_core,list,focus,free_one,op}.rs.
- to_core.rs: ad_window_to_core helper (pub(crate) re-exported so
  tree/get.rs keeps its existing crate::windows::ad_window_to_core path)
- list.rs: ad_list_windows + ad_free_windows
- focus.rs: ad_focus_window
- free_one.rs: ad_free_window (single-struct free from ad_launch_app;
  Unit 5b renames to ad_release_window_fields)
- op.rs: ad_window_op

Remove now-unused convert::{window_info_to_c, free_window_info_fields}
re-exports.

No behavior or ABI change. Header symbol set identical.
2026-04-16 03:23:56 -07:00
Lahfir
7463123095 refactor(ffi): split apps.rs into apps/ submodules
Move apps.rs into apps/{list,launch,close}.rs — one concern per file.
Drop the now-unused convert::{app_info_to_c, free_app_info_fields}
re-exports from convert/mod.rs (apps/list.rs imports from the concrete
convert::app submodule directly).

No behavior or ABI change. Header symbol set identical; emission order
shifts per cbindgen's module-path sort (ordering-only diff).
2026-04-16 03:22:49 -07:00
Lahfir
2cf6bb01b7 refactor(ffi): split actions.rs into actions/ submodules
Move actions.rs into actions/{conversion,resolve,execute,result}.rs.
- conversion.rs: direction_from_c + key_combo_from_c + action_from_c + tests
- resolve.rs: ad_resolve_element
- execute.rs: ad_execute_action
- result.rs: action_result_to_c + ad_free_action_result + tests

actions/mod.rs declares submodules; the #[no_mangle] entries remain
discoverable by the cdylib linker and cbindgen without re-exports.
Drop the unused opt_string_to_c re-export from convert/mod.rs (all new
modules import from crate::convert::string:: directly).

No behavior or ABI change. cbindgen header symbol set unchanged;
emission order shifts due to cbindgen's module-path topological sort
(ordering-only diff per Unit R verification criteria).
2026-04-16 03:21:53 -07:00
Lahfir
f3f8c733c2 refactor(ffi): split tree.rs into tree/ submodules, strip inline comments
Move tree.rs into tree/flatten.rs (flatten_tree + flatten_recursive +
strings_to_c_array + its tests), tree/free.rs (free_c_string_array,
free_node_fields, ad_free_tree + null-free test) and tree/get.rs
(ad_get_tree). Drop inline //-comments that restate what the next line
already says.

Behavior unchanged: flatten_tree is still the existing recursive DFS
layout (Unit 4 rewrites to iterative BFS so child ranges address direct
children). cbindgen header byte-identical.
2026-04-16 03:19:49 -07:00
Lahfir
a9661cd69e refactor(ffi): split convert.rs into convert/ submodules
Move convert.rs helpers into convert/{string,rect,window,app,surface}.rs.
Each submodule carries just the helpers for one kind (C-string helpers,
rect conversion, window-info conversion, app-info conversion, surface-info
conversion) plus its focused test(s). convert/mod.rs re-exports the same
public surface so every caller keeps its existing
`use crate::convert::{string_to_c, ...}` paths intact.

No behavior, signature, or ABI change; cbindgen header is byte-identical.
Unit R precondition; Unit 7 later refines string helpers (lossy/NUL-safe).
2026-04-16 03:18:00 -07:00
Lahfir
c2ea56c295 refactor(ffi): split types.rs into one module per type
Move the 28 packed #[repr(C)] structs and #[repr(i32)] enums from
crates/ffi/src/types.rs into crates/ffi/src/types/{point,rect,node,
action,...}.rs, one declaration per file. types/mod.rs carries
explicit pub mod + pub use per type, so callers keep the same
`use crate::types::AdFoo` paths and cbindgen emits a byte-identical
generated header.

Purely mechanical; no behavior, signature, or ABI change. Unit R
precondition for later fix work where individual types are extended
(AdTreeOptions.surface, AdImageBuffer length encapsulation, etc.).
2026-04-16 03:16:57 -07:00
Lahfir
f54087fb3a refactor(ffi): eliminate error.rs unwrap with static CStr fallback
Replace the infallible-today .unwrap() on CString::new fallback with a
MessageSource enum that carries either an owned CString or a 'static
CStr pointer. Interior-NUL payloads now deterministically resolve to
the NUL_BYTE_FALLBACK constant rather than relying on an unwrap that
could panic if the literal ever gained a NUL byte.

Adds a regression test for interior-NUL handling.

Precondition for Unit 1's panic boundary, which needs a last-error
path that never allocates and never panics.
2026-04-16 03:14:31 -07:00
Lahfir
fe2cf4446d chore: ignore .context/ for local tooling artifacts 2026-04-13 19:11:54 -07:00
Jake Rosoman
88fec35f5f fix(ffi): use dynamic error codes and fix Box::from_raw UB in action result 2026-04-06 14:21:50 +10:00
Jake Rosoman
3cd32d49ef feat(ffi): copy generated header to include/ for distribution 2026-04-06 14:17:23 +10:00
Jake Rosoman
bae931d7c5 style(ffi): apply rustfmt formatting 2026-04-06 14:16:09 +10:00
Jake Rosoman
ecba145827 feat(ffi): add surfaces list and get_tree FFI 2026-04-06 14:14:55 +10:00
Jake Rosoman
15115149a5 feat(ffi): add screenshot capture and image buffer 2026-04-06 14:13:27 +10:00
Jake Rosoman
2adcbb4cc0 feat(ffi): add clipboard, mouse, and drag FFI functions 2026-04-06 14:12:05 +10:00
Jake Rosoman
297dfeb3e6 feat(ffi): add window list, focus, and operations 2026-04-06 14:10:33 +10:00
Jake Rosoman
42c61ff0c1 feat(ffi): add action types, execute_action, resolve_element 2026-04-06 14:08:50 +10:00
Jake Rosoman
80b0e1fbf4 feat(ffi): add app lifecycle FFI functions 2026-04-06 14:06:22 +10:00
Jake Rosoman
34b04e16bc feat(ffi): add tree flattening with depth-first AdNodeTree 2026-04-06 14:04:59 +10:00
Jake Rosoman
b0e42c13a2 feat(ffi): add adapter lifecycle and permissions check 2026-04-06 14:02:09 +10:00
Jake Rosoman
272071b133 feat(ffi): add thread-local error handling with C accessors 2026-04-06 14:00:01 +10:00
Jake Rosoman
c724bc4892 feat(ffi): add primitive, window, app, surface C types with conversions 2026-04-06 13:57:14 +10:00
Jake Rosoman
920e7ef719 feat: scaffold ffi crate with cbindgen 2026-04-06 13:53:27 +10:00
Lahfir
ccc734ecbc docs: add surface-first snapshot rule for overlays
When a sheet, alert, popover, or menu is open, agents should snapshot
the specific surface instead of the full window. Background refs are
irrelevant while an overlay has focus and waste tokens.
2026-03-03 12:34:50 -08:00
github-actions[bot]
eb607e2b8e
chore(main): release 0.1.11 (#17)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-03 09:54:25 -05:00
Lahfir
39b2bc6348 fix: show skill install prompt on all success paths
promptSkillInstall() was only called after fresh download. Now also
shows when binary already exists or comes from AGENT_DESKTOP_BINARY_PATH.
2026-03-03 06:50:02 -08:00
github-actions[bot]
ceb02470c1
chore(main): release 0.1.10 (#16)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-03 09:23:49 -05:00
Lahfir
208af12459 fix: add clawhub login step before sync in CI
clawhub CLI requires explicit login, env var alone is not enough.
2026-03-03 06:22:06 -08:00
github-actions[bot]
4abb4b9484
chore(main): release 0.1.9 (#15)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-03 09:19:22 -05:00
Lahfir
97665203a4
feat: scalable skill architecture with ClawHub auto-publishing (#14)
* feat: restructure skills for ClawHub publishing with CI auto-publish

- Sync core skill to 54 commands (add notification commands)
- Move macOS skill from .claude/skills/ to git-tracked skills/
- Extract Notification Center section to references/notifications.md
- Add ClawHub metadata (version, tags, requirements) to all SKILL.md
- Remove macos.md from core skill (moved to platform skill)
- Create scripts/link-skills.sh for local dev symlinks
- Add publish-skills CI job to release.yml (ClawHub auto-publish)
- Add skill install prompt to npm postinstall

* refactor: consolidate macOS into single agent-desktop skill

Merge agent-desktop-macos back into agent-desktop/references/macos.md
as a single publishable skill. Includes Notification Center content
inline rather than as a separate reference file.
2026-03-03 06:16:15 -08:00
Lahfir
b8149816b7
Merge pull request #13 from lahfir/release-please--branches--main--components--agent-desktop
chore(main): release 0.1.8
2026-03-01 00:56:57 -08:00
github-actions[bot]
b53a545dc9
chore(main): release 0.1.8 2026-03-01 08:36:36 +00:00
Lahfir
4a300c8cb0 feat: implement --compact flag to collapse single-child unnamed nodes
Activate the existing --compact CLI flag (previously a no-op) to reduce
tree verbosity by collapsing pass-through container nodes that carry no
semantic information (no ref, no name, no value, no description, no
states, exactly one child).

Slack with -i --compact: 264 → 214 nodes, ~5,967 → ~5,117 tokens (14%
reduction). All 161 refs preserved. Finder unaffected (3% reduction).

Handles Electron's empty-string-vs-None pattern using is_none_or.
2026-03-01 00:36:20 -08:00
Lahfir
a19c1b5132 feat: add electron/web app compatibility for accessibility tree traversal
Skip depth budget for non-semantic AXGroup/AXGenericElement wrappers with
empty name and value, allowing default --max-depth 10 to find 100+ refs
in Electron apps like Slack and VS Code (previously found only 3).

Raise resolver search depth from 20 to ABSOLUTE_MAX_DEPTH (50) so ref
resolution succeeds for deeply nested Electron elements.

Fix surface detection to check if focused window itself is the target
surface (Electron reports dialogs as focused window, not as children)
and check both AXRole and AXSubrole for surface matching.

Update Phase 2 (Windows) and Phase 3 (Linux) docs with equivalent
web/Electron compatibility patterns for their respective APIs.
2026-02-28 19:03:11 -08:00
Lahfir
4d9daeadce
Merge pull request #12 from lahfir/release-please--branches--main--components--agent-desktop
chore(main): release 0.1.7
2026-02-27 16:25:21 -08:00
github-actions[bot]
614f82bedc
chore(main): release 0.1.7 2026-02-28 00:24:51 +00:00
Lahfir
b1fd368f19
Merge pull request #11 from lahfir/feat/notification-management-macos
feat: add notification management commands (macOS)
2026-02-27 16:24:35 -08:00