← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.45\ No newline at end of file+1.9.46\ No newline at end of file
CHANGELOG.md+9
@@ -3,6 +3,15 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.9.46 - 2026-08-16++- **fusion_set_seat_takeover: the seat-takeover sweep is now a STICKY setting** (default ON,+  mirrors auto-update; no-args = read). Gates only the passive 20s sweep - a verb-driven+  fusion_start still grabs the seat, since asking for Fusion implies wanting the seat.+- **Dashboard Settings panel now carries BOTH sticky defaults**: silent Fusion auto-update and+  the seat auto-takeover, each toggleable with live toasts. One lockstep version everywhere+  again (bridge zip + dashboard binary + pkg all 1.9.46).+ ## 1.9.45 - 2026-08-16  - **Seat conflicts: ALWAYS take over the session, even mid-session (John's call).** The proven
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.45",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.45/adom-bridge-fusion-v1.9.45.zip",-    "sha256": "1dbdcec7d90f98044b8a9595c3a7c6b5581f0c30bc4ea9a3886d577e958dee69",-    "size": 391583,+    "version": "1.9.46",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.46/adom-bridge-fusion-v1.9.46.zip",+    "sha256": "3f37b7c7b4ed81065f6a44fbf40131c5d736639f334d502ce6ffa2aa8c48d5af",+    "size": 392587,     "verbPrefixes": [         "fusion_"     ],
bin/fusion-dashboard
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.45",+  "version": "1.9.46",   "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.44"+version = "1.9.46" edition = "2021"  [dependencies]
dashboard/src/main.rs+5−2
@@ -84,6 +84,7 @@ fn gather_status(ai_thread: &str, target: &str) -> serde_json::Value {     let mcp = unwrap_data(&bridge(ai_thread, target, "fusion_mcp_status", "{}"));     // No-args form is a pure READ of the sticky pref (verified in server.py).     let auto_upd = unwrap_data(&bridge(ai_thread, target, "fusion_set_auto_update", "{}"));+    let seat = unwrap_data(&bridge(ai_thread, target, "fusion_set_seat_takeover", "{}"));      let running = rd["fusionRunning"].as_bool().or(rd["running"].as_bool()).unwrap_or(false);     let ready = rd["ready"].as_bool().unwrap_or(false);@@ -117,7 +118,8 @@ fn gather_status(ai_thread: &str, target: &str) -> serde_json::Value {                 "_hint": if upd_offered && !upd_handled {"An update is WAITING. Auto-update normally clicks it silently; offer to run it now."} else {"No pending update (auto-update clicks nags silently by default; sticky opt-out via fusion_set_auto_update)."}},         },         "settings": {-            "autoUpdateFusion": auto_upd["autoUpdateFusion"].as_bool().unwrap_or(true)+            "autoUpdateFusion": auto_upd["autoUpdateFusion"].as_bool().unwrap_or(true),+            "autoTakeOverSeat": seat["autoTakeOverSeat"].as_bool().unwrap_or(true)         },         "_hint": "This is the Dashboard status payload. Every LED carries its own _hint naming the verb that fixes it. POST /api/action {action, args} to act; GET /api/status to re-read; subscribe to /events (SSE) for live pushes; POST /ui/toast {message,kind} to narrate on top of the app."     })@@ -132,11 +134,12 @@ fn action(ai_thread: &str, target: &str, name: &str, args: &serde_json::Value) -         "tour" => bridge(ai_thread, target, "fusion_demo", "{\"mode\":\"tour\"}"),         "tour_progress" => bridge(ai_thread, target, "fusion_demo_progress", &a),         "set_auto_update" => bridge(ai_thread, target, "fusion_set_auto_update", &a),+        "set_seat_takeover" => bridge(ai_thread, target, "fusion_set_seat_takeover", &a),         "mcp_enable" => bridge(ai_thread, target, "fusion_mcp_enable", "{}"),         "aps_signin" => bridge(ai_thread, target, "fusion_aps_signin", "{}"),         "import_basic_part" => import_basic_part(ai_thread, target, args),         _ => serde_json::json!({"error": format!("unknown action {}", name),-            "_hint": "Actions: start, stop, signin, tour, tour_progress, set_auto_update, mcp_enable, aps_signin, import_basic_part."}),+            "_hint": "Actions: start, stop, signin, tour, tour_progress, set_auto_update, set_seat_takeover, mcp_enable, aps_signin, import_basic_part."}),     } } 
dashboard/ui/index.html+15−5
@@ -122,6 +122,11 @@ dialog::backdrop{background:rgba(0,0,0,.55)}     <div class="led-sub">Silently clicks Fusion's update nag in the background so you always run the latest build. Sticky.</div></div>     <div class="toggle" id="tglAutoUpdate" role="switch" aria-checked="true"></div>   </div>+  <div class="setrow">+    <div><div class="led-label">Auto take-over of the Fusion seat</div>+    <div class="led-sub">When Autodesk pops "Active Sessions Exceeded", click through BOTH dialogs in the background: this machine takes the seat, the other machine's Fusion suspends resumably, you get one toast. Sticky.</div></div>+    <div class="toggle" id="tglSeatTakeover" role="switch" aria-checked="true"></div>+  </div>   <div class="btnrow" style="justify-content:flex-end"><button class="btn" id="btnCloseSettings">Close</button></div> </dialog> @@ -188,14 +193,19 @@ $('#btnImportPart').onclick=async()=>{ };  // Settings-$('#btnSettings').onclick=()=>{const t=$('#tglAutoUpdate');const on=(lastStatus&&lastStatus.settings&&lastStatus.settings.autoUpdateFusion)!==false;t.classList.toggle('is-on',on);t.setAttribute('aria-checked',String(on));$('#dlgSettings').showModal();};+$('#btnSettings').onclick=()=>{const s=(lastStatus&&lastStatus.settings)||{};+  const t=$('#tglAutoUpdate');const on=s.autoUpdateFusion!==false;t.classList.toggle('is-on',on);t.setAttribute('aria-checked',String(on));+  const t2=$('#tglSeatTakeover');const on2=s.autoTakeOverSeat!==false;t2.classList.toggle('is-on',on2);t2.setAttribute('aria-checked',String(on2));+  $('#dlgSettings').showModal();}; $('#btnCloseSettings').onclick=()=>$('#dlgSettings').close();-$('#tglAutoUpdate').onclick=async(e)=>{+function wireToggle(sel,actionName,label){$(sel).onclick=async(e)=>{   const t=e.currentTarget;const now=!t.classList.contains('is-on');   t.classList.toggle('is-on',now);t.setAttribute('aria-checked',String(now));-  await act('set_auto_update',{enabled:now});-  toast('auto-update '+(now?'on':'off')+' (sticky)','ok');-};+  await act(actionName,{enabled:now});+  toast(label+' '+(now?'on':'off')+' (sticky)','ok');+};}+wireToggle('#tglAutoUpdate','set_auto_update','auto-update');+wireToggle('#tglSeatTakeover','set_seat_takeover','seat take-over');  // Setup: walk the not-green facets in order, one action per click. const SETUP_ORDER=[
describe.py+1
@@ -207,6 +207,7 @@ _T = [     ("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_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, {}),     ("fusion_state", "The RENDER-ME verb (kicad_state parity, issues #28/#30): running + window + dialog + busy state PLUS a fresh background screenshot of whatever Fusion is showing - the one verb that makes every background fusion_show_* result visible. SERVER-SIDE ONLY by design: it answers even while the add-in main thread is busy or wedged (activeDocument is best-effort with a short budget and skipped when busy). blockingDialogs carries the CLASSIFIED modal list (cause + resolution). imageAs:base64 inlines the PNG (auto-refused over 4MB - pull the screenshot path via pull_file instead).", {"screenshot": "optional bool (default true)", "imageAs": "optional str path|base64 (default path)"}, {"success": "bool", "running": "bool", "activeDocument": "str", "mainThreadBusy": "dict|null", "blockingDialogs": "list", "screenshot": "str path", "screenshotBase64": "str (imageAs:base64 only)"}, 60, None, False, {}),     ("fusion_export_iges", "Export to IGES.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/board.iges"}),     ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}),
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.44"+DASH_VERSION="1.9.46" 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.44",+  "version": "1.9.46",   "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.44",+  "version": "1.9.46",   "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+41
@@ -4986,6 +4986,45 @@ def _signin_goal_met(kind) -> bool: _last_seat_takeover_notice = [0.0]  +def _seat_takeover_enabled() -> bool:+    """Default TRUE (John: "always just take over the session"). Sticky opt-out for users+    who'd rather resolve seat conflicts themselves - mirrors autoUpdateFusion."""+    return bool(_get_bridge_pref("autoTakeOverSeat", True))+++def _handle_set_seat_takeover(fusion_info: dict, args: dict) -> dict:+    """Read/toggle the mid-session seat-takeover sweep. STICKY per user. Scope note: this+    gates only the PASSIVE 20s sweep; a verb-driven fusion_start still grabs the seat+    (the user asked for Fusion, so taking the seat is implied - John's July rule)."""+    if "enabled" not in (args or {}):+        cur = _seat_takeover_enabled()+        return {"success": True, "autoTakeOverSeat": cur,+                "_hint": ("Seat takeover is currently %s. ON BY DEFAULT: when Autodesk pops the "+                          "'Active Sessions Exceeded' dialogs mid-session, the bridge clicks "+                          "Continue through BOTH in the background - this machine takes the seat, "+                          "the other machine's Fusion suspends RESUMABLY, one toast tells the user. "+                          "Opt out: fusion_set_seat_takeover {\"enabled\": false} - STICKY."+                          % ("ON" if cur else "OFF")),+                "statusVerb": "fusion_readiness"}+    enabled = bool(args.get("enabled"))+    _set_bridge_pref("autoTakeOverSeat", enabled)+    return {"success": True, "autoTakeOverSeat": enabled,+            "narrate": ("I'll automatically take the Fusion seat on this machine whenever "+                        "Autodesk flags the session limit." if enabled else+                        "I'll leave Autodesk seat-conflict dialogs for you to resolve yourself."),+            "_hint": ("Sticky (~/.adom/fusion-bridge/prefs.json). %s"+                      % ("Takeover ON: mid-session seat dialogs are clicked through silently."+                         if enabled else+                         "Takeover OFF: the sweep will NOT touch seat dialogs; fusion_start "+                         "still grabs the seat when the user asks for Fusion.")),+            "statusVerb": "fusion_readiness"}+++# Registered HERE, next to the def - naming it in the COMMAND_HANDLERS literal (built ~3000+# lines earlier) is the import-time NameError that crash-looped the bridge on 2026-07-22.+COMMAND_HANDLERS["set_seat_takeover"] = _handle_set_seat_takeover++ def _seat_sweep_tick():     """ALWAYS take over the Autodesk seat (John, 2026-08-16: "always just take over the     session"). The launch/readiness paths already auto-resolve the 'Active Sessions@@ -4997,6 +5036,8 @@ def _seat_sweep_tick():     takes the seat; the other machine's Fusion suspends resumably, nothing is lost).     Toasts the user AFTER a real takeover so they know the other box got suspended."""     try:+        if not _seat_takeover_enabled():+            return         if not find_licensing_dialog():             return         r = _resolve_seat_dialog(max_clicks=3)
tests/run_verb_tests.py+2−1
@@ -118,6 +118,7 @@ t("fusion_aps_versions", "weekly", {"query": "BQ25792"}, {"ok": True, "fields": t("fusion_aps_download", "weekly", {"query": "BQ25792", "saveDir": SCRATCH}, {"ok": True}) t("fusion_aps_get", "daily", {"path": "/project/v1/hubs"}, {"ok": True}) t("fusion_uninstall", "daily", {"dryRun": True}, {"ok": True, "fields": ["removed", "skipped", "dryRun"]})+t("fusion_set_seat_takeover", "daily", {}, {"ok": True, "fields": ["autoTakeOverSeat"]}, "no-args form is a pure pref read") # APS mutations: only into the AI workspace, weekly, with cleanup t("fusion_aps_create_folder", "manual", note="needs projectId/parentFolderId of the test workspace") t("fusion_aps_upload", "manual", note="cloud write; exercised via build flows weekly")@@ -300,7 +301,7 @@ _SECTION_BY_VERB = {     "fusion_web_export": "exports",     "fusion_load_design_rules": "exports", "fusion_set_design_rules": "exports",     "fusion_readiness": "bridge-lifecycle", "fusion_describe": "bridge-lifecycle",-    "fusion_uninstall": "bridge-lifecycle", "fusion_set_auto_update": "bridge-lifecycle",+    "fusion_uninstall": "bridge-lifecycle", "fusion_set_seat_takeover": "bridge-lifecycle", "fusion_set_auto_update": "bridge-lifecycle", }  def call_tour(target, verb, spec_args):