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
Update 13 file(s)
11 files changed
+158−19
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.177+1.9.178
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.177",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.177/adom-bridge-fusion-v1.9.177.zip",- "sha256": "d1836a63ecac8945e8b8a66af1c31e0eedc42295da0da1c06a74166be69c469d",- "size": 484686,+ "version": "1.9.178",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.178/adom-bridge-fusion-v1.9.178.zip",+ "sha256": "9e6b0481950310332991ee3d1c60ee1a98d095c96b74c0a55264e35008e9d8be",+ "size": 484687, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.177",+ "version": "1.9.178", "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.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.9.177"+version = "1.9.178" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.177"+version = "1.9.178" edition = "2021" [dependencies]
dashboard/src/main.rs+65@@ -1205,6 +1205,19 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) { "delivered": app.hub.client_count(), "_hint": "delivered = live SSE viewers that showed it. 0 means nobody has the page open - open it first."})); }+ ("POST", "/api/say-try") => {+ // "Say it" strip Try-it (dock-bar `say` contract, John 2026-08-23: teach users to+ // ASK for the app by its noun). Stages the phrase into this app's own AI thread+ // via the Hydrogen control API - submit:false, so the USER presses Enter; we never+ // run a prompt on their behalf. Reachable on 127.0.0.1:47084 from WSL2 (mirrored+ // networking); in a cloud container it is not, and the UI falls back to copy.+ let phrase = body["phrase"].as_str().unwrap_or("").to_string();+ if phrase.is_empty() {+ respond_json(&mut stream, &serde_json::json!({"ok": false, "why": "phrase required"}));+ } else {+ respond_json(&mut stream, &stage_say_phrase(&phrase));+ }+ } ("POST", "/ui/click") => { // CLI-driven front-end click: the page clicks its own control (same path a user takes). let target = body["target"].as_str().unwrap_or("").to_string();@@ -1248,6 +1261,58 @@ fn url_for(port: u16) -> String { .unwrap_or_else(|| format!("http://127.0.0.1:{}/", port)) } +/// Stage a "say it" phrase into this app's AI thread via the Hydrogen control API+/// (127.0.0.1:47084). Thread found by its `app:adom/fusion-bridge` tag; run-prompt is+/// called with submit:false so the phrase only lands in the input box - the user submits.+/// Every failure is a graceful {ok:false, why} the UI turns into a copy fallback.+fn stage_say_phrase(phrase: &str) -> serde_json::Value {+ let hy = "http://127.0.0.1:47084";+ // 1. find the thread sid by app tag+ let out = Command::new("curl")+ .args(["-s", "-m", "2",+ &format!("{}/ai-threads/history?tag=app:adom/fusion-bridge", hy)])+ .output();+ let hist = match out {+ Ok(o) if !o.stdout.is_empty() => String::from_utf8_lossy(&o.stdout).to_string(),+ _ => return serde_json::json!({"ok": false, "why": "Hydrogen control API unreachable (not WSL2/mirrored networking?)"}),+ };+ let v: serde_json::Value = match serde_json::from_str(&hist) {+ Ok(v) => v,+ Err(_) => return serde_json::json!({"ok": false, "why": "control API answered non-JSON"}),+ };+ // The sid may sit at several depths depending on the endpoint's shape - take the first+ // "sid" string found anywhere (same envelope-drift defense as the bridge's _find_controls_any).+ fn first_sid(v: &serde_json::Value) -> Option<String> {+ match v {+ serde_json::Value::Object(m) => {+ if let Some(s) = m.get("sid").and_then(|s| s.as_str()) {+ return Some(s.to_string());+ }+ m.values().find_map(first_sid)+ }+ serde_json::Value::Array(a) => a.iter().find_map(first_sid),+ _ => None,+ }+ }+ let sid = match first_sid(&v) {+ Some(s) => s,+ None => return serde_json::json!({"ok": false, "why": "no AI thread tagged app:adom/fusion-bridge yet - open the app from the dock once"}),+ };+ // 2. stage the phrase, never submit+ let payload = serde_json::json!({"sid": sid, "prompt": phrase, "submit": false}).to_string();+ let out = Command::new("curl")+ .args(["-s", "-m", "3", "-X", "POST", "-H", "Content-Type: application/json",+ "-d", &payload, &format!("{}/ai-threads/run-prompt", hy)])+ .output();+ match out {+ Ok(o) if o.status.success() && !o.stdout.is_empty() => {+ serde_json::json!({"ok": true, "staged": true, "sid": sid,+ "_hint": "phrase staged in the app's AI thread input - the user presses Enter to run it"})+ }+ _ => serde_json::json!({"ok": false, "why": "run-prompt call failed"}),+ }+}+ /// Probe 8930-8940 for an already-serving FUSION dashboard. "dashboardVersion" alone is /// not enough: the KiCad dashboard also serves it (found live on 8931 - the first probe /// draft would have handed the Fusion dock card KiCad's URL). Require a facet only OUR
dashboard/ui/index.html+63@@ -37,6 +37,24 @@ header{height:44px;display:flex;align-items:center;gap:12px;padding:0 16px;backg .iconbtn:hover{background:var(--elevated);border-color:var(--border)} .iconbtn:active{transform:scale(.9);background:var(--overlay)} main{max-width:1080px;margin:0 auto;padding:20px 16px 60px}+/* Say-it strip: the FIRST thing on the page - teaches asking for the app by its noun */+.saystrip{background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;padding:14px 16px;margin-bottom:22px}+.saytitle{font-family:'Familjen Grotesk',sans-serif;font-size:15px;font-weight:600;color:var(--accent)}+.saydesc{font-size:12.5px;color:var(--text-2);margin:4px 0 10px;line-height:1.5}+.saydesc b{color:var(--text)}+.saytry-hint{color:var(--accent)}+.saychips{display:flex;flex-wrap:wrap;gap:8px}+.saychip{display:inline-flex;align-items:center;gap:0;background:var(--elevated);border:1px solid var(--border);border-radius:999px;overflow:hidden;transition:border-color .15s ease}+.saychip:hover{border-color:var(--accent)}+.saychip .phrase{padding:6px 10px 6px 14px;font-size:13px;color:var(--text-body);cursor:pointer;background:none;border:none;font-family:'Satoshi',sans-serif;transition:color .15s ease}+.saychip .phrase:hover{color:var(--text)}+.saychip .phrase:active{transform:translateY(1px)}+.saychip .phrase::before{content:'\201C';color:var(--text-2)}+.saychip .phrase::after{content:'\201D';color:var(--text-2)}+.saychip .try{padding:6px 12px 6px 8px;font-size:11.5px;font-weight:600;color:var(--accent);cursor:pointer;background:none;border:none;border-left:1px solid var(--border);font-family:'Familjen Grotesk',sans-serif;transition:background .15s ease}+.saychip .try:hover{background:var(--overlay)}+.saychip .try:active{transform:translateY(1px)}+.saychip .phrase:focus-visible,.saychip .try:focus-visible{outline:2px solid var(--accent);outline-offset:-2px} h2{font-family:'Familjen Grotesk',sans-serif;font-size:17px;color:var(--text);margin:26px 0 12px} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px} .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px}@@ -208,6 +226,15 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0} </div> </header> <main>+ <!-- SAY IT (dock-bar `say` contract, John 2026-08-23): teach the ASK before the status.+ "for fusion I might say 'open the step in fusion'... those should all be taught first+ and foremost in those dashboards, possibly even at the top of it." -->+ <div class="saystrip" id="saystrip">+ <div class="saytitle">Say it</div>+ <div class="saydesc">You never need this page's buttons: just tell your AI what you want, using the word <b>fusion</b>. Click a phrase to copy it; <span class="saytry-hint">Try</span> types it into this app's AI thread for you (you still press Enter).</div>+ <div class="saychips" id="sayChips"></div>+ </div>+ <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>@@ -351,6 +378,42 @@ async function act(action,args){return api('api/action',{method:'POST',headers:{ let lastStatus=null; let curTarget=localStorage.getItem('fdTarget')||'';++// SAY IT strip (dock-bar `say` contract): keep this list in lockstep with dockbar.json's+// `say` array - the dock interstitial and this strip must teach the same phrases.+const SAY_PHRASES=[+ 'open the step in fusion',+ 'export this board to fusion',+ 'build the parts library in fusion',+ 'sign me into fusion',+ 'search my fusion cloud files for the amp board',+ 'export the fusion design as STEP',+];+function copyText(t){+ // clipboard API needs a secure context; webviews sometimes lack it - textarea fallback.+ if(navigator.clipboard&&navigator.clipboard.writeText)return navigator.clipboard.writeText(t).catch(()=>copyTextLegacy(t));+ return Promise.resolve(copyTextLegacy(t));+}+function copyTextLegacy(t){const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();try{document.execCommand('copy');}catch(e){}ta.remove();}+(function renderSayChips(){+ const host=$('#sayChips');+ SAY_PHRASES.forEach(p=>{+ const chip=document.createElement('div');chip.className='saychip';+ const btn=document.createElement('button');btn.className='phrase';btn.textContent=p;+ btn.setAttribute('data-tip','Copy this phrase - paste it to any AI thread');+ btn.onclick=async()=>{await copyText(p);toast('Copied - paste it to your AI','ok');};+ const tr=document.createElement('button');tr.className='try';tr.textContent='Try';+ tr.setAttribute('data-tip',"Type this phrase into the app's AI thread for you - you press Enter to run it");+ tr.onclick=async()=>{+ try{+ const r=await api('api/say-try',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({phrase:p})});+ if(r.ok){toast('Typed into your AI thread - press Enter there to run it','ok');return;}+ await copyText(p);toast('Could not reach your AI thread ('+(r.why||'unreachable')+') - copied instead','err');+ }catch(e){await copyText(p);toast('Copied - paste it to your AI (thread not reachable)','err');}+ };+ chip.appendChild(btn);chip.appendChild(tr);host.appendChild(chip);+ });+})(); let targetsCache=[]; function statusUrl(){return 'api/status'+(curTarget?('?target='+encodeURIComponent(curTarget)):'');} async function loadTargets(){
dockbar.json+8@@ -4,6 +4,14 @@ "title": "Fusion Dashboard", "favicon": "docs/img/dock-fusion.png" },+ "say": [+ "open the step in fusion",+ "export this board to fusion",+ "build the parts library in fusion",+ "sign me into fusion",+ "search my fusion cloud files for the amp board",+ "export the fusion design as STEP"+ ], "launch": { "mode": "container_command", "console": "show",
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.177"+DASH_VERSION="1.9.178" 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.177",+ "version": "1.9.178", "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+12−9@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.9.177",+ "version": "1.9.178", "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",@@ -51,7 +55,7 @@ "skills/pcb-stackup/**", "skills/fusion-demo/**", "skills/fusion-multiprofile-signin/**",- "dock.json",+ "dockbar.json", "docs/img/dock-fusion.svg", "skills/fusion-dashboard/**", "skills/fusion-dark-mode/**",@@ -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,9 +111,5 @@ "label": "Part volume", "prompt": "How much material does each printed part in this assembly use?" }- ],- "author": {- "name": "John Lauer",- "email": "[email protected]"- }-}+ ]+}