← Commit history
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"
-}
+  }
+}\ No newline at end of file