app
Adom Bridge
Public Made by Adomby adom
Adom Bridge unleashes your AI onto your full PC: full power, full safety. The AI breaks out of the container it runs in and onto your real machine, managing and running your entire laptop as you: launch apps, move files, drive any window, control KiCad, Fusion 360 or your real signed-in browser. Works with any AI, cloud or local (Claude Desktop, Claude Code, Codex), no Hydrogen required. Bridge and its bridges are an operating system for AI, with managed Node and Python runtimes, and a human-onl
← Commit history
Publish 1.9.34
8 files changed
+404−321
README.md+8−2@@ -17,12 +17,18 @@ That installs the **container-side** pieces: |---|---| | `adom-desktop` | the main verb catalog — screenshots, files, shell, windows, notifications | | `adom-desktop-pup` | real-Chrome (Puppeteer) automation: sessions, tabs, eval, recording |-| `adom-desktop-kicad` | KiCad control: open/edit schematics + PCBs, exports, 3D viewer |-| `adom-desktop-fusion` | Fusion 360 control: designs, exports, BOM/CPL |+| `adom-desktop-notify` | native Windows toasts — progress bars, sticky reminders, buttons that round-trip |+| `adom-desktop-bridge-sdk` | author your own bridge: bridge.json, health/describe/rich-hint contracts, lifecycle verbs |+| `adom-desktop-bridge-ownership` | the boundary — what AD core owns vs what a bridge author owns |+| `adom-desktop-multi-ad` | address multiple Adom Desktops on one relay (`--target`, fan-out) |+| `adom-desktop-eagle` | EAGLE commands for Fusion 360 Electronics (`fusion_electron_run`) | | `adom-desktop-direct-api` | the loopback HTTP API for sibling apps | | `adom-desktop-installer` | how to drive the Windows installer (silent flags, logs) |+| `adom-desktop-windows-host` | driving the Windows host from Claude Code (AppContainer sandbox escape) | | `adom-desktop-discovery` | how a fresh container finds + refreshes the CLI | +> The **KiCad** and **Fusion 360** bridge skills (`adom-desktop-kicad`, `adom-desktop-fusion`) install from their own cloud-owned bridge pages, not this bundle.+ ## What it looks like The desktop app, connected to two containers, with the bridge bar and live activity log:
SKILL.md+54−10@@ -153,13 +153,6 @@ curl -sf http://127.0.0.1:8766/health **You are running on a Docker container. You have NO access to the user's desktop.** Guide them step-by-step, ask questions, wait for answers, and verify each step. -> **⚠ ALWAYS give the config for the CURRENT container — and make sure the relay is up first.**->-> When the user asks for "the copy-paste", "the server config", "the JSON", or anything similar:-> 1. **Source of truth is `adom-desktop setup_desktop`** — its `server_config` field is the ready-to-paste JSON, derived from *this* container's own relay URL.-> 2. **NEVER read the config out of `hydrogen_preferences`** (`adom-cli carbon user get` → `settings["desktop.container.*"]`). Those entries can be for *other* containers (e.g. a `dashboard` entry while the user is in `newdash`) and will be wrong. The current container's identity comes from its own `VSCODE_PROXY_URI` env var (`https://<host>/proxy/{{port}}/` → relay `wss://<host>/proxy/8765/`).-> 3. **Before handing over the config or claiming "connected", verify the relay service is actually running on your end** so the `…/proxy/8765/` URL doesn't return a **500** (500 = proxy has no upstream = relay not running). Probe `curl -sf http://127.0.0.1:8766/health` (note: `ss`/`netstat` may not see in-container sockets — probe with curl), and if it's down, `adom-desktop serve &` and re-check before proceeding.- ### Step 1: Check if already connected ```bash@@ -506,6 +499,8 @@ A dynamic bridge gets **one stable port** that AD reuses across every spawn and `spawn.persistent` (in `bridge.json`) is now surfaced in `bridge_list` (`spawn.persistent`) and means: **AD auto-respawns this bridge on death**, on its stable port (a supervisor runs each ~15 s). So "kill the bridge to load new code" → AD brings it right back at the same port the client keeps dialing. `bridge_install` / `refresh_bridges` also no longer fail with the cache-dir lock (AD reaps the process holding the dir as its cwd, then swaps; in-place merge fallback if a handle lingers). +**v1.8.186/187 — first-class bridges (panel + remote management + optional manifest fields).** The AD window has a full-height **Bridges sidebar** that shows each bridge as a mini-app (hero image, LED, rich tooltip); clicking one opens a wide pop-out with a **live console** (its `~/.adom/bridge-logs/<name>.log` + the per-bridge command history), a **ports/process debug view** (live PIDs — `>1` flags a runaway), persistent-vs-one-off + codebase-language badges, and all management actions (install/uninstall/**kill**/restart/pause/resume + "filter activity log to this bridge" + "copy contribution prompt"). The same management is available **remotely** to cloud/WSL2 threads via verbs: `bridge_info`/`bridge_detail` (rich aggregate), `bridge_log_read {name, sinceOffset?}` (**cursor-streaming remote tail -f**), `bridge_kill` (wipe one bridge's processes). As of v1.8.186 **all** bridges write their per-bridge log (bundled kicad/fusion360/puppeteer included, not just dynamic ones). New **optional** `bridge.json` fields (all back-compat, surfaced in the panel): `hero` (a relative filename on your wiki page, or a full URL — your app-icon; absent → a monogram tile), `languages` (override the auto-scan, e.g. `["Python"]`), `releasedAt` (ISO date; absent → bridge.json mtime).+ **v1.8.156 — strays + self-documenting hints.** AD now tracks every bridge PID it spawns (persisted across AD restarts) and **reaps all stray instances** before respawning — so you don't end up with 4 copies of one bridge fighting over the routing. `bridge_list` surfaces `spawn.instanceCount` per dynamic bridge (normally 0 or 1; **>1 = strays piled up** — visible at a glance). If a bridge verb fails to reach a healthy bridge, the error `_hint` tells you to retry / check `instanceCount` / `process_list`; and a verb response right after a (re)spawn carries an `_ad_note` saying the external host reconnects within a few seconds — **retry** instead of treating a transient "not connected" as terminal. For verb discovery from a bridge, `GET <ADOM_DIRECT_API_URL>/commands` lists every reachable verb (see [`skills/DIRECT_API.md`](DIRECT_API.md)). #### Bridge status chip — self-reported LED + tooltip (v1.8.152+)@@ -548,8 +543,10 @@ Reference implementations: [`scripts/sample-bridges/hello-python/server.py`](../ ### Desktop Notifications -- `notify_user` -- Send a desktop notification with optional action buttons.- - `title`, `body`, `level` (info/warning/error/emergency), `actions` (array of button labels)+- `notify_user` -- Send a native Windows toast (own AUMID + Adom logo). v1.9.33: rich types — progress bar, sticky reminder, action buttons that **bring AD to the foreground** on click + round-trip via `notify_response`.+ - `title`, `body`, `level` (info/success/warning/error/emergency), `actions` (label buttons) — plus `scenario` ("reminder"=sticky), `durationLong`, `buttons` (`[{label, arguments?}]`), `progress` (`{title, value 0..1, status, valueOverride}`), `inputs`, `id` (correlation id for the round-trip).+ - Full guide + copy-paste examples per type: the **adom-desktop-notify** skill ([`skills/NOTIFY.md`](NOTIFY.md)).+- `notify_response` -- Poll which toast button the user clicked, by correlation `id`. `{pending:true}` until they click; `{pending:false, action}` once they do (consumed on read). ### KiCad Tools @@ -1151,13 +1148,32 @@ adom-desktop watch '{"command":"fusion_walk_cloud_tree","args":{...},"interval": - `fusion_search_cloud_files` -- **Long-running.** Recursive substring search on file names across cloud folders. v1.0.2+ of the add-in adds: per-folder timeout, doEvents() every 50 files, per-file try/except, iterative BFS, and a stale-lock watchdog — Fusion stays responsive throughout (no "Not Responding" freeze). - **Args (no hard upper caps in v1.0.2+):** `query` (required, substring, case-insensitive), `projectName` (optional, defaults to active), `folderPath` (optional starting subfolder — **narrow with this**), `recursive` (default false), `maxDepth` (default 2), `maxFolders` (default 10), `maxResults` (default 20), `folderTimeout` (default 30s), `searchTimeout` (default 120s), `timeout` (HTTP envelope, default 620s). - **Before calling:** if the user knows roughly where the file is (e.g. customer-named folder), **ask them**. Naming a subfolder cuts 5-40 min searches down to ~10s.- - **Why slow:** Autodesk's free Fusion 360 Python API has no indexed file-search endpoint. We walk the Data API one folder at a time — each = one HTTP round-trip to Autodesk. **This is an Autodesk API limitation, not adom-desktop's.** When telling the user the search is taking a while, attribute it to Autodesk / Fusion 360, not to adom-desktop's bridge. Autodesk's PAID Autodesk Platform Services (APS, formerly Forge) Data Management API has indexed search; teams with APS credentials can fork the bridge at `plugins/fusion360/addin/AdomBridge/` to add an `aps_search` verb and PR it back to https://wiki.adom.inc/adom/adom-desktop — the community benefits.+ - **Why slow:** Autodesk's free Fusion 360 Python API has no indexed file-search endpoint. We walk the Data API one folder at a time — each = one HTTP round-trip to Autodesk. **This is an Autodesk API limitation, not adom-desktop's.** When telling the user the search is taking a while, attribute it to Autodesk / Fusion 360, not to adom-desktop's bridge. **There is now a FAST native alternative — `aps_search` (see "APS — fast indexed cloud search" below).** It hits Autodesk Platform Services' indexed Data Management API and returns ~instantly, even when Fusion is CLOSED. Call `aps_status` first; if `configured:true`, prefer `aps_search` and only fall back to this folder-walking verb when APS isn't set up. - **Interpreting results (critical for no-false-negatives):** the response contains `searchComplete` (bool), `foldersSkipped`, `filesSkipped`, `truncated`, `folderLimitReached`, `searchTimedOut`. `searchComplete:true` ONLY when **all five** are clean — that's the confidence flag. If `searchComplete:false`, the search hit a cap before exhausting the scope; **don't report "file not found"** — re-run with broader caps OR narrower `folderPath`. - **Case sensitivity:** fully case-insensitive both directions (query and file names lowercased). Substring match, not whole-word — `"cosm"` matches `COSMIIC`, `COSMOCOIL`, etc. - **Cost feedback in response:** `costAnalysis: {elapsedSeconds, foldersPerSecond, estimatedSecondsPer100Folders}` so the AI can budget the next search realistically. - **Real numbers from a live test:** searching `Main/Molecules` recursively (173 folders deep, max 8 depth) for "cosmiic" returned in 168 s with `searchComplete:true, totalFound:3`. Fusion stayed `main_thread:responsive` throughout. - **Pair with `watch`** for streaming progress updates: see below. +#### APS — fast indexed cloud search (v1.8.182+)++`aps_search` is a **native, ~instant** alternative to the folder-walking `fusion_search_cloud_files`. It queries **Autodesk Platform Services** (APS, formerly Forge) **Data Management API**, which has a real server-side index — so a search that takes minutes by folder-walking returns in well under a second, and it works **even when Fusion 360 is closed** (it's pure HTTPS, no bridge required). adom-desktop runs its own 3-legged **PKCE** OAuth, so the user's APS **client secret never leaves their machine**.++**One-time setup (user side):**+1. Register a free app at <https://aps.autodesk.com> → enable the **Data Management API**.+2. Set the app's **Callback URL** to exactly `http://localhost:8910/aps/callback`.+3. Put the **Client ID** in AD config key `aps_client_id` (in `~/.adom/config.json`) or via Settings.+4. Run `aps_auth` once — it opens the system browser for Autodesk sign-in. Tokens cache in `~/.adom/aps_tokens.json` and auto-refresh; you won't be prompted again.++**Verbs:**+- `aps_status` — **call this first.** Non-prompting. Returns `{configured, signedIn}`. `configured:false` → no client id; use `fusion_search_cloud_files`. `configured:true, signedIn:false` → run `aps_auth`. Both true → `aps_search` is ready.+- `aps_auth` — interactive sign-in (opens browser). One-time per device.+- `aps_hubs` — `{hubs[]{id,name}}` (your Autodesk teams).+- `aps_projects {hubId?}` — `{hubId, projects[]{id,name}}` (default: first hub).+- `aps_search {query, projectName?, projectId?, hubId?}` — `{query, hubId, count, results[]{name,id,type,lastModified,project}}`. `query` is a case-insensitive substring of the file's displayName. Without `projectName`/`projectId` it searches up to 8 projects in the hub. Open a hit with `fusion_open_cloud_file {fileName, projectName}`.++**Decision flow for an AI doing a cloud file search:** `aps_status` → if `configured` (sign in with `aps_auth` if needed) use `aps_search`; else fall back to `fusion_search_cloud_files` and tell the user that registering a free APS app (above) would make future searches instant.+ **Export formats for `fusion_export_cloud_file`:** | Format | Extension | Use case |@@ -1462,6 +1478,34 @@ The mouse/keyboard verbs above use `SendInput` = real OS input, which only lands `role` ∈ `button|edit|link|checkbox|menuitem|tab|listitem|combobox|radiobutton|document|group|spinner`. **Caveat (surfaced in `_hint`, not hidden):** some shadow-DOM controls aren't in the UIA tree — if `find_control` reports `invokable:false`/not-found, the control genuinely needs a **foregrounded** `desktop_click`. So: prefer `desktop_ui_click` (background) → fall back to `desktop_click` (foreground) only when UIA can't reach it. +#### Near-complete UIA surface (v1.9.0+) — enumerate, read, drive, with a foreground-hint contract++v1.9.0 expands UIA from ~3 patterns to a near-complete agent-drivable client. **Every UIA verb now returns a `foreground` block** so you know — *before and after* — whether a call steals the user's screen:++```+"foreground": { "predicted": "background|fallback-possible|foreground", // from the static class + the resolved path, BEFORE acting+ "observed": "background|foreground|n/a", // MEASURED (GetForegroundWindow before/after); reads = n/a+ "raised": true|false, // did THIS call bring a window to front?+ "pathTaken": "uia-pattern|sendinput-fallback|setfocus|read-only",+ "reason": "<short>" }+```++**Shared selector on every verb:** `{hwnd|window(titleContains)}` + `{name|contains|automationId|role}` + `scope:"window"(default)|"popups"|"subtree:<automationId>"` + `nth:<int>` (0-based — target the Nth match without the exact name). Prefer `automationId` (stable, locale-independent). Rects are screen px with `centerX/centerY`; `space:"image"+shotId` is accepted wherever a coordinate is taken.++- **Enumeration (read-only, ungated):**+ - `desktop_ui_tree {window|hwnd, maxDepth?, roleFilter?[], nameContains?, maxNodes?(500), scope?}` — **flagship.** Dump the a11y subtree; nodes carry rect + invokable/settable + value/toggleState/expandState/selected/rangeValue + `children[]`. Honors Chromium's lazy tree (wake-retry); `truncated:true` when it hits `maxNodes` (logged, never silent).+ - `desktop_ui_from_point {x,y | space:"image",shotId,x,y}` — the control under a pixel.+ - `desktop_find_controls {selector, maxResults?(25)}` — ALL matches as an array with `nth` indices (vs `find_control`'s single best).+- **Reading (read-only, ungated):** `desktop_ui_text {selector, range?:"all"|"selection"|"visible"}` (TextPattern); `desktop_ui_get {selector}` (current value/toggle/range/selection/expand state); `desktop_ui_grid_get {selector, row?, col?}` (GridPattern cell + rowCount/columnCount).+- **Actions (gated by shell-auto-approve; background-first via the pattern, SendInput-click fallback only when the pattern is absent → then `raised:true`):** `desktop_ui_toggle {selector, state?}`, `desktop_ui_expand {selector, action?:expand|collapse|toggle}`, `desktop_ui_select {selector, action?:select|add|remove}`, `desktop_ui_scroll {selector, dir?, amount?}`, `desktop_ui_set_range {selector, value}`, `desktop_ui_focus {selector}` (**bg:foreground** — usually raises; reports it), `desktop_ui_window {window, action:minimize|maximize|restore|close|move|resize}`.+- **Events (v1.9.3 — REACT instead of poll; read-only, ungated):**+ - `desktop_ui_watch {event, window?|hwnd?, properties?}` → `{watchId}`. `event` ∈ `focus` (desktop-wide "what got focus") | `window-opened`/`window-closed` (new/closed top-level windows — these three can be global) | `invoke` | `selection` | `text-changed` | `text-selection` | `menu-opened` | `tooltip-opened` | `notification` | `live-region` | `structure` | `property` (these REQUIRE a `window`, else `errorCode:window_required` — avoids a desktop firehose). Cap 16 watches.+ - `desktop_ui_events {sinceSeq?, watchId?, max?}` → DRAIN the buffer (poll cursor; there is NO server-push over the relay). Returns `{events:[{seq, watchId, event, ts, element:{name,role,automationId,rect,hwnd}, eventType|property|changeType}], nextSeq, highestSeq, missedEvents, activeWatches}`. Poll with `sinceSeq=nextSeq`. Events ALSO appear in the AD GUI activity-log. The 500-deep ring is shared across all watches; `missedEvents:true` = you polled too slowly.+ - `desktop_ui_unwatch {watchId|"all"}` → stop. **Canonical use:** waiting on an OTP/confirm dialog → `ui_watch {event:"window-opened"}`, poll `ui_events` until the window appears, then act on it (no busy screenshot loop).+- **Introspection:** `desktop_ui_capabilities {}` — **read-only, ungated.** The foreground-hint TABLE per verb (`staticClass` + measured `{win32, chromium-pattern, chromium-fallback}`). **Consult it BEFORE a call to know if it'll steal the screen, without acting.** `desktop_ui_selftest {confirm?, window?}` writes/refreshes it (empirically measures when `confirm:true`).++**Integrity boundary:** an elevated/admin window or the UAC secure desktop returns `errorCode:"uia_integrity_blocked"` (never a silent no-op) — a normal-integrity bridge cannot drive a higher-integrity process.+ #### Zero-math coordinate clicks — `coordMap` + image-space (v1.8.175+) You no longer recompute scale/offset/DPI to click what you see in a screenshot. Every screenshot verb returns a **`coordMap`**:
dist/linux/adom-desktop⋯ 1 unchanged line ⋯
package.json+50−50@@ -1,50 +1,50 @@-{- "slug": "adom-desktop",- "version": "1.8.182",- "type": "app",- "hero": { "path": "docs/hero.png" },- "description": "Laptop bridge: file transfer, screenshots, notifications, KiCad + Fusion 360 control, real-Chrome (Puppeteer) automation, and Hydrogen Desktop proxy. One install gives Claude every desktop verb it needs.",- "keywords": [- "adom-desktop",- "tauri",- "claude-code",- "bridge",- "kicad",- "fusion360",- "puppeteer",- "chrome",- "websocket",- "cli",- "screenshot",- "recording",- "desktop-automation"- ],- "tags": [- "adom-desktop",- "tauri",- "claude-code",- "bridge",- "kicad",- "fusion360",- "puppeteer",- "desktop-automation"- ],- "scripts": {- "install": "./install.sh",- "uninstall": "./uninstall.sh"- },- "install": {- "binary_name": "adom-desktop"- },- "dependencies": {},- "files": [- "SKILL.md",- "README.md",- "install.sh",- "uninstall.sh",- "docs/**",- "skills/**",- "dist/**",- "version.json"- ]-}+{ + "slug": "adom-desktop", + "version": "1.9.34", + "type": "app", + "hero": { "path": "docs/hero.png" }, + "description": "Laptop bridge: file transfer, screenshots, notifications, KiCad + Fusion 360 control, real-Chrome (Puppeteer) automation, and Hydrogen Desktop proxy. One install gives Claude every desktop verb it needs.", + "keywords": [ + "adom-desktop", + "tauri", + "claude-code", + "bridge", + "kicad", + "fusion360", + "puppeteer", + "chrome", + "websocket", + "cli", + "screenshot", + "recording", + "desktop-automation" + ], + "tags": [ + "adom-desktop", + "tauri", + "claude-code", + "bridge", + "kicad", + "fusion360", + "puppeteer", + "desktop-automation" + ], + "scripts": { + "install": "./install.sh", + "uninstall": "./uninstall.sh" + }, + "install": { + "binary_name": "adom-desktop" + }, + "dependencies": {}, + "files": [ + "SKILL.md", + "README.md", + "install.sh", + "uninstall.sh", + "docs/**", + "skills/**", + "dist/**", + "version.json" + ] +}
page.json+2−2@@ -1,6 +1,6 @@ { "slug": "adom-desktop",- "version": "1.8.182",+ "version": "1.9.34", "type": "app", "description": "Laptop bridge: file transfer, screenshots, notifications, KiCad + Fusion 360 control, real-Chrome (Puppeteer) automation, and Hydrogen Desktop proxy. One install gives Claude every desktop verb it needs.", "keywords": [@@ -55,7 +55,7 @@ }, "hero": { "type": "image",- "path": "hero-v2.png"+ "path": "docs/hero.png" }, "updated_at": "2026-06-25T02:44:45.079Z", "title": "Adom Desktop",
skills/adom-desktop-pup/SKILL.md+231−231@@ -1,231 +1,231 @@-name: pup-user-invocable: true-description: "Control Puppeteer browser windows on the user's desktop via the adom-desktop CLI. Use for: opening/closing browser windows, reloading pages, taking screenshots, evaluating JS, flashing taskbar alerts, navigating URLs, checking errors, managing multi-session Chrome. Trigger words: pup, puppeteer, browser window, browser reload, browser screenshot, pup reload, pup screenshot, pup alert, browser debug, visual debug, open in browser, reload browser, flash taskbar, browser_reload, browser_alert_window, browser_open_window, browser_screenshot, browser_eval."--# Pup — Puppeteer Browser Control--Control Chrome browser windows on the user's desktop via the `adom-desktop` CLI.--## How to Run Commands--All `browser_*` commands are run via the `adom-desktop` CLI using Bash:--```bash-adom-desktop <command> '<json_args>'-```--**Examples:**-```bash-adom-desktop browser_reload '{"sessionId":"dart2"}'-adom-desktop browser_alert_window '{"sessionId":"dart2"}'-adom-desktop browser_screenshot '{"sessionId":"dart2"}'-adom-desktop browser_open_window '{"sessionId":"myapp","profile":"myapp","url":"http://localhost:3000"}'-adom-desktop browser_eval '{"sessionId":"dart2","expr":"document.title"}'-```--**IMPORTANT:** These are NOT MCP tools. They are Bash commands. Use the Bash tool to run them.--## Available Commands--### Session Management-| Command | Description | Required Args |-|---|---|---|-| `browser_open_window` | **Spawns a new pup window**. Only use when there isn't an existing session of that name. | `sessionId`, `profile`, `url` |-| `browser_close_window` | Close a specific session | `sessionId` |-| `browser_focus_window` | Bring window to foreground | `sessionId` (optional) |-| `browser_alert_window` | Flash taskbar icon (non-intrusive) | `sessionId` (optional) |-| `browser_switch_window` | Switch global active session | `sessionId` |-| `browser_list_windows` | List all open sessions | none |--### ⚠ Don't double-open windows — `browser_navigate` reuses, `browser_open_window` spawns--If a session already exists (verify with `browser_list_windows` or `browser_list_tabs`), point an existing tab at a new URL with **`browser_navigate`** — passing `tabId` if you want to be explicit:--```bash-adom-desktop browser_navigate '{"sessionId":"chip-fetcher","tabId":"tab-1","url":"<new-url>"}'-```--Using `browser_open_window` against a session that's already alive **spawns a second physical pup window** with the same title. The new tab is tracked but the old window is orphaned — the user sees two windows showing the same content. Verified painful 2026-05-04: opened a chip-fetcher dashboard verification tab while the original UL Pro detail tab was still active; user noticed the duplicate before I did. Cleanup required `desktop_close_window` and a tracked-tab marker dance.--**Decision tree:**-- Need a new session entirely (different `sessionId`) → `browser_open_window`.-- Need to reuse an existing session's tab → `browser_navigate` (with `tabId` if multiple tabs).-- Need a second tab in the same session → `browser_open_tab` (not `browser_open_window`).--### ⚠ Cross-sessionId duplicates — same app, two windows--The double-open trap above catches *same-sessionId* re-opens. There's a second flavor that catches you across conversation boundaries: **a previous session left a pup window open under one `sessionId`, and the new conversation opens the same app under a different `sessionId`** — adom-desktop happily creates a second window because the keys differ. The user sees two pup windows showing the same content.--**Always run `browser_status '{}'` before `browser_open_window`** at the start of a new conversation, especially after a context compaction. Look for any session whose URL maps to the app you're about to open. Reuse it (`browser_navigate`) instead of creating a peer.--Verified 2026-05-07 with chipsmith: an earlier session had `sessionId: cs-v2` open at `…/proxy/8872/`. New conversation opened `sessionId: chipsmith` at the same port — two windows for the same chip. User noticed the duplicate first.--When closing a duplicate, prefer the OLDER one (lower `cb=` timestamp in URL); the newer one usually has the working state you just built up.--### Page Commands (all accept optional `sessionId`)-| Command | Description | Key Args |-|---|---|---|-| `browser_reload` | Reload page + clear error log | `sessionId` |-| `browser_navigate` | Navigate to new URL | `url`, `sessionId` |-| `browser_screenshot` | Capture page screenshot (lossless PNG) | `sessionId`, `maxWidth`, `fullPage` |-| `browser_eval` | Evaluate JS expression in page | `expr`, `sessionId` |-| `browser_errors` | Get console/page errors | `sessionId` |-| `browser_wait` | Wait for content to settle | `ms` (default 3000) |-| `browser_status` | Check all sessions | none |-| `browser_close` | Close ALL sessions | none |--## Common Patterns--### Reload + Alert (after code changes)-```bash-adom-desktop browser_reload '{"sessionId":"dart2"}' && \-adom-desktop browser_alert_window '{"sessionId":"dart2"}'-```--### Debug Loop-1. Edit code-2. `browser_reload`-3. `browser_wait` (3-5s for 3D tiles)-4. `browser_screenshot`-5. `browser_errors`-6. Analyze, repeat if needed--### Check Connectivity-```bash-adom-desktop ping-```--## Pick ONE surface — respect the user's window-mode preference--Hydrogen stores a per-session **window mode** preference: `webview` (default) or `pup`. Check it before opening a window:--```bash-adom-cli hydrogen probe # → { ..., "window_mode": "webview" | "pup" }-```--- **`webview`** → open as a Hydrogen webview tab (`webview open-or-refresh`)-- **`pup`** → open via Adom Desktop (`pup browser_open_window`). If Adom Desktop isn't reachable, fall back to webview and warn.--**Never open the same content in both at once.** User-feedback 2026-04-26: *"why the fuck did you open something in pup-and some stuff in webview. pick one or the other."*--Two costs of dual-opening:-1. Two copies of the page → user sees changes in one but not the- other → "why doesn't my upload show up?" or "which one is- real?".-2. Background WS / polling traffic doubles, which makes the- Cloudflare-blipping problem worse.--When window-mode is `webview`, the decision rule:--| Use case | Surface | Why |-|---|---|---|-| Automated test, screenshot, eval, ralph loop, scripted multi-step demo | **pup** (even in webview mode — this is automation, not user-facing) | scriptable; doesn't compete with the user's tabs; pup's `browser_eval` works on every tab |-| Showing the user a finished result they should look at | **Hydrogen webview tab** (`adom-cli hydrogen webview open-or-refresh`) | the tab persists in their workspace; they can interact normally |-| Both at once for the SAME content | **don't** | pick whichever role the situation calls for and stick with it |--When window-mode is `pup`, everything goes through Adom Desktop — including user-facing results.--If you've opened the user's Hydrogen tab for a dashboard / viewer,-use **that** tab for screenshots too via `adom-cli hydrogen-screenshot panel --name "<tab-name>"`. Don't ALSO open a pup-window of the same URL. If you started in pup for testing and the-user wants to see the final state, route them to a Hydrogen tab-once and close the pup window.--Memory rule still applies for testing: pup is the testing surface,-NOT Hydrogen. The point of THIS rule is: don't open both for the-SAME content at the SAME time.--## Multi-URL workflows — ONE window, MANY tabs--When you'd open more than one URL for the same user task (vendor-fetch, multi-doc review, compare-pages workflows), put them in-**tabs of one window**, not separate windows. User feedback-2026-04-26: *"when you make all those pup windows, they clutter-up the user's desktop, so you should open all of those as tabs-in one pup window."*--```bash-# 1) Create the window with the first URL.-adom-desktop browser_open_window '{- "sessionId":"my-task","profile":"my-task",- "url":"https://first.example.com"-}'--# 2) Add the rest as tabs in the SAME session.-adom-desktop browser_open_tab '{"sessionId":"my-task","url":"https://second.example.com"}'-adom-desktop browser_open_tab '{"sessionId":"my-task","url":"https://third.example.com"}'--# 3) Activate a specific tab when you want to screenshot or eval it.-adom-desktop browser_switch_tab '{"sessionId":"my-task","tabId":"tab-2"}'-adom-desktop browser_screenshot '{"sessionId":"my-task","maxWidth":1500}'-adom-desktop browser_eval '{"sessionId":"my-task","expr":"document.title"}'-```--`browser_open_tab` lives alongside the documented `browser_*`-verbs. Same JSON-args convention; same `sessionId` semantics.--Pick a stable `profile` name across runs of the same task so the-cookie jar persists — the user logs in once and reuses for 12+-months. Cross-vendor cookie blending is fine because cookies are-per-domain.--## Verify what you opened — `"ok": true` is not enough--`browser_open_window` / `browser_open_tab` returns `"ok": true` as-soon as the navigation starts, NOT when the page actually-rendered useful content. Always sanity-check the result:--```bash-# Title check — fastest. 404s, anti-bot blocks, and login walls-# all change the title.-adom-desktop browser_eval '{"sessionId":"my-task","expr":"document.title"}'--# Screenshot — slowest, most thorough. Use when title isn't-# discriminating (e.g. SPA that updates title async).-adom-desktop browser_screenshot '{"sessionId":"my-task"}'-```--**Never blindly template a part number into a vendor URL pattern-without verifying the page exists.** The cost of a 404 you didn't-catch is the user spotting it instead. (Real example: shipped a-manufacturer-search-URL pattern that 404'd on `ti.com/product/`-and `nxp.com/` for parts those vendors don't make. Using a-`google.com/search?q=…` link routes through Google's resolver-and gets a real product hit on the first result.)--## Key Rules--- **Always pass `sessionId`** — never use `'{}'` as args. The default targets the *active* session which may not be what you want.-- **`adom-desktop browser_<cmd> <json>` — never `adom-desktop pup <cmd> --flags`.** There is no `pup` subcommand at the top level; that pattern fails with "Invalid JSON args".-- **No semicolons in `browser_eval`** — use comma operator or IIFE instead.-- **Sessions persist** — IndexedDB, cookies, localStorage survive restarts. Profile name is the persistence key.-- **Each session = independent Chrome window** — crash-isolated. Multiple tabs share one session and one cookie jar.-- **Never kill Chrome broadly** — use `browser_close_window` for specific sessions.-- **Always reload + alert after browser-facing code changes** — the user expects to see updates immediately.--## Full Reference--For detailed docs (profiles, sleep/wake recovery, troubleshooting, first-time setup), see `~/.claude/skills/adom/guides/pup.md`.--## Pup windows beat Hydrogen webviews for dashboards--For long-running dashboards (chip-fetcher progress, chipfit batch results, watchdog status, build watcher), prefer a dedicated pup browser window over a Hydrogen webview tab. Reasons:--1. **Hydrogen real estate** — webviews occupy a panel slot. The user is already coding/browsing in Hydrogen; a permanent dashboard tab pushes other content off-screen.-2. **Two-pup pattern** — works well: one pup window pinned as the dashboard, a second pup used for ad-hoc scraping/clicking. They don't fight each other.-3. **Persistence** — pup windows survive Hydrogen reloads.--The pattern applies to chip-fetcher, chipfit, chipsmith, any tool that emits a continuous progress UI.--## Reuse pup sessions — don't recreate--Each `browser_open_window` call with the same `sessionId` reuses the existing window if it's already open. Don't close + reopen reflexively; just `browser_reload` or navigate. Closing and reopening loses any auth state the window had.--## Don't close tabs the user didn't ask to close--The agent owns the windows it opened (by session-id). The user's pre-existing tabs in those same windows are NOT yours to close. When in doubt: open a new session-id, work there, close that one when done. Past pattern: closed a user's research tab while cleaning up after a verification run.+--- +name: pup +user-invocable: true +description: "Control Puppeteer browser windows on the user's desktop via the adom-desktop CLI. Use for: opening/closing browser windows, reloading pages, taking screenshots, evaluating JS, flashing taskbar alerts, navigating URLs, checking errors, managing multi-session Chrome. Trigger words: pup, puppeteer, browser window, browser reload, browser screenshot, pup reload, pup screenshot, pup alert, browser debug, visual debug, open in browser, reload browser, flash taskbar, browser_reload, browser_alert_window, browser_open_window, browser_screenshot, browser_eval." +--- + +# Pup — Puppeteer Browser Control + +Control Chrome browser windows on the user's desktop via the `adom-desktop` CLI. + +## How to Run Commands + +All `browser_*` commands are run via the `adom-desktop` CLI using Bash: + +```bash +adom-desktop <command> '<json_args>' +``` + +**Examples:** +```bash +adom-desktop browser_reload '{"sessionId":"dart2"}' +adom-desktop browser_alert_window '{"sessionId":"dart2"}' +adom-desktop browser_screenshot '{"sessionId":"dart2"}' +adom-desktop browser_open_window '{"sessionId":"myapp","profile":"myapp","url":"http://localhost:3000"}' +adom-desktop browser_eval '{"sessionId":"dart2","expr":"document.title"}' +``` + +**IMPORTANT:** These are NOT MCP tools. They are Bash commands. Use the Bash tool to run them. + +## Available Commands + +### Session Management +| Command | Description | Required Args | +|---|---|---| +| `browser_open_window` | **Spawns a new pup window**. Only use when there isn't an existing session of that name. | `sessionId`, `profile`, `url` | +| `browser_close_window` | Close a specific session | `sessionId` | +| `browser_focus_window` | Bring window to foreground | `sessionId` (optional) | +| `browser_alert_window` | Flash taskbar icon (non-intrusive) | `sessionId` (optional) | +| `browser_switch_window` | Switch global active session | `sessionId` | +| `browser_list_windows` | List all open sessions | none | + +### ⚠ Don't double-open windows — `browser_navigate` reuses, `browser_open_window` spawns + +If a session already exists (verify with `browser_list_windows` or `browser_list_tabs`), point an existing tab at a new URL with **`browser_navigate`** — passing `tabId` if you want to be explicit: + +```bash +adom-desktop browser_navigate '{"sessionId":"chip-fetcher","tabId":"tab-1","url":"<new-url>"}' +``` + +Using `browser_open_window` against a session that's already alive **spawns a second physical pup window** with the same title. The new tab is tracked but the old window is orphaned — the user sees two windows showing the same content. Verified painful 2026-05-04: opened a chip-fetcher dashboard verification tab while the original UL Pro detail tab was still active; user noticed the duplicate before I did. Cleanup required `desktop_close_window` and a tracked-tab marker dance. + +**Decision tree:** +- Need a new session entirely (different `sessionId`) → `browser_open_window`. +- Need to reuse an existing session's tab → `browser_navigate` (with `tabId` if multiple tabs). +- Need a second tab in the same session → `browser_open_tab` (not `browser_open_window`). + +### ⚠ Cross-sessionId duplicates — same app, two windows + +The double-open trap above catches *same-sessionId* re-opens. There's a second flavor that catches you across conversation boundaries: **a previous session left a pup window open under one `sessionId`, and the new conversation opens the same app under a different `sessionId`** — adom-desktop happily creates a second window because the keys differ. The user sees two pup windows showing the same content. + +**Always run `browser_status '{}'` before `browser_open_window`** at the start of a new conversation, especially after a context compaction. Look for any session whose URL maps to the app you're about to open. Reuse it (`browser_navigate`) instead of creating a peer. + +Verified 2026-05-07 with chipsmith: an earlier session had `sessionId: cs-v2` open at `…/proxy/8872/`. New conversation opened `sessionId: chipsmith` at the same port — two windows for the same chip. User noticed the duplicate first. + +When closing a duplicate, prefer the OLDER one (lower `cb=` timestamp in URL); the newer one usually has the working state you just built up. + +### Page Commands (all accept optional `sessionId`) +| Command | Description | Key Args | +|---|---|---| +| `browser_reload` | Reload page + clear error log | `sessionId` | +| `browser_navigate` | Navigate to new URL | `url`, `sessionId` | +| `browser_screenshot` | Capture page screenshot (lossless PNG) | `sessionId`, `maxWidth`, `fullPage` | +| `browser_eval` | Evaluate JS expression in page | `expr`, `sessionId` | +| `browser_errors` | Get console/page errors | `sessionId` | +| `browser_wait` | Wait for content to settle | `ms` (default 3000) | +| `browser_status` | Check all sessions | none | +| `browser_close` | Close ALL sessions | none | + +## Common Patterns + +### Reload + Alert (after code changes) +```bash +adom-desktop browser_reload '{"sessionId":"dart2"}' && \ +adom-desktop browser_alert_window '{"sessionId":"dart2"}' +``` + +### Debug Loop +1. Edit code +2. `browser_reload` +3. `browser_wait` (3-5s for 3D tiles) +4. `browser_screenshot` +5. `browser_errors` +6. Analyze, repeat if needed + +### Check Connectivity +```bash +adom-desktop ping +``` + +## Pick ONE surface — respect the user's window-mode preference + +Hydrogen stores a per-session **window mode** preference: `webview` (default) or `pup`. Check it before opening a window: + +```bash +adom-cli hydrogen probe # → { ..., "window_mode": "webview" | "pup" } +``` + +- **`webview`** → open as a Hydrogen webview tab (`webview open-or-refresh`) +- **`pup`** → open via Adom Desktop (`pup browser_open_window`). If Adom Desktop isn't reachable, fall back to webview and warn. + +**Never open the same content in both at once.** User +feedback 2026-04-26: *"why the fuck did you open something in pup +and some stuff in webview. pick one or the other."* + +Two costs of dual-opening: +1. Two copies of the page → user sees changes in one but not the + other → "why doesn't my upload show up?" or "which one is + real?". +2. Background WS / polling traffic doubles, which makes the + Cloudflare-blipping problem worse. + +When window-mode is `webview`, the decision rule: + +| Use case | Surface | Why | +|---|---|---| +| Automated test, screenshot, eval, ralph loop, scripted multi-step demo | **pup** (even in webview mode — this is automation, not user-facing) | scriptable; doesn't compete with the user's tabs; pup's `browser_eval` works on every tab | +| Showing the user a finished result they should look at | **Hydrogen webview tab** (`adom-cli hydrogen webview open-or-refresh`) | the tab persists in their workspace; they can interact normally | +| Both at once for the SAME content | **don't** | pick whichever role the situation calls for and stick with it | + +When window-mode is `pup`, everything goes through Adom Desktop — including user-facing results. + +If you've opened the user's Hydrogen tab for a dashboard / viewer, +use **that** tab for screenshots too via `adom-cli hydrogen +screenshot panel --name "<tab-name>"`. Don't ALSO open a pup +window of the same URL. If you started in pup for testing and the +user wants to see the final state, route them to a Hydrogen tab +once and close the pup window. + +Memory rule still applies for testing: pup is the testing surface, +NOT Hydrogen. The point of THIS rule is: don't open both for the +SAME content at the SAME time. + +## Multi-URL workflows — ONE window, MANY tabs + +When you'd open more than one URL for the same user task (vendor +fetch, multi-doc review, compare-pages workflows), put them in +**tabs of one window**, not separate windows. User feedback +2026-04-26: *"when you make all those pup windows, they clutter +up the user's desktop, so you should open all of those as tabs +in one pup window."* + +```bash +# 1) Create the window with the first URL. +adom-desktop browser_open_window '{ + "sessionId":"my-task","profile":"my-task", + "url":"https://first.example.com" +}' + +# 2) Add the rest as tabs in the SAME session. +adom-desktop browser_open_tab '{"sessionId":"my-task","url":"https://second.example.com"}' +adom-desktop browser_open_tab '{"sessionId":"my-task","url":"https://third.example.com"}' + +# 3) Activate a specific tab when you want to screenshot or eval it. +adom-desktop browser_switch_tab '{"sessionId":"my-task","tabId":"tab-2"}' +adom-desktop browser_screenshot '{"sessionId":"my-task","maxWidth":1500}' +adom-desktop browser_eval '{"sessionId":"my-task","expr":"document.title"}' +``` + +`browser_open_tab` lives alongside the documented `browser_*` +verbs. Same JSON-args convention; same `sessionId` semantics. + +Pick a stable `profile` name across runs of the same task so the +cookie jar persists — the user logs in once and reuses for 12+ +months. Cross-vendor cookie blending is fine because cookies are +per-domain. + +## Verify what you opened — `"ok": true` is not enough + +`browser_open_window` / `browser_open_tab` returns `"ok": true` as +soon as the navigation starts, NOT when the page actually +rendered useful content. Always sanity-check the result: + +```bash +# Title check — fastest. 404s, anti-bot blocks, and login walls +# all change the title. +adom-desktop browser_eval '{"sessionId":"my-task","expr":"document.title"}' + +# Screenshot — slowest, most thorough. Use when title isn't +# discriminating (e.g. SPA that updates title async). +adom-desktop browser_screenshot '{"sessionId":"my-task"}' +``` + +**Never blindly template a part number into a vendor URL pattern +without verifying the page exists.** The cost of a 404 you didn't +catch is the user spotting it instead. (Real example: shipped a +manufacturer-search-URL pattern that 404'd on `ti.com/product/` +and `nxp.com/` for parts those vendors don't make. Using a +`google.com/search?q=…` link routes through Google's resolver +and gets a real product hit on the first result.) + +## Key Rules + +- **Always pass `sessionId`** — never use `'{}'` as args. The default targets the *active* session which may not be what you want. +- **`adom-desktop browser_<cmd> <json>` — never `adom-desktop pup <cmd> --flags`.** There is no `pup` subcommand at the top level; that pattern fails with "Invalid JSON args". +- **No semicolons in `browser_eval`** — use comma operator or IIFE instead. +- **Sessions persist** — IndexedDB, cookies, localStorage survive restarts. Profile name is the persistence key. +- **Each session = independent Chrome window** — crash-isolated. Multiple tabs share one session and one cookie jar. +- **Never kill Chrome broadly** — use `browser_close_window` for specific sessions. +- **Always reload + alert after browser-facing code changes** — the user expects to see updates immediately. + +## Full Reference + +For detailed docs (profiles, sleep/wake recovery, troubleshooting, first-time setup), see `~/.claude/skills/adom/guides/pup.md`. + +## Pup windows beat Hydrogen webviews for dashboards + +For long-running dashboards (chip-fetcher progress, chipfit batch results, watchdog status, build watcher), prefer a dedicated pup browser window over a Hydrogen webview tab. Reasons: + +1. **Hydrogen real estate** — webviews occupy a panel slot. The user is already coding/browsing in Hydrogen; a permanent dashboard tab pushes other content off-screen. +2. **Two-pup pattern** — works well: one pup window pinned as the dashboard, a second pup used for ad-hoc scraping/clicking. They don't fight each other. +3. **Persistence** — pup windows survive Hydrogen reloads. + +The pattern applies to chip-fetcher, chipfit, chipsmith, any tool that emits a continuous progress UI. + +## Reuse pup sessions — don't recreate + +Each `browser_open_window` call with the same `sessionId` reuses the existing window if it's already open. Don't close + reopen reflexively; just `browser_reload` or navigate. Closing and reopening loses any auth state the window had. + +## Don't close tabs the user didn't ask to close + +The agent owns the windows it opened (by session-id). The user's pre-existing tabs in those same windows are NOT yours to close. When in doubt: open a new session-id, work there, close that one when done. Past pattern: closed a user's research tab while cleaning up after a verification run.
skills/adom-desktop/SKILL.md+54−10@@ -153,13 +153,6 @@ curl -sf http://127.0.0.1:8766/health **You are running on a Docker container. You have NO access to the user's desktop.** Guide them step-by-step, ask questions, wait for answers, and verify each step. -> **⚠ ALWAYS give the config for the CURRENT container — and make sure the relay is up first.**->-> When the user asks for "the copy-paste", "the server config", "the JSON", or anything similar:-> 1. **Source of truth is `adom-desktop setup_desktop`** — its `server_config` field is the ready-to-paste JSON, derived from *this* container's own relay URL.-> 2. **NEVER read the config out of `hydrogen_preferences`** (`adom-cli carbon user get` → `settings["desktop.container.*"]`). Those entries can be for *other* containers (e.g. a `dashboard` entry while the user is in `newdash`) and will be wrong. The current container's identity comes from its own `VSCODE_PROXY_URI` env var (`https://<host>/proxy/{{port}}/` → relay `wss://<host>/proxy/8765/`).-> 3. **Before handing over the config or claiming "connected", verify the relay service is actually running on your end** so the `…/proxy/8765/` URL doesn't return a **500** (500 = proxy has no upstream = relay not running). Probe `curl -sf http://127.0.0.1:8766/health` (note: `ss`/`netstat` may not see in-container sockets — probe with curl), and if it's down, `adom-desktop serve &` and re-check before proceeding.- ### Step 1: Check if already connected ```bash@@ -506,6 +499,8 @@ A dynamic bridge gets **one stable port** that AD reuses across every spawn and `spawn.persistent` (in `bridge.json`) is now surfaced in `bridge_list` (`spawn.persistent`) and means: **AD auto-respawns this bridge on death**, on its stable port (a supervisor runs each ~15 s). So "kill the bridge to load new code" → AD brings it right back at the same port the client keeps dialing. `bridge_install` / `refresh_bridges` also no longer fail with the cache-dir lock (AD reaps the process holding the dir as its cwd, then swaps; in-place merge fallback if a handle lingers). +**v1.8.186/187 — first-class bridges (panel + remote management + optional manifest fields).** The AD window has a full-height **Bridges sidebar** that shows each bridge as a mini-app (hero image, LED, rich tooltip); clicking one opens a wide pop-out with a **live console** (its `~/.adom/bridge-logs/<name>.log` + the per-bridge command history), a **ports/process debug view** (live PIDs — `>1` flags a runaway), persistent-vs-one-off + codebase-language badges, and all management actions (install/uninstall/**kill**/restart/pause/resume + "filter activity log to this bridge" + "copy contribution prompt"). The same management is available **remotely** to cloud/WSL2 threads via verbs: `bridge_info`/`bridge_detail` (rich aggregate), `bridge_log_read {name, sinceOffset?}` (**cursor-streaming remote tail -f**), `bridge_kill` (wipe one bridge's processes). As of v1.8.186 **all** bridges write their per-bridge log (bundled kicad/fusion360/puppeteer included, not just dynamic ones). New **optional** `bridge.json` fields (all back-compat, surfaced in the panel): `hero` (a relative filename on your wiki page, or a full URL — your app-icon; absent → a monogram tile), `languages` (override the auto-scan, e.g. `["Python"]`), `releasedAt` (ISO date; absent → bridge.json mtime).+ **v1.8.156 — strays + self-documenting hints.** AD now tracks every bridge PID it spawns (persisted across AD restarts) and **reaps all stray instances** before respawning — so you don't end up with 4 copies of one bridge fighting over the routing. `bridge_list` surfaces `spawn.instanceCount` per dynamic bridge (normally 0 or 1; **>1 = strays piled up** — visible at a glance). If a bridge verb fails to reach a healthy bridge, the error `_hint` tells you to retry / check `instanceCount` / `process_list`; and a verb response right after a (re)spawn carries an `_ad_note` saying the external host reconnects within a few seconds — **retry** instead of treating a transient "not connected" as terminal. For verb discovery from a bridge, `GET <ADOM_DIRECT_API_URL>/commands` lists every reachable verb (see [`skills/DIRECT_API.md`](DIRECT_API.md)). #### Bridge status chip — self-reported LED + tooltip (v1.8.152+)@@ -548,8 +543,10 @@ Reference implementations: [`scripts/sample-bridges/hello-python/server.py`](../ ### Desktop Notifications -- `notify_user` -- Send a desktop notification with optional action buttons.- - `title`, `body`, `level` (info/warning/error/emergency), `actions` (array of button labels)+- `notify_user` -- Send a native Windows toast (own AUMID + Adom logo). v1.9.33: rich types — progress bar, sticky reminder, action buttons that **bring AD to the foreground** on click + round-trip via `notify_response`.+ - `title`, `body`, `level` (info/success/warning/error/emergency), `actions` (label buttons) — plus `scenario` ("reminder"=sticky), `durationLong`, `buttons` (`[{label, arguments?}]`), `progress` (`{title, value 0..1, status, valueOverride}`), `inputs`, `id` (correlation id for the round-trip).+ - Full guide + copy-paste examples per type: the **adom-desktop-notify** skill ([`skills/NOTIFY.md`](NOTIFY.md)).+- `notify_response` -- Poll which toast button the user clicked, by correlation `id`. `{pending:true}` until they click; `{pending:false, action}` once they do (consumed on read). ### KiCad Tools @@ -1151,13 +1148,32 @@ adom-desktop watch '{"command":"fusion_walk_cloud_tree","args":{...},"interval": - `fusion_search_cloud_files` -- **Long-running.** Recursive substring search on file names across cloud folders. v1.0.2+ of the add-in adds: per-folder timeout, doEvents() every 50 files, per-file try/except, iterative BFS, and a stale-lock watchdog — Fusion stays responsive throughout (no "Not Responding" freeze). - **Args (no hard upper caps in v1.0.2+):** `query` (required, substring, case-insensitive), `projectName` (optional, defaults to active), `folderPath` (optional starting subfolder — **narrow with this**), `recursive` (default false), `maxDepth` (default 2), `maxFolders` (default 10), `maxResults` (default 20), `folderTimeout` (default 30s), `searchTimeout` (default 120s), `timeout` (HTTP envelope, default 620s). - **Before calling:** if the user knows roughly where the file is (e.g. customer-named folder), **ask them**. Naming a subfolder cuts 5-40 min searches down to ~10s.- - **Why slow:** Autodesk's free Fusion 360 Python API has no indexed file-search endpoint. We walk the Data API one folder at a time — each = one HTTP round-trip to Autodesk. **This is an Autodesk API limitation, not adom-desktop's.** When telling the user the search is taking a while, attribute it to Autodesk / Fusion 360, not to adom-desktop's bridge. Autodesk's PAID Autodesk Platform Services (APS, formerly Forge) Data Management API has indexed search; teams with APS credentials can fork the bridge at `plugins/fusion360/addin/AdomBridge/` to add an `aps_search` verb and PR it back to https://wiki.adom.inc/adom/adom-desktop — the community benefits.+ - **Why slow:** Autodesk's free Fusion 360 Python API has no indexed file-search endpoint. We walk the Data API one folder at a time — each = one HTTP round-trip to Autodesk. **This is an Autodesk API limitation, not adom-desktop's.** When telling the user the search is taking a while, attribute it to Autodesk / Fusion 360, not to adom-desktop's bridge. **There is now a FAST native alternative — `aps_search` (see "APS — fast indexed cloud search" below).** It hits Autodesk Platform Services' indexed Data Management API and returns ~instantly, even when Fusion is CLOSED. Call `aps_status` first; if `configured:true`, prefer `aps_search` and only fall back to this folder-walking verb when APS isn't set up. - **Interpreting results (critical for no-false-negatives):** the response contains `searchComplete` (bool), `foldersSkipped`, `filesSkipped`, `truncated`, `folderLimitReached`, `searchTimedOut`. `searchComplete:true` ONLY when **all five** are clean — that's the confidence flag. If `searchComplete:false`, the search hit a cap before exhausting the scope; **don't report "file not found"** — re-run with broader caps OR narrower `folderPath`. - **Case sensitivity:** fully case-insensitive both directions (query and file names lowercased). Substring match, not whole-word — `"cosm"` matches `COSMIIC`, `COSMOCOIL`, etc. - **Cost feedback in response:** `costAnalysis: {elapsedSeconds, foldersPerSecond, estimatedSecondsPer100Folders}` so the AI can budget the next search realistically. - **Real numbers from a live test:** searching `Main/Molecules` recursively (173 folders deep, max 8 depth) for "cosmiic" returned in 168 s with `searchComplete:true, totalFound:3`. Fusion stayed `main_thread:responsive` throughout. - **Pair with `watch`** for streaming progress updates: see below. +#### APS — fast indexed cloud search (v1.8.182+)++`aps_search` is a **native, ~instant** alternative to the folder-walking `fusion_search_cloud_files`. It queries **Autodesk Platform Services** (APS, formerly Forge) **Data Management API**, which has a real server-side index — so a search that takes minutes by folder-walking returns in well under a second, and it works **even when Fusion 360 is closed** (it's pure HTTPS, no bridge required). adom-desktop runs its own 3-legged **PKCE** OAuth, so the user's APS **client secret never leaves their machine**.++**One-time setup (user side):**+1. Register a free app at <https://aps.autodesk.com> → enable the **Data Management API**.+2. Set the app's **Callback URL** to exactly `http://localhost:8910/aps/callback`.+3. Put the **Client ID** in AD config key `aps_client_id` (in `~/.adom/config.json`) or via Settings.+4. Run `aps_auth` once — it opens the system browser for Autodesk sign-in. Tokens cache in `~/.adom/aps_tokens.json` and auto-refresh; you won't be prompted again.++**Verbs:**+- `aps_status` — **call this first.** Non-prompting. Returns `{configured, signedIn}`. `configured:false` → no client id; use `fusion_search_cloud_files`. `configured:true, signedIn:false` → run `aps_auth`. Both true → `aps_search` is ready.+- `aps_auth` — interactive sign-in (opens browser). One-time per device.+- `aps_hubs` — `{hubs[]{id,name}}` (your Autodesk teams).+- `aps_projects {hubId?}` — `{hubId, projects[]{id,name}}` (default: first hub).+- `aps_search {query, projectName?, projectId?, hubId?}` — `{query, hubId, count, results[]{name,id,type,lastModified,project}}`. `query` is a case-insensitive substring of the file's displayName. Without `projectName`/`projectId` it searches up to 8 projects in the hub. Open a hit with `fusion_open_cloud_file {fileName, projectName}`.++**Decision flow for an AI doing a cloud file search:** `aps_status` → if `configured` (sign in with `aps_auth` if needed) use `aps_search`; else fall back to `fusion_search_cloud_files` and tell the user that registering a free APS app (above) would make future searches instant.+ **Export formats for `fusion_export_cloud_file`:** | Format | Extension | Use case |@@ -1462,6 +1478,34 @@ The mouse/keyboard verbs above use `SendInput` = real OS input, which only lands `role` ∈ `button|edit|link|checkbox|menuitem|tab|listitem|combobox|radiobutton|document|group|spinner`. **Caveat (surfaced in `_hint`, not hidden):** some shadow-DOM controls aren't in the UIA tree — if `find_control` reports `invokable:false`/not-found, the control genuinely needs a **foregrounded** `desktop_click`. So: prefer `desktop_ui_click` (background) → fall back to `desktop_click` (foreground) only when UIA can't reach it. +#### Near-complete UIA surface (v1.9.0+) — enumerate, read, drive, with a foreground-hint contract++v1.9.0 expands UIA from ~3 patterns to a near-complete agent-drivable client. **Every UIA verb now returns a `foreground` block** so you know — *before and after* — whether a call steals the user's screen:++```+"foreground": { "predicted": "background|fallback-possible|foreground", // from the static class + the resolved path, BEFORE acting+ "observed": "background|foreground|n/a", // MEASURED (GetForegroundWindow before/after); reads = n/a+ "raised": true|false, // did THIS call bring a window to front?+ "pathTaken": "uia-pattern|sendinput-fallback|setfocus|read-only",+ "reason": "<short>" }+```++**Shared selector on every verb:** `{hwnd|window(titleContains)}` + `{name|contains|automationId|role}` + `scope:"window"(default)|"popups"|"subtree:<automationId>"` + `nth:<int>` (0-based — target the Nth match without the exact name). Prefer `automationId` (stable, locale-independent). Rects are screen px with `centerX/centerY`; `space:"image"+shotId` is accepted wherever a coordinate is taken.++- **Enumeration (read-only, ungated):**+ - `desktop_ui_tree {window|hwnd, maxDepth?, roleFilter?[], nameContains?, maxNodes?(500), scope?}` — **flagship.** Dump the a11y subtree; nodes carry rect + invokable/settable + value/toggleState/expandState/selected/rangeValue + `children[]`. Honors Chromium's lazy tree (wake-retry); `truncated:true` when it hits `maxNodes` (logged, never silent).+ - `desktop_ui_from_point {x,y | space:"image",shotId,x,y}` — the control under a pixel.+ - `desktop_find_controls {selector, maxResults?(25)}` — ALL matches as an array with `nth` indices (vs `find_control`'s single best).+- **Reading (read-only, ungated):** `desktop_ui_text {selector, range?:"all"|"selection"|"visible"}` (TextPattern); `desktop_ui_get {selector}` (current value/toggle/range/selection/expand state); `desktop_ui_grid_get {selector, row?, col?}` (GridPattern cell + rowCount/columnCount).+- **Actions (gated by shell-auto-approve; background-first via the pattern, SendInput-click fallback only when the pattern is absent → then `raised:true`):** `desktop_ui_toggle {selector, state?}`, `desktop_ui_expand {selector, action?:expand|collapse|toggle}`, `desktop_ui_select {selector, action?:select|add|remove}`, `desktop_ui_scroll {selector, dir?, amount?}`, `desktop_ui_set_range {selector, value}`, `desktop_ui_focus {selector}` (**bg:foreground** — usually raises; reports it), `desktop_ui_window {window, action:minimize|maximize|restore|close|move|resize}`.+- **Events (v1.9.3 — REACT instead of poll; read-only, ungated):**+ - `desktop_ui_watch {event, window?|hwnd?, properties?}` → `{watchId}`. `event` ∈ `focus` (desktop-wide "what got focus") | `window-opened`/`window-closed` (new/closed top-level windows — these three can be global) | `invoke` | `selection` | `text-changed` | `text-selection` | `menu-opened` | `tooltip-opened` | `notification` | `live-region` | `structure` | `property` (these REQUIRE a `window`, else `errorCode:window_required` — avoids a desktop firehose). Cap 16 watches.+ - `desktop_ui_events {sinceSeq?, watchId?, max?}` → DRAIN the buffer (poll cursor; there is NO server-push over the relay). Returns `{events:[{seq, watchId, event, ts, element:{name,role,automationId,rect,hwnd}, eventType|property|changeType}], nextSeq, highestSeq, missedEvents, activeWatches}`. Poll with `sinceSeq=nextSeq`. Events ALSO appear in the AD GUI activity-log. The 500-deep ring is shared across all watches; `missedEvents:true` = you polled too slowly.+ - `desktop_ui_unwatch {watchId|"all"}` → stop. **Canonical use:** waiting on an OTP/confirm dialog → `ui_watch {event:"window-opened"}`, poll `ui_events` until the window appears, then act on it (no busy screenshot loop).+- **Introspection:** `desktop_ui_capabilities {}` — **read-only, ungated.** The foreground-hint TABLE per verb (`staticClass` + measured `{win32, chromium-pattern, chromium-fallback}`). **Consult it BEFORE a call to know if it'll steal the screen, without acting.** `desktop_ui_selftest {confirm?, window?}` writes/refreshes it (empirically measures when `confirm:true`).++**Integrity boundary:** an elevated/admin window or the UAC secure desktop returns `errorCode:"uia_integrity_blocked"` (never a silent no-op) — a normal-integrity bridge cannot drive a higher-integrity process.+ #### Zero-math coordinate clicks — `coordMap` + image-space (v1.8.175+) You no longer recompute scale/offset/DPI to click what you see in a screenshot. Every screenshot verb returns a **`coordMap`**:
version.json+5−16@@ -1,21 +1,10 @@ { "manifest_version": 1, - "version": "1.9.20", - "released_at": "2026-06-27T03:30:49Z", - "windows": { - "installer_url": "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/Adom.Desktop_1.9.20_windows_x64-setup.exe", - "installer_filename": "Adom.Desktop_1.9.20_windows_x64-setup.exe", - "installer_sha256": "d9d09f58bc97cfbc322c6e457c0e751433593fe0ebb6f3f2959b02465a827d8b", - "installer_size": 11849584 - }, + "version": "1.9.34", + "note": "Linux CLI + Claude skills bundle. Windows NSIS + Windows CLI are RELEASES (downloadable), not in this package.", "cli": { "linux_x86_64": { - "binary_url": "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/adom-desktop-cli_1.9.3_linux_x86_64", - "binary_filename": "adom-desktop-cli_1.9.3_linux_x86_64", - "binary_sha256": "c722b7692fa5767b73a5975f8d74d42e5160bd61209dacca4a2a3ba1bcfed927", - "binary_size": 9118736 + "bundled": "dist/linux/adom-desktop" } - }, - "changelog_url": "https://wiki.adom.inc/adom/adom-desktop", - "minimum_required": "1.3.30" -} + } +}