← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.126\ No newline at end of file+1.9.127\ 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.126",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.142/adom-bridge-fusion-v1.9.126.zip",-  "sha256": "f4cdb87d2f4180078279d496c1d27fd95d25994f0f5dd9cf13be19cc00e1f7e9",-  "size": 460314,+  "version": "1.9.127",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.143/adom-bridge-fusion-v1.9.127.zip",+  "sha256": "42a3212a44cab8a2e2abcdd3264d8fb96fbb8f9099975045ba23e6dd5af5bace",+  "size": 462339,   "verbPrefixes": [     "fusion_"   ],
aps.py+9−4
@@ -1461,10 +1461,15 @@ def thumbnail_state(project_id: str, folder_id: str, wip_urn: str = "", file_nam         u = str(u or "").split("?", 1)[0]         return u.rsplit("vf.", 1)[-1] if "vf." in u else u     want_tok = _vf_token(want_urn)-    fn = str(file_name or "").lower()+    def _norm(s):+        # Part names round-trip through KiCad/EAGLE/upload with commas, dots, dashes and+        # underscores mangled differently at each hop ("TJA1051T_3,118" vs+        # "tja1051t-3-118" - issue #47 failure mode 4). Compare alphanumerics only.+        return "".join(c for c in str(s or "").lower() if c.isalnum())+    fn = _norm(file_name)     for v in included:         vid_tok = _vf_token(v.get("id"))-        vname = str((v.get("attributes") or {}).get("displayName") or "").lower()+        vname = _norm((v.get("attributes") or {}).get("displayName"))         # names match EITHER direction: the f3d is uploaded as ADOM-<part>-3D, so the         # part name is a substring of the file, not a prefix (caught live: not_found on a         # file that was sitting right there)@@ -1473,9 +1478,9 @@ def thumbnail_state(project_id: str, folder_id: str, wip_urn: str = "", file_nam             hit_version = v             break     if hit_version is None and file_name:-        fn2 = str(file_name).lower()+        fn2 = _norm(file_name)         for it in items:-            nm = str((it.get("attributes") or {}).get("displayName") or "").lower()+            nm = _norm((it.get("attributes") or {}).get("displayName"))             if nm.startswith(fn2) or fn2 in nm:                 hit_item = it                 break
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.126",+  "version": "1.9.127",   "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.",
describe.py+1−1
@@ -14,7 +14,7 @@ _STATUS = "fusion_get_app_state" # Compact source table: name -> (summary, input, output, timeout, statusVerb, longRunning, example) _T = [     # ── Lifecycle ──────────────────────────────────────────────────────────-    ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates, prove carries the verdict). stepPath is required for a PROVEN library - no real 3D body without it.", {"libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),+    ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.", {"libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),     ("fusion_start", "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.",      {}, {"success": "bool", "addinReady": "bool", "status": "str"}, 150, None, True, {}),     ("fusion_stop", "Gracefully STOP Fusion 360: close docs cleanly + WM_CLOSE, then wait. NO force-kill - the clean opposite of fusion_start. If a window will not close it returns failure; use fusion_kill then.",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.142",+  "version": "1.9.143",   "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+10−7
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.142",+  "version": "1.9.143",   "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",@@ -70,7 +74,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.",@@ -99,9 +106,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+91−6
@@ -7091,6 +7091,21 @@ def _show_library_view(name, suffix, lbr_path=None):             print("[Fusion Bridge] resolved %r -> %r from the library (avoids a Create-new prompt)"                   % (name, resolved))             name = resolved+        # ⚠️ EDIT DRIVES THE ACTIVE DOCUMENT, NOT A PATH (page issue #47, root cause of+        # failure modes 1 AND 2): in a batch, several libraries (and f3ds) are open at+        # once, and EDIT lands in whichever is active. The artifact scan read the right+        # FILE while EDIT drove the WRONG document - so Fusion offered to CREATE a part+        # that plainly existed, or refused with "Not in Electronics editor". Make the+        # target library active, verified, before every EDIT.+        act = _ensure_library_active(lbr_path, timeout_s=30)+        if not act.get("ok"):+            return {"success": False, "errorCode": "library_not_active",+                    "error": "Could not make %s the ACTIVE document (active: %r), so EDIT "+                             "was not issued - it would have driven the wrong library." % (+                                 lbr_path.rsplit("/", 1)[-1], act.get("activeDocument")),+                    "_hint": "fusion_open_lbr the library, then re-call. In a batch, close "+                             "the previous part's documents first (the create_library chain "+                             "now does this itself)."}     edit = _proxy_to_addin("electron_run", {"command": "EDIT %s.%s" % (name, suffix)}, timeout=15)     # WE MAY NOT HAVE BEEN ABLE TO PRE-CHECK (no libraryPath, nothing remembered, or the     # library was opened by another route). If Fusion prompted anyway, answer it HERE - a@@ -9195,11 +9210,35 @@ def _prove_capture(label: str, view: str) -> dict:             w, h = shot.get("width"), shot.get("height")             out["width"], out["height"] = w, h             out["ok"] = not (w and h) or (int(w) >= 400 and int(h) >= 300)+            # INLINE COPY FOR BROWSER CALLERS (issue #47): the path is unreadable from+            # wiki.adom.inc web control - it cannot open C:/tmp. A ~500px data URI per+            # frame keeps the whole four-frame set renderable straight from the poll+            # response while the full-res file stays on disk for same-box callers.+            try:+                out["thumbnailDataUrl"] = _frame_data_url(p, max_width=500)+            except Exception:+                pass     except Exception as e:-        out["error"] = str(e)[:120]+        out["error"] = str(e)[:400]     return out  +def _frame_data_url(path: str, max_width: int = 500) -> str:+    """Downscale an on-disk frame to a data: URI (best effort, PIL-free: Fusion boxes have+    no PIL, so serve the file bytes as-is when small enough, else skip the inline copy)."""+    import base64 as _b64+    try:+        raw = open(path, "rb").read()+    except Exception:+        return ""+    # The bridge's captures are relay-sized webp (~150-250KB). Cap the inline copy so a+    # four-frame poll response stays comfortably under relay body limits.+    if len(raw) > 300_000:+        return ""+    mime = "image/webp" if path.lower().endswith(".webp") else "image/png"+    return "data:%s;base64,%s" % (mime, _b64.b64encode(raw).decode())++  def _close_bridge_doc_verified(name: str, timeout_s: int = 30) -> dict:     """Close a BRIDGE-owned scratch document deterministically - no dialog survives this call.@@ -9417,7 +9456,18 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict:         _p3d_urn = _p3d_urn or (args.get("f3dUrn") or "").strip()         opened3d = None         if _p3d_urn:-            opened3d = dispatch_command("open_by_urn", {"urn": _p3d_urn})+            # A JUST-MINTED f3d may not be durably openable yet (page issue #47 failure+            # mode 3: bind success + thumbnail ready, open refused moments later). The+            # lineage commits asynchronously - retry over ~45s before failing the frame.+            import time as _t3r+            for _attempt in range(3):+                opened3d = dispatch_command("open_by_urn", {"urn": _p3d_urn})+                if opened3d.get("success"):+                    break+                if _attempt < 2:+                    print("[Fusion Bridge] bound f3d not openable yet (attempt %d) - "+                          "waiting 15s for the lineage to commit" % (_attempt + 1))+                    _t3r.sleep(15)         if opened3d and opened3d.get("success"):             e3 = _prove_capture("package3d", "3d-editor")             chk = _count_3d_bodies()@@ -9486,7 +9536,9 @@ def _handle_prove_library(fusion_info: dict, args: dict) -> dict:     # A failed FRAME belongs in `missing` too (run 7d4427d443f5 read "missing=[]" while     # 2 of 4 frames had failed - the reader should never have to diff the evidence list     # to learn what is absent).-    missing += ["frame:%s(%s)" % (e.get("step"), str(e.get("error"))[:60])+    # FULL error text (issue #47: the [:60] cut landed exactly where the actionable half+    # of every message began - "Fusion offere[d ...]" - and cost the reporter a diagnosis)+    missing += ["frame:%s(%s)" % (e.get("step"), str(e.get("error")))                 for e in evidence if not (e.get("ok") and e.get("screenshot"))]     proven = (not missing) and len(captured) == 4     out = {"success": proven, "proven": proven, "libraryPath": lbr, "deviceset": want,@@ -9713,6 +9765,17 @@ def _handle_prove_result(fusion_info: dict, args: dict) -> dict:                 "error": "No fusion_prove_library run has completed on this box yet.",                 "_hint": "Run fusion_prove_library {libraryPath}; if the relay times out, the "                          "bridge keeps working - poll this verb until the result appears."}+    # INLINE FRAMES ARE OPT-IN (issue #47's web-control ask): each evidence entry carries+    # thumbnailDataUrl, but four base64 frames on EVERY 15s poll would bloat the relay+    # body. Pass {inlineFrames:true} on the FINAL fetch to get them; plain polls return+    # paths + metadata only.+    if not args.get("inlineFrames"):+        try:+            res = json.loads(json.dumps(res))+            for e in ((res.get("prove") or {}).get("evidence") or []) + (res.get("evidence") or []):+                e.pop("thumbnailDataUrl", None)+        except Exception:+            pass     # LIVE PROGRESS FOR WEB-CONTROL (John, 2026-08-21: "as accurate of progress bars as we     # can ... you should know how long stuff takes by measuring it"). Computed AT READ TIME     # from the persisted stage plan + this machine's measured stage history, so every poll@@ -9888,10 +9951,28 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:                 # the 3D frame never depends on re-parsing the lbr for it                 "f3dUrn": ((state.get("bind") or {}).get("wip_urn") or "")})             _leave("prove")-            state.update(stage="done", success=bool(prove.get("proven")), prove=prove)+            state.update(stage="done", success=bool(prove.get("proven")),+                         verdict=("proven" if prove.get("proven") else "not_proven"),+                         prove=prove)             _save()         except Exception as e:-            state.update(stage="failed", success=False, error=str(e)[:200]); _save()+            state.update(stage="failed", success=False, verdict="not_proven",+                         error=str(e)[:400]); _save()+        finally:+            # A BATCH MUST NOT NEED THE CALLER TO SWEEP BETWEEN PARTS (page issue #47,+            # failure mode 5: unsaved library docs accumulated until the add-in stopped+            # 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.+            try:+                _lbr_done = (state.get("libraryPath") or lbr_p or "")+                if _lbr_done:+                    _stem_done = _lbr_done.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0]+                    _closed = _close_bridge_doc_verified(_stem_done)+                    state["libraryDocClosed"] = bool(_closed.get("closed"))+                    _save()+            except Exception:+                pass      _th.Thread(target=_chain, daemon=True).start()     return {"success": True, "started": True, "runId": run_id,@@ -9929,7 +10010,11 @@ def _handle_check_3d_thumbnail(fusion_info: dict, args: dict) -> dict:             import xml.etree.ElementTree as _ET             root = _ET.parse(lbr).getroot()             for p3 in root.iter("package3d"):-                urn = (p3.get("urn") or "").strip() or urn+                # wip_urn, not urn: this bridge writes <package3d urn="" wip_urn="...">+                # (the same one-attribute miss that broke prove's 3D frame, issue #46 -+                # and unfixed HERE it made tja1051t/type-c report thumbnail not_found+                # with the file sitting right there, issue #47 failure mode 4)+                urn = (p3.get("urn") or p3.get("wip_urn") or "").strip() or urn                 file_name = file_name or (p3.get("name") or "")         except Exception:             pass