← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.10.12\ No newline at end of file+1.10.13\ No newline at end of file
addin/AdomBridge/commands/app_state.py+1−1
@@ -93,7 +93,7 @@ def handle_get_app_state(app: adsk.core.Application, args: dict) -> dict:     else:         hints.append("Not in Electronics workspace. Use fusion_show_2d_board to enter board view, "                       "or fusion_open_cloud_file to open an electronics project (.fprj).")-    hints.append("Check fusion_window_info for blocking dialogs before running commands.")+    hints.append("Check fusion_state {screenshot:false} (dialogs + focusEvents) before running commands if something looks stuck.")     hint = " ".join(hints)      return {
addin/AdomBridge/commands/cloud_documents.py+24−6
@@ -1,3 +1,4 @@+import re """Cloud document management for Fusion 360.  Provides commands to save documents to Autodesk's cloud hub,@@ -841,7 +842,7 @@ def handle_check_recovery(app: adsk.core.Application, args: dict) -> dict:     }  -def _ecad_open_reprimand(app):+def _ecad_open_reprimand(app, intent=None):     """Detect when an AI opened an electronics CHILD file instead of the PROJECT.      Electronics files are a parent/child chain: PROJECT (EcadDesignProductType) ->@@ -861,9 +862,26 @@ def _ecad_open_reprimand(app):            ' schematic (SchematicProductType) -> board/.brd (BoardProductType) -> 3D (the leaf,'            ' generated from the .brd). ALWAYS open the PROJECT file; its schematic and board open'            ' from it. NEVER open a schematic, .brd, or 3D file directly to get the board.')+    if pt == 'DesignProductType':+        # A DesignProductType is every mechanical design in Fusion; it is only the wrong file+        # when it is the 3D leaf of an electronics chain (issue #86: a legitimate mechanical+        # open was told "STOP - WRONG FILE"). Judge by evidence, or by the caller's stated intent.+        if intent == 'mechanical':+            return 'OK: opened a mechanical design (DesignProductType), as requested.'+        name = ''+        try:+            name = (app.activeDocument.name or '') if app.activeDocument else ''+        except Exception:+            pass+        looks_ecad = bool(re.search(r'(?i)(\b3d\b|_3d\b|\bpcb\b|\bboard\b)', name))+        if intent == 'electronics' or looks_ecad:+            return ('STOP - WRONG FILE: you opened a 3D model file (DesignProductType) that looks like '+                    'the 3D leaf of an electronics design, not the project.' + nav)+        return ('OK: opened a mechanical design (DesignProductType). If you actually wanted an '+                'electronics board, its 3D is a child of the PROJECT (EcadDesignProductType): open '+                'the project and switch views there; pass intent:"electronics" to make this a refusal.')     label = {'SchematicProductType': 'the SCHEMATIC',-             'BoardProductType': 'the BOARD (.brd) / its 3D PCB',-             'DesignProductType': 'a 3D model file'}.get(pt)+             'BoardProductType': 'the BOARD (.brd) / its 3D PCB'}.get(pt)     if label:         return ('STOP - WRONG FILE: you opened ' + label + ' (' + pt + ') directly, not the project.'                 + nav)@@ -985,7 +1003,7 @@ def handle_open_cloud_file(app: adsk.core.Application, args: dict) -> dict:     except Exception:         pass -    _rep = _ecad_open_reprimand(app)+    _rep = _ecad_open_reprimand(app, (args or {}).get('intent'))     return {         "success": True,         "output": ((_rep + " || ") if _rep.startswith("STOP") else "") + f"Opened '{target.name}' from project '{project.name}'",@@ -1883,13 +1901,13 @@ def handle_open_by_urn(app: adsk.core.Application, args: dict) -> dict:                 "_hint": "documents.open failed — an open-path issue, NOT the Fusion license. A "                          "read-only/expired/trial Fusion still opens+views files (only save/export "                          "are blocked). Likely a slow/large assembly, a blocking Electronics picker, "-                         "or a transient cloud error — retry / check fusion_window_info for a modal.",+                         "or a transient cloud error — retry / check fusion_state {screenshot:false} for a modal.",                 "data": {"fileName": df.name, "fileId": df.id}}     try:         doc.activate()     except Exception:         pass-    _rep = _ecad_open_reprimand(app)+    _rep = _ecad_open_reprimand(app, (args or {}).get('intent'))     return {"success": True, "output": ((_rep + " || ") if _rep.startswith("STOP") else "") + ("Opened '%s'." % df.name),             "_hint": _rep,             "data": {"fileName": df.name, "fileId": df.id, "resolved": tried}}
addin/AdomBridge/http_server.py+1−1
@@ -399,7 +399,7 @@ class AdomBridgeHandler(BaseHTTPRequestHandler):                 "_hint": (                     "Add-in is busy with a long-running command. Do NOT retry add-in commands. "                     "Do NOT press Escape — the add-in is working, not stuck on a dialog. "-                    "Commands that still work: fusion_window_info, fusion_screenshot_fusion, "+                    "Commands that still work: fusion_state, fusion_screenshot_fusion, "                     "fusion_click_fusion, fusion_send_key, fusion_close_window."                 ),             }
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.10.12",+  "version": "1.10.13",   "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.",
contracts/describe.json+8−4
@@ -1,6 +1,6 @@ {   "contract": "fusion-bridge verb catalog",-  "frozen": "2026-09-10",+  "frozen": "2026-09-19",   "note": "The Python bridge and the Rust bridge must both serve this catalog byte-for-byte in meaning: same names, same args, same result shapes, same timeouts. Regenerate ONLY from describe.py and diff in CI.",   "source": "describe.py",   "verbs": [@@ -250,7 +250,7 @@         "results": "[{name,id,projectName,lastModified}]"       },       "statusVerb": null,-      "summary": "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk).",+      "summary": "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk). A ZERO from the server index is not absence: the reply then carries data.fallback (a name scan of the newest 400 items), data.completeness, and a hint to fusion_aps_browse a known sibling's folder (issue #85).",       "timeoutSeconds": 60     },     {@@ -2017,6 +2017,7 @@         "urn": "urn:adsk.wipprod:fs.file:vf.xxxx?version=1"       },       "input": {+        "intent": "optional \"mechanical\" | \"electronics\": what you meant to open. A DesignProductType is every mechanical design; the STOP-WRONG-FILE hint now fires only for the 3D leaf of an electronics chain (by name, or intent electronics); intent mechanical confirms the open (issue #86)",         "urn": "required str"       },       "longRunning": true,@@ -2064,7 +2065,8 @@         "filePath": "C:/Temp/part.f3d"       },       "input": {-        "filePath": "required str (WINDOWS path)"+        "filePath": "required str (WINDOWS path)",+        "intent": "optional \"mechanical\" | \"electronics\" (see fusion_open_by_urn)"       },       "longRunning": true,       "name": "fusion_open_design",@@ -2543,7 +2545,9 @@     {       "example": {},       "input": {-        "enabled": "optional bool (omit to read)"+        "enabled": "optional bool (omit to read)",+        "nativeViewport": "optional bool (default true with screenshot): also export the native viewport frame and count bodies; the reply carries nativeViewport {path, ok, bodies, productType}, geometry {bodies} (API truth, independent of pixels) and windowPixels {dominantColorFraction, blank} for the window capture. A blank window capture of a Design viewport is a known WGC artefact: trust nativeViewport, never foreground Fusion for a capture (issue #84)",+        "viewport": "optional str: \"native\" = only the frame Fusion itself renders (activeViewport.saveAsImageFile through the add-in), no window capture"       },       "longRunning": false,       "name": "fusion_set_driving_badge",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.10.12"+version = "1.10.13" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.12"+version = "1.10.13" edition = "2021"  [dependencies]
describe.py+4−4
@@ -106,13 +106,13 @@ _T = [      {}, {"success": "bool", "browser": "{name,exe,profileDir}|null"}, 10, None, False, {}),     ("fusion_aps_forget_browser", "Forget the remembered Autodesk sign-in browser (re-detect next time).",      {}, {"success": "bool", "cleared": "bool"}, 10, None, False, {}),-    ("fusion_aps_search", "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk).",+    ("fusion_aps_search", "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk). A ZERO from the server index is not absence: the reply then carries data.fallback (a name scan of the newest 400 items), data.completeness, and a hint to fusion_aps_browse a known sibling's folder (issue #85).",      {"query": "required str", "limit": "optional int (default 50)", "hubId": "optional str", "projectId": "optional str"},      {"count": "int", "results": "[{name,id,projectName,lastModified}]"}, 60, None, False, {"query": "Molecule", "limit": 10}),     ("fusion_aps_open", "Find a cloud design by name and open it in Fusion (any folder depth). Fire-and-poll.",      {"query": "required str", "wait": "optional bool"}, {"opening": "bool", "match": "{name}", "statusVerb": "str"}, 60, _STATUS, True, {"query": "BME690 Molecule"}),     ("fusion_open_by_urn", "Open a cloud ELECTRONICS DESIGN by URN. PCB/ELECTRONICS NAV RULE: a board is NOT a standalone file - the schematic, 2D board and 3D board are VIEWS inside ONE electronics DESIGN. Open the DESIGN document first (fusion_open_cloud_file or fusion_open_by_urn), THEN switch to its schematic and its 2D board (fusion_show_2d_board) and 3D board (fusion_show_3d_board). A Select-Electronics-Design-File picker IS that design list of its schematic + board. An EMPTY 2D board usually means you opened a derivative or the wrong file, or it is out of sync - reopen the parent electronics DESIGN and switch to its board view; do NOT open a .brd or .sch as a standalone file.",-     {"urn": "required str"}, {"success": "bool", "output": "str"}, 240, _STATUS, True, {"urn": "urn:adsk.wipprod:fs.file:vf.xxxx?version=1"}),+     {"urn": "required str", "intent": "optional \"mechanical\" | \"electronics\": what you meant to open. A DesignProductType is every mechanical design; the STOP-WRONG-FILE hint now fires only for the 3D leaf of an electronics chain (by name, or intent electronics); intent mechanical confirms the open (issue #86)"}, {"success": "bool", "output": "str"}, 240, _STATUS, True, {"urn": "urn:adsk.wipprod:fs.file:vf.xxxx?version=1"}),     ("fusion_aps_browse", "Browse hubs/projects/folders without launching Fusion.",      {"hubId": "optional str", "projectId": "optional str", "folderId": "optional str"}, {"items": "[{name,type,id}]"}, 60, None, False, {}),     ("fusion_aps_recent", "Most-recently-modified designs across the team hub, newest first.",@@ -230,7 +230,7 @@ _T = [     # the verb suite's reverse drift check. An undescribed verb is invisible to ab's Verbs     # tab and to every caller reading fusion_describe - never let these drift again.     ("fusion_open_cloud_file", "Open a CLOUD file by name (the front door to every design flow). PCB/ELECTRONICS NAV RULE: open the electronics DESIGN document (EcadDesignProductType), never a child .brd/.sch/3D directly - then switch views with fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board. LONG-RUNNING: fire it, then poll fusion_get_app_state until activeDocument matches (cloud opens on electronics designs take 1-3 min). A crash-recovery modal can block the open - fusion_dismiss_blocking_dialogs.", {"fileName": "required str", "projectName": "optional str (defaults to active project)", "folderPath": "optional str"}, {"success": "bool", "documentName": "str"}, 240, "fusion_get_app_state", True, {"fileName": "BQ25792", "projectName": "Main"}),-    ("fusion_open_design", "Open a LOCAL design file (routes through the add-in import). Prefer cloud flows (fusion_open_cloud_file) for team designs; this is for local .f3d/.step style files.", {"filePath": "required str (WINDOWS path)"}, {"success": "bool"}, 240, None, True, {"filePath": "C:/Temp/part.f3d"}),+    ("fusion_open_design", "Open a LOCAL design file (routes through the add-in import). Prefer cloud flows (fusion_open_cloud_file) for team designs; this is for local .f3d/.step style files.", {"filePath": "required str (WINDOWS path)", "intent": "optional \"mechanical\" | \"electronics\" (see fusion_open_by_urn)"}, {"success": "bool"}, 240, None, True, {"filePath": "C:/Temp/part.f3d"}),     ("fusion_close_document", "Close ONE document WITHOUT the modal save dialog (save:false discards silently - the automation-safe close). Omit name to close the active document.", {"name": "optional str (default: active document)", "save": "optional bool default false"}, {"success": "bool", "closed": "str"}, 60, None, False, {"save": False}),     ("fusion_close_all_documents", "Close ALL open documents without save dialogs. DESTRUCTIVE to unsaved user work - confirm with the human before calling on a box you do not own. By default it SKIPS scratch 'Untitled' docs; pass closeUntitled:true to close the UNMODIFIED ones too (page issue #59: the library/bind flow left ~5 per import, 16 after three, and no caller could clear them), and force:true to also discard MODIFIED Untitled docs, which may hold real unsaved work.", {"save": "optional bool default false", "closeUntitled": "optional bool default false - also close UNMODIFIED Untitled scratch docs", "force": "optional bool default false - with closeUntitled, also discard MODIFIED Untitled docs"}, {"success": "bool", "closed": "[str]", "wouldPrompt": "[str]|null"}, 60, None, False, {"closeUntitled": True}),     ("fusion_reset_workspace", "Bring Fusion back to a clean slate between runs: closes named documents AND the Untitled scratch the library/bind flow leaves behind (page issue #59). Fusion always keeps ONE blank document alive, so a single remaining Untitled is correct rather than a failure. MODIFIED Untitled docs are left untouched unless force:true, because those can hold real unsaved work.", {"force": "optional bool default false - also discard MODIFIED Untitled docs"}, {"success": "bool", "closedByAddin": "list", "untitledScratchClosed": "int", "remaining": "list"}, 90, None, False, {}),@@ -257,7 +257,7 @@ _T = [     ("fusion_show_library", "Eight-surfaces contract: the LIBRARY surface. libraryPath (WINDOWS .lbr) opens it; name alone handles the already-open case (symmetric with kicad_show_library). Background; returns a screenshot path.", {"capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).", "libraryPath": "optional str (WINDOWS .lbr)", "name": "optional str (already-open library name)"}, {"success": "bool", "view": "str", "screenshot": "str"}, 240, None, True, {"libraryPath": "C:/Temp/PART.lbr"}),     ("fusion_show_project", "Eight-surfaces contract: bring the PROJECT/design to its top level. No args = activate this session's generated test project (fusion_make_test_project). query = cloud design name (already-open match wins, else fire-and-poll open). urn = open_by_urn passthrough. Background; returns a screenshot path.", {"capture": "optional bool - ONE call = ONE consent: the bridge settles the surface (waits for the add-in main thread + a paint beat) and returns proof inline as shot:{title,image,thumbnailDataUrl,maxWidth} (base64 frame + a ready-to-use data: URL for BROWSER callers who cannot pull a file; set captureMaxWidth to shrink it - 320 keeps a /command body small, default 640. The bridge encodes PNG; the relay may transcode to WebP, so sniff rather than assume). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).", "query": "optional str (cloud design name)", "urn": "optional str", "fileName": "alias of query"}, {"success": "bool", "view": "str", "document": "str", "screenshot": "str"}, 240, "fusion_get_app_state", True, {"query": "BQ25792"}),     ("fusion_set_window_sizing", "Read/toggle auto-sizing Fusion's WINDOW after launch (STICKY, default ON). Fusion opens at whatever geometry it last had, which after an automated launch is often a small default; the bridge grows it to ~92% of the monitor work area, centered, using SetWindowPos with NOACTIVATE (never ShowWindow/maximize) so focus is never stolen. A window the user maximized, or already sized to >=60% of the work area, is left alone. No args = read.", {"enabled": "optional bool (omit to read)"}, {"success": "bool", "sizeFusionWindow": "bool"}, 15, "fusion_state", False, {}),-    ("fusion_set_driving_badge", "Read/toggle the 'Adom is driving Fusion' TASKBAR OVERLAY BADGE (STICKY per user, default ON). While the bridge drives Fusion, its Windows taskbar button wears a small Adom mark so the user knows at a glance who is in control; it sheds automatically after 5 minutes of driving-verb idle. No args = read. Turning it off clears any badge immediately.", {"enabled": "optional bool (omit to read)"}, {"success": "bool", "showDrivingBadge": "bool"}, 15, "fusion_state", False, {}),+    ("fusion_set_driving_badge", "Read/toggle the 'Adom is driving Fusion' TASKBAR OVERLAY BADGE (STICKY per user, default ON). While the bridge drives Fusion, its Windows taskbar button wears a small Adom mark so the user knows at a glance who is in control; it sheds automatically after 5 minutes of driving-verb idle. No args = read. Turning it off clears any badge immediately.", {"enabled": "optional bool (omit to read)", "viewport": "optional str: \"native\" = only the frame Fusion itself renders (activeViewport.saveAsImageFile through the add-in), no window capture", "nativeViewport": "optional bool (default true with screenshot): also export the native viewport frame and count bodies; the reply carries nativeViewport {path, ok, bodies, productType}, geometry {bodies} (API truth, independent of pixels) and windowPixels {dominantColorFraction, blank} for the window capture. A blank window capture of a Design viewport is a known WGC artefact: trust nativeViewport, never foreground Fusion for a capture (issue #84)"}, {"success": "bool", "showDrivingBadge": "bool"}, 15, "fusion_state", False, {}),     ("fusion_resolve_seat_dialog", "Readiness EXECUTOR (Rust port, Phase 3): detect + resolve the Autodesk seat/licensing modal ('Active Sessions Exceeded' / 'Suspend Remote Session') in the background, at most 3 UIA clicks per call, screenshot-verified. Idempotent: nothing up answers clicks:0. fusion_readiness calls this itself when it sees the dialog; call it directly only when you are driving the launch by hand.", {"maxClicks": "optional int (default 3)"}, {"clicks": "int", "resolved": "bool"}, 60, None, False, {}),     ("fusion_apply_fusion_update", "Readiness EXECUTOR (Rust port, Phase 3): click Fusion's update offer IN THE BACKGROUND when auto-update is on (sticky pref; fusion_set_auto_update). Never the exit-class buttons while a host op is active. Answers {applied, button, why}.", {}, {"applied": "bool", "button": "str|null", "why": "str"}, 60, None, False, {}),     ("fusion_set_seat_takeover", "Read/toggle the mid-session Autodesk seat-takeover sweep (STICKY per user, default ON). When Autodesk pops 'Active Sessions Exceeded' mid-session, the bridge clicks Continue through BOTH dialogs in the background: this machine takes the seat, the other machine's Fusion suspends RESUMABLY, the user gets one toast. No args = read current value. Gates only the passive sweep - a verb-driven fusion_start still grabs the seat (the user asked for Fusion, so the seat is implied).", {"enabled": "optional bool (omit to read)"}, {"success": "bool", "autoTakeOverSeat": "bool"}, 15, "fusion_readiness", False, {}),
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.10.12"+DASH_VERSION="1.10.13" 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.10.12",+  "version": "1.10.13",   "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−27
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.10.12",+  "version": "1.10.13",   "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,28 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "agent_permissions": {-    "allow_bash": [],-    "allow": [-      "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",-      "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."-    ],-    "soft_deny": [-      "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",-      "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",-      "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",-      "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."-    ],-    "environment": [-      "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",-      "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."-    ]-  },-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.10.12"+version = "1.10.13" edition = "2021" license = "MIT" 
rust/bridge/src/main.rs+19
@@ -786,6 +786,25 @@ pub(crate) fn native_aps_search(args: &Value) -> Value {     results.sort_by(|a, b| b["lastModified"].as_str().unwrap_or("").cmp(a["lastModified"].as_str().unwrap_or("")));     let n = results.len();     let shown: Vec<Value> = results.into_iter().take(limit).collect();+    if n == 0 {+        // Zero from the server index is not "the file does not exist" (issue #85: a model the+        // index missed sat in the same folder as its sibling hit). Fall back to a name scan of+        // the recently modified items the bridge can list without a full walk.+        let ql = query.to_lowercase();+        let toks: Vec<&str> = ql.split_whitespace().collect();+        let recent = native_aps_recent(&json!({"limit": 400, "hubId": args["hubId"], "projectId": args["projectId"]}));+        let fb: Vec<Value> = recent["data"]["results"].as_array().cloned().unwrap_or_default().into_iter()+            .filter(|i| { let nm = i["name"].as_str().unwrap_or("").to_lowercase(); !toks.is_empty() && toks.iter().all(|t| nm.contains(t)) }).take(limit).collect();+        let nf = fb.len();+        let folder_hint = fb.first().and_then(|f| f["folderId"].as_str().or(f["parentFolderId"].as_str())).map(|f| format!(" The first fallback hit lives in folder {f}; fusion_aps_browse {{folderId}} lists that folder in full.")).unwrap_or_default();+        return json!({"success": true, "output": format!("0 index match(es) for '{query}'; {nf} by name in recent items"),+            "data": {"query": query, "count": 0, "results": [], "scanned": {"hubs": scanned.0, "projects": scanned.1, "folders": scanned.2},+                     "fallback": {"source": "recent-items name scan (newest 400 across the scanned projects)", "count": nf, "results": fb},+                     "completeness": "The APS server index can lag a new or renamed file by minutes to hours and tokenises names, so a zero here does not prove absence."},+            "shell": "rust",+            "_hint": format!("No index match for '{query}'.{} {}Next: fusion_aps_browse a known sibling's folder (scoped, no cloud walk), or pass projectId to narrow the scan. Do not conclude the file is missing from an index zero alone.",+                if nf > 0 { format!(" {nf} recently modified item(s) match by name (data.fallback); open one with fusion_aps_open or fusion_open_by_urn.") } else { " Nothing in the newest 400 items matches by name either.".to_string() }, folder_hint)});+    }     json!({"success": true, "output": format!("{n} match(es) for '{query}'"),            "data": {"query": query, "count": n, "results": shown, "scanned": {"hubs": scanned.0, "projects": scanned.1, "folders": scanned.2}}, "shell": "rust",            "_hint": format!("Found {n} match(es) via the APS server index across {} project(s), newest first. Pass projectId to scope to one project (faster). Use fusion_aps_open to open a hit in Fusion.", scanned.1)})
rust/bridge/src/orchestrations.rs+78−1
@@ -580,11 +580,32 @@ pub fn fusion_state(ctx: &Ctx, args: &Value) -> Value {         let app = ureq::post("http://127.0.0.1:8774/command").timeout(Duration::from_secs(5)).send_json(json!({"command": "get_app_state", "args": {}})).ok().and_then(|r| r.into_json::<Value>().ok());         if let Some(a) = app { result["activeDocument"] = a["data"]["activeDocument"].clone(); }     }-    if args["screenshot"].as_bool() != Some(false) {+    let want_native = args["nativeViewport"].as_bool() != Some(false) && args["screenshot"].as_bool() != Some(false);+    let native_only = args["viewport"].as_str() == Some("native");+    if want_native {+        // Issue #84: a WGC window capture of a GPU Design viewport can be blank or stale while+        // the document holds hundreds of bodies. Ask Fusion itself for the frame and the body+        // count, so geometry presence is reported separately from window pixels.+        let png_path = crate::fusionpath::stage_root(&sd).map(|r| r.join(format!("native-viewport-{}.png", (now_f() * 1000.0) as u64)).to_string_lossy().replace('\\', "/")).unwrap_or_else(|_| sd.join("native-viewport.png").to_string_lossy().replace('\\', "/"));+        let script = format!("import adsk.core, adsk.fusion\napp=adsk.core.Application.get(); res={{}}\nd=app.activeDocument; res['document']=d.name if d else None\np=app.activeProduct; res['productType']=p.productType if p else None\nacc=[0]\ndef _walk(c):\n    acc[0]+=c.bRepBodies.count\n    for i in range(c.occurrences.count): _walk(c.occurrences.item(i).component)\ntry:\n    ds=adsk.fusion.Design.cast(p)\n    if ds: _walk(ds.rootComponent); res['bodies']=acc[0]\nexcept Exception as e: res['bodiesError']=str(e)[:80]\nvp=app.activeViewport\nif vp:\n    try:\n        ok=vp.saveAsImageFile('{png_path}', 1600, 1000); res['path']='{png_path}' if ok else None; res['ok']=bool(ok)\n    except Exception as e: res['viewportError']=str(e)[:100]\nresult=res\n");+        let r = crate::addin::addin_http("run_modeling_script", &json!({"script": script}), 25);+        let nv = r["data"]["result"].clone();+        if nv.is_object() {+            result["nativeViewport"] = json!({"path": nv["path"], "ok": nv["ok"], "bodies": nv["bodies"], "productType": nv["productType"], "document": nv["document"], "capturedAt": now_f(), "source": "app.activeViewport.saveAsImageFile through the add-in (no foreground, no window pixels)"});+            result["geometry"] = json!({"bodies": nv["bodies"], "productType": nv["productType"], "source": "Fusion API body count, independent of any capture"});+        } else {+            result["nativeViewport"] = json!({"ok": false, "error": r["error"].as_str().unwrap_or("add-in did not answer").chars().take(160).collect::<String>()});+        }+    }+    if args["screenshot"].as_bool() != Some(false) && !native_only {         let shot = crate::small_verbs::screenshot_fusion(&json!({}), &ctx.caller);         if shot["success"].as_bool() == Some(true) {             let p = shot["savedTo"].as_str().unwrap_or("").to_string();             result["screenshot"] = json!(p);+            if let Some(frac) = png_dominant_fraction(&p) {+                result["windowPixels"] = json!({"dominantColorFraction": (frac * 1000.0).round() / 1000.0, "blank": frac >= 0.97,+                    "note": "fraction of the window capture that is one colour; above 0.97 the capture shows no geometry (a GPU Design viewport under WGC can render blank or stale while the model is fine)"});+            }             if args["imageAs"].as_str() == Some("base64") {                 if let Ok(raw) = std::fs::read(&p) {                     if raw.len() <= 4 * 1024 * 1024 { result["screenshotBase64"] = json!(crate::etiquette::b64_pub(&raw)); }@@ -596,6 +617,16 @@ pub fn fusion_state(ctx: &Ctx, args: &Value) -> Value {     let mut hints: Vec<String> = Vec::new();     if let Some(b) = &busy { hints.push(format!("Main thread busy ({}, {}s) - add-in verbs are rejected right now; window/screenshot verbs still work.", b["command"].as_str().unwrap_or("?"), b["elapsedSeconds"])); }     if !blocking.is_empty() { hints.push("A modal is up - see blockingDialogs for the classified cause + resolution.".into()); }+    if result["windowPixels"]["blank"].as_bool() == Some(true) {+        let bodies = result["geometry"]["bodies"].as_u64().unwrap_or(0);+        if result["nativeViewport"]["ok"].as_bool() == Some(true) {+            hints.push(format!("The window capture is blank (GPU Design viewport under WGC) while Fusion itself reports {bodies} bod{} and rendered nativeViewport.path: trust the native frame for geometry, never the window pixels, and do NOT foreground Fusion to fix a capture.", if bodies == 1 { "y" } else { "ies" }));+        } else {+            hints.push("The window capture is blank and the native viewport export did not answer; nothing here substantiates visible geometry. Re-call with screenshot:true once the add-in answers, or read geometry through fusion_inspect_bodies.".into());+        }+    } else if result["nativeViewport"]["ok"].as_bool() == Some(true) && result["nativeViewport"]["productType"].as_str() == Some("DesignProductType") {+        hints.push("nativeViewport.path is the frame Fusion itself rendered (activeViewport.saveAsImageFile); for a Design document prefer it over the window capture, which can lag the model.".into());+    }     if hints.is_empty() { hints.push("Fusion is clear. The screenshot shows the current surface - this is the verb to render any fusion_show_* result (they open in the background by design).".into()); }     result["_hint"] = json!(hints.join(" "));     result["output"] = json!(json!({"running": true, "activeDocument": result["activeDocument"], "mainThreadBusy": busy, "blockingDialogs": blocking, "screenshot": result["screenshot"]}).to_string());@@ -900,3 +931,49 @@ mod doc_name_tests {         assert!(!super::doc_matches("ADS858~1", "0603WAF3300T5E"));     } }++/// Fraction of a PNG's sampled pixels that share the dominant (quantised) colour. 1.0 is a+/// flat image; a Fusion window capture above ~0.97 shows chrome and nothing else (issue #84).+pub fn png_dominant_fraction(path: &str) -> Option<f64> {+    let f = std::fs::File::open(path).ok()?;+    let dec = png::Decoder::new(std::io::BufReader::new(f));+    let mut reader = dec.read_info().ok()?;+    let mut buf = vec![0u8; reader.output_buffer_size()];+    let info = reader.next_frame(&mut buf).ok()?;+    let bpp = match info.color_type { png::ColorType::Rgba => 4, png::ColorType::Rgb => 3, png::ColorType::GrayscaleAlpha => 2, png::ColorType::Grayscale => 1, _ => return None };+    let (w, h) = (info.width as usize, info.height as usize);+    if w == 0 || h == 0 { return None; }+    let step = ((w * h) / 20000).max(1);+    let mut counts: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();+    let mut total = 0u32;+    let mut i = 0usize;+    while i < w * h {+        let o = i * bpp;+        if o + bpp > buf.len() { break; }+        let (r, g, b) = if bpp >= 3 { (buf[o], buf[o + 1], buf[o + 2]) } else { (buf[o], buf[o], buf[o]) };+        let key = ((r as u32 >> 3) << 10) | ((g as u32 >> 3) << 5) | (b as u32 >> 3);+        *counts.entry(key).or_insert(0) += 1; total += 1;+        i += step;+    }+    let top = counts.values().copied().max().unwrap_or(0);+    if total == 0 { None } else { Some(top as f64 / total as f64) }+}++#[cfg(test)]+mod png_tests {+    #[test]+    fn flat_and_busy_images_are_told_apart() {+        let dir = std::env::temp_dir().join(format!("pngtest-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap();+        for (name, busy) in [("flat.png", false), ("busy.png", true)] {+            let p = dir.join(name); let f = std::fs::File::create(&p).unwrap();+            let mut enc = png::Encoder::new(std::io::BufWriter::new(f), 64, 64); enc.set_color(png::ColorType::Rgb); enc.set_depth(png::BitDepth::Eight);+            let mut w = enc.write_header().unwrap();+            let mut data = vec![255u8; 64 * 64 * 3];+            if busy { for (i, px) in data.chunks_mut(3).enumerate() { px[0] = (i % 251) as u8; px[1] = (i * 7 % 241) as u8; px[2] = (i * 13 % 239) as u8; } }+            w.write_image_data(&data).unwrap();+        }+        assert!(super::png_dominant_fraction(&dir.join("flat.png").to_string_lossy()).unwrap() > 0.99);+        assert!(super::png_dominant_fraction(&dir.join("busy.png").to_string_lossy()).unwrap() < 0.5);+        let _ = std::fs::remove_dir_all(&dir);+    }+}
tools/check-naming.sh

  
tools/check-portable.sh

  
tools/promote.sh

  
tools/release.sh