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.12: launch watches licensing/error dialogs in code (auto-dismiss benign, ask on seat-decisions)
7 files changed
+154−9
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.6.11+1.6.12
CHANGELOG.md+14@@ -3,6 +3,20 @@ 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.12 - 2026-07-03++- **Launch now WATCHES for licensing/launch dialogs in CODE (not just a skill), found live on the+ ADOMBASELINE first-run test.** `_handle_launch` enumerates ALL top-level windows the instant the+ Fusion process is up and classifies the dialogs that block a first launch but are owned by the+ Autodesk FAMILY (AdskIdentityManager / FusionLauncher), so the Fusion-scoped classifier misses+ them: **"Active Sessions Exceeded"** + **"Suspend Remote Session"** (seat conflict) and **"Error+ Launching Streamed Application / missing or incomplete / re-install"** (an incomplete webdeploy+ folder). Benign launch-error acks are **auto-dismissed** (WM_CLOSE, background). A seat-conflict is+ a **DECISION** dialog: the bridge returns `needsUserDecision:true` + `setupDialogs[]` + a `_hint`+ that says fire a `notify_user` and ASK the user which option to take (the options+ suspend/shut-down Fusion on their OTHER machine) - it NEVER auto-picks. New helpers in+ `dialog_classify.py`: `classify_launch_dialogs`, `close_dialog_bg`, `DECISION_CATEGORIES`.+ ## 1.6.11 - 2026-07-03 - **Detect a system-wide (`--globalinstall`) Fusion, not just per-user (fixes a broken onboarding
adom-bridge-fusion-manifest.json+5−5@@ -1,13 +1,13 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.6.11",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.11/adom-bridge-fusion-v1.6.11.zip",- "sha256": "c9d494cbf710ac86be6bb9bd851fc2cba38ba088d8adf8a910995d50a350aa40",- "size": 299827,+ "version": "1.6.12",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.12/adom-bridge-fusion-v1.6.12.zip",+ "sha256": "566613333a5ab685a6ec1ed51545cf8d5080ff9698a31571c4766af56b229dc0",+ "size": 302393, "verbPrefixes": [ "fusion_" ], "healthEndpoint": "/status",- "released_at": "2026-07-03T16:15:00Z"+ "released_at": "2026-07-03T17:00:00Z" }
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.6.11",+ "version": "1.6.12", "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.",
handlers/dialog_classify.py+109@@ -64,8 +64,43 @@ _RULES = [ (("what do you want to design", "what to design"), "what_to_design", "Fusion's start picker. fusion_send_key {\"key\":\"escape\"} to dismiss."),++ # ── Launch / licensing / setup dialogs (owned by AdskIdentityManager /+ # FusionLauncher, so classify_launch_dialogs enumerates ALL top-level+ # windows to catch them - get_fusion_window_info would miss them). ──++ # Autodesk SEAT conflict — a DECISION dialog. The options suspend / shut down+ # Fusion on the user's OTHER machine (risking its unsaved work), so NEVER+ # auto-pick: notify + ASK the user which option they want.+ (("active sessions exceeded", "more active sessions", "more sessions running than are allowed",+ "sessions running than are allowed"),+ "session_conflict_decision",+ "DECISION DIALOG - do NOT auto-pick. This Autodesk seat is already active on another machine; "+ "the options SUSPEND or SHUT DOWN Fusion THERE (risking that machine's unsaved work). fire a "+ "notify_user and ASK the user which option they want, then click it. (Suspend = pausable/"+ "resumable; Shut down = saves the other machine's work to a recovery file.)"),++ (("suspend remote session",),+ "session_suspend_confirm",+ "Confirmation of a chosen 'suspend the OTHER machine's Fusion'. Only click Continue if the user "+ "ALREADY chose suspend; otherwise Go Back and ASK. Never auto-confirm a seat action."),++ # webdeploy launch failure - an INCOMPLETE/stale production folder (missing+ # FusionLauncher.exe.ini). Benign to dismiss; means you launched the wrong hash.+ (("error launching streamed application", "missing or incomplete", "please re-install",+ "re-install the application"),+ "launch_error",+ "Launch error from an INCOMPLETE webdeploy folder (missing FusionLauncher.exe.ini). Safe to "+ "dismiss (OK/close) - it is only an acknowledgement. Launch the COMPLETE production folder (the "+ "hash dir that HAS FusionLauncher.exe.ini); detect_fusion now prefers it."), ] +# Categories that require a USER decision — the launch code must NOT auto-dismiss+# these; it surfaces them + fires a notify_user + asks.+DECISION_CATEGORIES = {"session_conflict_decision", "session_suspend_confirm"}+# Launch/setup categories enumerated across ALL top-level windows (not just Fusion).+_LAUNCH_CATEGORIES = DECISION_CATEGORIES | {"launch_error"}+ def _category_for_title(title: str): t = (title or "").strip().lower()@@ -97,6 +132,80 @@ def _category_for_title(title: str): "Identify it from the screenshot, then take the work-preserving action.") +def _enumerate_all_top_level():+ """(hwnd, title) for every visible, titled top-level window.++ Unlike get_fusion_window_info (scoped to the Fusion process), this catches+ dialogs owned by the Autodesk FAMILY - AdskIdentityManager (sign-in / seat+ conflict) and FusionLauncher (streamed-app launch errors) - which are the+ ones that block a first launch. Best-effort; never raises.+ """+ import ctypes+ results = []+ user32 = ctypes.windll.user32 if hasattr(ctypes, "windll") else None+ if not user32:+ return results+ WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)++ def _cb(hwnd, _lparam):+ try:+ if user32.IsWindowVisible(hwnd):+ n = user32.GetWindowTextLengthW(hwnd)+ if n:+ buf = ctypes.create_unicode_buffer(n + 1)+ user32.GetWindowTextW(hwnd, buf, n + 1)+ if buf.value.strip():+ results.append((hwnd, buf.value))+ except Exception:+ pass+ return True++ try:+ user32.EnumWindows(WNDENUMPROC(_cb), 0)+ except Exception:+ pass+ return results+++def classify_launch_dialogs() -> list:+ """Classify LAUNCH / licensing / setup dialogs across ALL top-level windows.++ Each entry: {hwnd, title, category, resolution, decision}. `decision:true`+ means a USER choice is required (seat conflict) - the launch path must NOT+ auto-dismiss it; it notifies + asks. Benign `launch_error` dialogs (decision:+ false) can be closed. Empty list if none. Never raises.+ """+ out = []+ for hwnd, title in _enumerate_all_top_level():+ classified = _category_for_title(title)+ if not classified:+ continue+ category, resolution = classified+ if category not in _LAUNCH_CATEGORIES:+ continue+ out.append({+ "hwnd": hwnd,+ "title": title,+ "category": category,+ "resolution": resolution,+ "decision": category in DECISION_CATEGORIES,+ })+ return out+++def close_dialog_bg(hwnd) -> bool:+ """WM_CLOSE a window in the BACKGROUND (PostMessage = no focus steal). Used to+ auto-dismiss benign launch-error acks. Never raises; returns True if posted."""+ import ctypes+ user32 = ctypes.windll.user32 if hasattr(ctypes, "windll") else None+ if not user32 or not hwnd:+ return False+ try:+ return bool(user32.PostMessageW(ctypes.c_void_p(int(hwnd)), 0x0010, 0, 0)) # WM_CLOSE+ except Exception:+ return False++ def classify_blocking_dialogs() -> list: """Return classified blocking dialogs currently open in Fusion.
page.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-desktop-fusion-bridge",- "version": "1.6.12",+ "version": "1.6.13", "type": "app", "description": "Drive Autodesk Fusion 360 from the cloud via Adom Desktop: 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 with browse/recent/versions, cloud download/upload and folder creation, and in-app parametric modeling. Never-charge: APS calls are capped to the free tier.", "dependencies": {},
server.py+23−1@@ -33,7 +33,7 @@ from handlers.fusion_ui import ( close_window, get_fusion_window_info, )-from handlers.dialog_classify import classify_blocking_dialogs+from handlers.dialog_classify import classify_blocking_dialogs, classify_launch_dialogs, close_dialog_bg from install_addin import install as install_addin import aps import describe@@ -134,6 +134,28 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict: if err: return err + # 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.+ launch_dialogs = classify_launch_dialogs()+ for _dlg in launch_dialogs:+ if not _dlg.get("decision"):+ close_dialog_bg(_dlg.get("hwnd")) # benign ack (incomplete-webdeploy launch error)+ _decisions = [d for d in launch_dialogs if d.get("decision")]+ if _decisions:+ return {+ "success": True,+ "needsUserDecision": True,+ "setupDialogs": launch_dialogs,+ "resolvedPath": fusion_info.get("exe_path", ""),+ "_hint": ("A licensing/seat DECISION dialog is up during launch (e.g. 'Active Sessions "+ "Exceeded'). Do NOT auto-pick - the options affect the user's OTHER machine. "+ "Fire a notify_user, ASK the user which option they want, then click it with "+ "desktop_ui_click. " + " ".join(d["resolution"] for d in _decisions)),+ }+ _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)