app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
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.
← Commit history
v1.9.46: seat-takeover sticky setting + both toggles in Dashboard Settings (lockstep)
14 files changed
+93−25
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.45+1.9.46
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]"- }+ ] }
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):