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
Update 11 file(s)
9 files changed
+97−18
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.176+1.9.177
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.176",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.176/adom-bridge-fusion-v1.9.176.zip",- "sha256": "ecc9a01e99e8611ca3d53a861a88977ac316767edebacfe91c96c321c166d4e1",- "size": 483333,+ "version": "1.9.177",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.177/adom-bridge-fusion-v1.9.177.zip",+ "sha256": "d1836a63ecac8945e8b8a66af1c31e0eedc42295da0da1c06a74166be69c469d",+ "size": 484686, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.176",+ "version": "1.9.177", "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/fusion-bridge", "author": "Adom Inc.",
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.9.176"+version = "1.9.177" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.176"+version = "1.9.177" edition = "2021" [dependencies]
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.176"+DASH_VERSION="1.9.177" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.176",+ "version": "1.9.177", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+11−8@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.176",+ "version": "1.9.177", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -11,6 +11,10 @@ "uninstall": "./uninstall.sh" }, "tags": [+ "ab",+ "bridge",+ "fusion",+ "desktop-app", "fusion360", "autodesk", "cad",@@ -75,7 +79,10 @@ "build a basic parts library", "add a 3d model to a component", "make a fusion 360 library with 3d",- "set up the fusion bridge"+ "set up the fusion bridge",+ "fusion dashboard",+ "fusion status page",+ "open the fusion launch page" ], "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "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.",@@ -104,9 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}
server.py+76@@ -5061,7 +5061,57 @@ def _auto_update_enabled() -> bool: # Buttons Fusion puts on its update nag / Job Status panel, best-first.+# ⚠ TWO CLASSES (#50, 2026-08-23 - 9/9 chains lost Fusion mid-PROVE): "Update Now" only+# STARTS a background download (safe anytime), but "Install Now"/"Restart Now" EXIT Fusion+# to apply the update. The sweep used to click all four blindly on its 300s cadence, which+# lines up exactly with the observed clean exit at +344s into a chain. Exit-class buttons+# may now only be clicked when Fusion is verifiably IDLE (no long op, no recent driving,+# no open documents). _UPDATE_BUTTONS = ("Update Now", "Update now", "Install Now", "Restart Now")+_UPDATE_BUTTONS_EXIT_CLASS = ("Install Now", "Restart Now")+++def _bridge_action_ledger(action: str, detail: dict):+ """Append one background-action record to ~/.adom/fusion-bridge/actions.jsonl.++ The bridge log truncates on every respawn (found investigating #50: a morning of+ evidence was gone by the time the report landed), so anything the bridge does to+ Fusion BEHIND the user's back - update clicks, seat takeovers, dialog dismissals,+ doc closes, hang-gate kills - is also written here, append-only, survives respawns.+ One grep answers 'did the bridge touch Fusion at HH:MM?'. Never raises."""+ try:+ import json as _j, time as _t, pathlib as _pl+ p = _pl.Path(os.path.expanduser("~")) / ".adom" / "fusion-bridge" / "actions.jsonl"+ p.parent.mkdir(parents=True, exist_ok=True)+ rec = {"at": _t.strftime("%Y-%m-%d %H:%M:%S"), "epoch": round(_t.time(), 1),+ "action": action, "pid": os.getpid()}+ rec.update(detail or {})+ with open(p, "a", encoding="utf-8") as f:+ f.write(_j.dumps(rec) + "\n")+ except Exception:+ pass+++def _fusion_verifiably_idle() -> dict:+ """May the bridge do something that EXITS Fusion right now? Only when ALL of:+ no registered long op, no driving verb for 3+ minutes, and no open documents+ (best-effort; an unreachable add-in counts as NOT idle - safe default)."""+ import time as _t+ if _LONG_OP.get("name"):+ return {"idle": False, "why": "long op in flight: %s" % _LONG_OP["name"]}+ drove_ago = _t.time() - float(_LAST_DRIVE_AT[0] or 0)+ if _LAST_DRIVE_AT[0] and drove_ago < 180:+ return {"idle": False, "why": "driving verb %ds ago" % int(drove_ago)}+ try:+ st = _proxy_to_addin("document_info", {}, timeout=6)+ docs = (st.get("data") or {}).get("openDocuments") or []+ if not st.get("success"):+ return {"idle": False, "why": "add-in unreachable - cannot prove no open docs"}+ if docs:+ return {"idle": False, "why": "%d document(s) open" % len(docs)}+ except Exception:+ return {"idle": False, "why": "doc check failed - cannot prove idle"}+ return {"idle": True, "why": "no long op, no recent driving, no open documents"} def _find_controls_any(obj, depth=0):@@ -5130,11 +5180,28 @@ def _apply_fusion_update_silently(hwnd=None) -> dict: offer = _find_update_offer(hwnd) if not offer.get("offered"): return {"applied": False, "why": "no update offered"}+ # ⚠ #50 gate: "Install Now"/"Restart Now" EXIT Fusion to apply the update. Clicking one+ # while a chain is running killed Fusion mid-PROVE 9/9 times (clean exit, no crash+ # artifact, matching this sweep's 300s cadence). Exit-class buttons only fire when+ # Fusion is verifiably idle; otherwise defer - the janitor retries every 300s and will+ # land it the moment the work is done. "Update Now" (download only) stays safe anytime.+ if offer["button"] in _UPDATE_BUTTONS_EXIT_CLASS:+ idle = _fusion_verifiably_idle()+ if not idle.get("idle"):+ _bridge_action_ledger("auto_update_deferred",+ {"button": offer["button"], "notIdle": idle.get("why")})+ print("[Fusion Bridge] auto-update: %r on screen but Fusion is NOT idle (%s) - "+ "DEFERRED, will retry when idle" % (offer["button"], idle.get("why")))+ return {"applied": False, "deferred": True, "button": offer["button"],+ "why": "exit-class update button deferred: %s" % idle.get("why")} r = _unwrap(_ad_call("desktop_ui_click", {"hwnd": int(offer["hwnd"]), "name": offer["button"]}, timeout=30)) ok = bool(r.get("invoked") or r.get("success")) if ok: print("[Fusion Bridge] auto-update: clicked %r in the background" % offer["button"])+ _bridge_action_ledger("auto_update_click",+ {"button": offer["button"], "invoked": ok,+ "exitClass": offer["button"] in _UPDATE_BUTTONS_EXIT_CLASS}) return {"applied": ok, "button": offer["button"], "why": ("clicked %r in the background; Fusion downloads now and swaps on next launch" % offer["button"]) if ok else "found the button but the UIA invoke did not take"}@@ -5312,6 +5379,7 @@ def _close_idle_bridge_docs() -> dict: closed.append(name) print("[Fusion Bridge] janitor: closed dirty bridge doc %r (verified gone, " "any Save-As answered in the same call)" % name)+ _bridge_action_ledger("idle_doc_close", {"doc": name}) except Exception: pass return {"closed": closed, "why": "closed %d dirty bridge doc(s)" % len(closed)}@@ -5389,6 +5457,7 @@ def _dismiss_save_prompt_if_bridge_doc() -> dict: if r2.get("invoked") or r2.get("success"): print("[Fusion Bridge] save-prompt sentinel: clicked \'Don\'t Save\' for bridge doc %r " "(attributed by window title)" % matched)+ _bridge_action_ledger("save_prompt_dismiss", {"doc": matched, "how": "dont_save"}) return {"handled": True, "why": "clicked Don't Save for bridge doc '%s'" % matched} except Exception: pass@@ -5398,6 +5467,7 @@ def _dismiss_save_prompt_if_bridge_doc() -> dict: if esc.get("success"): print("[Fusion Bridge] save-prompt sentinel: ESCAPED the prompt for bridge doc %r " "(Cancel - doc stays open unsaved, main thread freed)" % matched)+ _bridge_action_ledger("save_prompt_dismiss", {"doc": matched, "how": "escape"}) return {"handled": True, "why": "escaped (Cancel) for bridge doc '%s'" % matched} except Exception: pass@@ -6427,6 +6497,7 @@ def _seat_sweep_tick(): _unwrap(_ad_call("desktop_ui_click", {"hwnd": _d.get("hwnd"), "name": "No"}, timeout=12)) print("[Fusion Bridge] janitor: declined a stray Create-new prompt (%s)" % _d.get("matchedOn", "title"))+ _bridge_action_ledger("create_confirm_declined", {"matchedOn": _d.get("matchedOn")}) except Exception: pass @@ -6450,6 +6521,8 @@ def _seat_resolve_section(): pass print("[Fusion Bridge] seat sweep: took over the session (clicks=%s, verified=%s)" % (r.get("clicks"), r.get("verified")))+ _bridge_action_ledger("seat_takeover",+ {"clicks": r.get("clicks"), "verified": r.get("verified")}) except Exception: pass @@ -10929,6 +11002,9 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No _h = _confirmed_hung() if _h.get("hung"): _ok = False+ _bridge_action_ledger("hang_gate_kill",+ {"heldSeconds": _h.get("heldSeconds"),+ "signals": _h.get("signals")}) try: dispatch_command("kill", {}) _time.sleep(6)