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.40: auto-resolve seat dialog via UIA background click in deterministic launch loop
5 files changed
+80−87
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.39+1.6.40
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.40 - 2026-07-06++- **THE seat-dialog fix: `fusion_start` now auto-resolves "Active Sessions Exceeded" via UIA in the+ BACKGROUND** (John pushed me back to UIA - it was the answer). The launch handler is now a+ deterministic state loop: poll the add-in round-trip (ground truth for drivable) while, each tick,+ detecting the seat dialog (`find_licensing_dialog`, by process+size) and invoking its native+ 'Continue' button with `desktop_ui_click` via the AD direct API - which UIA-Invokes it WITHOUT+ foregrounding (proven: "Clicked in the BACKGROUND - the window did NOT come to the foreground").+ Suspend is pre-selected, so Continue grabs the license. This un-sticks the launch, which is what+ caused the endless foreground-stealing: a blocked seat dialog made Fusion retry sign-in in a loop,+ foregrounding every retry. No more kill+relaunch (the licensing server keeps the seat anyway) and+ no notify/punt to the user.+ ## 1.6.39 - 2026-07-06 - **Dev skill: added THE CORE GOAL** (John) - the entire reason this bridge exists is for the AI to
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.39",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.39/adom-bridge-fusion-v1.6.39.zip",- "sha256": "5df9ca5388a850cd77291bd4fba74e2124205e36b492a0e1fa2f736de436ca4e",- "size": 340095,+ "version": "1.6.40",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.40/adom-bridge-fusion-v1.6.40.zip",+ "sha256": "f82a746b72475e247f420a14a7b67a313eca9c12d6a02dc5ed0cb57db960ede1",+ "size": 339593, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-07-06T12:17:21Z"+ "released_at": "2026-07-06T12:56:14Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.39",+ "version": "1.6.40", "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.",
server.py+60−80@@ -116,6 +116,27 @@ def _reclaim_seat_from_peers() -> int: return n +def _resolve_seat_via_uia(dlg: dict) -> bool:+ """Resolve the 'Active Sessions Exceeded' seat dialog by UIA-invoking its native+ 'Continue' button (Suspend is pre-selected) - entirely IN THE BACKGROUND.++ THE breakthrough (John 2026-07-06, after a long day of failing): UIA Invoke does+ NOT foreground the window, so this grabs the license without ever disturbing the+ user - unlike SendInput coordinate clicks (need foreground) or kill+relaunch (the+ server keeps the seat). Proven live: `desktop_ui_click {hwnd, name:"Continue"}`+ returned "Clicked in the BACKGROUND - the window did NOT come to the foreground"+ and the dialog cleared. The bridge calls the AD `desktop_ui_click` verb on ITSELF+ via the AD 1.9.84 direct API. Never raises."""+ try:+ hwnd = dlg.get("hwnd")+ if not hwnd or not ad_client.available():+ return False+ res = ad_client.call("desktop_ui_click", {"hwnd": hwnd, "name": "Continue"})+ return bool(res and (res.get("success") or res.get("status") == "ok"))+ except Exception:+ return False++ 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@@ -172,86 +193,43 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict: if _dlg.get("category") in AUTO_DISMISS_CATEGORIES: close_dialog_bg(_dlg.get("hwnd")) # benign ack (WM_CLOSE == Cancel/OK) - # SEAT/LICENSING dialog ("Active Sessions Exceeded") -> AUTO-RECLAIM the license,- # never ask (John 2026-07-06: "just grab the license back... no harm"). DETECT IT- # DETERMINISTICALLY by owning PROCESS + dialog size (find_licensing_dialog), NOT the- # "Fusion360" window title that fooled the title-based classifier for a whole day.- # 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) + let the Autodesk- # server release it, kill the local instance, and relaunch clean. One-shot guarded.- _lic = find_licensing_dialog()- if _lic and not (args or {}).get("_seatReclaimAttempted"):- _reclaim_seat_from_peers()+ # ── DETERMINISTIC launch state loop (John 2026-07-06: track + handle state in+ # CODE, in the BACKGROUND, never punt to the user, never foreground). Poll the+ # add-in round-trip (the ground truth for "drivable") while, each tick:+ # • AUTO-RESOLVING the seat/licensing dialog ("Active Sessions Exceeded") via+ # UIA Invoke of its 'Continue' button - background, no foreground (THE fix;+ # Suspend is pre-selected so Continue grabs the license). This is what+ # un-sticks the launch: a blocked seat dialog made Fusion retry sign-in in a+ # loop, foregrounding on every retry - the source of the endless fg-steal.+ # • dismissing benign recovery/startup dialogs.+ # Detection is deterministic (find_licensing_dialog by owning process + size,+ # NOT the "Fusion360" title). No kill/relaunch (the server keeps the seat), no+ # notify (we handle it ourselves). ──+ _RELAY_BUDGET = 50 # keep total handler time under AD's ~60s relay cap+ _deadline = _t.time() + max(6, int(_RELAY_BUDGET - (_t.time() - _start)))+ _seat_clicks = 0+ addin_ok = False+ while _t.time() < _deadline:+ if _probe_addin(timeout=1.5):+ addin_ok = True+ break try:- handle_fusion_kill(fusion_info, {})+ _lic = find_licensing_dialog() except Exception:- pass- _t.sleep(4.0) # give the Autodesk licensing server a moment to release the seat- 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:- _signin = any(d.get("category") == "signin_required" for d in _needs_user)- _decision = any(d.get("decision") for d in _needs_user)- notify_payload = ({"title": "Fusion needs your Autodesk sign-in",- "body": "Fusion is at the sign-in screen. Complete it (or I will drive it) "- "so I can keep going.", "level": "warning"} if _signin else- {"title": "Fusion launch needs a decision",- "body": "A licensing dialog needs your choice.", "level": "warning"})- # A seat/licensing DECISION is a TRUE WALL (must NOT auto-pick - it can- # suspend or shut down Fusion on the user's OTHER machine, risking its- # unsaved work). As of AD 1.9.84 we fire notify_user OURSELVES via the- # direct API, cross-AD (reach_user) so the toast lands on whichever machine- # the user is at - this bridge may be running unattended on a VM. Sign-in- # is deliberately NOT auto-notified: automate it first (see hint); the- # payload is the last-resort escalation only.- notified = None- if _decision and ad_client.available():- notified = ad_client.notify(reach_user=True, **notify_payload)- return {- "success": True,- "needsUserDecision": True,- "needsSignin": _signin,- "setupDialogs": launch_dialogs,- "resolvedPath": fusion_info.get("exe_path", ""),- "notifyUser": notify_payload,- "notifyDelivered": bool(notified and (notified.get("action") == "displayed"- or notified.get("status") == "ok")),- "_hint": (("First-run Autodesk SIGN-IN required. AUTOMATE IT FIRST, don't punt to the user. "- "PROVEN sequence (2026-07-05): (1) if the user's Autodesk web session is already "- "warm, clear any STALE 'signin.autodesk.com' tabs first - a dead tab holds an "- "EXPIRED code and is the #1 cause of 'Sign-in request expired'. (2) desktop_ui_"- "click 'Retry browser sign-in' (or 'Sign In') on the Fusion window to mint ONE "- "fresh request. (3) On the 'You're signed in' page, click 'Open Product' with "- "desktop_click (SendInput = isTrusted) NOT desktop_ui_click (UIA Invoke does NOT "- "fire the protocol handler). (4) An in-page 'open Autodesk Identity Manager?' "- "overlay appears (NOT an owned popup - screenshot catches it, popup-capture "- "won't): tick 'Always allow' then click 'Open', both via desktop_click, FAST "- "before the code expires. (5) poll fusion_readiness - the window flips 'Signing "- "in' -> 'Home'. nbrowser (CDP) can drive steps 3-4 headlessly IF it's on the "- "SAME browser window Fusion opened. NEVER auto-enter the password/2FA. If you "- "genuinely must ask the user, notify_user works directly now (AD 1.9.84); when "- "you're on a VM and the user is elsewhere, add `target` from the `targets` verb "- "(or 'all') so the toast lands where they are. " if _signin else "")- + (("A licensing/seat DECISION dialog is up - do NOT auto-pick. I already fired "- "notify_user cross-AD (notifyDelivered=%s); ASK the user which option, then "- "click it. " % (bool(notified and (notified.get("action") == "displayed"- or notified.get("status") == "ok"))))- if _decision else "")- + " ".join(d["resolution"] for d in _needs_user)),- }-- _RELAY_BUDGET = 50 # keep total handler time under AD's ~60s relay cap- _remaining = max(6, int(_RELAY_BUDGET - (_t.time() - _start)))- addin_ok = wait_for_addin(timeout=_remaining, dismiss_dialogs=True)+ _lic = None+ if _lic and _seat_clicks < 5:+ if _resolve_seat_via_uia(_lic):+ _seat_clicks += 1+ else:+ try:+ dismiss_recovery_dialog()+ except Exception:+ pass+ _t.sleep(2.5) if not addin_ok:- # Add-in not up within our budget. On a first launch this is EXPECTED- # (still initializing), not a failure and not a retry-able timeout.+ # Add-in not up within our budget. On a first launch this is EXPECTED (still+ # initializing / signing in), not a failure and not a retry-able timeout. return { "success": True, "stillLaunching": True,@@ -259,11 +237,13 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict: "statusVerb": "fusion_readiness", "recoveryFilesRelocated": reloc["moved"], "resolvedPath": fusion_info.get("exe_path", ""),+ "seatDialogResolved": _seat_clicks > 0, "_hint": (- "Fusion is LAUNCHING but the add-in isn't up yet - a FIRST launch can take "- "2-4 min (component downloads / updates / cloud sync). This is NOT a failure and "- "NOT a timeout to retry: do NOT re-call fusion_start. POLL fusion_readiness every "- "~10s until ready:true (or fusion_addin_status until it responds), then drive."+ ("Auto-resolved a seat/licensing dialog in the background (UIA, no foreground). "+ if _seat_clicks else "")+ + "Fusion is LAUNCHING but the add-in isn't up yet - a FIRST launch / a fresh "+ "sign-in can take 2-4 min. This is NOT a failure and NOT a timeout to retry: do NOT "+ "re-call fusion_start. POLL fusion_readiness until ready:true, then drive." ), }