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 15 file(s)
9 files changed
+200−21
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.221+1.9.222
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.221",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.221/adom-bridge-fusion-v1.9.221.zip",- "sha256": "bfd566d96136bf0fa15a38c022f3504c2619e5f7580c5c94107ef87eded2b4ec",- "size": 510907,+ "version": "1.9.222",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.222/adom-bridge-fusion-v1.9.222.zip",+ "sha256": "9efebec63b8355b56f0308792c807c631e6e6622861fcaebdbf74a1a2687cca8",+ "size": 514182, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.221",+ "version": "1.9.222", "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.221"+version = "1.9.222" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.221"+version = "1.9.222" 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.221"+DASH_VERSION="1.9.222" 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.221",+ "version": "1.9.222", "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.221",+ "version": "1.9.222", "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+179−3@@ -333,6 +333,86 @@ def _cli_notify_all(title: str, body: str, level: str) -> dict: return {"delivered": False, "error": str(e)[:200]} +def _addin_restart_info(fire_toast: bool = True) -> dict:+ """Everything a CALLER needs to tell a HUMAN that Fusion must restart, plus a toast.++ ⛔ THE ADONIS INCIDENT (John, 2026-08-27). A brand-new user installed ab, already had+ Fusion open, and drove the fusion bridge from the wiki through web-control. The bridge+ wrote a fresh add-in into Fusion's add-in dirs - Fusion only loads add-ins AT LAUNCH, so+ his running Fusion could not see it - and then said NOTHING. It printed the sync to a+ console nobody reads, returned a bare "add-in not responding", and the verbs failed. He+ had no way to know the product was fine and merely needed a relaunch; John and Colby had+ to tell him by hand. Once he restarted, the library import worked perfectly first time.+ Everything needed to warn him was already known at that moment.++ So this returns a payload a web-control caller can render as a QUESTION ("we need to+ restart Fusion; you have N documents open; shall we, and we will save them first"), and+ fires an ab toast so the human is told even if the caller ignores the payload.++ Document counts come from the add-in, which by definition is NOT answering here, so they+ are reported as unknown rather than guessed - `documentsKnown` says which. A count we+ cannot take is not a count we may invent.+ """+ import time as _t+ info = {+ "restartRequired": True,+ "reason": "addin_installed_while_fusion_running",+ "explanation": ("The AdomBridge add-in was installed or updated while Fusion was "+ "already running. Fusion loads add-ins only at launch, so this "+ "Fusion cannot see it until it restarts. Nothing is wrong with the "+ "install and nothing is lost - it just needs a relaunch."),+ "userMessage": ("Fusion needs a quick restart to finish setting up the Adom add-in. "+ "We can save your open documents first."),+ "remedyVerb": "fusion_restart_for_addin",+ "documentsKnown": False,+ "openDocuments": None,+ "unsavedDocuments": None,+ }+ # Best effort: if the add-in answers at all (an OLD copy still serving), we can count.+ try:+ st = _proxy_to_addin("get_app_state", {}, timeout=8) or {}+ docs = st.get("documents")+ if isinstance(docs, list):+ info["documentsKnown"] = True+ info["openDocuments"] = len(docs)+ info["unsavedDocuments"] = sum(1 for d in docs if not d.get("isSaved"))+ info["documentNames"] = [d.get("name") for d in docs][:20]+ except Exception:+ pass++ if fire_toast:+ # ONE toast per staged install, not per failed verb: Adonis would have had a dozen+ # verbs fail, and a dozen identical toasts is its own bad experience.+ try:+ import pathlib as _pl, json as _js, os as _os+ d = _pl.Path(_os.path.expanduser("~")) / ".adom" / "fusion-bridge"+ d.mkdir(parents=True, exist_ok=True)+ f = d / "addin_restart_toast.json"+ last = 0.0+ if f.exists():+ try:+ last = float((_js.loads(f.read_text()) or {}).get("at") or 0)+ except Exception:+ last = 0.0+ if _t.time() - last > 900: # at most once every 15 minutes+ body = info["userMessage"]+ if info.get("documentsKnown"):+ body = ("Fusion needs a quick restart to finish setting up the Adom "+ "add-in. You have %d document(s) open%s - we will save them "+ "first." % (info["openDocuments"],+ (", %d unsaved" % info["unsavedDocuments"])+ if info.get("unsavedDocuments") else ""))+ r = _cli_notify_all("Fusion needs a restart", body, "info")+ info["toast"] = {"delivered": bool(r.get("delivered")),+ "error": r.get("error")}+ f.write_text(_js.dumps({"at": _t.time()}))+ else:+ info["toast"] = {"delivered": False, "suppressed": "already toasted recently"}+ except Exception as e:+ info["toast"] = {"delivered": False, "error": str(e)[:200]}+ return info++ def _handle_notify_owner(args: dict) -> dict: """LAST-RESORT: toast the user's MAIN desktop (cross-AD) to ask for a human step. @@ -12071,12 +12151,36 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No "including a 20s probe - if that happens it restarts Fusion " "itself and says so."), }+ # TELL THE HUMAN WHY, AND OFFER THE FIX (the Adonis incident, John 2026-08-27).+ # This used to return a bare "add-in not responding" and a hint aimed at an AI+ # ("fix it YOURSELF - never ask the user"). That advice is right for an agent+ # driving headlessly and WRONG when a person is sitting in front of Fusion with+ # documents open: a brand-new user drove this from the wiki, every verb failed,+ # and nothing ever told him the product was fine and just needed a relaunch.+ # The payload now carries what a web-control caller needs to ASK him, and a+ # toast fires so he is told even if the caller drops it on the floor.+ _ri = _addin_restart_info() return { "success": False,- "error": "Fusion 360 is running but AdomBridge add-in not responding.",- "errorCode": "fusion_addin_not_responding",+ "error": ("Fusion 360 is running but the AdomBridge add-in is not responding "+ "- it was installed or updated while Fusion was already open, and "+ "Fusion only loads add-ins at launch."),+ "errorCode": "addin_restart_required", "hostResponding": True,- "_hint": "The add-in isn't responding. Fix it YOURSELF - never ask the user: the bridge auto-installs the add-in to ALL Fusion add-in dirs (2025+ Fusion scans %APPDATA%/Autodesk/FusionAddins; the legacy API/AddIns dirs are silently ignored - issue #63), so restart Fusion via fusion_stop + fusion_start and it loads (runOnStartup). Verify with fusion_addin_status.",+ "restartRequired": True,+ "userMessage": _ri["userMessage"],+ "openDocuments": _ri.get("openDocuments"),+ "unsavedDocuments": _ri.get("unsavedDocuments"),+ "documentsKnown": _ri.get("documentsKnown"),+ "remedyVerb": "fusion_restart_for_addin",+ "toast": _ri.get("toast"),+ "_hint": ("ASK THE USER, do not silently restart their Fusion: show "+ "userMessage, and if documentsKnown say how many documents are "+ "open. On yes, call fusion_restart_for_addin {save:true} - it "+ "saves open documents, restarts Fusion, waits for the add-in and "+ "reports back. An AI driving with no human present may call it "+ "directly. (Legacy callers: this was errorCode "+ "fusion_addin_not_responding.)"), } # ── Busy gate: reject add-in commands immediately if a long command is running ──@@ -12617,6 +12721,77 @@ result = {"units": "mm", "worldSpace": bool(_WORLD), ''' +def _handle_restart_for_addin(fusion_info: dict, args: dict) -> dict:+ """Save open documents, restart Fusion, wait for the add-in, report back.++ The remedy half of the Adonis incident (John, 2026-08-27): telling a user "restart+ Fusion" is still making them do our job. This does it, and keeps the promise the+ message makes - "we will save your open documents first" - because a restart that+ quietly discards someone's work would be far worse than the original silence.++ save:false skips saving (an AI on an unattended box that knows the docs are scratch).+ """+ import time as _t+ save = args.get("save", True)+ t0 = _t.time()+ steps = []++ if save:+ # Save FIRST, and treat a failure as a STOP, not a warning: the whole point is+ # that nobody loses work to our restart.+ try:+ sv = _proxy_to_addin("close_all_documents", {"save": True}, timeout=180) or {}+ steps.append({"step": "save", "ok": bool(sv.get("success")),+ "closed": (sv.get("data") or {}).get("closed"),+ "wouldPrompt": (sv.get("data") or {}).get("wouldPrompt")})+ _wp = (sv.get("data") or {}).get("wouldPrompt")+ if _wp:+ return {"success": False, "errorCode": "unsaved_named_document",+ "error": ("Cannot restart safely: %s has never been saved, so Fusion "+ "would raise its Save box and block the restart."+ % ", ".join(repr(n) for n in _wp)),+ "steps": steps,+ "_hint": ("Ask the user where to save it (fusion_save_to_cloud "+ "{name, projectName}), then retry. Nothing was closed and "+ "no dialog was raised.")}+ except Exception as e:+ return {"success": False, "errorCode": "save_failed", "steps": steps,+ "error": "Refusing to restart because saving failed: %s" % str(e)[:200],+ "_hint": "Nothing was restarted; the user's documents are untouched."}++ try:+ # handle_fusion_stop, imported at module top from handlers.close_fusion. My first+ # draft called a name that does not exist (_handle_fusion_stop) behind a globals()+ # check that would have silently taken a fallback path instead of failing loudly.+ st = handle_fusion_stop(detect_fusion(), {})+ steps.append({"step": "stop", "ok": bool((st or {}).get("success", True))})+ except Exception as e:+ steps.append({"step": "stop", "ok": False, "error": str(e)[:200]})++ _t.sleep(3)+ try:+ lr = _handle_launch(detect_fusion(), {"waitForAddin": True}) or {}+ steps.append({"step": "start", "ok": bool(lr.get("success"))})+ except Exception as e:+ return {"success": False, "errorCode": "restart_failed", "steps": steps,+ "error": "Fusion was stopped but did not come back: %s" % str(e)[:200],+ "_hint": "Launch it with fusion_start and check fusion_readiness "+ "(needsSignin true means Autodesk sign-in is blocking)."}++ ready = _addin_round_trip_ready(tries=6, gap=5.0)+ return {+ "success": bool(ready),+ "addinReady": bool(ready),+ "tookSec": round(_t.time() - t0, 1),+ "steps": steps,+ "errorCode": None if ready else "addin_still_not_responding",+ "_hint": ("Fusion restarted and the add-in answered - re-run the verb that failed."+ if ready else+ "Fusion restarted but the add-in still is not answering. That is a real "+ "defect, not a user problem: check fusion_addin_status and file it."),+ }++ def _handle_inspect_bodies(fusion_info: dict, args: dict) -> dict: """fusion_inspect_bodies - what actually got built, in millimetres.""" world = bool(args.get("worldSpace"))@@ -12653,6 +12828,7 @@ def _handle_inspect_bodies(fusion_info: dict, args: dict) -> dict: COMMAND_HANDLERS["inspect_bodies"] = _handle_inspect_bodies+COMMAND_HANDLERS["restart_for_addin"] = _handle_restart_for_addin