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 73 file(s)
10 files changed
+117−24
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.236+1.9.237
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.236",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.236/adom-bridge-fusion-v1.9.236.zip",- "sha256": "c10bbe2d278be537edeb57d0b4bef237c203cd4f16595db75873eef11faa2efe",- "size": 527812,+ "version": "1.9.237",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.237/adom-bridge-fusion-v1.9.237.zip",+ "sha256": "8180d60b5add20106e2fbb11afddbc8f583159c6ff50dc6526a8ed960f2c1723",+ "size": 528977, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.236",+ "version": "1.9.237", "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.236"+version = "1.9.237" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.236"+version = "1.9.237" 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.236"+DASH_VERSION="1.9.237" 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.236",+ "version": "1.9.237", "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.236",+ "version": "1.9.237", "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+80−6@@ -303,7 +303,7 @@ def _find_adom_desktop_cli() -> str | None: return None -def _cli_notify_all(title: str, body: str, level: str) -> dict:+def _cli_notify_all(title: str, body: str, level: str, buttons=None) -> dict: """Deliver a toast to EVERY connected desktop via `adom-bridge --target all notify_user`. This is the reliable cross-AD path when the bridge's in-process ad_client is down (headless VM): the CLI joins the relay itself and `--target all` reaches the user's real machine (not just this VM).@@ -316,9 +316,16 @@ def _cli_notify_all(title: str, body: str, level: str) -> dict: # STICKY by default: a human-wall alert must NOT vanish in a few seconds (John 2026-07-14 - the # first toasts disappeared before he noticed them). scenario:reminder keeps it on screen until the # user acts; it REQUIRES >=1 button, so include one. durationLong is a belt-and-suspenders ~25s.+ # ⛔ A NOTICE THAT OFFERS AN ACTION MUST CARRY THE CHOICE (John, 2026-08-28: "i just+ # clicked 'got it' on the restart notice, but why do you not give me a choice? i have+ # no idea if 'got it' means you're restarting or not"). The default single "Got it"+ # stays for plain FYI toasts, but a caller with a real decision passes its own+ # buttons and reads the click back via notify_response. Never print a promise+ # ("we will save your documents first") on a toast whose only button is an+ # acknowledgement - that is the text making an offer the UI cannot keep. payload = json.dumps({"title": title, "body": body, "level": level, "scenario": "reminder", "durationLong": True,- "buttons": [{"label": "Got it"}]})+ "buttons": buttons or [{"label": "Got it"}]}) try: r = _sp.run([exe, "--target", "all", "notify_user", payload], capture_output=True, text=True, timeout=30,@@ -328,11 +335,68 @@ def _cli_notify_all(title: str, body: str, level: str) -> dict: # 'displayed'/'ok' in the output as delivered. The Windows exe may print nothing yet still # deliver, so a clean exit code is accepted too. delivered = (r.returncode == 0) or ("displayed" in out) or ('"status": "ok"' in out) or ("status':'ok" in out)- return {"delivered": bool(delivered), "targets": "all", "error": None if delivered else out[:200]}+ nid = None+ try:+ import re as _re+ m = _re.search(r'"id"\s*:\s*"([0-9a-f]{8,})"', out)+ nid = m.group(1) if m else None+ except Exception:+ pass+ return {"delivered": bool(delivered), "targets": "all", "id": nid,+ "error": None if delivered else out[:200]} except Exception as e: return {"delivered": False, "error": str(e)[:200]} +def _watch_restart_choice(notification_id: str, timeout_s: float = 1800.0):+ """Wait (in the background) for the user's answer to the restart notice and honour it.++ "Save and restart" -> fusion_restart_for_addin {save:true}, which saves first and+ REFUSES rather than discarding a never-saved document. "I'll handle it" -> nothing:+ the user keeps control and the offer is simply recorded. Either way the choice lands+ in the action ledger, so "why did my Fusion restart" has an answer with a timestamp.+ """+ import threading as _th++ def _run():+ import time as _t+ deadline = _t.time() + timeout_s+ exe = _find_adom_desktop_cli()+ while exe and _t.time() < deadline:+ try:+ import subprocess as _sp, json as _js+ r = _sp.run([exe, "--target", "all", "notify_response",+ _js.dumps({"id": notification_id})],+ capture_output=True, text=True, timeout=30,+ creationflags=getattr(_sp, "CREATE_NO_WINDOW", 0))+ out = (r.stdout or "")+ i = out.find("{")+ x = _js.loads(out[i:]) if i >= 0 else {}+ x = x.get("data") or x+ btn = x.get("buttonId") or x.get("button") or x.get("response")+ if x.get("pending") is False or btn or x.get("dismissed"):+ if btn in ("restart", "Save and restart"):+ _bridge_action_ledger("restart_choice", {"choice": "restart",+ "notification": notification_id})+ res = _handle_restart_for_addin(detect_fusion(), {"save": True})+ _bridge_action_ledger("restart_for_addin_result",+ {"success": res.get("success"),+ "errorCode": res.get("errorCode")})+ if not res.get("success"):+ _cli_notify_all("Fusion restart did not complete",+ str(res.get("error") or res.get("errorCode"))[:200],+ "error")+ else:+ _bridge_action_ledger("restart_choice",+ {"choice": btn or "dismissed",+ "notification": notification_id})+ return+ except Exception:+ pass+ _t.sleep(3)+ _th.Thread(target=_run, daemon=True).start()++ def _addin_restart_info(fire_toast: bool = True) -> dict: """Everything a CALLER needs to tell a HUMAN that Fusion must restart, plus a toast. @@ -402,10 +466,20 @@ def _addin_restart_info(fire_toast: bool = True) -> dict: "first." % (info["openDocuments"], (", %d unsaved" % info["unsavedDocuments"]) if info.get("unsavedDocuments") else ""))- r = _cli_notify_all("Fusion needs a restart", body, "info")+ # Two real choices, and the bridge honours whichever is pressed. A user who+ # is mid-work keeps control with "I'll handle it"; one who wants it done gets+ # the save + restart the message promises. There is no button that means+ # "you have been told", because that leaves the user exactly where Adonis was.+ r = _cli_notify_all(+ "Fusion needs a restart", body, "info",+ buttons=[{"label": "Save and restart", "id": "restart"},+ {"label": "I'll handle it", "id": "later"}]) info["toast"] = {"delivered": bool(r.get("delivered")),- "error": r.get("error")}- f.write_text(_js.dumps({"at": _t.time()}))+ "id": r.get("id"), "error": r.get("error"),+ "choices": ["Save and restart", "I'll handle it"]}+ f.write_text(_js.dumps({"at": _t.time(), "id": r.get("id")}))+ if r.get("id"):+ _watch_restart_choice(r["id"]) else: info["toast"] = {"delivered": False, "suppressed": "already toasted recently"} except Exception as e:
skills/fusion-web-control/SKILL.md+16@@ -187,3 +187,19 @@ advisory. It resolves green ("Library imported, N of N parts proven") or red wit lingers ~20s, and hides. Drive it directly with `fusion_activity_banner {action, title, detail, percent, etaSec, tone}` if you orchestrate the steps yourself. It is an ADVISORY: Fusion has no read-only mode, so a determined click still lands. Verified on hardware.++## A notice that offers an action must carry the choice++John, 2026-08-28: *"i just clicked 'got it' on the restart notice, but why do you not give+me a choice? i have no idea if 'got it' means you're restarting or not."* The toast's body+promised "we will save your documents first" while its only button was an acknowledgement:+the text made an offer the UI could not keep. That is lazy, and it leaves the user exactly+where Adonis was.++The restart notice now offers **"Save and restart"** and **"I'll handle it"**, and the+bridge honours whichever is pressed (`_watch_restart_choice` polls `notify_response` and+either runs `fusion_restart_for_addin {save:true}` or records the decline). Both land in+the action ledger, so "why did my Fusion restart" has a timestamped answer.++Rule for any toast: if the body describes something the bridge *could do*, the buttons must+let the user say yes or no to exactly that. A bare "Got it" is only for pure FYI.