← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.320\ No newline at end of file+1.9.321\ 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.320",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.320/adom-bridge-fusion-v1.9.320.zip",-  "sha256": "1b4207723298410474a377ff4f7166704f3613f0c64e901038f88c4a64c2a820",-  "size": 599846,+  "version": "1.9.321",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.321/adom-bridge-fusion-v1.9.321.zip",+  "sha256": "d3f15f4830e3431219c6bf71769685ec4e4181db1137744c4d3979735f860330",+  "size": 601633,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.320",+  "version": "1.9.321",   "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.320"+version = "1.9.321" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.320"+version = "1.9.321" 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.320"+DASH_VERSION="1.9.321" 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.320",+  "version": "1.9.321",   "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.320",+  "version": "1.9.321",   "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+212−74
@@ -15785,13 +15785,116 @@ _AUTODESK_SIGNIN_HOST = "signin.autodesk.com" _LIBIO_DIP = {     "manage_tab": (296, 42),     "group": (455, 103),-    "item0": (466, 125),    # Push changes to library.io-    "item1": (466, 146),    # Pull (when managed) / Link (when not)-    "item2": (466, 166),    # Link (when managed) / Unlink (when not)-    "item3": (466, 187),    # Unlink (when managed)     "open_icon": (352, 80),   # OPEN icon - its default action is Open Library Manager } +# The LIBRARY.IO dropdown, read off the screen 2026-09-01. It has these FOUR items in this+# order, and the order does NOT change with link state (an unlinked library still shows Pull+# and Unlink, greyed):+#     0 Push changes to managed library on library.io+#     1 Pull changes from managed library on library.io+#     2 Link to managed library on library.io+#     3 Unlink from managed library on library.io+_LIBIO_MENU_ITEMS = ["push", "pull", "link", "unlink"]+++def _popup_menu_rect(timeout_s=5.0):+    """Locate the open LIBRARY.IO dropdown and return its rect.++    The menu is NOT part of Fusion's main window - it is a separate always-on-top Qt window+    (className Qt683QWindow, title "Fusion360"), which is why a window capture of Fusion shows+    no menu and a capture of the popup itself comes back black. So we never try to READ it; we+    MEASURE it with desktop_list_windows and derive each item's centre from the rect. That is+    what replaced the hardcoded per-item DIP offsets, which silently pointed at the DESIGN+    ribbon whenever a library was not actually in front.++    Fusion's docked panels share the title "Fusion360", so filter on the exact class and a+    menu-sized rect, and take the topmost (lowest z).+    """+    import time as _t+    deadline = _t.time() + timeout_s+    while _t.time() < deadline:+        try:+            resp = _ad_call("desktop_list_windows", {}, timeout=25)+            wins = _find_key(resp, "windows") or []+            cands = []+            for w in wins:+                r = w.get("rect") or {}+                if str(w.get("className") or "") != "Qt683QWindow":+                    continue+                if str(w.get("title") or "") != "Fusion360":+                    continue+                if not (200 <= int(r.get("width") or 0) <= 1400):+                    continue+                if not (60 <= int(r.get("height") or 0) <= 700):+                    continue+                cands.append((int(w.get("z") or 99), w, r))+            if cands:+                cands.sort(key=lambda c: c[0])+                _z, w, r = cands[0]+                return {"hwnd": w.get("hwnd"), "left": int(r["left"]), "top": int(r["top"]),+                        "width": int(r["width"]), "height": int(r["height"])}+        except Exception:+            pass+        _t.sleep(0.4)+    return None+++def _wait_for_top_window(title_substr, timeout_s=25.0):+    """Wait for a top-level window whose title contains title_substr, via desktop_list_windows.+    Fusion's own dialogs (Select Managed Library, Library Manager) are REAL windows and are+    fully UIA-readable, unlike its ribbon and its menus."""+    import time as _t+    want = title_substr.lower()+    deadline = _t.time() + timeout_s+    while _t.time() < deadline:+        try:+            resp = _ad_call("desktop_list_windows", {}, timeout=25)+            for w in (_find_key(resp, "windows") or []):+                if want in str(w.get("title") or "").lower():+                    return w+        except Exception:+            pass+        _t.sleep(0.8)+    return None+++_PICKER_COLUMNS = ["name", "type", "version", "managedFolder", "description"]+++def _picker_rows(hwnd):+    """Read the Select Managed Library table into records.++    Its rows are UIA TreeItems under one QTreeWidget and stream in row-major order, five cells+    per row. Only REALIZED rows are exposed (Qt virtualizes the list), so this returns what is+    on screen, not necessarily the whole catalogue."""+    try:+        resp = _ad_call("desktop_ui_tree", {"hwnd": hwnd, "maxDepth": 14}, timeout=60)+    except Exception as e:+        return [], str(e)+    cells = []++    def walk(n):+        if isinstance(n, dict):+            if n.get("role") == "TreeItem" and n.get("name"):+                r = n.get("rect") or {}+                cells.append({"text": str(n["name"]), "x": r.get("centerX"), "y": r.get("centerY")})+            for v in n.values():+                if isinstance(v, (dict, list)):+                    walk(v)+        elif isinstance(n, list):+            for i in n:+                walk(i)++    walk(resp)+    rows = []+    for i in range(0, len(cells) - (len(_PICKER_COLUMNS) - 1), len(_PICKER_COLUMNS)):+        chunk = cells[i:i + len(_PICKER_COLUMNS)]+        row = {k: c["text"] for k, c in zip(_PICKER_COLUMNS, chunk)}+        row["_at"] = [chunk[0]["x"], chunk[0]["y"]]+        rows.append(row)+    return rows, None+  def _fusion_hwnd_rect():     """(hwnd, rect, scale) for Fusion's main window, or None."""@@ -15836,15 +15939,39 @@ def _ribbon_click(win, key, why, popup=False):     return {"step": key, "at": [x, y], "scale": scale}  -def _libio_menu(win, item_key, why, steps):-    """MANAGE tab -> LIBRARY.IO group -> one menu item."""+def _libio_menu(win, item, why, steps):+    """MANAGE tab -> LIBRARY.IO group -> one menu item, by NAME not by guessed offset.++    Returns None on success, or a typed error dict when the dropdown never opened - which is+    the honest answer for "a library was not really in front", the failure that used to show up+    as a mystery `picker_missing` after the clicks had quietly driven the Design ribbon.+    """     import time as _t+    if item not in _LIBIO_MENU_ITEMS:+        return {"success": False, "errorCode": "bad_menu_item",+                "error": "%r is not a LIBRARY.IO menu item; expected one of %s"+                         % (item, ", ".join(_LIBIO_MENU_ITEMS))}     steps.append(_ribbon_click(win, "manage_tab", "open the library editor's MANAGE tab"))     _t.sleep(1.4)     steps.append(_ribbon_click(win, "group", "open the LIBRARY.IO command group"))-    _t.sleep(1.4)-    steps.append(_ribbon_click(win, item_key, why, popup=True))+    _t.sleep(1.0)+    rect = _popup_menu_rect()+    if not rect:+        return {"success": False, "errorCode": "libio_menu_missing", "steps": steps,+                "error": "The LIBRARY.IO dropdown did not open.",+                "_hint": "Almost always means a LIBRARY is not really in front - Fusion's API "+                         "can report a library as active while an empty Untitled design holds "+                         "the visible tab. Re-open the library, confirm the ribbon reads "+                         "COMPONENT/DOCUMENT/MANAGE (not SOLID/SURFACE), then retry."}+    idx = _LIBIO_MENU_ITEMS.index(item)+    n = len(_LIBIO_MENU_ITEMS)+    x = int(rect["left"] + rect["width"] / 2)+    y = int(rect["top"] + (idx + 0.5) * rect["height"] / n)+    # A popup item goes through the raw mouse verb: re-focusing Fusion here would dismiss it.+    _ad_call("desktop_mouse_click", {"x": x, "y": y, "reason": why}, timeout=25)+    steps.append({"step": item, "at": [x, y], "menuRect": rect})     _t.sleep(2.0)+    return None   def _library_in_front(expect_library=True):@@ -15985,7 +16112,9 @@ def _handle_managed_library_push(fusion_info: dict, args: dict) -> dict:     if err:         return err     steps = []-    _libio_menu(ctx["win"], "item0", "Push changes to library.io", steps)+    menu_err = _libio_menu(ctx["win"], "push", "Push changes to library.io", steps)+    if menu_err:+        return menu_err     dlg = _wait_for_dialog("Managed Library", 25)     if dlg:   # an unlinked library answers with the Create dialog instead         return {"success": False, "errorCode": "not_linked_yet", "steps": steps,@@ -16004,12 +16133,15 @@ def _handle_managed_library_pull(fusion_info: dict, args: dict) -> dict:     if err:         return err     steps = []-    _libio_menu(ctx["win"], "item1", "Pull changes from the managed library on library.io", steps)+    menu_err = _libio_menu(ctx["win"], "pull",+                           "Pull changes from the managed library on library.io", steps)+    if menu_err:+        return menu_err     alerts = _fusion_alerts(120)     return {"success": not alerts, "library": ctx["doc"], "steps": steps, "alerts": alerts,-            "_hint": "Pull requested. On an UNLINKED library this menu slot is Link instead - "-                     "check alerts[] and fusion_managed_library_status." if not alerts else-                     "Fusion raised an alert - read alerts[]."}+            "_hint": "Pull requested. Fusion greys this item out on a library that was never "+                     "linked, so a silent no-op there is expected - link it first." if not alerts+                     else "Fusion raised an alert - read alerts[] and treat this as failed."}   def _handle_managed_library_unlink(fusion_info: dict, args: dict) -> dict:@@ -16018,78 +16150,84 @@ def _handle_managed_library_unlink(fusion_info: dict, args: dict) -> dict:     if err:         return err     steps = []-    # linked -> Unlink is slot 3; unlinked -> slot 2 (and is a no-op)-    _libio_menu(ctx["win"], args.get("slot") or "item3",-                "Unlink from the managed library on library.io", steps)+    menu_err = _libio_menu(ctx["win"], "unlink",+                           "Unlink from the managed library on library.io", steps)+    if menu_err:+        return menu_err     alerts = _fusion_alerts(120)     return {"success": not alerts, "library": ctx["doc"], "steps": steps, "alerts": alerts,-            "_hint": "Pass slot:'item2' if this library was never linked (the menu is shorter)."}+            "_hint": "Unlink requested. Fusion greys this item out on a library that was never "+                     "linked, so a silent no-op there is expected." if not alerts+                     else "Fusion raised an alert - read alerts[] and treat this as failed."}   def _handle_managed_library_link(fusion_info: dict, args: dict) -> dict:-    """Link this library to an EXISTING managed library. With no `name`, returns the picker's-    contents so a caller can choose; with `name`, selects that row and confirms.--    The LIBRARY.IO menu is ORDER-SENSITIVE and its length depends on state (measured live):-      unlinked -> Push, Link, Unlink            (Link is slot 1)-      linked   -> Push, Pull, Link, Unlink      (Link is slot 2)-    Fusion exposes no UIA for the popup, so the verb tries the likely slot and self-corrects-    rather than guessing once and reporting a false negative."""+    """Link this library to an EXISTING managed library on library.io.++    With no `name`, returns the picker's contents (name, type, version, managed folder,+    description) and cancels, so a caller can choose. With `name`, selects that row and+    confirms.++    Both the menu and the picker are handled by measurement, not by guesswork: the LIBRARY.IO+    dropdown is located as a window and its items derived from its rect, and the picker is a+    REAL dialog whose rows are readable over UIA.+    """     err, ctx = _managed_precheck(require_hub=False)     if err:         return err     import time as _t     steps = []-    dlg = None-    for slot in ("item1", "item2"):-        _libio_menu(ctx["win"], slot, "Link to a managed library on library.io", steps)-        dlg = _wait_for_dialog("Select Managed Library", 12)-        if dlg:-            steps.append({"step": "picker", "slot": slot})-            break+    menu_err = _libio_menu(ctx["win"], "link", "Link to a managed library on library.io", steps)+    if menu_err:+        return menu_err+    dlg = _wait_for_top_window("Select Managed Library", 25)     if not dlg:         return {"success": False, "errorCode": "picker_missing", "steps": steps,                 "alerts": _fusion_alerts(120),-                "_hint": "The Select Managed Library picker never appeared from either menu slot. "-                         "Check that a LIBRARY (not a design) is in front and that Fusion is "-                         "maximized."}-    tree = _ad_call("desktop_ui_tree", {"hwnd": dlg.get("hwnd"), "maxDepth": 8,-                                        "maxNodes": 400}, timeout=90) or {}-    names = []--    def _walk(n, d=0):-        if d > 8 or not isinstance(n, dict):-            return-        nm = (n.get("name") or "").strip()-        if nm and len(nm) < 80:-            names.append({"name": nm, "center": [(n.get("rect") or {}).get("centerX"),-                                                 (n.get("rect") or {}).get("centerY")]})-        for c in (n.get("children") or []):-            _walk(c, d + 1)-    _walk(tree.get("root") or {})+                "error": "The LIBRARY.IO menu opened but Select Managed Library never appeared.",+                "_hint": "Fusion greys out Link on a library that is already linked - call "+                         "fusion_managed_library_unlink first, or check alerts[]."}+    hwnd = dlg.get("hwnd")+    rows, read_err = _picker_rows(hwnd)     want = (args.get("name") or "").strip()++    def _close(why):+        _ad_call("desktop_ui_click", {"hwnd": hwnd, "name": "Cancel", "reason": why}, timeout=30)+     if not want:-        _ad_call("desktop_ui_click", {"hwnd": dlg.get("hwnd"), "name": "Cancel",-                                      "reason": "close the picker after listing it"}, timeout=30)+        _close("close the Select Managed Library picker after listing it read-only")         return {"success": True, "listedOnly": True, "steps": steps,-                "managedLibraries": [n["name"] for n in names][:80],-                "_hint": "Pass name:<one of these> to link. The list is what THIS Autodesk "-                         "account can reach - a managed library owned by another account does not "-                         "appear unless its managed folder is shared."}-    hit = next((n for n in names if n["name"].lower() == want.lower()), None)-    if not hit or not (hit["center"] or [None])[0]:-        _ad_call("desktop_ui_click", {"hwnd": dlg.get("hwnd"), "name": "Cancel",-                                      "reason": "close the picker; requested library not listed"}, timeout=30)+                "managedLibraries": rows, "count": len(rows), "readError": read_err,+                "_hint": "Pass name:<one of these> to link. This is what THIS Autodesk account "+                         "can reach: library.io's public managed folders plus any managed folder "+                         "this account belongs to. A managed library owned by another account "+                         "does NOT appear until its managed folder is shared with this one. Qt "+                         "virtualizes the list, so only realized rows are returned - scroll or "+                         "filter in the dialog if the one you want is missing."}+    hit = next((r for r in rows if r["name"].lower() == want.lower()), None)+    if not hit or not (hit.get("_at") or [None])[0]:+        _close("close the picker; the requested managed library was not listed")         return {"success": False, "errorCode": "not_listed", "steps": steps,-                "managedLibraries": [n["name"] for n in names][:80],-                "error": "%r is not in this account's managed-library list." % want}-    _ad_call("desktop_click", {"x": int(hit["center"][0]), "y": int(hit["center"][1]),-                               "cursor": False, "reason": "select %s in the picker" % want}, timeout=25)+                "managedLibraries": rows, "count": len(rows),+                "error": "%r is not in this account's managed-library list." % want,+                "_hint": "Names are exact. If you expect a library shared from another Autodesk "+                         "account, it only appears once its MANAGED FOLDER is shared with this "+                         "account on library.io - being in the same Fusion hub is not enough."}+    _ad_call("desktop_click", {"x": int(hit["_at"][0]), "y": int(hit["_at"][1]), "cursor": False,+                               "reason": "select %s in the Select Managed Library picker" % want},+             timeout=25)     _t.sleep(1)-    ok = _ad_call("desktop_ui_click", {"hwnd": dlg.get("hwnd"), "name": "OK",+    ok = _ad_call("desktop_ui_click", {"hwnd": hwnd, "name": "OK",                                        "reason": "confirm the managed-library link"}, timeout=30) or {}-    steps.append({"step": "confirm", "ok": bool(ok.get("success"))})-    return {"success": True, "linkedTo": want, "steps": steps, "alerts": _fusion_alerts(120)}+    steps.append({"step": "confirm", "row": hit["name"], "clicked": bool(_find_key(ok, "success"))})+    _t.sleep(2)+    gone = _wait_for_top_window("Select Managed Library", 6) is None+    alerts = _fusion_alerts(120)+    return {"success": bool(gone) and not alerts, "linkedTo": hit["name"], "library": ctx["doc"],+            "managedFolder": hit.get("managedFolder"), "version": hit.get("version"),+            "steps": steps, "alerts": alerts, "dialogClosed": gone,+            "_hint": "The picker closed and Fusion raised no alert." if gone and not alerts else+                     "The picker is still up or Fusion alerted - treat this as NOT linked."}   def _handle_library_manager(fusion_info: dict, args: dict) -> dict:@@ -16205,18 +16343,18 @@ def _handle_managed_library_create(fusion_info: dict, args: dict) -> dict:     # window-fraction math (925,273). cursor:false matters too - the glowing cursor overlay     # dismisses Fusion's Qt popup menus.     win["dpiScale"] = _window_dpi_scale(win.get("hwnd"))-    _libio_menu(win, "item0", "Push changes to library.io", steps)-    dlg = _wait_for_dialog("Create Managed Library", 25)+    menu_err = _libio_menu(win, "push", "Push changes to library.io", steps)+    if menu_err:+        return menu_err+    dlg = _wait_for_top_window("Create Managed Library", 25)     if not dlg:         return {"success": False, "errorCode": "create_dialog_missing", "steps": steps,                 "alerts": _fusion_alerts(120),-                "error": "The Create Managed Library dialog never appeared - the ribbon click "-                         "probably missed (the ribbon is invisible to UIA, so this verb drives "-                         "measured DIP offsets).",+                "error": "The LIBRARY.IO menu opened but Create Managed Library never appeared.",                 "windowRect": win.get("rect"),-                "_hint": "The Fusion window must be MAXIMIZED (the offsets are window-rect "-                         "fractions). Recalibrate _RIBBON_FRAC from a desktop_screenshot_screen "-                         "if Fusion's ribbon layout changed."}+                "_hint": "Push offers Create only for a library that is NOT yet linked. If this "+                         "library is already linked, fusion_managed_library_push pushes a new "+                         "version instead - check fusion_alerts."}     ok = _ad_call("desktop_ui_click", {"hwnd": dlg.get("hwnd"), "name": "Create",                                        "reason": "create the managed library on library.io"},                   timeout=30) or {}