← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.236\ No newline at end of file+1.9.237\ No newline at end of file
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]"-  }-}\ No newline at end of file+  ]+}
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.