← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.262\ No newline at end of file+1.9.263\ 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.262",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.262/adom-bridge-fusion-v1.9.262.zip",-  "sha256": "322d4aee01e4522fed0ce6adeda757b7b353142b4435f10536118d428ba164b0",-  "size": 554511,+  "version": "1.9.263",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.263/adom-bridge-fusion-v1.9.263.zip",+  "sha256": "67b364b0418e02b31bf1fb282b3363f5fd351b0c0a27592f074d2ebd6f69cf07",+  "size": 556122,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.262",+  "version": "1.9.263",   "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.262"+version = "1.9.263" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.262"+version = "1.9.263" edition = "2021"  [dependencies]
handlers/taskbar_overlay.py+1−1
@@ -6,7 +6,7 @@ glance; the same signal belongs on FUSION's button whenever this bridge drives i THE LESSON (stolen from pup, who paid for it first - src/server.js): an in-bridge ITaskbarList3::SetOverlayIcon is IN-PROCESS-ONLY. The COM call returns S_OK cross-process and the shell silently ignores it (verified live here too: badge never-rendered). The working path is AD-core's `desktop_taskbar` verb (ab >=1.9.106): hand+rendered). The working path is ab-core's `desktop_taskbar` verb (ab >=1.9.106): hand it the badge PNG as base64 and ab paints the overlay natively, cross-process.      set:   desktop_taskbar {hwnd, overlay: {icon: <b64 png>, tooltip}}
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.262"+DASH_VERSION="1.9.263" 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.262",+  "version": "1.9.263",   "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.262",+  "version": "1.9.263",   "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+152−55
@@ -3970,6 +3970,31 @@ def _orchestrate_make_3d_package(args: dict) -> dict:         return {"success": False, "error": "Create3DPackage failed: " + str(cp.get("error")),                 "_hint": "The .lbr must be adom-lbr-generated; its FIRST package's footprint is loaded into the generator."}     _time.sleep(2)+    # FOOTPRINT IDENTITY (John, 2026-08-29): the generator loads the FIRST package of the OPEN+    # library and names its document after that package. Read the name back and refuse when it+    # is not the package this mint is for: a chip on the wrong footprint must fail here, not+    # get uploaded, bound and "proven".+    _expected_pkg = ""+    try:+        import xml.etree.ElementTree as _ETg+        _pk = _ETg.parse(lbr_path).getroot().find(".//library/packages/package")+        _expected_pkg = (_pk.get("name") if _pk is not None else "") or ""+    except Exception:+        _expected_pkg = ""+    _gen_doc = ""+    try:+        _di = _proxy_to_addin("document_info", {}, timeout=8)+        _ad = (_di.get("data") or {}).get("activeDocument")+        _gen_doc = str((_ad.get("name") if isinstance(_ad, dict) else _ad) or "").rstrip("*").strip()+    except Exception:+        _gen_doc = ""+    if _expected_pkg and _gen_doc and _gen_doc.lower() != _expected_pkg.lower() \+            and not _gen_doc.lower().startswith("untitled"):+        return {"success": False, "errorCode": "wrong_footprint_loaded",+                "error": ("The Package3D generator loaded footprint %r but this mint is for package %r; "+                          "the chip would have landed on the wrong footprint." % (_gen_doc, _expected_pkg)),+                "_hint": "Mint each part on a library whose FIRST package is that part's footprint "+                         "(fusion_install_library_bundle splits a merged library per part for this reason)."}     # BEFORE shot: the footprint (pads + courtyard) loaded in the 3D viewer, no chip yet.     before_shot = None     if cap_label:@@ -4300,6 +4325,44 @@ def _reconcile_orphaned_runs():         return 0  +def _sweep_orphan_scratch_docs() -> dict:+    """Close scratch documents a chain that died with a bridge respawn left open.++    Only when a run is on file as bridge_restarted_mid_run and not yet swept; only documents+    that are bridge scratch by construction: never-modified 'Untitled*' and '<pkg>_3d' mint+    designs. A user's named or modified document is never touched. Ledgered.+    """+    import tempfile as _tf, json as _js+    _p = os.path.join(_tf.gettempdir(), "fusion-prove-runs.json")+    if not os.path.exists(_p):+        return {"swept": 0}+    runs = _js.loads(open(_p, encoding="utf-8").read()) or {}+    pending = [rid for rid, st in runs.items()+               if st.get("errorCode") == "bridge_restarted_mid_run" and not st.get("scratchSwept")]+    if not pending:+        return {"swept": 0}+    if not _addin_round_trip_ready(tries=1):+        return {"swept": 0, "why": "add-in not answering"}+    di = _proxy_to_addin("document_info", {}, timeout=8)+    closed = []+    for d in ((di.get("data") or {}).get("openDocuments") or []):+        n = str(d.get("name") or "").rstrip("*").strip()+        modified = bool(d.get("isModified") or d.get("modified") or str(d.get("name") or "").endswith("*"))+        if (n.lower().startswith("untitled") and not modified) or n.lower().endswith("_3d"):+            try:+                r = _close_bridge_doc_verified(n)+                if r.get("closed"):+                    closed.append(n)+            except Exception:+                pass+    for rid in pending:+        runs[rid]["scratchSwept"] = True+        runs[rid]["scratchSweptDocs"] = closed+    open(_p, "w", encoding="utf-8").write(_js.dumps(runs))+    _bridge_action_ledger("orphan_scratch_docs_swept", {"runs": pending, "closed": closed})+    return {"swept": len(closed), "closed": closed}++ def _publish_run_state(run_id: str, state: dict):     """Write a run's state to the SAME per-run store fusion_prove_result already reads. @@ -10918,7 +10981,7 @@ def _open_doc_names() -> set:         return set()  -def _close_docs_opened_since(before: set) -> list:+def _close_docs_opened_since(before: set, keep: set = None) -> list:     """Close every document that was NOT open when `before` was snapshotted. Used by the     create_library chain on BOTH success and failure. Everything a chain opens is bridge     scratch by construction: Create3DPackage's generator design (an 'Untitled(N)' - one in@@ -10927,8 +10990,11 @@ def _close_docs_opened_since(before: set) -> list:     by part 3 of a 20-part matrix (2026-08-23): ~2 leaked per chain, and Fusion slows with     every open doc. Never touches a doc that pre-dates the chain (the user's own work)."""     closed = []+    keep = {str(k).lower() for k in (keep or ())}     try:         for n in sorted(_open_doc_names() - set(before or ())):+            if n.rstrip("*").strip().lower() in keep:+                continue                 # the finished library stays open for the user             _note_bridge_doc(n)          # so the save-prompt sentinel owns any prompt it raises             r = _close_bridge_doc_verified(n)             if r.get("closed"):@@ -12018,7 +12084,8 @@ COMMAND_HANDLERS["prove_result"] = _handle_prove_result   -def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libraries") -> dict:+def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libraries",+                              replace_existing: bool = False) -> dict:     """Save the bound library into the user's cloud project so Fusion's Library Manager lists it.      Folder is created if missing (project root/<folder>). A same-named library already there is@@ -12035,15 +12102,36 @@ def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libr         out["folderCreated"] = bool(r.get("success")) and not bool((r.get("data") or {}).get("existed"))     except Exception as e:         out["folderNote"] = str(e)[:120]+    _older = []     try:         lst = _proxy_to_addin("list_cloud_files", {"folderPath": folder}, timeout=60)-        names = [str(f.get("name") or "") for f in ((lst.get("data") or {}).get("files") or [])]-        out["duplicatesBefore"] = sum(1 for n in names if n.lower() == name.lower())+        for f in ((lst.get("data") or {}).get("files") or []):+            if str(f.get("name") or "").lower() == name.lower():+                _older.append({"id": f.get("id"), "versionNumber": f.get("versionNumber")})+        out["duplicatesBefore"] = len(_older)+        out["olderSameName"] = [f.get("id") for f in _older]     except Exception:         out["duplicatesBefore"] = None-    act = _ensure_library_active(lbr_path)-    if not act.get("ok"):-        out["error"] = "library document is not active, so it could not be saved: " + str(act.get("why") or "")[:120]+    # Make the library the active document ourselves (the generic helper also demands a+    # library workspace and gave up after the prove left a 3D document in front, 2026-08-29).+    import time as _tpa+    _deadline = _tpa.time() + 120+    _seen = None+    _active_ok = False+    while _tpa.time() < _deadline:+        try:+            _proxy_to_addin("activate_document", {"name": stem}, timeout=10)+            st = _proxy_to_addin("get_app_state", {}, timeout=8)+            _seen = (st.get("data") or {})+            if stem.lower() in str(_seen.get("activeDocument") or "").lower():+                _active_ok = True+                break+        except Exception as e:+            _seen = {"error": str(e)[:80]}+        _tpa.sleep(2)+    if not _active_ok:+        out["error"] = ("library document %r could not be made active (last seen: %s), so it was not "+                        "saved" % (stem, json.dumps(_seen)[:160]))         return out     r = _proxy_to_addin("save_to_cloud", {"name": name, "folderPath": folder,                                           "description": "Imported by Adom (bound 3D packages)"},@@ -12054,8 +12142,32 @@ def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libr         return out     out.update(saved=True, project=d.get("project"), folder=d.get("folder") or folder,                fileId=d.get("fileId"), versionId=d.get("versionId"), versionNumber=d.get("versionNumber"))+    # UPDATE IN PLACE (John, 2026-08-29: a re-import must update THE library, not mint a second).+    # Fusion's saveAs always creates a new document, so one-library-per-name means retiring the+    # older same-named documents in OUR folder once the new save has landed. That removes cloud+    # documents, so it runs only when the caller asks (replaceExisting:true), only for files in+    # <project>/<folder> with this exact name, by id, and never the one just saved. Without the+    # flag the older copies are left alone and named in olderSameName so the caller can decide.+    replaced = []+    if replace_existing and d.get("fileId") and _older:+        for f in _older:+            fid = f.get("id")+            if not fid or fid == d.get("fileId"):+                continue+            try:+                rr = _proxy_to_addin("delete_cloud_file", {"fileId": fid, "folderPath": folder}, timeout=60)+                if rr.get("success"):+                    replaced.append(fid)+            except Exception:+                pass+    out["replaced"] = replaced+    out["note"] = (("replaced %d older library document(s) of the same name in %s; a board that "+                    "referenced an old document by id must re-link to this one" % (len(replaced), folder))+                   if replaced else+                   ("%d older library document(s) of the same name remain in %s (pass replaceExisting:true "+                    "to retire them)" % (len(_older), folder) if _older else "first library of this name in %s" % folder))     _bridge_action_ledger("library_saved_to_cloud", {"name": name, "folder": folder,-                                                     "fileId": d.get("fileId")})+                                                     "fileId": d.get("fileId"), "replaced": replaced})     return out  def _handle_create_library(fusion_info: dict, args: dict) -> dict:@@ -12116,6 +12228,14 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:                              "restarted under a user today. Poll this runId." % (_rid, _rv.get("stage"))}     except Exception:         pass+    # HYGIENE AFTER A RESPAWN (2026-08-29): a chain killed by a bridge restart cannot close its+    # own scratch documents (its list died with the process; five Untitled tabs were left). Any+    # run the reconcile marked bridge_restarted_mid_run and nobody swept gets swept here, before+    # a new chain adds to the pile: never-modified Untitled docs and *_3d mint designs only.+    try:+        _sweep_orphan_scratch_docs()+    except Exception:+        pass     if not lbr_p and not (sym_p or mod_p):         return {"success": False, "errorCode": "no_input",                 "error": "Give me EITHER libraryPath (a ready .lbr) OR kicadSymbolPath / "@@ -12495,7 +12615,8 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:                     state["stageLabel"] = "saving the library to your cloud Library Manager"                     _save()                     state["cloudLibrary"] = _publish_library_to_cloud(-                        target_lbr, _pub.get("name") or "", _pub.get("folder") or "Adom Libraries")+                        target_lbr, _pub.get("name") or "", _pub.get("folder") or "Adom Libraries",+                        replace_existing=bool(_pub.get("replaceExisting")))                 except Exception as _pe:                     state["cloudLibrary"] = {"saved": False, "error": str(_pe)[:200]}                 _save()@@ -12547,10 +12668,15 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:             # responding and one part's mess failed every part after it). Close THIS             # chain's library document on success AND failure - prove's own close is one             # path here, this is the guarantee. Idempotent when already closed.+            # LEAVE THE LIBRARY OPEN FOR THE USER (John, 2026-08-29: "otherwise they then have+            # to go find it"). A bundle import ends with the saved library in front of the+            # user and only the scratch closed. Other callers keep the old close-everything+            # contract (batch matrices sweep between parts).+            _keep_open = bool(args.get("_publishToCloud")) and bool((state.get("bind") or {}).get("success"))+            _lbr_done = (state.get("libraryPath") or lbr_p or "")+            _stem_done = _lbr_done.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0] if _lbr_done else ""             try:-                _lbr_done = (state.get("libraryPath") or lbr_p or "")-                if _lbr_done:-                    _stem_done = _lbr_done.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0]+                if _lbr_done and not _keep_open:                     _closed = _close_bridge_doc_verified(_stem_done)                     state["libraryDocClosed"] = bool(_closed.get("closed"))                     _save()@@ -12560,10 +12686,19 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:             # 'Untitled' generators, the saved 3D, anything a frame left behind), not just             # the library. Pre-existing docs are untouched.             try:-                state["scratchDocsClosed"] = _close_docs_opened_since(_docs_before)+                state["scratchDocsClosed"] = _close_docs_opened_since(+                    _docs_before, keep={_stem_done.lower()} if _keep_open else set())                 _save()             except Exception:                 pass+            if _keep_open and _stem_done:+                try:+                    _proxy_to_addin("activate_document", {"name": _stem_done}, timeout=10)+                    state["libraryDocClosed"] = False+                    state["libraryLeftOpen"] = True+                    _save()+                except Exception:+                    pass      # THE RUN MUST BE ON FILE BEFORE THE RUNID LEAVES THIS FUNCTION (2026-08-28, cold-path     # run bc3141915abf): the page polled 0.4s after receiving the runId and got@@ -12726,7 +12861,10 @@ def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict:                                                      # the end state the user is after: ONE library in                                                      # Fusion's Library Manager, not a file on disk                                                      "_publishToCloud": {"name": lib_name,-                                                                         "folder": "Adom Libraries"}})+                                                                         "folder": "Adom Libraries",+                                                                         # retire older same-named+                                                                         # copies only when asked+                                                                         "replaceExisting": bool(args.get("replaceExisting"))}})         out.update({k: chain.get(k) for k in ("started", "attached", "runId", "statusVerb", "stage")})         if not chain.get("success"):             out.update(success=False, error=chain.get("error"), errorCode=chain.get("errorCode"))@@ -13958,47 +14096,6 @@ def _handle_live_feed(fusion_info: dict, args: dict) -> dict:                           "container and publish it. kind is main|popup; proof frames come "                           "from fusion_prove_result evidence[] with kind:'proof'. Pass "                           "since:<tick> to get only what you have not seen.")})-    if action == "probe":-        # DIAGNOSTIC (2026-08-29): which capture call shape returns owned popups from inside the-        # bridge? The browser lane gets the full capture (screenshots[]); the bridge's own calls-        # get a bare inline image. Try the variants and report the reply keys for each.-        info = get_fusion_window_info() or {}-        hwnd = info.get("hwnd") or (info.get("mainWindow") or {}).get("hwnd")-        if not hwnd:-            return {"success": False, "error": "no Fusion window"}-        out = {"success": True, "hwnd": hwnd, "variants": {}}-        def _shape(r):-            if not isinstance(r, dict):-                return {"type": type(r).__name__}-            d = r.get("data") if isinstance(r.get("data"), dict) else None-            return {"keys": sorted(r.keys())[:14], "dataKeys": sorted(d.keys())[:14] if d else None,-                    "popups": len(r.get("screenshots") or (d or {}).get("screenshots") or []),-                    "error": str(r.get("error") or r.get("errorCode") or "")[:80]}-        for label, args in (("plain", {"hwnd": int(hwnd)}), ("inlineFalse", {"hwnd": int(hwnd), "inline": False}),-                            ("store", {"hwnd": int(hwnd), "store": True}), ("noResize", {"hwnd": int(hwnd), "resizeMax": 0}),-                            ("popups", {"hwnd": int(hwnd), "includePopups": True, "children": True})):-            try:-                out["variants"]["direct:" + label] = _shape(_ad_direct_api_call("desktop_screenshot_window", args, timeout=20))-            except Exception as e:-                out["variants"]["direct:" + label] = {"raised": str(e)[:80]}-        # the same POST with NO bridge token and NO delegate header (what a web page sends)-        try:-            base = (os.environ.get("ADOM_DIRECT_API_URL") or "").strip()-            if not base:-                base = "http://127.0.0.1:%s" % open(os.path.join(os.path.expanduser("~"), ".adom", "direct-api-port")).read().strip()-            body = json.dumps({"command": "desktop_screenshot_window", "args": {"hwnd": int(hwnd)}}).encode()-            req = urllib.request.Request(base.rstrip("/") + "/command", data=body,-                                         headers={"Content-Type": "application/json"}, method="POST")-            with urllib.request.urlopen(req, timeout=20) as resp:-                out["variants"]["loopback:noheaders"] = _shape(json.loads(resp.read()))-            out["directApiBase"] = base-        except Exception as e:-            out["variants"]["loopback:noheaders"] = {"raised": str(e)[:120]}-        try:-            out["variants"]["ad_client"] = _shape(ad_client.call("desktop_screenshot_window", {"hwnd": int(hwnd)}, timeout=20))-        except Exception as e:-            out["variants"]["ad_client"] = {"raised": str(e)[:80]}-        return out     if action == "stop":         _LIVE_FEED["stop"] = True         return {"success": True, "stopped": True, "tick": _LIVE_FEED["tick"]}