← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.212\ No newline at end of file+1.9.213\ 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.212",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.212/adom-bridge-fusion-v1.9.212.zip",-  "sha256": "0b083f79d09c8007ae271509509eb691d3a37301260e858f72c5174abf407767",-  "size": 500144,+  "version": "1.9.213",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.213/adom-bridge-fusion-v1.9.213.zip",+  "sha256": "79f355e5256271db6c151801635535ecd010bb5f0f5aff029f13ae8ff673882e",+  "size": 502904,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.212",+  "version": "1.9.213",   "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.212"+version = "1.9.213" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.212"+version = "1.9.213" 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.212"+DASH_VERSION="1.9.213" 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.212",+  "version": "1.9.213",   "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.212",+  "version": "1.9.213",   "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+98−2
@@ -3305,6 +3305,9 @@ def _orchestrate_open_lbr(args: dict) -> dict:         results.append(f"Verification: exported={verification.get('exported', False)}")      import os+    # Remember it so a later capture/EDIT can pre-check names offline instead of letting+    # Fusion raise "Create new footprint 'X'?" and dismissing it after the fact.+    _remember_open_library(file_path)     response = {         "success": True,         "output": f"Opened library: {os.path.basename(file_path)}",@@ -4206,16 +4209,65 @@ def _orchestrate_capture_library_views(args: dict) -> dict:     # EDIT is async - the editor takes a beat to actually SWITCH the view. Capturing too soon grabs     # the PREVIOUS view (a mislabeled shot). Settle AFTER the EDIT (before fit/capture); tunable.     settle = float(args.get("settle", 1.5))+    # ⛔ PRE-CHECK EVERY NAME OFFLINE, BEFORE ANY EDIT REACHES FUSION.+    # This verb used to fire EDIT on whatever it was handed and then call+    # _dismiss_dialogs_bg() to clear the "Create new footprint 'X'?" modal it had just+    # caused. That is backwards three ways: the modal freezes the add-in's main thread+    # (so every later verb reads as a crash), it dings the machine's alert sound at a user+    # who did not do anything, and dismissal is a RACE we lose whenever Fusion is slow.+    # The .lbr on disk already knows every symbol/package/deviceset name, so a name that+    # would prompt is knowable in milliseconds without Fusion. Resolve here; never ask.+    lbr_path = (args.get("libraryPath") or "").replace("\\", "/") or _remembered_open_library()+    resolved_names = {}+    name_errors = []+    if lbr_path:+        for pkg in pkgs:+            for v in views:+                ext = view_ext.get(v, v)+                r, err = _resolve_artifact_name(pkg, ext, lbr_path)+                if err:+                    name_errors.append({"package": pkg, "view": label_of.get(ext, ext),+                                        "error": err.get("error"),+                                        "available": err.get("available"),+                                        "closestMatches": err.get("closestMatches")})+                else:+                    resolved_names[(pkg, ext)] = r     out = []     for pkg in pkgs:         for v in views:             ext = view_ext.get(v, v)-            er = _proxy_to_addin("electron_run", {"command": f"EDIT {pkg}.{ext}"}, timeout=30)+            if lbr_path and (pkg, ext) not in resolved_names:+                # Would have prompted. Report it as a skip; do NOT hand it to Fusion.+                _bad = next((e for e in name_errors+                             if e["package"] == pkg and e["view"] == label_of.get(ext, ext)), {})+                out.append({"package": pkg, "view": label_of.get(ext, ext), "path": None,+                            "ok": False, "skipped": "name_not_in_library",+                            "error": _bad.get("error"),+                            "available": (_bad.get("available") or [])[:20],+                            "closestMatches": _bad.get("closestMatches"),+                            "_hint": "No EDIT was issued and no dialog was raised - this name "+                                     "is not in the library, so opening it would have popped "+                                     "Fusion's blocking 'Create new ...?' modal. Pick a name "+                                     "from `available`."})+                continue+            name = resolved_names.get((pkg, ext), pkg)+            er = _proxy_to_addin("electron_run", {"command": f"EDIT {name}.{ext}", "confirm": "no"}, timeout=30)             _t.sleep(settle)  # let the editor LOAD the new view before framing/capturing it             # ALWAYS catch + dismiss any dialog the EDIT raised - in the BACKGROUND (no focus steal).             # Common one: "Create new symbol/footprint '<name>'?" when <name> is a DEVICESET name but             # the symbol/package is named by the shared combo. Dismissing (Cancel/No) avoids a stray.+            # BACKSTOP ONLY. With the pre-check above this should never fire; if it does,+            # the pre-check has a hole (a library opened by a route that did not record its+            # path, or a name EAGLE renamed on load) and that is a BUG to fix at the source,+            # not a dialog to keep sweeping. Log it loudly so it cannot stay invisible.             dismissed = _dismiss_dialogs_bg()+            if dismissed:+                print("[Fusion Bridge] ⚠ PRE-CHECK HOLE: EDIT %s.%s still raised a dialog "+                      "(lbr_path=%r). Prevention failed; dismissal should not be load-bearing."+                      % (name, ext, lbr_path))+                _bridge_action_ledger("dialog_precheck_hole",+                                      {"name": name, "ext": ext, "lbrPath": lbr_path,+                                       "dismissed": dismissed})             _proxy_to_addin("electron_run", {"command": "WINDOW FIT"}, timeout=20)             _t.sleep(0.6)             shot = _capture_labeled(f"{pkg}_{label_of.get(ext, ext)}")@@ -7542,6 +7594,49 @@ def _artifact_name_from(args, *frag_keys):   +def _remember_open_library(lbr_path: str):+    """Record the .lbr we just opened so LATER verbs can pre-check names without a path.++    ⛔ THIS EXISTS TO PREVENT A MODAL, NOT TO BE TIDY (John, 2026-08-26, third report of the+    same dialog). `_resolve_artifact_name` could always stop a Create-new prompt, but it+    needs the .lbr on disk, and `fusion_capture_library_views` never took a libraryPath - so+    the ONE verb that EDITs the most names was the one verb structurally unable to run the+    guard, and it "handled" the prompt by raising it and dismissing it afterwards. Persisted+    (not just module state) because AD reaps and respawns this bridge between calls, which+    would drop in-memory state exactly when a capture follows an open.+    """+    # LOCAL imports + a LOUD except. Both matter: this module imports os/time inside+    # functions, so a module-level `time.time()` here raised NameError - and the bare+    # `except: pass` I first wrote ATE it, leaving the guard silently disarmed while the+    # code read as correct. A prevention guard that fails quietly is worse than none,+    # because it removes the pressure to notice.+    try:+        import pathlib as _pl, json as _js, os as _os, time as _tm+        d = _pl.Path(_os.path.expanduser("~")) / ".adom" / "fusion-bridge"+        d.mkdir(parents=True, exist_ok=True)+        (d / "open_library.json").write_text(_js.dumps(+            {"libraryPath": lbr_path.replace("\\", "/"), "at": _tm.time()}))+    except Exception as e:+        print("[Fusion Bridge] ⚠ could not remember the open library (%s: %s) - the "+              "Create-new-dialog pre-check will fall back to SET CONFIRM only"+              % (type(e).__name__, e))+++def _remembered_open_library():+    """The last .lbr opened, if it is still on disk. None when we genuinely do not know."""+    try:+        import pathlib as _pl, json as _js, os as _os+        f = _pl.Path(_os.path.expanduser("~")) / ".adom" / "fusion-bridge" / "open_library.json"+        if not f.exists():+            return None+        lp = (_js.loads(f.read_text()) or {}).get("libraryPath") or ""+        return lp if lp and _os.path.exists(lp) else None+    except Exception as e:+        print("[Fusion Bridge] ⚠ could not read the remembered library (%s: %s)"+              % (type(e).__name__, e))+        return None++ def _library_artifact_names(lbr_path: str) -> dict:     """The names that actually EXIST in a .lbr: {"sym": [...], "pac": [...], "dev": [...]}. @@ -7660,7 +7755,8 @@ def _show_library_view(name, suffix, lbr_path=None):     # instead of failing the frame.     edit = None     for _busy_try in range(7):-        edit = _proxy_to_addin("electron_run", {"command": "EDIT %s.%s" % (name, suffix)}, timeout=15)+        edit = _proxy_to_addin("electron_run", {"command": "EDIT %s.%s" % (name, suffix),+                                             "confirm": "no"}, timeout=15)         _etxt = str(edit.get("error") or "")         if edit.get("success"):             break