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
v1.6.34: focus-guard covers Autodesk family + seat-conflict auto-reclaim (background)
7 files changed
+124−27
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.33+1.6.34
CHANGELOG.md+13@@ -3,6 +3,19 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge · GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.34 - 2026-07-06++- **Focus-guard now covers the whole Autodesk launch FAMILY** (fix for the 1.6.33 guard, caught live).+ The guard only demoted `Fusion360.exe` windows, but the splash "Loading additional modules"+ (`FusionLauncher.exe`) and the "Signing in" / **"Active Sessions Exceeded"** dialogs+ (`AdskIdentityManager.exe`) slipped past and yanked the user's foreground. Now `_fusion_family_pids()`+ matches every family process image and the guard (150s) minimizes any of them that steals focus.+- **Seat conflict ("Active Sessions Exceeded") now AUTO-RECLAIMS the license instead of asking**+ (John: "just grab the license back... no harm, the user can reclaim on the other machine"). The+ dialog is a CEF modal that ignores background SendInput, so the launch handler frees the seat at the+ SOURCE via `_reclaim_seat_from_peers()` (cross-AD `fusion_stop` to peer ADs), kills the local instance+ stuck on the dialog, and relaunches clean - no conflict dialog, no user prompt, fully background.+ ## 1.6.33 - 2026-07-06 - **Fusion now launches in the BACKGROUND - never steals the user's foreground** (John's directive).
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.33",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.33/adom-bridge-fusion-v1.6.33.zip",- "sha256": "70b10a6c7eabfa5116fa749a1d02b76fc5b918787fcf64b5387da567f317bc65",- "size": 332961,+ "version": "1.6.34",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.34/adom-bridge-fusion-v1.6.34.zip",+ "sha256": "da0ae88c943bab901db9509c800359459a20507baf6e469cbb2c287afafa3a5d",+ "size": 334972, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-07-06T11:01:29Z"+ "released_at": "2026-07-06T11:20:43Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.33",+ "version": "1.6.34", "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", "homepage": "https://wiki.adom.inc/adom/adom-desktop-fusion-bridge", "author": "Adom Inc.",
fusion_detect.py+42−11@@ -299,6 +299,35 @@ def _dismiss_startup_dialogs(): _SW_SHOWMINNOACTIVE = 7 # show minimized, do NOT activate/steal focus +# Every process in the Autodesk launch family whose windows must NEVER steal the+# user's foreground: the main app (Fusion360.exe), the streamed-app launcher ++# splash ("Loading additional modules" - FusionLauncher.exe), and the identity/seat+# manager (the "Signing in" + "Active Sessions Exceeded" dialogs - AdskIdentityManager+# .exe). The 1.6.33 guard only knew Fusion360.exe, so the splash + seat dialog sailed+# straight past it and yanked focus (John, 2026-07-06). Match the whole family.+_FUSION_FAMILY_IMAGES = {+ "fusion360.exe", "fusionlauncher.exe", "adskidentitymanager.exe",+ "adsso.exe", "adcefwebbrowser.exe", "fusion360bootstrap.exe",+}+++def _fusion_family_pids() -> set:+ """PIDs of every running Autodesk-launch-family process (via one tasklist CSV)."""+ try:+ cnw = subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0+ out = subprocess.check_output(+ ["tasklist", "/FO", "CSV", "/NH"],+ stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=cnw,+ ).decode("utf-8", "replace")+ except Exception:+ return set()+ pids = set()+ for m in re.finditer(r'"([^"]+\.exe)","(\d+)"', out):+ if m.group(1).lower() in _FUSION_FAMILY_IMAGES:+ pids.add(int(m.group(2)))+ return pids++ def _fusion_pids() -> set: """PIDs of running Fusion360.exe processes (via tasklist CSV). Empty on error.""" try:@@ -332,14 +361,16 @@ def _popen_fusion_background(exe_path: str): ) -def _focus_guard(seconds: float = 120.0):- """For `seconds`, demote any Fusion window that steals the foreground.+def _focus_guard(seconds: float = 150.0):+ """For `seconds`, demote any Autodesk-FAMILY window that steals the foreground. - SURGICAL: only acts when the CURRENT foreground window belongs to a Fusion- process (i.e. Fusion just grabbed focus). It minimizes that window without- activating anything, so focus falls back to whatever the user was using. It- does NOT touch Fusion when the user is in another app, so it never fights a- user who deliberately opened Fusion. Best-effort; never raises."""+ SURGICAL: only acts when the CURRENT foreground window belongs to an Autodesk+ launch-family process (Fusion / launcher-splash / identity-seat manager) - i.e.+ something just grabbed focus off the user. It minimizes that window without+ activating anything, so focus falls back to whatever the user was in. It does+ NOT touch the family when the user is working in another app, so it never+ fights a user who deliberately foregrounds Fusion themselves. Best-effort;+ never raises.""" try: user32 = ctypes.windll.user32 except Exception:@@ -349,8 +380,8 @@ def _focus_guard(seconds: float = 120.0): last_refresh = 0.0 while time.time() < end: now = time.time()- if now - last_refresh > 3.0:- pids = _fusion_pids()+ if now - last_refresh > 2.0:+ pids = _fusion_family_pids() # whole family, not just Fusion360.exe last_refresh = now try: fg = user32.GetForegroundWindow()@@ -358,11 +389,11 @@ def _focus_guard(seconds: float = 120.0): wpid = ctypes.wintypes.DWORD(0) user32.GetWindowThreadProcessId(fg, ctypes.byref(wpid)) if wpid.value in pids:- # minimize the focus-stealing Fusion window, no activation+ # minimize the focus-stealing family window, no activation user32.ShowWindowAsync(fg, _SW_SHOWMINNOACTIVE) except Exception: pass- time.sleep(0.2)+ time.sleep(0.15) def _start_focus_guard(seconds: float = 120.0):
server.py+45−4@@ -92,6 +92,30 @@ def _addin_staleness(reported_version) -> dict: fusion_info = None +def _reclaim_seat_from_peers() -> int:+ """Free the Autodesk seat held by another machine by stopping Fusion on peer ADs.++ John (2026-07-06): "if the user asked you to do something with fusion you should+ just grab the license back. the user can always grab it back on the other machine,+ so there's no harm." So on a seat conflict we do NOT ask - we free the seat at the+ SOURCE via the cross-AD direct API (fully background + reliable, no CEF-dialog+ clicking), then relaunch locally with no conflict. Best-effort; returns the number+ of peers signalled (0 if the direct API/peers are unavailable)."""+ n = 0+ try:+ if not ad_client.available():+ return 0+ for peer in ad_client.peers():+ try:+ if ad_client.call("fusion_stop", {}, target=peer) is not None:+ n += 1+ except Exception:+ pass+ except Exception:+ pass+ return n++ def _handle_launch(fusion_info: dict, args: dict) -> dict: """Launch Fusion 360 and optionally wait for the AdomBridge add-in.""" # LIVE detect (not the stale bridge-start fusion_info snapshot) so a launch right@@ -142,14 +166,31 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict: # WATCH for launch/licensing dialogs the moment the process is up - IN CODE, so # a launch is never blind to them (the seat-conflict + streamed-app-error dialogs # are owned by AdskIdentityManager/FusionLauncher, invisible to the Fusion-scoped- # classifier). Auto-dismiss BENIGN errors; a DECISION dialog (seat conflict) is- # returned for the AI to notify_user + ASK - never auto-picked.+ # classifier). Auto-dismiss BENIGN errors. launch_dialogs = classify_launch_dialogs() for _dlg in launch_dialogs: if _dlg.get("category") in AUTO_DISMISS_CATEGORIES: close_dialog_bg(_dlg.get("hwnd")) # benign ack (WM_CLOSE == Cancel/OK)- # Surface anything needing the USER: seat DECISIONS + first-run SIGN-IN. Never- # auto-close these (a WM_CLOSE would kill the sign-in window).++ # SEAT CONFLICT ("Active Sessions Exceeded") -> AUTO-RECLAIM the license, never ask+ # (John 2026-07-06: "just grab the license back... no harm, the user can reclaim on+ # the other machine"). The dialog is a CEF modal that ignores background SendInput,+ # so instead of fighting it we free the seat at the SOURCE (stop Fusion on peer ADs+ # via cross-AD), kill the local instance stuck on the dialog, and relaunch clean -+ # no conflict dialog reappears. Guarded against infinite recursion by a one-shot arg.+ _seat = [d for d in launch_dialogs if d.get("category") == "session_conflict_decision"]+ if _seat and not (args or {}).get("_seatReclaimAttempted"):+ _reclaim_seat_from_peers()+ try:+ handle_fusion_kill(fusion_info, {})+ except Exception:+ pass+ _t.sleep(2.0)+ return _handle_launch(fusion_info, {**(args or {}), "_seatReclaimAttempted": True})++ # Surface anything still needing the USER: remaining seat DECISIONS (only if the+ # auto-reclaim above couldn't free it - e.g. the seat holder isn't a reachable AD)+ # + first-run SIGN-IN. Never auto-close these (a WM_CLOSE would kill the window). _needs_user = [d for d in launch_dialogs if d.get("decision") or d.get("category") == "signin_required"] if _needs_user:
skills/fusion-bridge-dev/SKILL.md+17−5@@ -16,11 +16,23 @@ 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.** `STARTUPINFO` alone is not enough — Fusion re-foregrounds itself- several times as its splash/main window initialize. `ensure_fusion_running()` fires- `_start_focus_guard()`: a daemon thread that, for ~120s, minimizes (no-activate) **only** a Fusion- window that is **currently the foreground** (it detects focus-theft via `GetForegroundWindow` +- PID match, so it never fights a user who deliberately opened Fusion). Reuse it for any new launch.+- **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.+- **Seat conflict ("Active Sessions Exceeded") = AUTO-RECLAIM, never ask.** John (2026-07-06): "just+ grab the license back… the user can always grab it back on the other machine, so there's no harm."+ The dialog is a CEF modal that ignores background `SendInput`, so do NOT try to click it. Instead+ `_handle_launch` frees the seat at the SOURCE — `_reclaim_seat_from_peers()` stops Fusion on peer ADs+ via the cross-AD direct API (fully background, reliable) — then kills the local instance stuck on the+ dialog and relaunches clean (guarded by a one-shot `_seatReclaimAttempted` arg). Do not reintroduce a+ notify+ask path for the seat dialog. - **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