← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.81+1.9.82
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.81",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.93/adom-bridge-fusion-v1.9.81.zip",-    "sha256": "00661fd66efb0b3d033982d2d8cedbe5350f4a1dba22fc7270ae6c261ee8eeb2",-    "size": 420956,+    "version": "1.9.82",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.94/adom-bridge-fusion-v1.9.82.zip",+    "sha256": "e772a4d701edbc358114b5d85a89afd19921f3e05a9e2699e63ad4dcfffcc717",+    "size": 422153,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.81",+  "version": "1.9.82",   "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.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.93"+version = "1.9.94" edition = "2021"  [dependencies]
describe.py+8−8
@@ -121,11 +121,11 @@ _T = [      # ── Electronics ────────────────────────────────────────────────────────     ("fusion_show_schematic", "Switch the OPEN electronics design to its SCHEMATIC view (the symmetric partner of fusion_show_2d_board / fusion_show_3d_board, via Fusion's SwitchSchDocCmd). A VIEW of the project, not a standalone file. Drive it for demos with fusion_electron_zoom / _pan / _select.",-     {}, {"success": "bool", "activeWorkspace": "str", "productType": "str"}, 70, None, True, {}),+     {"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} (~640px base64 PNG). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)."}, {"success": "bool", "activeWorkspace": "str", "productType": "str"}, 70, None, True, {}),     ("fusion_show_2d_board", "Switch the OPEN electronics design to its 2D PCB Editor (board layout) view. This is a VIEW of the project, not a file to open standalone - if the board is empty, you opened a child/derivative instead of the EcadDesignProductType PROJECT; reopen the project. Drive the view with fusion_electron_run WINDOW commands.",-     {}, {"activeWorkspace": "str"}, 70, None, True, {}),+     {"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} (~640px base64 PNG). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)."}, {"activeWorkspace": "str"}, 70, None, True, {}),     ("fusion_show_3d_board", "Switch the OPEN electronics design to its 3D PCB view (populated board). A VIEW of the project, not a standalone file. Orbit it for demos via fusion_run_modeling_script (app.activeViewport.camera + vp.refresh()).",-     {}, {"activeWorkspace": "str", "screenshots": "[...]"}, 70, None, True, {}),+     {"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} (~640px base64 PNG). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30)."}, {"activeWorkspace": "str", "screenshots": "[...]"}, 70, None, True, {}),     ("fusion_open_by_urn_note", "(see fusion_open_by_urn)", {}, {}, 30, None, False, {}),     ("fusion_board_info", "Raw board geometry XML (copper/arcs/lines) for the open board.",      {}, {"rawXml": "str"}, 60, None, False, {}),@@ -197,16 +197,16 @@ _T = [     ("fusion_export_skp", "Export to SketchUp (.skp).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.skp"}),      # ── Library SHOW verbs + bare-artifact wrapping (issue #29, shipped 1.9.9) ──-    ("fusion_show_symbol", "Open a schematic SYMBOL in Fusion's library editor - first-class, no hand-built EDIT strings. Pass name + libraryPath (.lbr, opened first; omit libraryPath if a library is already open), OR pass a BARE <symbol> XML fragment as symbolXml and the bridge generates a minimal wrapper library around it (deviceset + gate synthesized) so it shows in the Library Manager. Background-safe: never steals the foreground.", {"name": "str (deviceset/symbol name; optional if symbolXml carries name=)", "libraryPath": "optional str (WINDOWS path .lbr)", "symbolXml": "optional str (bare <symbol ...>...</symbol>)", "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch", "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),-    ("fusion_show_footprint", "Open a FOOTPRINT (package) in Fusion's library editor. Same contract as fusion_show_symbol: name + libraryPath, or a bare <package> fragment as packageXml (wrapper library generated).", {"name": "str", "libraryPath": "optional str", "packageXml": "optional str (bare <package ...>...</package>)", "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch", "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),-    ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)", "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch", "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}),+    ("fusion_show_symbol", "Open a schematic SYMBOL in Fusion's library editor - first-class, no hand-built EDIT strings. Pass name + libraryPath (.lbr, opened first; omit libraryPath if a library is already open), OR pass a BARE <symbol> XML fragment as symbolXml and the bridge generates a minimal wrapper library around it (deviceset + gate synthesized) so it shows in the Library Manager. Background-safe: never steals the foreground.", {"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} (~640px base64 PNG). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).", "name": "str (deviceset/symbol name; optional if symbolXml carries name=)", "libraryPath": "optional str (WINDOWS path .lbr)", "symbolXml": "optional str (bare <symbol ...>...</symbol>)", "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch", "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),+    ("fusion_show_footprint", "Open a FOOTPRINT (package) in Fusion's library editor. Same contract as fusion_show_symbol: name + libraryPath, or a bare <package> fragment as packageXml (wrapper library generated).", {"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} (~640px base64 PNG). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).", "name": "str", "libraryPath": "optional str", "packageXml": "optional str (bare <package ...>...</package>)", "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch", "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),+    ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"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} (~640px base64 PNG). Use this instead of polling fusion_state after a show - page-side settle polling costs one human consent per poll (issue #30).", "name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)", "foreground": "optional bool - DEFAULT IS BACKGROUND (no focus steal; response carries windowEtiquette:background). true REQUIRES foregroundReason and shows it to the user as a 3s ab caption before any focus change - only when the USER asked to watch", "foregroundReason": "required WITH foreground:true - a short user-facing sentence shown as the 3s caption (e.g. 'You asked to watch the 3D board build')"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}),     ("fusion_demo_progress", "Poll a running fusion_demo tour: {percent, elapsedSec, estimatedSec, step, stepLabel, done, failed, shots, shotCount, stepShot}. Poll ~1-2.5s and render a REAL progress bar. LIVE EVIDENCE (issue #30): each frame MAY carry stepShot {step, title, image} - a ~640px base64 capture the BRIDGE took at that step's completion, one per step, riding the first frame after it completes (render these during the run; the page should not guess-snap). shots[] streams too: mid-run it carries paths for every completed step; pass imagesAs:base64 + sinceShot:<count you have> to receive only NEW shots fully encoded. done:true = the full gallery (base64 by default). Finished tours are DISK-SNAPSHOTTED, so the final frame stays pollable even across a bridge respawn.", {"demoId": "required str (from fusion_demo mode:tour; omitted = the single running tour)", "imagesAs": "optional str paths|base64|both", "sinceShot": "optional int - shots you already received; response windows to the new ones"}, {"percent": "int", "step": "str", "stepLabel": "str", "done": "bool", "shots": "list", "shotCount": "int", "stepShot": "dict|absent"}, 15, None, False, {"demoId": "tour-abc123", "imagesAs": "base64", "sinceShot": 3}),      ("fusion_make_test_project", "Create a NEW electronics test project FROM SCRATCH around a library part - the Web Control demo's PROJECT button, and the answer to 'a customer only has a component page'. Generates a minimal test circuit server-side (the part + a generated 330R series resistor with VCC/GND stubs on a small board) as real EAGLE .sch/.brd, then imports the pair as a new electronics design. NO cloud, NO sign-in, no pre-existing content. After it, fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board all work on the result. Wiring uses the deviceset's own connects (first two pins), so 2-terminal parts get a real circuit and big ICs get a structurally-valid integration proof.", {"libraryPath": "required str (WINDOWS path .lbr; stage from container first)", "deviceset": "optional str (defaults to the library's first deviceset)"}, {"success": "bool", "schPath": "str", "brdPath": "str", "circuit": "str"}, 300, None, True, {"libraryPath": "C:/Temp/IN-S42ATR.lbr"}),     ("fusion_screenshot_fusion", "Background PrintWindow capture of Fusion's MAIN window (largest same-title window; works occluded/bottom-of-z-order, never raises anything) or a specific dialog by hwnd. Returns savedTo (PNG/WebP on the box; pull with pull_file). (Live since early versions but undescribed until 2026-08-15 - it was invisible to callers AND to the drift check.)", {"hwnd": "optional int (a dialog; default the main window)"}, {"success": "bool", "savedTo": "str", "sizeKB": "int"}, 60, None, False, {}),     ("fusion_bind_library_3d", "The CORRECT library in ONE call (the Library button's real bar: the 3D chip renders in the Content Manager preview): mints the part's 3D as a cloud f3d (make_3d_package), injects the wip_urn binding into the .lbr ON DISK (packages3d + package3dinstances), and reopens the library. Needs a signed-in Fusion session - the one cloud-dependent step in the wiki-to-EE circle. Autodesk's thumbnail preview lags ~2-3 min behind a fresh mint. NOTE: distinct from the older fusion_build_library_3d (multi-part batch pipeline).", {"libraryPath": "required str (WINDOWS .lbr path)", "stepPath": "required str (WINDOWS .step path)", "deviceset": "optional str (defaults to the library's first)"}, {"success": "bool", "wip_urn": "str", "bound": "bool", "reopened": "bool"}, 480, None, True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),-    ("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.", {"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.", {"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_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} (~640px base64 PNG). 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} (~640px base64 PNG). 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_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.9.93"+DASH_VERSION="1.9.94" 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.93",+  "version": "1.9.94",   "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,16 +1,20 @@ {   "slug": "fusion-bridge",-  "version": "1.9.93",+  "version": "1.9.94",   "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": {-    "adom/adom-desktop": "*"+    "adom/adom-desktop": "^1.9.0"   },   "scripts": {     "install": "./install.sh",     "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+63
@@ -8178,6 +8178,13 @@ def _assert_active_document(expect: str) -> dict | None:     return None  +# The EIGHT SURFACES show verbs. `capture:true` is honoured on exactly these: the caller+# asked to be SHOWN something, so proof belongs on the same response (page issue #30).+_SHOW_SURFACE_VERBS = {+    "show_symbol", "show_footprint", "show_3d_package", "show_library",+    "show_project", "show_schematic", "show_2d_board", "show_3d_board",+}+ _WINDOW_RAISING = {     "open_cloud_file", "open_by_urn", "aps_open", "open_design", "open_lbr",     "open_schematic", "open_board", "import_electronics", "import_step",@@ -8191,6 +8198,45 @@ _WINDOW_RAISING = { }  ++def _capture_surface_shot(verb: str, args: dict) -> dict:+    """Settle, then capture the surface - the bridge's half of issue #30's capture:true.++    SETTLE IS OURS, NOT THE PAGE'S. We opened the window, so we know what to wait for:+    the add-in main thread going idle (the surface is still drawing while it is busy),+    then a short paint pause. A page cannot see either, which is why it ends up polling.+    Returns ab's gallery item shape: {title, image}, image being a ~640px base64 PNG.+    """+    import time as _t+    label = {+        "show_symbol": "Symbol", "show_footprint": "Footprint",+        "show_3d_package": "3D package", "show_library": "Library",+        "show_project": "Project", "show_schematic": "Schematic",+        "show_2d_board": "2D board", "show_3d_board": "3D board",+    }.get(verb, verb)+    deadline = _t.time() + float(args.get("captureTimeoutSeconds", 120))+    while _t.time() < deadline:+        try:+            st = _proxy_to_addin("get_app_state", {}, timeout=6)+            if st.get("success"):+                break+        except Exception:+            pass+        _t.sleep(3)+    # The canvas paints a beat after the main thread frees up; capturing on the same tick+    # produced blank-canvas evidence on ab's regression thread.+    _t.sleep(float(args.get("capturePaintDelay", 2.5)))+    try:+        shot = screenshot_fusion_window()+    except Exception as e:+        return {"title": label, "image": None, "error": str(e)[:160]}+    path = shot.get("savedTo") or shot.get("localSafePath")+    if not shot.get("success") or not path:+        return {"title": label, "image": None,+                "error": shot.get("error") or "capture failed"}+    return {"title": label, "image": _shot_b64_small(path), "path": path}++ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> dict:     """Thin wrapper: run the verb, then guarantee the windowEtiquette annotation lands on     EVERY backgrounded window-raising result - the inner dispatcher's closure only covered@@ -8199,6 +8245,23 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) ->     Idempotent with the inner annotation (the note is only appended once)."""     _fg = bool((args or {}).get("foreground"))     res = _dispatch_command_inner(command, args, caller_identity)+    # ── capture:true - ONE call = ONE consent = surface opened AND proven (issue #30) ──+    # ab measured NINE human consent prompts for a single Symbol button under an+    # "Allow ONCE" web-control grant: the show verb, then eight state-verb polls while the+    # page guessed at when the surface had settled. Page-side settle polling is+    # structurally wrong when every cross-origin call costs a consent, and the bridge is+    # the only party that actually KNOWS when the surface is ready - it opened it. So the+    # bridge owns settle + proof, and returns the frame inline.+    try:+        _cmd = command[7:] if command.startswith("fusion_") else command+        if (_cmd in _SHOW_SURFACE_VERBS and bool((args or {}).get("capture"))+                and isinstance(res, dict) and res.get("success") is not False):+            res["shot"] = _capture_surface_shot(_cmd, args or {})+    except Exception as _e:+        try:+            res["shot"] = {"title": command, "image": None, "error": str(_e)[:160]}+        except Exception:+            pass     try:         cmd = command[7:] if command.startswith("fusion_") else command         if cmd in _WINDOW_RAISING and not _fg and isinstance(res, dict):