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.53: honest dormant states, multi-box pills, taskbar driving badge
16 files changed
+289−28
BRIDGE_VERSION+1−1CHANGELOG.md+18adom-bridge-fusion-manifest.json+4−4bin/fusion-dashboardbridge.json+1−1dashboard/Cargo.toml+1−1dashboard/src/main.rs+44−5dashboard/ui/index.html+34−6fusion_detect.py+26handlers/taskbar_overlay.py+119install.sh+1−1package.json+1−1page.json+11−8resources/adom-overlay.icoserver.py+26tools/release-files.txt+2BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.52+1.9.53
CHANGELOG.md+18@@ -3,6 +3,24 @@ 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.53 - 2026-08-17++- **Honest state detection (John: "you aren't detecting my state all that well").** Sign-in,+ add-in, and MCP are UNKNOWABLE while Fusion is closed - the bridge now persists last-verified+ observations (lastReadyAt / lastSignedInAt on readiness, lastEnabledAt on mcp_status) and the+ Dashboard paints those facets AMBER-dormant ("configured, resumes when Fusion starts") instead+ of broken-red. Red now means "actually needs setup".+- **Setup is clean for configured users**: dormant facets are excluded from the pending list, so+ a weeks-old setup shows "Everything is configured - Fusion just is not running" with Start.+- **Multi-box is first-class.** New /api/targets + target pills in the Dashboard header (running+ dot per box, sticky choice); every status read and action carries the chosen box. One-seat copy+ explains the workflow: start Fusion on the conference box, the seat follows automatically, the+ laptop stays undisturbed.+- **Taskbar badge: "Adom is driving Fusion" (John's overlay idea).** New handlers/taskbar_overlay.py+ (ITaskbarList3::SetOverlayIcon via ctypes, no deps) badges Fusion's taskbar button with the Adom+ mark while the background envelope is active - set on first envelope, cleared when the refcount+ drains. resources/adom-overlay.ico ships in the zip.+ ## 1.9.52 - 2026-08-16 - **The catalog now teaches the full foreground contract.** fusion_describe's foreground arg docs
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.52",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.52/adom-bridge-fusion-v1.9.52.zip",- "sha256": "eaa4e8f6383c10fdd90e007199eb1077d080e8371fdab5ff31b170cd58370c8a",- "size": 399141,+ "version": "1.9.53",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.53/adom-bridge-fusion-v1.9.53.zip",+ "sha256": "1c42f1d13bee5b915b0885bc53c87e637cf149daef80d6c096a86cd061545381",+ "size": 403956, "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.52",+ "version": "1.9.53", "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.46"+version = "1.9.53" edition = "2021" [dependencies]
dashboard/src/main.rs+44−5@@ -90,6 +90,12 @@ fn gather_status(ai_thread: &str, target: &str) -> serde_json::Value { let ready = rd["ready"].as_bool().unwrap_or(false); let needs_signin = rd["needsSignin"].as_bool().unwrap_or(false); let addin = rd["addinVersion"].as_str().unwrap_or("").to_string();+ // Configured-but-dormant beats broken-red (John, 2026-08-17): while Fusion is closed,+ // sign-in/add-in/MCP are UNKNOWABLE - paint them amber with the last verified time,+ // never red. Red is reserved for "actually needs setup".+ let last_signed = rd["lastSignedInAt"].as_str().unwrap_or("");+ let last_ready = rd["lastReadyAt"].as_str().unwrap_or("");+ let mcp_last = mcp["lastEnabledAt"].as_str().unwrap_or(""); let aps_ok = aps["signedIn"].as_bool().or(aps["configured"].as_bool()).unwrap_or(false) || aps["ready"].as_bool().unwrap_or(false); let mcp_on = mcp["enabled"].as_bool().unwrap_or(false);@@ -104,13 +110,22 @@ fn gather_status(ai_thread: &str, target: &str) -> serde_json::Value { "fusionRunning": {"on": running, "label": "Fusion running", "_hint": if running {"Fusion is up."} else {"fusion_start launches it in the BACKGROUND (never steals focus)."}}, "addinReady": {"on": ready, "label": "Bridge add-in", "version": addin,- "_hint": if ready {"Add-in serving - all fusion_* verbs live."} else {"Not serving. If needsSignin, run fusion_signin - it self-drives to the password step."}},+ "state": if ready {"on"} else if !running && !last_ready.is_empty() {"dormant"} else {"off"},+ "_hint": if ready {"Add-in serving - all fusion_* verbs live.".to_string()}+ else if !running && !last_ready.is_empty() {format!("Dormant - resumes when Fusion starts (last verified {}).", last_ready)}+ else {"Not serving. If needsSignin, run fusion_signin - it self-drives to the password step.".to_string()}}, "signedIn": {"on": running && !needs_signin, "label": "Autodesk sign-in",- "_hint": if needs_signin {"fusion_signin {email} self-drives the whole wall: launches, clicks, types the email, toasts the user ONLY at their password."} else {"Signed in."}},+ "state": if running && !needs_signin {"on"} else if !running && !last_signed.is_empty() {"dormant"} else {"off"},+ "_hint": if needs_signin {"fusion_signin {email} self-drives the whole wall: launches, clicks, types the email, toasts the user ONLY at their password.".to_string()}+ else if !running && !last_signed.is_empty() {format!("Signed in (verified {}). Unknowable while Fusion is closed - dormant, not broken.", last_signed)}+ else if running {"Signed in.".to_string()} else {"Unknown until Fusion runs.".to_string()}}, "aps": {"on": aps_ok, "label": "APS cloud search", "_hint": if aps_ok {"fusion_aps_search gives ~2s cloud lookups."} else {"Not configured: fusion_aps_set_client_id then fusion_aps_signin (silent when the browser session is warm)."}}, "mcp": {"on": mcp_on, "label": "Fusion MCP server",- "_hint": if mcp_on {"fusion_mcp_call drives Autodesk's own MCP tools."} else {"fusion_mcp_enable turns it on (~10s, drives the Preferences dialog with a caption)."}},+ "state": if mcp_on {"on"} else if !running && !mcp_last.is_empty() {"dormant"} else {"off"},+ "_hint": if mcp_on {"fusion_mcp_call drives Autodesk's own MCP tools.".to_string()}+ else if !running && !mcp_last.is_empty() {format!("Enabled in Fusion's preferences (verified {}) - resumes when Fusion starts.", mcp_last)}+ else {"fusion_mcp_enable turns it on (~10s, drives the Preferences dialog with a caption).".to_string()}}, "datahub": {"on": aps_ok, "label": "Data Hub API", "_hint": "Data Hub rides the APS auth: file listings, versions, urn resolution via fusion_aps_get."}, "update": {"on": !upd_offered || upd_handled, "label": "Fusion up to date",@@ -234,6 +249,8 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) { let method = it.next().unwrap_or("").to_uppercase(); let path_full = it.next().unwrap_or("/").to_string(); let path = path_full.split('?').next().unwrap_or("/").to_string();+ let qs_target = path_full.split('?').nth(1).unwrap_or("").split('&')+ .find_map(|kv| kv.strip_prefix("target=").map(|v| v.replace("%20", " "))); let mut content_len = 0usize; loop { let mut h = String::new();@@ -296,8 +313,30 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) { } } }+ ("GET", "/api/targets") => {+ // Multi-box support (John, 2026-08-17): he runs Fusion on a conference box so+ // the laptop stays undisturbed - the dashboard must treat that as first-class.+ let out = Command::new("adom-bridge").arg("targets").output();+ let mut rows = Vec::new();+ if let Ok(o) = out {+ if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&o.stdout) {+ for t in v["targets"].as_array().unwrap_or(&Vec::new()) {+ let name = t["name"].as_str().unwrap_or("").to_string();+ let rd = unwrap_data(&bridge(&app.ai_thread, &name, "fusion_readiness", "{}"));+ rows.push(serde_json::json!({+ "name": name,+ "running": rd["running"].as_bool().unwrap_or(false),+ "ready": rd["ready"].as_bool().unwrap_or(false),+ "lastReadyAt": rd["lastReadyAt"],+ }));+ }+ }+ }+ respond_json(&mut stream, &serde_json::json!({"targets": rows,+ "_hint": "One Autodesk seat: start Fusion on whichever box suits the moment - the seat follows automatically (the other box's Fusion suspends resumably). Pass ?target=<name> on /api/status and target in /api/action bodies."}));+ } ("GET", "/api/status") | ("GET", "/state") => {- let t = app.tgt();+ let t = qs_target.clone().unwrap_or_else(|| app.tgt()); app.log(format!("status target={}", t)); let st = gather_status(&app.ai_thread, &t); app.hub.broadcast("state", &st);@@ -313,7 +352,7 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) { } ("POST", "/api/action") => { let name = body["action"].as_str().unwrap_or("").to_string();- let t = app.tgt();+ let t = body["target"].as_str().map(|s| s.to_string()).unwrap_or_else(|| app.tgt()); app.log(format!("action {} target={}", name, t)); let out = action(&app.ai_thread, &t, &name, &body["args"]); respond_json(&mut stream, &out);
dashboard/ui/index.html+34−6@@ -65,6 +65,12 @@ dialog::backdrop{background:rgba(0,0,0,.55)} #appinfo.show{display:block} #appinfo a{color:var(--accent)} .brand{cursor:default}+#targets{display:flex;gap:8px;margin:0 0 14px}+.tpill{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:999px;border:1px solid var(--border);background:var(--surface);color:var(--text-body);cursor:pointer;font:500 13.5px 'Satoshi',sans-serif}+.tpill.active{border-color:var(--accent);color:var(--text);background:var(--elevated)}+.tdot{width:8px;height:8px;border-radius:999px;background:var(--text-2)}+.tdot.on{background:var(--green)}+.seatnote{font-size:12.5px;color:var(--text-2);margin:-6px 0 12px} </style> </head> <body>@@ -83,6 +89,8 @@ dialog::backdrop{background:rgba(0,0,0,.55)} </header> <main> <h2>Status</h2>+ <div id="targets"></div>+ <div class="seatnote" id="seatnote" style="display:none">One Autodesk seat, many boxes: start Fusion wherever suits the moment - the seat follows automatically and the other box's Fusion suspends resumably.</div> <div class="grid" id="ledGrid"></div> <div class="btnrow"> <button class="btn primary" id="btnSetup">Setup</button>@@ -149,9 +157,28 @@ const consoleBuf=[]; window.onerror=(m)=>{consoleBuf.push("onerror: "+m);}; function toast(msg,kind){const t=document.createElement('div');t.className='toast '+(kind||'');t.textContent=msg;$('#toasts').appendChild(t);setTimeout(()=>t.remove(),kind==='err'?9000:4000);} async function api(path,opts){const r=await fetch(path,opts);return r.json();}-async function act(action,args){return api('api/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,args:args||{}})});}+async function act(action,args){return api('api/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,target:curTarget||undefined,args:args||{}})});} let lastStatus=null;+let curTarget=localStorage.getItem('fdTarget')||'';+let targetsCache=[];+function statusUrl(){return 'api/status'+(curTarget?('?target='+encodeURIComponent(curTarget)):'');}+async function loadTargets(){+ try{+ const d=await api('api/targets');+ targetsCache=d.targets||[];+ const el=$('#targets');el.innerHTML='';+ if(targetsCache.length>1){$('#seatnote').style.display='';}+ if(!curTarget&&targetsCache.length){curTarget=(targetsCache.find(t=>t.running)||targetsCache[0]).name;localStorage.setItem('fdTarget',curTarget);}+ for(const t of targetsCache){+ const b=document.createElement('button');b.className='tpill'+(t.name===curTarget?' active':'');+ b.setAttribute('data-tip',(t.running?'Fusion is running here':'Fusion is closed here')+(t.lastReadyAt?(' - last ready '+t.lastReadyAt):''));+ b.innerHTML='<span class="tdot'+(t.running?' on':'')+'"></span>'+t.name;+ b.onclick=()=>{curTarget=t.name;localStorage.setItem('fdTarget',curTarget);loadTargets();refresh();toast('switched to '+t.name)};+ el.appendChild(b);+ }+ }catch(e){}+} function renderLeds(st){ const g=$('#ledGrid');g.innerHTML=''; const leds=st.leds||{};@@ -159,7 +186,7 @@ function renderLeds(st){ const keys=ORDER.filter(k=>leds[k]).concat(Object.keys(leds).filter(k=>!ORDER.includes(k))); for(const key of keys){ const l=leds[key];- const cls=l.pending?'warn':(l.on?'on':'off');+ const cls=l.pending?'warn':(l.state==='dormant'?'warn':(l.on?'on':'off')); const el=document.createElement('div');el.className='card'; el.innerHTML=`<div class="led-row"><span class="led ${cls}"></span><span class="led-label">${l.label}</span></div><div class="led-sub">${l._hint||''}</div>`; g.appendChild(el);@@ -167,7 +194,7 @@ function renderLeds(st){ $('#subject').textContent=st.target?('target: '+st.target):'no bridge target'; $('#meta').textContent='bridge '+(st.bridgeVersion||'?')+' | dashboard '+(st.dashboardVersion||''); }-async function refresh(){try{lastStatus=await api('api/status');renderLeds(lastStatus);}catch(e){toast('status failed: '+e,'err');}}+async function refresh(){try{lastStatus=await api(statusUrl());renderLeds(lastStatus);}catch(e){toast('status failed: '+e,'err');}} $('#btnRefresh').onclick=()=>{toast('refreshing status');refresh();}; $('#btnStart').onclick=async()=>{toast('starting Fusion in the background');await act('start');setTimeout(refresh,4000);}; $('#btnStop').onclick=async()=>{toast('closing Fusion');await act('stop');setTimeout(refresh,4000);};@@ -214,10 +241,11 @@ const SETUP_ORDER=[ {led:'aps',action:'aps_signin',label:'Set up APS cloud search'}, {led:'mcp',action:'mcp_enable',label:'Enable the Fusion MCP server'}, ];-function setupPending(){if(!lastStatus)return[];return SETUP_ORDER.filter(s=>{const l=lastStatus.leds[s.led];return l&&!l.on;});}+function setupPending(){if(!lastStatus)return[];return SETUP_ORDER.filter(s=>{const l=lastStatus.leds[s.led];return l&&!l.on&&l.state!=='dormant';});} function renderSetup(){ const p=setupPending();- $('#setupBody').innerHTML=p.length?('Remaining steps:<br>'+p.map((s,i)=>`${i+1}. ${s.label}`).join('<br>')):'Everything is configured. You are ready to go.';+ const dormant=lastStatus&&Object.values(lastStatus.leds).some(l=>l.state==='dormant');+ $('#setupBody').innerHTML=p.length?('Remaining steps:<br>'+p.map((s,i)=>`${i+1}. ${s.label}`).join('<br>')):(dormant?'Everything is configured - Fusion just is not running on '+(curTarget||'this box')+'. Start Fusion (or start it on another box; the Autodesk seat follows automatically).':'Everything is configured. You are ready to go.'); $('#btnSetupNext').style.display=p.length?'':'none'; } $('#btnSetup').onclick=async()=>{await refresh();renderSetup();$('#dlgSetup').showModal();};@@ -296,7 +324,7 @@ brand.addEventListener('mouseleave',()=>{clearTimeout(aiShow);aiHide=setTimeout( ai.addEventListener('mouseenter',()=>clearTimeout(aiHide)); ai.addEventListener('mouseleave',()=>{aiHide=setTimeout(()=>ai.classList.remove('show'),220);}); -refresh();setInterval(refresh,15000);+loadTargets().then(refresh);setInterval(refresh,15000);setInterval(loadTargets,60000); </script> </body> </html>
fusion_detect.py+26@@ -732,6 +732,17 @@ _ENVELOPE_COUNT = 0 _ENVELOPE_SAVED = {} +def _fusion_main_hwnd_quick():+ """Largest titled family window's hwnd, or None - cheap, for the taskbar badge."""+ try:+ wins = family_windows() or []+ if not wins:+ return None+ return max(wins, key=lambda w: w.get("width", 0) * w.get("height", 0)).get("hwnd")+ except Exception:+ return None++ def begin_background_envelope(seconds: float, minimize: bool = False): """Guard the next `seconds` against Fusion-family focus theft. Fire-and-forget.""" global _ENVELOPE_COUNT@@ -752,6 +763,16 @@ def begin_background_envelope(seconds: float, minimize: bool = False): _ENVELOPE_SAVED["lsfw"] = bool(ctypes.windll.user32.LockSetForegroundWindow(1)) except Exception: _ENVELOPE_SAVED["lsfw"] = False+ # Taskbar badge (John, 2026-08-17): while Adom drives Fusion, its taskbar+ # button wears the Adom mark - the user can glance down and know WHY the+ # app is busy. Set on first envelope, cleared when the refcount drains.+ try:+ from handlers.taskbar_overlay import set_driving_badge+ hw = _fusion_main_hwnd_quick()+ if hw:+ set_driving_badge(hw)+ except Exception:+ pass threading.Thread(target=_envelope_watch, args=(seconds, minimize), daemon=True).start() except Exception: pass@@ -842,6 +863,11 @@ def _envelope_watch(seconds: float, minimize: bool): ctypes.windll.user32.LockSetForegroundWindow(2) except Exception: pass+ try:+ from handlers.taskbar_overlay import clear_driving_badge+ clear_driving_badge()+ except Exception:+ pass def _focus_guard(seconds: float = 150.0, restore_fg_lock=None, restore_min_anim=None):
handlers/taskbar_overlay.pyadded+119@@ -0,0 +1,119 @@+"""Taskbar overlay badge: "Adom is driving Fusion" (John, 2026-08-17).++pup badges its own Chrome taskbar buttons to show state; the same signal belongs on+FUSION's taskbar button whenever this bridge is driving it, so the user can glance at+the taskbar and know why Fusion is busy. Windows exposes exactly this as+ITaskbarList3::SetOverlayIcon - callable cross-process with just an hwnd, no window+ownership needed. ctypes COM, no dependencies.++Wired to the background envelope's refcount: first envelope (driving starts) badges the+Fusion main window with resources/adom-overlay.ico ("Adom is driving Fusion"); when the+refcount drains to zero the badge clears. Best-effort everywhere - a missing icon, a+COM failure, or a dead hwnd must never break a verb.+"""++import ctypes+import os+import threading++_LOCK = threading.Lock()+_HICON = None+_BADGED_HWND = None++_CLSID_TaskbarList = "{56FDF344-FD6D-11D0-958A-006097C9A090}"+_IID_ITaskbarList3 = "{EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF}"+# ITaskbarList3 vtable: IUnknown 0-2 | HrInit 3, AddTab 4, DeleteTab 5, ActivateTab 6,+# SetActiveAlt 7 | MarkFullscreenWindow 8 | SetProgressValue 9, SetProgressState 10,+# RegisterTab 11, UnregisterTab 12, SetTabOrder 13, SetTabActive 14, ThumbBarAddButtons 15,+# ThumbBarUpdateButtons 16, ThumbBarSetImageList 17, SetOverlayIcon 18, ...+_VTBL_HRINIT = 3+_VTBL_SETOVERLAYICON = 18+++class _GUID(ctypes.Structure):+ _fields_ = [("Data1", ctypes.c_ulong), ("Data2", ctypes.c_ushort),+ ("Data3", ctypes.c_ushort), ("Data4", ctypes.c_ubyte * 8)]+++def _guid(s: str) -> "_GUID":+ g = _GUID()+ ctypes.oledll.ole32.CLSIDFromString(ctypes.c_wchar_p(s), ctypes.byref(g))+ return g+++def _icon_path() -> str:+ return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),+ "resources", "adom-overlay.ico")+++def _load_icon():+ global _HICON+ if _HICON:+ return _HICON+ p = _icon_path()+ if not os.path.isfile(p):+ return None+ IMAGE_ICON, LR_LOADFROMFILE = 1, 0x0010+ h = ctypes.windll.user32.LoadImageW(None, p, IMAGE_ICON, 32, 32, LR_LOADFROMFILE)+ _HICON = h or None+ return _HICON+++def _taskbar_call(hwnd, hicon, description):+ """CoCreate ITaskbarList3 on THIS thread and call SetOverlayIcon once."""+ ole32 = ctypes.oledll.ole32+ ole32.CoInitialize(None)+ try:+ clsid = _guid(_CLSID_TaskbarList)+ iid = _guid(_IID_ITaskbarList3)+ ptr = ctypes.c_void_p()+ CLSCTX_INPROC_SERVER = 1+ ole32.CoCreateInstance(ctypes.byref(clsid), None, CLSCTX_INPROC_SERVER,+ ctypes.byref(iid), ctypes.byref(ptr))+ vtbl = ctypes.cast(ptr, ctypes.POINTER(ctypes.POINTER(ctypes.c_void_p))).contents+ proto_init = ctypes.WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p)+ proto_overlay = ctypes.WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p,+ ctypes.c_void_p, ctypes.c_void_p, ctypes.c_wchar_p)+ proto_release = ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)+ hr_init = proto_init(vtbl[_VTBL_HRINIT])(ptr)+ hr = proto_overlay(vtbl[_VTBL_SETOVERLAYICON])(+ ptr, ctypes.c_void_p(hwnd), ctypes.c_void_p(hicon or 0),+ description if description else None)+ proto_release(vtbl[2])(ptr)+ return hr == 0 and hr_init == 0+ finally:+ ole32.CoUninitialize()+++def set_driving_badge(hwnd) -> bool:+ """Badge the window's taskbar button with the Adom mark ('Adom is driving Fusion')."""+ global _BADGED_HWND+ try:+ if not hwnd:+ return False+ with _LOCK:+ hicon = _load_icon()+ if not hicon:+ return False+ ok = _taskbar_call(int(hwnd), hicon, "Adom is driving Fusion")+ if ok:+ _BADGED_HWND = int(hwnd)+ return bool(ok)+ except Exception:+ return False+++def clear_driving_badge(hwnd=None) -> bool:+ """Remove the badge (from the given hwnd, or whichever window we badged last)."""+ global _BADGED_HWND+ try:+ with _LOCK:+ target = int(hwnd) if hwnd else _BADGED_HWND+ if not target:+ return False+ ok = _taskbar_call(target, None, None)+ if ok and (hwnd is None or _BADGED_HWND == int(hwnd)):+ _BADGED_HWND = None+ return bool(ok)+ except Exception:+ return 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.46"+DASH_VERSION="1.9.53" 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.46",+ "version": "1.9.53", "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.46",+ "version": "1.9.53", "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]"- }+ ] }
resources/adom-overlay.icoadded⋯ 1 unchanged line ⋯
server.py+26@@ -1529,6 +1529,22 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict: if needs_signin: result["needsSignin"] = True result["statusVerb"] = "fusion_readiness"+ # LAST-KNOWN state (John, 2026-08-17: "shouldn't i be fully setup already other than+ # fusion running?"): sign-in/add-in state is UNKNOWABLE while Fusion is closed, and a+ # dashboard that paints "unknown" as broken-red misreads a fully-configured box. So+ # persist the good observations and always report them: a closed Fusion with+ # lastSignedInAt set = "configured, dormant", not "needs setup".+ try:+ import datetime as _dt+ _now_iso = _dt.datetime.now().isoformat(timespec="seconds")+ if ready and running:+ _set_bridge_pref("lastReadyAt", _now_iso)+ if running and not needs_signin and ready:+ _set_bridge_pref("lastSignedInAt", _now_iso)+ result["lastReadyAt"] = _get_bridge_pref("lastReadyAt", None)+ result["lastSignedInAt"] = _get_bridge_pref("lastSignedInAt", None)+ except Exception:+ pass if licensing: result["licensingDialog"] = True result["statusVerb"] = "fusion_readiness"@@ -4627,7 +4643,12 @@ def _handle_mcp_status(fusion_info: dict, args: dict) -> dict: """Is Autodesk's Fusion MCP server up, and what does it expose?""" s = _mcp_new_session() if s.get("error"):+ # Enabled-ness is a persistent Fusion PREFERENCE, but it is only observable while+ # Fusion runs. Report the last GOOD observation so a closed Fusion reads as+ # "configured, dormant" instead of broken (John, 2026-08-17). return {"success": True, "enabled": False, "url": _MCP_URL, "error": s.get("error"),+ "lastEnabledAt": _get_bridge_pref("mcpLastEnabledAt", None),+ "fusionRunning": _is_fusion_running(), "_hint": _mcp_off_hint(), "statusVerb": "fusion_mcp_status"} tools = [] tl = _mcp_rpc("tools/list")@@ -4635,6 +4656,11 @@ def _handle_mcp_status(fusion_info: dict, args: dict) -> dict: tools = [{"name": t.get("name"), "description": (t.get("description") or "").split("\n")[0][:160]} for t in ((tl.get("result") or {}).get("tools") or [])]+ try:+ import datetime as _dt+ _set_bridge_pref("mcpLastEnabledAt", _dt.datetime.now().isoformat(timespec="seconds"))+ except Exception:+ pass return {"success": True, "enabled": True, "url": _MCP_URL, "sessionId": s.get("sessionId"), "serverInfo": s.get("serverInfo"), "toolCount": len(tools), "tools": tools,
tools/release-files.txt+2@@ -53,3 +53,5 @@ resources/samples/ADS8588SIPM.step server.py start.bat uninstall.sh+handlers/taskbar_overlay.py+resources/adom-overlay.ico