← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.306\ No newline at end of file+1.9.307\ 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.306",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.306/adom-bridge-fusion-v1.9.306.zip",-  "sha256": "264ee2e707f92350cd585eef8c267fb3e07697da0e358686ba2c9ffa6b6cdeb7",-  "size": 587898,+  "version": "1.9.307",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.307/adom-bridge-fusion-v1.9.307.zip",+  "sha256": "345900735e22a1584e43880742c189f82af2800910863342444538bf157eff48",+  "size": 589478,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.306",+  "version": "1.9.307",   "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.306"+version = "1.9.307" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.306"+version = "1.9.307" 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.306"+DASH_VERSION="1.9.307" 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.306",+  "version": "1.9.307",   "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.306",+  "version": "1.9.307",   "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+111−4
@@ -1410,6 +1410,104 @@ def _post_open_screenshot(result: dict, settle_time: float = 2.0) -> dict:     return _apply_failure_dialogs(result)  +# ── Fusion's OWN error toasts, read programmatically (John, 2026-09-01) ──────+# Fusion draws many errors as CEF overlays INSIDE the canvas: no top-level window, so+# neither Win32 enumeration nor UIA can see them, and check_dialogs reported "no dialogs"+# while an Error box sat on the user's screen ("Cannot create a managed library from a+# document not saved in a Fusion Hub"). Fusion DOES log every one of them to its own+# AppLogFile as a `New Alert message` line, so the bridge reads that instead of guessing+# from pixels. Bridge-side on purpose: it works with the add-in wedged and needs no+# Fusion restart to deploy.+_ALERT_RE = re.compile(r"New Alert message\.\s*(?:Tooltip:\s*(?P<tip>.*?))?"+                       r"(?:,\s*Short Message:\s*(?P<short>.*?))?"+                       r"(?:,\s*Long Message:\s*(?P<long>.*?))?\s*$")+++def _fusion_log_path():+    """Newest Fusion AppLogFile for the signed-in user, or None."""+    try:+        base = os.path.join(os.path.expanduser("~"), "AppData", "Local", "Autodesk",+                            "Autodesk Fusion 360")+        best = None+        for entry in os.listdir(base):+            d = os.path.join(base, entry, "logs")+            if not os.path.isdir(d):+                continue+            for f in os.listdir(d):+                if f.startswith("AppLogFile") and f.endswith(".log"):+                    fp = os.path.join(d, f)+                    m = os.path.getmtime(fp)+                    if best is None or m > best[0]:+                        best = (m, fp)+        return best[1] if best else None+    except Exception:+        return None+++def _fusion_alerts(since_sec: float = 300.0, tail_bytes: int = 400000) -> list:+    """Fusion's own alert toasts from the last `since_sec` seconds.++    Returns [{at, text}] newest last. Never raises."""+    import time as _t, datetime as _dt+    path = _fusion_log_path()+    if not path:+        return []+    try:+        size = os.path.getsize(path)+        with open(path, "r", encoding="utf-8", errors="replace") as fh:+            if size > tail_bytes:+                fh.seek(size - tail_bytes)+                fh.readline()+            lines = fh.readlines()+    except Exception:+        return []+    cutoff = _t.time() - max(1.0, since_sec)+    out = []+    for line in lines:+        if "New Alert message" not in line:+            continue+        stamp = None+        m0 = re.match(r"\s*(\d{8}T\d{6})", line)+        if m0:+            try:+                stamp = _dt.datetime.strptime(m0.group(1), "%Y%m%dT%H%M%S").timestamp()+            except Exception:+                stamp = None+        if stamp is not None and stamp < cutoff:+            continue+        m = _ALERT_RE.search(line)+        text = ""+        if m:+            text = (m.group("short") or m.group("tip") or "").strip()+        if not text:+            text = line.split("New Alert message.", 1)[-1].strip()[:400]+        out.append({"at": m0.group(1) if m0 else None, "text": text[:400]})+    return out+++def _with_fusion_alerts(resp: dict, since_sec: float = 300.0) -> dict:+    """Attach Fusion's own alert toasts to a dialog report, so an in-canvas CEF error is+    never invisible just because it has no window (John, 2026-09-01)."""+    try:+        alerts = _fusion_alerts(since_sec)+        if alerts:+            resp = dict(resp or {})+            resp["fusionAlerts"] = alerts+            resp["_hint"] = ("Fusion raised %d alert(s) in the last %ds - the newest is %r. "+                             "These are in-canvas CEF overlays with no top-level window, so "+                             "they do NOT appear in dialogs[]; read fusionAlerts and treat the "+                             "driving command as FAILED unless you know otherwise."+                             % (len(alerts), int(since_sec), alerts[-1].get("text", "")[:160]))+        elif not (resp or {}).get("dialogs"):+            resp = dict(resp or {})+            resp["fusionAlerts"] = []+            resp["_hint"] = ("No Fusion dialogs, owned popups, or alert toasts in the last "+                             "%ds." % int(since_sec))+    except Exception:+        pass+    return resp++ def _capture_dialog_array(settle: float = 0.6) -> dict | None:     """After a mutating op, enumerate + screenshot any owned popups / modal dialogs so     the AI can SEE and ANALYZE them. Titles alone are not enough: a dialog's body text@@ -2698,11 +2796,20 @@ COMMAND_HANDLERS = {     # can ANALYZE before acting. Use while polling a long op (e.g. a Hub upload) or any     # time you suspect a dialog is up. Mutating verbs attach this automatically; this is     # the manual entry point. See the fusion-driving skill.-    "check_dialogs": lambda fi, args: (+    "check_dialogs": lambda fi, args: _with_fusion_alerts(         _capture_dialog_array(settle=float(args.get("settle", 0.3)))-        or {"success": True, "dialogsDetected": 0, "dialogs": [],-            "_hint": "No Fusion dialogs/owned popups are currently up."}-    ),+        or {"success": True, "dialogsDetected": 0, "dialogs": []},+        float(args.get("alertsSinceSec", 300))),+    # Fusion's own error/warning toasts (the CEF overlays no window enumeration can see).+    "fusion_alerts": lambda fi, args: {+        "success": True,+        "alerts": _fusion_alerts(float(args.get("sinceSec", 300))),+        "logFile": _fusion_log_path(),+        "_hint": ("Fusion draws many errors as in-canvas CEF overlays with NO top-level "+                  "window, so check_dialogs alone cannot see them; these come from Fusion's "+                  "own AppLogFile 'New Alert message' entries. An empty list means Fusion "+                  "raised no toast in the window - not that a command succeeded."),+    },     "addin_status": lambda fi, args: _handle_addin_status(),     # LAST-RESORT human escalation (John 2026-07-07): when the bridge is truly blocked     # on the user (password/2FA, UAC), send an ab toast that reaches their MAIN machine