app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
Publish 1.7.9
3 files changed
+290−499
SKILL.md+273−496@@ -1,520 +1,297 @@ ----name: fusion-bridge-dev-description: How to develop, version, and PUBLISH the Adom Desktop Fusion 360 bridge — where the source canonically lives, and the rule to ALWAYS publish to the wiki AND push the GitHub backup on every change. Read before editing/shipping the fusion bridge. Trigger words — fusion bridge dev, publish fusion bridge, fusion bridge repo, where does the fusion bridge live, ship fusion bridge, bump fusion bridge, adom-desktop-fusion-bridge.+name: adom-desktop-fusion+public: true+description: "Launch and drive Fusion 360 on the user's laptop from this container via the adom-desktop CLI. Includes FAST cloud-file search via APS (Autodesk Platform Services) — search/browse/open the team's Fusion cloud designs in seconds (set up once per company, each user signs in once, never charged). Plus open .f3d/.f3z designs, open cloud files, close documents, dismiss recovery dialogs, capture Fusion window screenshots, send keys + click, run the add-in (STEP/GLB/.lbr import-export, BOM/parameters). Trigger words: fusion, Fusion 360, fast cloud search, fusion_aps_search, search fusion files, find a design, browse cloud files, fusion_aps_open, aps, open fusion, launch fusion, open design, f3d, f3z, import STEP, export STEP, export GLB, export lbr, Fusion electronics, Fusion PCB, BOM from Fusion, Fusion parameters, Fusion API, dismiss recovery, Fusion screenshot, fusion_click, fusion_send_key, fusion bridge, close fusion." --- -# Fusion bridge — dev + publish workflow--## ★ THE CORE GOAL: the AI runs EVERYTHING so the user never has to (why this bridge exists)--The entire reason adom-desktop and its bridges exist is so that **the AI drives the whole flow-end to end and the user does nothing manual** (John, 2026-07-06). This bridge is a FAILURE any time-it punts a step back to the user. That means the code + verbs must automate, on their own:-**install → launch (background) → Autodesk sign-in → seat/licensing conflicts → every startup dialog-→ recovery/update prompts → the actual work (open/model/export).**--- **NEVER ask the user to do something manually** - not "click Continue", not "sign in yourself",- not "close that dialog". The ONLY acceptable exceptions are a true hard wall the OS forbids us to- cross: a **UAC/elevation** prompt, or a **password/2FA** you must not enter for them. Everything- else you AUTOMATE. If you catch yourself about to ask the user to click/close/choose something,- that is a bug in this bridge - fix the bridge so it does it.-- **Detect and handle state in CODE, deterministically** - do NOT rely on the AI noticing a dialog- and following a skill (the AI is unreliable at that). Hard signals (add-in round-trip, owning- process, window size, `family_windows()`), not titles or vibes. See the background-launch state- machine + `find_licensing_dialog()`.-- When something can't yet be fully automated (e.g. a browser OAuth handoff), the goal is still to- push automation as far as it goes and shrink the manual part to zero over time - not to hand it back.--Hold every change to this bar: does it move the bridge toward "the user does nothing"? If a flow-still needs the user, that's the next thing to fix, not the user's job.--## ⛔ Background-launch invariant: NEVER steal the user's foreground (HARD RULE)--The bridge runs on a machine the **user is actively working on**. Anything this bridge launches or-opens must appear in the **BACKGROUND** and must **never yank focus** off what the user is doing-(John, 2026-07-06: "make sure new windows ALWAYS only open in the background so as not to disturb my-main work"). This is not best-effort etiquette — it is enforced **in code**, and any new window--opening path you add MUST follow it:--- **Launch minimized + not-activated.** Start GUI processes with `STARTUPINFO.wShowWindow =- SW_SHOWMINNOACTIVE (7)` + `CREATE_NO_WINDOW`. See `_popen_fusion_background()` in `fusion_detect.py`- — copy that, never a bare `subprocess.Popen([exe])` (which opens Fusion in the foreground).-- **Guard the startup window — match the whole Autodesk FAMILY, not just `Fusion360.exe`.**- `STARTUPINFO` alone is not enough: Fusion re-foregrounds itself several times as it initializes, and- the noisiest offenders are NOT `Fusion360.exe` — the splash "Loading additional modules" is- `FusionLauncher.exe`, and the "Signing in" + **"Active Sessions Exceeded"** dialogs are- `AdskIdentityManager.exe`. A guard scoped to only `Fusion360.exe` lets those yank focus (the exact- bug John hit 2026-07-06). `ensure_fusion_running()` fires `_start_focus_guard()`: a daemon that, for- ~150s, minimizes (no-activate) **only** the foreground window when it belongs to the Autodesk launch- family (`_FUSION_FAMILY_IMAGES` = Fusion360 / FusionLauncher / AdskIdentityManager / …, matched by- process image via `_fusion_family_pids()`). It acts ONLY on focus-theft, so it never fights a user- who deliberately foregrounds Fusion. Any window-family you add MUST be in that set.-- **⚠️ ctypes HANDLE TRUNCATION — the silent killer (this bit us 2026-07-06).** Any Win32 call that- RETURNS or ACCEPTS a handle (`HWND`, `HANDLE`) MUST have its `restype`/`argtypes` set, or ctypes- defaults to `c_int` (32-bit) and **TRUNCATES the handle on 64-bit Windows**. `GetForegroundWindow()`- with no `restype` handed back a garbage HWND, so `ShowWindowAsync` silently no-op'd and the focus- guard minimized NOTHING for two releases (1.6.33/1.6.34) while looking correct — the user still had- to alt-tab back themselves. Always: `user32.GetForegroundWindow.restype = wintypes.HWND`,- `ShowWindowAsync.argtypes = [wintypes.HWND, c_int]`, etc. VERIFY by objective window state, not by- eye or by sampling the foreground (a 0.1s-poll guard minimizes too fast to catch in a snapshot):- `GetWindowPlacement(...).showCmd == 2` (SW_SHOWMINIMIZED) on the family window == guard is working.-- **The guard only runs on a FRESH launch.** `ensure_fusion_running()` starts it after it spawns the- process; if Fusion is ALREADY running, `fusion_start` returns early and the guard never fires, so an- already-foreground Fusion stays put. When you need a clean background state, `fusion_kill` first,- confirm the process is gone, THEN launch.-- **Seat conflict ("Active Sessions Exceeded") = AUTO-RESOLVE via UIA, in the background, never ask- (SOLVED live 2026-07-06 after a very long day).** The definitive facts, hard-won:- - It is a **Qt** dialog (`automationId` = `QTApplication.QTSessionBlockedStartup...`), NOT CEF.- - Its window **TITLE is literally "Fusion360"** — the seat text is body content — so NEVER detect by- title. There are **multiple variants of different SIZES**: "Active Sessions Exceeded" (~1235x527,- with radios) and the **"Suspend Remote Session" confirm (only 823x262)**.- - **DETECT by the OWNED-POPUP signal, not a size window (fixed 2026-07-06 v1.6.46/1.6.47 after a- fragile 320px height floor silently MISSED the 262px confirm and readiness lied that it was gone).**- A seat/sign-in modal is ALWAYS an **owned popup** of the main Fusion window (`GetWindow(hwnd,- GW_OWNER)` != 0; confirmed by `desktop_screenshot_window`'s `ownedPopupCount` on the main window).- The splash and the maximized main app are NEVER owned popups. `find_licensing_dialog()` returns the- owned family window that is **dialog-SHAPED: width ≥ 480** (excludes Fusion's own ~300px-wide side- panels — the Data Panel/browser rail are owned popups too, and matching them false-blocked `ready`- the moment a doc was open) **and height ≥ 180** (excludes toolbar strips), smaller than the maxed- main app. ctypes `GetWindow` MUST have `restype/argtypes=HWND` set or the owner handle truncates.- - Its native **"Continue"** button IS exposed to **UIA** (`desktop_ui_click {hwnd, name:"Continue"}`),- and UIA **Invoke runs in the BACKGROUND — no foreground, no focus steal, no cursor move**. Suspend- is the pre-selected radio, so Continue grabs the license. (The radios themselves are web content- NOT in the a11y tree — do NOT depend on selecting them.)- - **DO NOT** kill+relaunch to "free the seat at the source": the Autodesk licensing SERVER keeps the- seat even when the other machine is OFF, so that never clears it. `_reclaim_seat_from_peers()` is- dead code, kept only as a comment-marker — do not resurrect it.- - **DO NOT** coordinate-click it (`desktop_click`/SendInput): that needs foreground (disturbs the- user) AND the focus-guard fights you by minimizing the dialog.- - **RESOLVE + SCREENSHOT-VERIFY.** The FIRST UIA Invoke on a mid-render dialog silently NO-OPs, so- `_resolve_seat_dialog()` re-invokes "Continue" and confirms it TRULY cleared by re-reading the- parent window's `ownedPopupCount` via `desktop_screenshot_window` (parent/child screenshotting) —- NOT by re-running the size heuristic that once false-'resolved' it. Do the screenshot check;- "I clicked it" is not "it's gone."- - **AUTO-RESOLVE ANYWHERE, in CODE, not just the launch loop.** The seat dialog also appears LATER —- minutes after sign-in, when the license server notices too many sessions — long after the launch- loop exits. So `fusion_readiness` SELF-HEALS: whenever it detects the dialog it auto-resolves it in- the background and re-probes. The seat is handled deterministically in code no matter when it shows- up; the AI never has to notice or act (John: "do all this tracking from your code ... the ai never- follows skills"). A STALE add-in port (8774 from a just-killed instance) answers while the CURRENT- Fusion is blocked, so the seat check takes PRIORITY over the add-in probe in both paths.-- **The add-in still loads while minimized** — background operation (open/model/export) needs no- visible window. So minimized costs you nothing.-- **Foreground is opt-in, only to SHOW the user something.** The ONLY time you may foreground/restore- a Fusion window is when a verb's explicit job is to show the user (and even then, per the user's- standing rule, you surface the exact window + their viewer). Never `SetForegroundWindow` as a side- effect of automation.-- **Applies to every window surface**, not just the Fusion launcher: any child process, helper, or- dialog you spawn opens background/no-activate by the same means. When you add one, add a focus-guard- or launch flag — do not assume it will behave.--If you catch yourself about to `Popen` a GUI or call a foreground API, stop and route it through the-background-launch helpers. A window that steals the user's focus is a **regression**, full stop.--## You OWN this bridge (do NOT file your own bugs as adom-desktop requests)-AD core only HOSTS/LOADS this bridge: the `adom-desktop` CLI, the relay/passthrough, bridge lifecycle-+ streaming this bridge into `…\bridges-cache\fusion360`, and the core `desktop_*` verbs. **Everything-`fusion_*` is yours** - the verbs + their `_hint`s, the APS integration (auth/config/quota; port 8910-is AD's vestigial native APS, this bridge uses 8917), the add-in code AND its deployment into Fusion,-the SKILL.md, the publish loop. A `fusion_*` bug is a bug in THIS repo: fix it here and republish,-never as an adom-desktop discussion ask. (See the repo `CLAUDE.md`.)--## Your wiki structure: ONE page, THREE artifacts (know this cold)--You are a single wiki page (`wiki.adom.inc/adom/adom-desktop-fusion-bridge`) that ships **three distinct-things**, each with a different consumer:--1. **The page git repo — the ONE source of truth.** Everything lives here: `server.py`, `aps.py`,- `addin/`, `handlers/`, `skills/`, the `*.md` docs, **`page.json`** (the skills-pkg manifest), and- **`adom-bridge-fusion-manifest.json`** (the bridge manifest). Edit here, push here (+ GitHub backup).--2. **The Release (the zip) + the bridge manifest — consumed by ADOM-DESKTOP on `bridge_install`.**- - The **Release zip** (`adom-bridge-fusion-v<ver>.zip`, uploaded via `adom-wiki release upload`) is the- BUILT bridge (server + add-in). Binaries/zips are **Releases, NEVER git** (`*.zip` is gitignored and- `repo push` silently skips it — see PUBLISHING.md).- - The **bridge manifest** (`adom-bridge-fusion-manifest.json`, text, in git) tells adom-desktop **who you- are**: name, version, the `url` pointing at the Release zip, `sha256`, `size`, verb prefixes, health- endpoint. adom-desktop streams that zip by URL into its bridge cache and hash-verifies it.--3. **The adom-wiki pkg (`adom-wiki pkg install adom/adom-desktop-fusion-bridge`) — NOTHING BUT SKILLS.**- It gives a user's container the skills to know how to talk to YOU and to Fusion. `page.json` is its- manifest; `install.sh` copies the top-level `SKILL.md` + every `skills/*/` into `~/.claude/skills/`.- **No binaries belong in this pkg** — that's what the Release is for.--### The install handshake (how a container gets your skills)-- An AI asks adom-desktop to install you → adom-desktop installs the **Release** (via the manifest) AND- **tells the AI to `adom-wiki pkg install` your pkg** → the container gets ALL your skills.-- An AI may also `adom-wiki pkg install` you on its own. Then your skills **auto-update via the pkg- mechanism** on every refresh. **THAT is the goal** — your skills always current, everywhere.--### The hero gets unlinked — re-check it-You have a hero we worked hard on (`screenshots/hero.png`, declared in `page.json`). **The wiki sometimes-UNLINKS it after certain updates (e.g. a `pkg publish`).** Every so often, verify `page.json` still-declares the hero and the page still shows it; **re-link it if it dropped.** TODO: file a bug to the-`adom/wiki` discussion about the hero getting unlinked on updates.--### Your STANDING JOB: document EVERY verb in skills-The skills pkg must document **absolutely every verb you support** — a **core skill** + **sub-skills** for-the extra areas. For each verb capture: what it does, **why** it exists, **when** to use it, why it's-**valuable**, and its **pitfalls**. Walk `fusion_describe` / `VERBS.md` and ensure each verb has skill-coverage. **Constantly refine and expand** — this is never "done." (Current coverage: fusion-electronics,-fusion-libraries, fusion-eagle-commands, fusion-aps-search/signin, fusion-onboarding; the full per-verb-sweep is ongoing.)--## How the add-in actually deploys (VERIFIED by shipping v1.5.1, 2026-06-28)-Fusion loads the add-in from a per-user dir Autodesk has MOVED across versions: **2025+ Fusion-scans `%APPDATA%\Roaming\Autodesk\FusionAddins\AdomBridge\`** and SILENTLY ignores the legacy-`%APPDATA%\Roaming\Autodesk\Autodesk Fusion[ 360]\API\AddIns\AdomBridge\` dirs (issue #63,-root-caused live: an add-in only in the legacy dir never loads). `install_addin.py` installs to ALL-locations (TARGET_CANDIDATES); if a future Fusion stops loading it, FIRST suspect the dir moved-again. ⛔ NEVER ask the user to restart Fusion / enable the add-in - YOU restart-(`fusion_stop`+`fusion_start`; `runOnStartup` does the rest). Legacy note (superseded):-the add-in previously lived only at `...\API\AddIns\AdomBridge\`,-NOT from this repo or the cache. `install_addin.py` (`_sync_directory`) copies `addin/AdomBridge` there.-**`bridge_install` only updates the CACHE (`method: in_place_merge`); it does NOT push the add-in into-Fusion.** And the sync canNOT overwrite the add-in while **Fusion is running** (it holds the files open),-so a bridge-server respawn alone does nothing. The exact sequence that worked (after the wiki publish):-1. `bridge_install {"manifestUrl": ".../adom-bridge-fusion-manifest.json"}` (add `"force": true` to re-merge).- Its OWN output says: "AD reaps + respawns it from the new cache on the next call (no manual kill needed)."- So do NOT run `bridge_kill` - AD restarts the bridge SERVER (new server.py/describe.py) automatically on- the next verb. (I ran `bridge_kill` once here; it was pointless.)-2. **`fusion_stop`** (graceful; `fusion_kill` if wedged) - REQUIRED, releases the add-in file locks. Skipping this leaves the OLD add-in live.-4. Run the cache's `install_addin.py` (`cd <cache>\fusion360 && python install_addin.py`); it prints- `Updated: commands\cloud_documents.py ...`.-5. **Verify by SHA256** (`Get-FileHash` on the Roaming file == `sha256sum` of that file unzipped from the- published v<ver>.zip). NEVER use `findstr` - it false-negatives on these UTF-8 files (dash/box chars).-6. `fusion_start`, then confirm a `fusion_*` verb behaves new.-- A bumped `BRIDGE_VERSION` does NOT prove the running add-in changed - always hash-verify (step 5).-- NEVER hand-edit the cache or the deployed Roaming copy - edit this source, publish, reinstall.--## Canonical source = the wiki page (the ONE source of truth)-**`https://wiki.adom.inc/adom/adom-desktop-fusion-bridge`** (owner `adom`, public). A wiki-page IS a git repo; this is THE main repo for the bridge. AD installs the bridge from here-(its manifest + zip). Every version ships here.--- Local working clone: `~/project/adom-desktop-fusion-bridge`.-- Files on the page: `bridge.json`, `BRIDGE_VERSION`, `README.md`, `SKILL.md`, `aps.py`,- `server.py`, handlers/addin, the `adom-bridge-fusion-manifest.json` + version zips- (`adom-bridge-fusion-v<ver>.zip`), `adom-bridge-fusion-source.tar.gz`, and- `adom-bridge-fusion.bundle` (portable full git history).--## GitHub backup — ALWAYS push it too-`github.com/adom-inc/adom-desktop-fusion-bridge` (renamed from the old `adom-bridge-fusion`-to match the wiki slug). It is a **backup mirror**. **On EVERY change you publish to the-wiki, also `git push` to this GitHub repo.** Never let them drift again (they were 6 commits-out of sync once — that's the confusion we're preventing). Wiki = canonical, GitHub = backup,-keep them in lockstep.--## The publish ritual (every version)-1. Edit in `~/project/adom-desktop-fusion-bridge`. Bump `BRIDGE_VERSION` + `bridge.json`- `version` together.-2. `git commit` locally.-3. **Push to GitHub backup:** `git push origin main`.-4. Publish to the wiki via the raw `/files` API (the wiki CLIs are dead) — token- `${ADOM_WIKI_TOKEN:-$(cat /var/run/adom/api-key)}`:- `curl -X POST -H "Authorization: Bearer $TOK" -F "file=@<f>" \- https://wiki.adom.inc/api/v1/pages/adom-desktop-fusion-bridge/files`- Upload: the new `adom-bridge-fusion-v<ver>.zip`, the `adom-bridge-fusion-manifest.json`- (`{manifest_version,name,version,url,sha256,size,released_at}`, url = absolute `/files/`- path), `bridge.json`, `BRIDGE_VERSION`, a fresh `adom-bridge-fusion-source.tar.gz`- (`git archive`) and `adom-bridge-fusion.bundle` (`git bundle create … --all`).-5. **ANON-VERIFY**: re-download the zip + sha256-match, fetch the manifest, confirm version.-6. Install on a Fusion box: `adom-desktop --target <laptop> bridge_install- '{"manifestUrl":".../adom-bridge-fusion-manifest.json"}'`, then reap the old instance- (`bridge_kill` on AD ≥1.8.187, else taskkill the PID on its runtimePort) so the new code- loads, and verify via `bridge_log_read` / a `fusion_*` verb.--## The stale seed — do NOT publish from it-`adom-desktop` (private GitHub) → `plugins/fusion360/` was the bridge's ORIGINAL home and is-now a **frozen first-run fallback only** (AD bundles it offline). It is STALE and NOT-canonical. `release-bridge.sh` refuses to publish it. There is a `STALE.md` in that folder-pointing here. Never treat `plugins/fusion360` as the source; never publish from it.--Related: [[project_adom_desktop_fusion_bridge]], skills `fusion-aps-search`, `fusion-aps-signin`.--## Publishing a new version (zips are RELEASES, not git files)--Full guide: PUBLISHING.md. The one rule that bites everyone: the build **`.zip` is a release asset**-(`adom-wiki release upload <ref> <ver> <zip>`), NOT a git file - `*.zip` is gitignored and-`adom-wiki repo push` silently skips binaries (reports `ok`, but the URL 404s). Only the small-**manifest JSON** goes in the git repo (`adom-wiki repo push`), and its `url` points at the release-download URL (`/download/<org>/<slug>/<ver>/...zip`) with `sha256`/`size` matching the served asset.-Then `bridge_install {manifestUrl, force:true}`. Server-only change (server.py/describe.py/handlers) =-that's it (AD respawns the bridge server). Add-in change (addin/) = ALSO `fusion_stop` ->-`install_addin.py` -> hash-verify (Get-FileHash, not findstr) -> `fusion_start`. Community: fork the-page, release to your fork, `bridge_install` your manifest to test, then PR back.--## Publishing the SKILLS pkg (`adom-wiki pkg publish`) — the maze, SOLVED (2026-06-28)--The skills pkg (`adom-wiki pkg install adom/adom-desktop-fusion-bridge`) is SEPARATE from the Release.-Publishing it from this repo (which also holds `server.py`/`addin/`) was blocked by a maze; two fixes in-`page.json` unblock it:--1. **Make it skills-only with a `files` allowlist** — else the dep-checker scans `server.py` and flags its- `import aps/describe/handlers/eagle_lbr/adsk/PIL` as ~20 "undeclared deps" (false positives the SERVER- enforces, not just lint). Ship only what the pkg needs:- ```json- "files": ["SKILL.md","skills/**","screenshots/hero.png","README.md","VERBS.md","install.sh",- "uninstall.sh","page.json","MAKING_LIBRARIES.md","LIBRARY_FINDINGS.md","PUBLISHING.md",- "CREATING_BASIC_PARTS_LIBRARIES.md"]- ```- Use `skills/**` (a bare `skills/` matches nothing and errors).-2. **The publish DEMANDS a billboard hero** (`{type:billboard,headline,subhead,screenshot}`); it rejects- the page's `{type:image,path}`. So set billboard ONLY to get past publish:- ```json- "hero": {"type":"billboard","headline":"Adom Desktop - Fusion 360 Bridge",- "subhead":"Drive Fusion 360 from the cloud: electronics, exports, fast APS cloud search, and component libraries with real 3D",- "screenshot":"screenshots/hero.png"}- ```- The image must be in the repo (curl `/files/screenshots/hero.png` if your working copy lacks it).-3. `adom-wiki pkg publish --skip-lint` (the "prompt-injection"/"no video" warnings are soft false- positives). Bump `page.json` `version` each time; add `discovery_triggers` so AIs surface it.-4. **⛔ IMMEDIATELY REVERT THE HERO** (this is the bug John warned about). `pkg publish` overwrites the- page's hero with the billboard form, which the PAGE renders as a generic template (your custom- full-bleed `hero.png` shrinks to a thumbnail on the right). **Right after publishing, push the page- hero back to the image form** — via `adom-wiki repo push` (NOT another `pkg publish`, which re-breaks it):- ```bash- # page.json: "hero": {"type":"image","path":"screenshots/hero.png"}- adom-wiki repo push adom/adom-desktop-fusion-bridge --files page.json -m "restore full-bleed hero"- ```- The pkg stays published (registry snapshot is immutable); only the page display is restored.-5. **VERIFY**: `adom-wiki pkg install adom/adom-desktop-fusion-bridge` deploys skills to- `~/.claude/skills/`, AND re-open the live page to confirm the **full-bleed custom hero renders** (not- the thumbnail-in-template). Then `git commit` + `git push` `page.json` (image form) to GitHub for- lockstep. NOTE: pkg version (`page.json`, 1.6.x) is a SEPARATE track from the Release/bridge version- (`bridge.json`/`BRIDGE_VERSION`, 1.5.x). TODO: this publish↔hero conflict is a wiki bug — filed to- `adom/wiki`.--## Bundle deep READMEs INTO each skill (so the installed AI actually reads them)--When a skill points at companion docs (a step-by-step guide, a findings log), **copy those READMEs INTO-the skill's own folder** (next to its `SKILL.md`) and link them with **plain local paths**-(`[GUIDE.md](GUIDE.md)`). Do NOT use repo-relative links (`../../GUIDE.md`) - they resolve in the repo but-**break once the skill installs** to `~/.claude/skills/<skill>/` (there is no repo root above it). And do-NOT rely only on wiki URLs - a remote fetch is a step the AI may skip; a file sitting right next to the-skill gets read. In the bundled copies, rewrite image refs to **absolute wiki URLs**-(`https://wiki.adom.inc/<org>/<slug>/files/<img>.png`) so screenshots still resolve. The repo-root copies-stay for the wiki Files tab; **re-sync the bundled copies on every publish** (see PUBLISHING.md). Net: the-installed skill carries the full content locally + always-current.--## Install-completeness detection (fusion_detect.py) — READ before touching install/readiness--Root-caused live on a fresh Hyper-V VM (John, 2026-07-14). The webdeploy layout is NOT obvious:--- `%LOCALAPPDATA%\Autodesk\webdeploy\production\<hash>\` — a COMPLETE install is ~615 files- (**`Fusion360.exe`** ~880 KB + hundreds of DLLs + Qt/ Python/). Autodesk ALSO drops a tiny **8-file- launcher STUB** dir next to it (icons + `FusionLauncher.exe` + a ~412-byte `FusionLauncher.exe.ini`).- So a healthy install shows **two** hash dirs.-- **The completeness signal is `Fusion360.exe` (full size), NEVER the launcher or its `.ini`.** Two- earlier "fixes" were WRONG: keying off `FusionLauncher.exe` existing → true mid-stream (it lands- early) → premature `fusion_start` → *"Error Launching Streamed Application ... .ini missing or- incomplete"*. Keying off `FusionLauncher.exe.ini` existing/size → also wrong: the **complete app dir- has NO `.ini` at all**; only the throwaway stub has one, so a size gate would reject a healthy- install and accept nothing.-- Helpers: `_app_dir_complete()` (Fusion360.exe ≥ 200 KB), `_any_app_complete()`, `_find_fusion_launcher()`- (returns a launcher only when a complete app exists), `_incomplete_webdeploy_present()` (fresh-install- "installing" signal = a launcher present but NO complete app anywhere; disk-state, because the- streamer's tasklist presence is racy — flickered true only 3/25 polls during a real stream).-- `readiness`: gate `installed` on the app binary; `installing = _installer_running() OR- _incomplete_webdeploy_present()`. **Poll to `installed:true`, not to `installing:false`** (the latter- is racy). `_clean_incomplete_webdeploy()` no-ops when a complete app exists (never nuke the legit stub).--## Installer zip = RUNTIME ONLY (feedback from the AD thread, 2026-07-17)--The v1.6.76 zip was 16.7 MB - 15.3 MB of it demo MP4s, screenshots, and repo docs, streamed to-EVERY user's machine on bridge_install. Media and marketing belong on the WIKI PAGE (repo/Files),-never in the installer. Build the zip with the runtime whitelist (git ls-files MINUS-.mp4/.png/.jpg/.svg/.gif/.webm/.zip and screenshots/): server.py, aps.py, describe.py, handlers/,-addin/, resources/, skills/ (text), manifests. Result: 456 KB. If the zip is over ~1 MB,-something wrong is inside - list its contents before uploading.--## page.json `brief` vs `description` - they render in DIFFERENT places--The wiki puts these two fields in two different spots, and confusing them turns the Install card-into a README (John: "stop trying to turn that into a mine readme"):--- **`brief`** -> the **page-header one-liner** under the title. This is where the FEATURE COPY goes- (what the bridge does: libraries, exports, APS search, modeling...).-- **`description`** -> the **Install card body**, directly above the `adom-wiki pkg install` command.- This must say ONLY what the package install gives you. ~2 sentences. No feature list.--The canonical bridge `description`:--> "Installs this bridge's skills into your container so your AI knows how to drive the bridge. The-> bridge runtime itself is loaded by Adom Desktop from the release zip."--It matters beyond tidiness: users and AIs assume `pkg install` installs the BRIDGE. It does not - it-installs SKILLS into the container. The runtime is the release zip that AD streams via-`bridge_install`. Say that in the Install card, once, plainly.--Filed as SDK-wide asks on adom/adom-desktop: **#20** (every bridge page must state the pkg-vs-runtime-split) and **#21** (document + lint keeping the release zip and pkg tarball free of media bloat).--## Clicking without stealing focus: UIA first, SendInput last (John, 2026-07-22)--The background-launch invariant above covers windows we OPEN. This covers windows we CLICK. Same-principle, different verb set, and it is the one that bit us: `desktop_click` FOREGROUNDS.--> John, 2026-07-22: "try to NEVER bring windows to the foreground cuz its disruptive."--- `desktop_click` / `desktop_double_click` / `desktop_right_click` = **SendInput**. They- **FOREGROUND** the target and move the real cursor. LAST RESORT only.-- `desktop_ui_click` / `desktop_ui_set` / `desktop_find_control` = **UIA Invoke/SetValue**.- Programmatic: **no focus steal, no cursor move.** Chromium/Edge expose their a11y tree, so page- buttons and form fields ARE reachable by accessible name. **Always try these first.**-- Use `_click_background_first()` in `server.py` - UIA first, foreground click only as fallback.-- The ONE documented exception: **Fusion's "Sign In" button is a webview control with no UIA- node**, so only an image-space `desktop_click` reaches it. That does not license foregrounding- anywhere else.-- Never raise a window to get attention. **Toast** (`_cli_notify_all`) or flash the taskbar.-- Restoring a MINIMIZED window before capturing it is fine (minimized windows screenshot as a- ~237x39 title-bar sliver, so clicks land on nothing). Restore != raise-to-front.--### Human walls: toast the user AND tell the AI it can clear it itself--When only a human can clear something (2FA, credentials, a protocol dialog):--1. **Toast from CODE.** `_detect_signin_wall()` classifies the wall from window TITLES (cheap, no- OCR, no foregrounding); `_notify_signin_wall()` fires a sticky toast. Response carries- `notifyDelivered` so the AI knows not to nag.-2. **Still hand the AI a mechanical way to clear it.** A wall is not permission to give up - the- Adom user wants everything automatic. `_wall_hint()` returns exact commands: for an emailed 2FA- code, read it from Gmail via `adom-google`, then `fusion_signin_2fa {code}`, which types it via- background UIA.-3. **If the tool that would automate it is missing, upsell it** rather than dead-ending:- "installing adom-google would let me finish logins like this for you."--Put all of this in response **`_hint`s, not just a skill.** John, 2026-07-22: *"the ai never-follows skills well, so hints back in your cli calls is the way to get the ai to act correctly."*-Skills are reference; hints are what actually change behaviour at the callsite.--## The PROVEN Fusion sign-in: every step, FG vs BG, and why (2026-07-22)--Signing Fusion in was a mess for a long time. This is the sequence that ACTUALLY worked end to-end, live, with the account signed in and `ready:true`. **Only 2 of 10 steps touch the user's-screen.** Do not regress this.--| # | Step | FG/BG | Method that makes it background | Caption |-|---|------|-------|--------------------------------|---------|-| 1 | `fusion_start` (if not running) | **FG** | Fusion raises its own window on launch | **yes** |-| 2 | find Fusion hwnd + rect | BG | `desktop_list_windows` (also returns `rect`) | no |-| 3 | restore if minimized | **FG-ish** | `desktop_set_window_state` | **yes** |-| 4 | click **Sign In** | **BG** | `desktop_ui_click` UIA Invoke | no |-| 5 | capture authorize URL | BG | copy Chrome `History` + `-wal`/`-shm`, sqlite read | no |-| 6 | re-open URL in target profile | BG | `nbrowser_open_window {background:true}` | no |-| 7 | click "Continue with Google" | BG | CDP `nbrowser_click` | no |-| 8 | pick the account by email | BG | CDP `nbrowser_click` | no |-| 9 | click "Open Product" (`autodesk://`) | BG | CDP `nbrowser_click` | no |-| 10 | poll `fusion_readiness` | BG | HTTP | no |--### The three discoveries that made it work--1. **Fusion's "Sign In" button IS in the UIA tree and IS invokable.** I wrongly claimed for a long- time that it was an un-automatable webview control and foregrounded the window every time.- ```- name "Sign In" role Button invokable true- automationId QTApplication.MW1_0...Nu::QTSignInDialog.backgroundWidget.signin_button- ```- `desktop_ui_click {hwnd, name:"Sign In"}` clicks it with NO foreground. **Never SendInput it.**-2. **SSO beats password every time.** Click **"Continue with Google"** (or Apple/Microsoft) BEFORE- typing any email. If the browser profile already has that identity, the whole login completes- with **no password and no 2FA**. Typing an email first commits you to the password path.-3. **It must be a FRESH flow.** Re-navigating a `flowId` that already advanced lands on the- PASSWORD screen with no way back to the provider buttons. If the SSO button is missing, restart- Fusion for a fresh `request_id` rather than fighting the page.--### CDP only drives windows YOU opened--`nbrowser_*` refuses a tab in a window the user (or Fusion) opened: `not_owned`, and there is no-override. That is WHY step 6 exists - re-opening the captured URL in an **agent-owned** window is-what makes steps 7-9 background-drivable. A side effect is a second window, so close the original.--### Branches--| Condition | Detection | Action |-|---|---|---|-| wrong profile | captured dir != target dir | re-open in target |-| right profile already | dir match (work account is often `Default`!) | `already_right_profile`, do NOT open a duplicate |-| URL older than 150s | `ageSec > 150` | `stale` -> restart Fusion for a fresh request |-| no SSO button | button missing | not a fresh flow -> restart Fusion |-| 6-digit email code | window title `2-step verification` | read from Gmail via `adom-google`, submit with `fusion_signin_2fa` (background) |-| password screen | `h1` "Enter your password" | TRUE WALL - toast the user, never type it |-| `autodesk://` dialog | title has "Identity Manager" | `desktop_ui_click {name:"Open"}` |+# adom-desktop — Fusion 360 bridge -**Chrome profile dirs lie to you.** `chrome:[email protected]` maps to dir **`Default`**, and the-personal account to `Profile 1`. Always compare DIRS, never assume "Default == personal".+All commands dispatch to the Fusion Python bridge. From this container: -### ONE login must cover Fusion AND APS+```bash+adom-desktop fusion_<action> '<json_args>'+``` -John, 2026-07-22: *"why should the user have to login twice to autodesk to sign in to fusion and-setup aps? why aren't those just driven from 1 login?"*+The leading `fusion_` routes to the Fusion plugin; `<action>` is the plugin-side name. -They cannot share a token (Autodesk DPAPI-encrypts Fusion's store with app entropy, on purpose),-but they CAN share the **warm browser SSO session**. So:+## FIRST-TIME / COLD-START — read before you panic -- `fusion_readiness` now reports APS state on EVERY launch and its `_hint` says what to do.-- Right after a successful Fusion sign-in, while the profile's Autodesk SSO session is still warm,- the bridge starts APS consent itself - it goes through silently. Never make the user log in twice.+A fresh laptop may not have Fusion running (or may run a stale add-in). That is EXPECTED, not defeat.+Branch on the stable `errorCode`, never the prose: -### Captions: only when you actually disrupt+| response (`errorCode`) | what it means | what you do |+|---|---|---|+| `ready:true` (fusion_readiness) | Fusion up + add-in current | drive |+| `running:false` / `fusion_not_running` | installed, not launched | `fusion_start` (returns when the add-in is ready, or `stillLaunching` on a slow first launch) |+| `stillLaunching:true` (fusion_start) | first launch, add-in not up yet (can take 2-4 min) | do NOT re-call `fusion_start`; POLL `fusion_readiness` every ~10s until `ready:true` |+| `installed:false` / `host_app_not_installed` (AD >=1.9.79, + a `hostApp` field) | no Fusion host app | OFFER to install, then **`fusion_install_fusion`** (no shell approval; elevation-aware, streams the free trial) -> poll `fusion_readiness` (`installing:true`) until `installed:true` -> `fusion_start`. Details: [fusion-onboarding](skills/fusion-onboarding/SKILL.md) |+| `needsSignin:true` (`fusion_start` **and** `fusion_readiness`) | Fusion is running but stuck at the first-run Autodesk sign-in (main UI blocked, add-in can't serve, so `ready:false`) | AUTOMATE it (proven sequence, see [fusion-onboarding](skills/fusion-onboarding/SKILL.md)): (1) clear STALE `signin.autodesk.com` tabs (a dead tab = expired code = "Sign-in request expired"); (2) `desktop_ui_click` "Retry browser sign-in" / "Sign In" for ONE fresh request; (3) click "Open Product" with **`desktop_click` (SendInput=isTrusted)** — `desktop_ui_click` (UIA) does NOT fire the protocol; (4) on the in-page "open Autodesk Identity Manager?" overlay (NOT an owned popup) tick "Always allow" + "Open" via `desktop_click`, FAST; (5) poll `fusion_readiness`. `nbrowser` (CDP) can do 3-4 headlessly if it's on the SAME browser window. NEVER auto-enter password/2FA. To ask the user, `notify_user` works directly (AD ≥1.9.84); add a cross-AD `target` if you're on a VM |+| `addinStale:true` / `addin_stale` | Fusion runs an OLD add-in that never re-synced (silent-failure risk) | `fusion_stop` then `fusion_start` — the bridge re-syncs the add-in from cache on start (Fusion must be CLOSED) |+| `bridge_restarting` + `stillRunning:true` (AD ≥1.9.77) | the bridge is respawning after a `bridge_install` | NEVER a not-installed/fatal signal: poll `fusion_readiness` until `ready:true`, then retry. Read-only verbs (`fusion_readiness`/`fusion_describe`/`fusion_get_app_state`) forward straight through even mid-respawn — no sleep-and-recheck hacks needed |+| `blockingDialogs:[…]` | a modal is blocking the add-in | dismiss in the BACKGROUND, then the op auto-resumes | -Caption (`desktop_caption`, <= 3s) ONLY for steps that take over the screen - launching/restoring-Fusion. Do NOT caption background work; unnecessary captions are their own kind of rude. Always say-WHY, and whether you need anything from the user.+**Runtime note:** AD **provisions Python itself** (a portable no-UAC 3.12.13 runtime if none is on PATH — the bridge never bootstraps its own; check AD's `runtimes` verb). You never install Python. -### Clean up after yourself: the sign-in window janitor+## The 8 conventions (from the Bridge SDK) -John, 2026-07-22: *"if you open a browser window for sign in, you must run a loop on a schedule to-know when to close it so you never leave the user's desktop messy."*+1. **Poll readiness, don't launch blindly.** `fusion_readiness` (or AD's `bridge_readiness`) is read-only — it reports `installed`/`running`/`ready`/`addinStale` WITHOUT launching anything. Loop: readiness → act on the row above → drive.+2. **`fusion_readiness` is the standardized cold-start probe.** (Fusion is a detected HOST APP with no heavy download, so there's no `prewarm` step — readiness is the whole gate.)+3. **Core verbs live in the table below; `fusion_describe '{}'` is the full machine-readable catalog.** Read the table to act fast; hit `describe` for completeness — don't guess a verb.+4. **Background by default — automate first, NOTIFY only at a true wall.** This bridge drives the AI's workspace, not the user's screen: dismiss dialogs in the BACKGROUND (`fusion_close_window`, never `fusion_send_key`/Escape, which steals focus). AUTOMATE sign-in / dialogs yourself (see the proven sign-in sequence in [fusion-onboarding](skills/fusion-onboarding/SKILL.md)); reserve an AD **`notify_user`** for something only the user can do — a **UAC/elevation** prompt, a license accept, a **2FA/password** step you must not auto-fill, or a licensing **DECISION** that risks their other machine. **`notify_user` works directly over the bridge's direct API (AD ≥1.9.84) — call it, don't relay a payload for the AI to send.** When this bridge runs UNATTENDED (e.g. on a VM) and the user is elsewhere, add a cross-AD **`target`** (a peer clientName from the `targets` verb, or `"all"`) so the toast lands where they are. Only foreground Fusion to SHOW the user something. See [fusion-driving](skills/fusion-driving/SKILL.md) and [fusion-onboarding](skills/fusion-onboarding/SKILL.md).+5. **`ok:true` is not enough — verify.** A long op that "times out" at ~60s is usually STILL RUNNING server-side (the relay caps the request, not the work). Verify by artifacts (the saved file / a screenshot / `fusion_get_app_state`), don't blindly retry.+6. **Reuse the running Fusion — and when a restart IS needed, YOU do it, never the user.** Fusion is a singleton: `fusion_readiness` before `fusion_start`; if `ready`, just drive. But a stale/missing add-in or a wedged session needs a restart, and that is YOUR job — `fusion_stop` then `fusion_start`. NEVER tell the user "restart Fusion" / "enable the add-in in UTILITIES" — doing it for them is the entire point of this bridge. (Add-in installs land in ALL Fusion add-in dirs — 2025+ Fusion scans `%APPDATA%\Autodesk\FusionAddins`, older builds use `...\Autodesk Fusion[ 360]\API\AddIns` — so a restart always picks it up.)+7. **Surface `_hint` verbatim + branch on `errorCode`.** Every verb returns an actionable `_hint`; relay it to the user unchanged and key your logic off the stable `errorCode`, not the prose.+8. **Never write files to a shared cloud ROOT.** Uploads default to an `Adom AI Workspace` folder; see [fusion-cloud-hygiene](skills/fusion-cloud-hygiene/SKILL.md). -Any browser window WE open for OAuth is ours to close. Do not assume the flow closes it.+## [!] Electronics PCBs: ALWAYS open the PROJECT file (never the schematic / board / 3D) -- `_track_signin_window(sessionId, profile, kind)` registers every window we open- (`kind` = `fusion` | `aps`).-- `_signin_janitor_loop()` runs as a daemon thread started in `main()` and polls every 20s. It- closes a tracked window as soon as its goal is met (`fusion_readiness.ready` for `fusion`,- APS `signedIn` for `aps`) or it exceeds `_JANITOR_MAX_AGE` (15 min - an OAuth window older than- that is dead weight, the request expires in ~2 min anyway).-- Closing uses `nbrowser_close_window`, which only closes OUR OWN sessions. **Never** close a- window the user opened: `nbrowser_force_close` refuses other threads' windows by design, and- the user's own tabs are off limits.+A Fusion electronics design is NOT one file. It is a parent/child chain of FOUR files, each a+different `productType`: -Same discipline applies to captions: `desktop_caption` auto-clears, but if you ever pass-`persist:true` YOU must hide it.+```+PROJECT EcadDesignProductType <-- OPEN THIS ONE, ALWAYS+ |- schematic SchematicProductType+ |- board/.brd BoardProductType+ |- 3D PCB DesignProductType (generated from the .brd - the LEAF; opening it gives NO editable board)+``` -### Is there a CLI to sign Fusion in? (investigated 2026-07-22 - answer: effectively no)+- **Open the PROJECT** (`EcadDesignProductType`) with `fusion_open_cloud_file` / `fusion_open_by_urn`.+ Fusion then brings up its schematic + board as children; move between them with the schematic view,+ `fusion_show_2d_board`, and `fusion_show_3d_board`.+- **NEVER open the schematic, the .brd, or the 3D file directly** to "get the board." You get an+ isolated/empty view and waste time trying to derive a 2D board from the 3D. The 3D is the LEAF,+ generated FROM the .brd - it is not the board, and you cannot pull a board out of it.+- Same-named copies in OTHER projects (e.g. a drone assembly) are usually 3D-model derivatives+ (`DesignProductType`), NOT the project. Don't open those.+- `fusion_open_cloud_file` / `fusion_open_by_urn` REPRIMAND you in the response `_hint` when you open+ a child file instead of the project - read it and re-open the `EcadDesignProductType` project.++## ⚡ Fast cloud search (APS) — READ THIS FIRST for any "find/search/open a cloud design" task++To search the team's Fusion cloud files, use **`fusion_aps_search`**, NOT the legacy+`fusion_search_cloud_files` (which walks folders one-by-one and takes 30+ MINUTES). APS hits+Autodesk's **server-side index** → results in **seconds**, and works with Fusion closed.++| Verb | Does |+|---|---|+| `fusion_aps_search {query, projectId?, limit?}` | Fast name search across the whole team hub; newest-first; shows each file's project. |+| `fusion_aps_browse {} / {projectId} / {projectId,folderId}` | Browse hubs→projects→folders→files (cloud explorer, no Fusion). |+| `fusion_aps_open {query}` | Search + open the best match in Fusion (opens by exact URN — works for nested files). Returns immediately; the open finishes in the background — poll `fusion_get_app_state` (or pass `{wait:true}`). |+| `fusion_aps_recent {limit?}` | Most-recently-modified designs across the hub, newest-first. |+| `fusion_aps_file_info {query}` | Version history for a file — count, who/when last modified, size. |+| `fusion_aps_status {}` | configured? signedIn? token live? + never-charge usage meter. **Call this first** — its `_hint` tells you the next step. |+| `fusion_aps_signin {}` | One-time per user: opens Autodesk consent, captures the token. |+| `fusion_aps_set_client_id {clientId}` | Set the org's APS Client ID. |++**Auth model (important): set up ONCE per company, each user signs in once.** An admin registers a free APS app for the org one time (the AI drives the whole setup — see the `fusion-aps-signin` skill) → a **Client ID** (a public PKCE id, no secret, safe to share within the org). Then every teammate just runs `fusion_aps_signin` once with their own Autodesk login — **no plan, no card, no setup** for them. It's **per-organization** (each company runs its own setup; don't ship one global Client ID). **Nobody is ever charged** — the card is identity-verification only, and the bridge is free-endpoint-only + usage-capped.++If `fusion_aps_status` says not configured/signed in, follow its `_hint`. Full why + step-by-step setup: the bundled **`fusion-aps-search`** and **`fusion-aps-signin`** skills.++## ⛔ "Expired subscription — Read Only" / trial / personal Fusion — what it does NOT block++**An expired / "Read Only" / personal-use Fusion can OPEN, VIEW, and BROWSE files just fine.**+Autodesk keeps the account in **Basic Access for 365 days** after a subscription lapses. So in+read-only Fusion you CAN: **open cloud designs, view/inspect them, browse the Data Panel, and run+APS search** — everything this bridge does for *reading* works.++Read-only ONLY blocks **save, export, modify, and creating new designs** (the Save/Export buttons+are dimmed). So:++- ⛔ **NEVER tell the user an `open` / `view` / `search` failed "because the Fusion subscription+ is expired / read-only."** That is FALSE and confuses users (it's been said wrongly many times).+ If `fusion_aps_open` / `fusion_open_cloud_file` hangs or fails, it is a **code / approach+ problem in the open path** (URN resolution, the Electronics design picker, a slow assembly+ download, a stuck main thread) — debug/fix THAT, not the license. Try harder; don't blame the+ subscription.+- ✅ The ONLY place read-only is a legitimate explanation is **save / export** — e.g.+ `fusion_export_*` may be blocked (Save/Export dimmed). There, "renew the subscription" is valid.++(Source: Autodesk — "Term has Expired or Usage Limits Exceeded"; data stays in Basic Access ~365 days.)++## Commands (core bridge)++| CLI form | Action | Purpose | Key args |+|---|---|---|---|+| `fusion_start` / `fusion_launch` | `start` / `launch` | Launch Fusion 360 (handles webdeploy glob, waits for bridge, dismisses startup picker) | — |+| `fusion_open_design` | `open_design` | Open a local `.f3d` / `.f3z` design file | `path` |+| `fusion_open_cloud_file` | `open_cloud_file` | Open a design from the user's Fusion cloud | `project`, `path` |+| `fusion_close_document` | `close_document` | Close the active document | — |+| `fusion_close_window` | `close_window` | Close a specific Fusion window by HWND | `hwnd` |+| `fusion_close` | `close` | Close Fusion 360 entirely | — |+| `fusion_dismiss_recovery` | `dismiss_recovery` | Dismiss the "recover unsaved work" dialog | — |+| `fusion_relocate_recovery` | `relocate_recovery` | Move recovery dialog offscreen (for recording) | — |+| `fusion_window_info` | `window_info` | Enumerate Fusion windows (HWND, title, bounds) | — |+| `fusion_screenshot_fusion` | `screenshot_fusion` | Screenshot the main Fusion window | — |+| `fusion_screenshot_all` | `screenshot_all` | Screenshot every Fusion window | — |+| `fusion_click_fusion` | `click_fusion` | Click at (x, y) in the Fusion main window | `x`, `y` |+| `fusion_send_key` | `send_key` | Send a keyboard shortcut to Fusion | `hwnd`, `key` |+| `fusion_addin_status` | `addin_status` | Report whether the in-process Adom add-in is loaded + healthy | — |+| `fusion_generate_package` | `generate_package` | Generate an IPC package via Fusion's built-in EPG (~50 families) + raised-WHITE marking (longest-axis, measured 10% margin, multi-line MPN+variant) + colored STEP + sidecar manifest - in ONE call | `type`, `params` (mm), `etch`, `etchStyle`, `outputStep` |+| `fusion_set_preference` | `set_preference` | Set appearance/navigation prefs LIVE (no restart): `theme` (light/darkblue/darkgray/classic/device/dark), `invertScrollZoom`, `orbitScheme` (fusion360/alias/inventor/solidworks/tinkercad/powermill), `modelingOrientation` (yup/zup), `gestureNav`, `cameraPivot`, `lengthUnit` (mm/cm/m/in/ft). Per-key result in `applied`; some themes aren't in every build (falls back) | any of the keys |+| `fusion_get_preferences` | `get_preferences` | Read current appearance/navigation prefs as the friendly values `set_preference` accepts | — |++## Extended commands via the in-process add-in++If the Adom add-in is installed (`cd ~/project/adom-desktop/plugins/fusion360 && python install_addin.py` on the laptop), Fusion 360 exposes an additional API over its own ports. Typical extra commands proxied through the bridge:++- `fusion_import_step` — import a STEP file into the active document+- `fusion_export_step` — export active body/occurrence to STEP+- `fusion_export_glb` — export to GLB (used by the 3D viewer)+- `fusion_export_lbr` — export an Eagle library (used by the Eagle workflow)+- `fusion_open_lbr` / `fusion_open_symbol` — open symbol libraries+- `fusion_command` — generic `{"command": "<fusion_api_name>", "args": {...}}` pass-through for any Fusion API call+- `fusion_list_cloud_projects`, `fusion_list_cloud_files`, `fusion_walk_cloud_tree`, `fusion_search_cloud_files`, `fusion_open_cloud_file`, `fusion_export_cloud_file` — Autodesk cloud Data API operations++**Opening multi-design electronics files — pass `fileExtension`.** A Fusion electronics data file that links to multiple designs (common for boards with variants) makes `fusion_open_cloud_file` pop Fusion's native **"Select Electronics Design File" picker** — a CEF/Chromium modal whose list is **not keyboard- or Win32-navigable** (and screen-coord clicks are unreliable on multi-monitor/HiDPI), so it **can't be resolved headlessly**. Avoid it by opening the specific file directly: `fusion_open_cloud_file {"fileName":"<name>","fileExtension":"fbrd", ...}` (the `.fbrd` board lands straight in PCB Editor — live-confirmed). Opening the bare name or the `.fprj` project triggers the picker. If a picker still appears, `_diagnose_addin_timeout`/`blockingDialogs` will classify it (see below).++### Blocking-dialog awareness++Several Fusion modals (the multi-link "Select Electronics Design File" picker, "Fusion needs to update", Document Recovery, "Save changes?") block the add-in's main thread. The bridge now **classifies them by title** and, on a blocked command, returns `blockingDialogs: [{hwnd, title, category, resolution}]` with the precise cause + fix — instead of a misleading "add-in not responding / may have crashed". So a blocked open tells you *which* modal is up and how to clear it.++Resolution notes:+- **Multi-link picker** is a **CEF/web modal whose list is NOT keyboard- or Win32-navigable** (SendInput reaches only the native Cancel/OK; screen-coord clicks are unreliable on multi-monitor/HiDPI). It currently **must be resolved on the desktop**. *Headless fix tracked separately:* have the add-in open the specific linked design via the Fusion API, avoiding the picker entirely.+- **Update nag** is reported, not auto-dismissed (auto-confirming could start an update mid-run) — clear it on the desktop; doing so also commonly resolves recurring add-in crashes (host/add-in version drift).+- **Recovery** prompts: `fusion_dismiss_recovery` / `fusion_relocate_recovery`.++Check `fusion_addin_status` first — if the add-in isn't loaded, these will fail with a `_hint` pointing at the install script.++## Cloud search — defensive contract (add-in v1.0.2+)++`fusion_search_cloud_files` is the most-used long-running verb. Its v1.0.2 behavior is engineered specifically so the AI never misreports a failed search as "file not found":++| You read in the response | Mean |+|---|---|+| `searchComplete: true` + `totalFound: 0` | **Confirmed negative.** No match in the searched scope. |+| `searchComplete: true` + `totalFound: N` | All matches returned. |+| `searchComplete: false` (any reason) | **Search stopped early.** May or may not be more matches. Do NOT say "not found" to the user. Re-run with broader caps OR narrower `folderPath`. |++`searchComplete` is `true` ONLY when every one of these is clean: not `folderLimitReached`, not result-`truncated`, not `searchTimedOut`, `foldersSkipped == 0`, `filesSkipped == 0`. Five conditions, all five must pass.++### Performance + the slow-search etiquette++`fusion_search_cloud_files` runs at ~1 folder/sec because Autodesk's free Fusion 360 Python API has no indexed search endpoint — it walks the cloud Data API one folder at a time (30+ min on a real hub). **⚠️ Prefer `fusion_aps_search` instead** (see the APS section at the top) — it uses Autodesk Platform Services' server-side index and returns in **seconds**. This legacy walk verb remains only as a fallback for when APS isn't set up; if a user is hitting slow searches, set up APS (`fusion_aps_status` → its `_hint`) rather than tuning the walk.++**Before firing a broad search**, ask the user which subfolder is likely. Naming a folder like "Molecules" cuts 5-40 min searches down to ~10s. The verb's response includes a `costAnalysis` block (`foldersPerSecond`, `estimatedSecondsPer100Folders`) so the AI can budget realistically.++### Streaming progress++For interactive sessions, wrap long searches in `watch` so the AI gets one JSON-line progress event per `interval` seconds:++```bash+adom-desktop watch '{+ "command": "fusion_search_cloud_files",+ "args": {+ "query": "cosmiic",+ "projectName": "Main",+ "folderPath": "Molecules",+ "recursive": true,+ "maxFolders": 300,+ "maxDepth": 10,+ "searchTimeout": 340,+ "timeout": 400+ },+ "interval": 3+}'+``` -John asked whether Fusion auth can be triggered from a command line instead of its GUI, which-would remove the whole click-the-Sign-In-button problem. Investigated on the real machine.+Output:+```+{"event":"started", ...}+{"event":"progress", "foldersVisited": 12, "filesFound": 1, "elapsedSeconds": 15.1, "currentFolder": "Molecules/COSMIIC", ...}+{"event":"progress", "foldersVisited": 173, "filesFound": 3, "elapsedSeconds": 168.3, ...}+{"event":"complete", "result": {... full response with searchComplete:true ...}}+``` -**What is actually running** (via `process_list {nameFilter}` - `commandLine` is populated there-even when `shell_execute`/`run_script` are dead, which is how this was found):+The `watch` wrapper composes with the `Monitor` tool — each stdout line becomes a notification. +### Args (no hard upper caps in v1.0.2+)++| Arg | Default | Notes |+|---|---|---|+| `query` | (required) | Substring of file name. **Case-insensitive both directions.** |+| `projectName` | active | Cloud project name. |+| `folderPath` | project root | Starting subfolder. **NARROW HERE — biggest perf lever.** |+| `recursive` | `false` | Descend into subfolders. |+| `maxDepth` | 2 | Recursion depth cap. |+| `maxFolders` | 10 | Folder-visit cap. |+| `maxResults` | 20 | Result cap. |+| `folderTimeout` | 30s | Per-folder cloud-API timeout. Bridge skips slow folders. |+| `searchTimeout` | 120s | Overall wall-clock timeout. Set 300+ for big walks. |+| `timeout` | 620s | HTTP envelope timeout (CLI+direct API). Bump if `searchTimeout` > 180. |++Real-world live test: `Main/Molecules` recursive depth-10 cosmiic search hit 173 folders in 168s with `searchComplete:true, totalFound:3, foldersSkipped:0, filesSkipped:0`. Fusion stayed `main_thread: responsive` throughout.++## Quick examples++```bash+# Launch Fusion+adom-desktop fusion_start '{}'++# Open a local design+adom-desktop fusion_open_design '{"path":"C:/designs/board.f3d"}'++# Screenshot all Fusion windows+adom-desktop fusion_screenshot_all '{}'++# Run a Fusion API command (add-in required)+adom-desktop fusion_command '{+ "command":"export_step",+ "args":{"occurrence":"body1","output":"C:/tmp/part.step"}+}'++# Check add-in health+adom-desktop fusion_addin_status '{}' ```-IdentityService.exe-IdentityUpdateService.exe-AdskIdentityManager.exe --process_name Autodesk.IDSDK.DefaultProcess-v2 \- --server_name Autodesk.IDSDK.DefaultServer-v2- (in <webdeploy>/production/<hash>/Autodesk Identity Manager/)-Fusion360.exe (in <webdeploy>/production/<hash>/)-FusionLauncher.exe-``` -**Conclusion: no.** `AdskIdentityManager.exe` DOES take flags, but they are **plumbing, not auth-commands**: they name the IPC channel (`--process_name` / `--server_name`) that the Identity SDK-broker listens on. It is a long-running local IPC server; Fusion is its CLIENT and asks it to do-the login over that channel. There is no `--login` / `--user` / `--token` surface, and the-resulting tokens go into a DPAPI-encrypted store (app entropy), so you cannot write one in either.+## Error shape++Every failing response includes a `_hint` field. Surface it verbatim when a Fusion command fails.++## Bridge architecture (v1.8.31+) — dynamic ports++Earlier versions used hardcoded port `8773` for the bridge and `8774` for the in-process add-in. **v1.8.31 made the bridge use OS-assigned ephemeral ports.** The add-in port is still 8774 (it's inside Fusion's process and the manifest pins it), but that's an internal detail.++- The CLI (`adom-desktop fusion_*`) routes through adom-desktop's direct API → the bridge's runtime port. No callers need port numbers.+- The add-in v1.0.2 ships a stale-lock watchdog in `http_server.py`: if a command holds the main-thread lock longer than `per_command_timeout + 30s` (e.g. Fusion's native cloud SDK wedges on a slow API call), the watchdog force-releases so subsequent commands work without restarting Fusion. The wedged command is orphaned but no longer blocks everything else.+- The `searchComplete` contract (see above) prevents false-negative reporting on capped/skipped searches. Always check `data.searchComplete` before telling the user "file not found".++## How this bridge is packaged + delivered (3 artifacts)++This bridge is ONE wiki page (`wiki.adom.inc/adom/adom-desktop-fusion-bridge`) that ships **three things**:++1. **The page git repo** — the ONE source of truth (all source, `skills/`, the docs, `page.json`, and the+ bridge manifest). Edit + push here.+2. **The Release zip + the bridge manifest** (`adom-bridge-fusion-manifest.json`) — what **adom-desktop**+ consumes on `bridge_install`: the manifest tells adom-desktop who this bridge is (name, version, the+ Release-zip URL, sha256, size, verb prefixes, health), and it streams that zip into its bridge cache.+3. **The adom-wiki skills pkg** (`adom-wiki pkg install adom/adom-desktop-fusion-bridge`) — **skills only**.+ It deploys these skills into a container's `~/.claude/skills/` so the AI knows how to drive this bridge+ and Fusion.++**Install handshake:** when an AI has adom-desktop install this bridge, adom-desktop installs the Release+AND tells the AI to `adom-wiki pkg install` this pkg — so the container gets all these skills. An AI can+also install the pkg itself; then the skills **auto-update** via the pkg mechanism (the goal). Full detail++ the publish ritual + the hero-relink reminder: the **`fusion-bridge-dev`** skill.++## Related++- **Building component libraries (symbol + footprint + RENDERING 3D):** **`fusion-libraries`** (one part) and **`fusion-multipart-libraries`** (many parts in one `.lbr`, each with real 3D) — the latter's SOLVED recipe is `fusion_make_3d_package` (STEP → orient → f3d → upload → `wip_urn`, hand-written into the library, **no GUI dialogs**). Full tutorial: the **`fusion-multipart-libraries`** skill's `CREATING_BASIC_PARTS_LIBRARIES.md`. Operate Fusion safely with **`fusion-driving`** + **`fusion-cloud-save`**.+- **`fusion-aps-search`** + **`fusion-aps-signin`** — APS fast-search reasoning + the AI-driven one-time setup (bundled in this repo's `skills/`).+- `adom-desktop-kicad` — sibling bridge for KiCad.+- `adom-desktop-direct-api` — the direct API contract that callers route through.+- **Canonical repo:** the wiki page `https://wiki.adom.inc/adom/adom-desktop-fusion-bridge` (+ GitHub backup `adom-inc/adom-desktop-fusion-bridge`). The old `adom-inc/adom-desktop/plugins/fusion360/` is a stale seed.++## Quick demo on first install (sample library included) -**What IS command-line reachable: the last mile.** The `autodesk://` protocol handler is what-returns the token to a waiting Fusion, and protocol handlers are invocable by URL. So the final-browser click ("Open Product") is in principle replaceable by firing the `autodesk://` callback-directly. NOT yet verified live - do not claim it works until someone tests it.+The repo ships a ready sample so you can prove the bridge end to end right after installing:+`examples/ADS8588SIPM.fusion.lbr` (a real 4-sided LQFP-64 library) + `examples/ADS8588SIPM.step` (its 3D). -**So the practical answer stays: drive the GUI, but drive it in the BACKGROUND.** Fusion's Sign In-button is UIA-invokable, so no foreground is needed anyway (see the sign-in section above). That-already solves the annoyance a CLI would have solved.+1. Download both from this page's Files tab (or `adom-wiki repo clone`), then stage them onto Windows+ (serve + `Invoke-WebRequest`, since there is no container->Windows push verb).+2. **Import demo:** `fusion_open_lbr {"filePath":"C:/.../ADS8588SIPM.fusion.lbr","verify":true}` -> the+ symbol + 4-sided footprint render in the Electronics Library editor.+3. **3D-attach demo:** `fusion_attach_3d_package {"filePath":"C:/.../ADS8588SIPM.fusion.lbr",+ "modelPath":"C:/.../ADS8588SIPM.step","packageName":"ADS8588SIPM"}` -> click the Save dialog per the+ returned hint -> the real chip binds onto the footprint (Content Manager shows the 3D under the package). -**Bonus finding:** two `webdeploy/production/<hash>` dirs were live at once - `Fusion360.exe` from-one hash and `FusionLauncher.exe` from another. Per this repo's own notes Fusion NEVER prunes old-builds, so a dir count is NOT an update signal. Use `fusion_update_in_progress()`.+See the **`fusion-libraries`** skill's `MAKING_LIBRARIES.md` for the full walkthrough + screenshots.
page.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-desktop-fusion-bridge",- "version": "1.6.96",+ "version": "1.7.9", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Desktop loads the bridge runtime itself from the release zip.", "dependencies": {
skills/fusion-multiprofile-signin/SKILL.md+16−2@@ -26,10 +26,24 @@ Autodesk login is `[email protected]` in his work profile. ## The fix: `fusion_signin` ```bash-adom-desktop fusion_signin '{"profile":"chrome:[email protected]"}' # fix + REMEMBER your profile-adom-desktop fusion_signin '{}' # auto: probe/remember/ask+# BEST - one server-side call does the whole chain, beats the ~2-min clock:+adom-desktop fusion_signin '{"auto":true,"profile":"chrome:[email protected]"}'+adom-desktop fusion_signin '{"profile":"chrome:[email protected]"}' # staged/manual: fix + REMEMBER profile+adom-desktop fusion_signin '{}' # staged: probe/remember/ask ``` +### `auto:true` - the reliable path (do this)++The manual staged loop below needs THREE separate remote round-trips (click Sign In, capture, reopen)+and each one races Fusion's ~2-minute request expiry - on a slow or flaky relay you lose that race and+the callback shows "Sign-in request expired". `auto:true` collapses the whole chain into ONE+server-side call that runs **on the box**: it clicks Fusion's Sign In, polls the browser History+on-box (up to `waitSec`, default 32) for the fresh authorize URL, then re-opens it in the target+profile - so only ONE remote hop happens and everything else is local and fast, fitting inside the+2-min window. Pass the profile too (`{"auto":true,"profile":"chrome:[email protected]"}`) so it does not+have to stop and ask. Prefer this; use the staged calls below only to debug or when auto reports it+could not click/capture.+ What it does: 1. **Reads the full authorize URL out of the wrong browser's History.** Fusion writes a complete