← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.201\ No newline at end of file+1.9.202\ No newline at end of file
adom-bridge-fusion-manifest.json+3−3
@@ -1,9 +1,9 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.201",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.201/adom-bridge-fusion-v1.9.201.zip",-  "sha256": "2f4f3efa81d1cfb1712a101157e489d98a7fa965cef5fb039c401f3078402e7c",+  "version": "1.9.202",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.202/adom-bridge-fusion-v1.9.202.zip",+  "sha256": "eba8083cf16b8a6001c6c4a7cf8f9424ae3c78d9fc2ff738bf873391a0bbf11c",   "size": 495989,   "verbPrefixes": [     "fusion_"
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.201",+  "version": "1.9.202",   "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.201"+version = "1.9.202" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.201"+version = "1.9.202" edition = "2021"  [dependencies]
dashboard/src/main.rs+50
@@ -42,6 +42,13 @@ const FAVICON_PNG: &[u8] = include_bytes!("../ui/fusion-icon.png"); /// The app mark (icons contract, 2026-08-24): ONE line-drawing SVG for the tab favicon, /// the header mark, the dock card and the wiki page. Never Autodesk's filled badge. const FAVICON_SVG: &[u8] = include_bytes!("../../docs/img/dock-fusion.svg");+/// The SHARED newbie panel (adom-ui-design 2.0.0), vendored VERBATIM. Never reformat it:+/// it builds its noun-highlight with RegExp() rather than a literal precisely because a+/// page assembled as a template literal turns \b into a BACKSPACE and eats the word+/// boundaries silently - the regex still matches, just too greedily, so nothing looks+/// broken. Seven apps hand-rolled this panel and produced four paddings, two border+/// colours and three Copy drawings; this file is the fix for that class of drift.+const NEWBIE_PANEL_JS: &[u8] = include_bytes!("../ui/vendor/newbie-panel.js");  fn ok(msg: &str) {     println!("OK: {}", msg);@@ -1105,6 +1112,49 @@ fn handle_conn(app: &Arc<App>, mut stream: TcpStream) {         ("GET", "/") | ("GET", "/index.html") => {             respond_nostore(&mut stream, "text/html; charset=utf-8", UI_HTML.as_bytes());         }+        ("GET", "/newbie-panel.js") => {+            respond(&mut stream, "200 OK", "application/javascript; charset=utf-8", NEWBIE_PANEL_JS);+        }+        // SAME-ORIGIN PROXY for the module's two calls. The module defaults its apiBase to+        // '' and fetches these paths on the page's own origin, which is right: the page+        // cannot know the control API's dynamic port, and the server already resolves it.+        ("GET", "/updater/status") => {+            match control_api_base() {+                Some(base) => {+                    let out = Command::new("curl")+                        .args(["-s", "-m", "8", &format!("{}/updater/status", base)]).output();+                    match out {+                        Ok(o) if !o.stdout.is_empty() =>+                            respond(&mut stream, "200 OK", "application/json", &o.stdout),+                        _ => respond_json(&mut stream, &serde_json::json!({"ok": false,+                            "why": "control API did not answer /updater/status"})),+                    }+                }+                None => respond_json(&mut stream, &serde_json::json!({"ok": false,+                    "why": "workspace control API not found"})),+            }+        }+        ("POST", "/ai-threads/run-prompt") => {+            match control_api_base() {+                Some(base) => {+                    // 120s: opening drives the editor and CDP touches serialize through one+                    // gate, so calls QUEUE rather than fail. A short timeout here would+                    // manufacture the phantom failure whose retry double-sends.+                    let out = Command::new("curl")+                        .args(["-s", "-m", "120", "-X", "POST", "-H", "Content-Type: application/json",+                               "-d", &body.to_string(), &format!("{}/ai-threads/run-prompt", base)])+                        .output();+                    match out {+                        Ok(o) if !o.stdout.is_empty() =>+                            respond(&mut stream, "200 OK", "application/json", &o.stdout),+                        _ => respond_json(&mut stream, &serde_json::json!({"ok": false,+                            "why": "control API did not answer run-prompt within 120s"})),+                    }+                }+                None => respond_json(&mut stream, &serde_json::json!({"ok": false,+                    "why": "workspace control API not found"})),+            }+        }         ("GET", "/favicon.svg") => {             respond(&mut stream, "200 OK", "image/svg+xml", FAVICON_SVG);         }
dashboard/ui/index.html+34−109
@@ -54,32 +54,6 @@ 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-strip contract (adom-ui-design 1.9): ONE strip fleet-wide */-.say{background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--teal);border-radius:10px;padding:14px 16px;margin-bottom:18px;position:relative}-.say h2{font:600 15px var(--sans);margin:0;color:var(--text)}-.say-head p{color:var(--muted);font-size:13.5px;margin:4px 0 0}-.say-head p .noun{color:var(--teal)}-.say-x{position:absolute;top:8px;right:10px;background:none;border:none;color:var(--muted);font-size:18px;cursor:pointer;padding:2px 6px;border-radius:6px}-.say-x:hover{color:var(--text);background:var(--elevated)}-.say-chips{margin-top:6px}-.say-chip{display:inline-flex;align-items:center;gap:8px;background:var(--surface2);border:1px solid var(--line);border-radius:999px;padding:5px 6px 5px 12px;font-size:13.5px;margin:4px 6px 0 0;cursor:pointer;color:var(--text-body)}-.say-chip em{color:var(--teal);font-style:normal}-.say-chip .copy,.say-chip .send{font:500 11px var(--body);background:none;border:1px solid var(--line);border-radius:999px;padding:2px 9px;cursor:pointer}-.say-chip .copy{color:var(--muted)}-.say-chip .send{color:var(--teal);border-color:rgba(0,184,177,.45)}-.say-chip:hover .copy{color:var(--teal);border-color:var(--teal)}-.say-chip .send:hover{background:rgba(0,184,177,.12)}-/* dash-demo player toolbar: a remote control, not a feature */-#tourTransport{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:9500;display:flex;align-items:center;gap:6px;background:var(--overlay);border:1px solid var(--line);border-radius:999px;padding:6px 12px;box-shadow:0 8px 24px rgba(0,0,0,.45)}-.tt{background:none;border:none;color:var(--text);font-size:15px;cursor:pointer;padding:4px 8px;border-radius:8px;transition:background .15s ease,transform .06s ease}-.tt:hover{background:var(--elevated)}-.tt:active{transform:scale(.92)}-.tt.muted{color:var(--muted)}-.tt-exit{color:var(--red);font-size:12.5px}-.tt-sep{width:1px;height:16px;background:var(--line)}-.tt-step{font:12px var(--mono);color:var(--muted);padding:0 2px}-.say-folded{font-size:13px;color:var(--muted);cursor:pointer}-.say-folded:hover{color:var(--teal)} 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}@@ -277,13 +251,10 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0}   <!-- 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." -->-  <section class="say" id="saystrip">-    <div class="say-head"><h2>Say it</h2>-      <p>You never need this page's buttons: just tell your AI what you want, using the word <b class="noun">fusion</b>. Copy a phrase, or send it straight to a new AI thread.</p>-      <button class="say-x" id="sayDismiss" aria-label="Collapse" data-tip="Fold this - the ? in the header brings it back">&times;</button></div>-    <div class="say-chips" id="sayChips"></div>-    <div class="say-folded" id="sayFolded" style="display:none">Say it - <span id="sayCount">6</span> phrases</div>-  </section>+  <!-- SHARED newbie panel (adom-ui-design 2.0.0). The module owns the card, columns,+       table, both icons, clipboard + send paths, toasts, tooltips, X and ? toggle.+       We own only: our phrases, our noun, and what the demo button does. -->+  <div id="newbie"></div>    <h2>Status</h2>   <div id="targets"></div>@@ -296,15 +267,10 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0}     <button class="btn" id="btnStop" data-tip="Close Fusion gracefully on the selected box (documents are closed cleanly first)">Close Fusion</button>   </div> -  <div id="demosFolded" style="display:none;font-size:13px;color:var(--muted);cursor:pointer;margin:14px 0" data-tip="Tap to show the demos once (the header ? restores everything)">Demos - guided tour and part import</div>-  <div id="demosRegion">   <h2>Demos</h2>   <div class="card">     <div class="led-label">Guided tour</div>-    <div class="led-sub">Walks every surface on a sample part: library, symbol, footprint, 3D chip, a generated solo board in 2D and 3D. Runs entirely in the background.</div>-    <div class="btnrow" style="align-items:center">-      <button class="btn primary" id="btnTour" data-tip="Walk every layer of the bridge - library, symbol, footprint, the 3D chip, Data Hub, APS search, MCP, a generated board in 2D and 3D, and real exports - narrated, captioned, with a live progress bar">Play the demo</button>-    </div>+    <div class="led-sub">Walks every surface on a sample part: library, symbol, footprint, 3D chip, a generated solo board in 2D and 3D. Runs entirely in the background. Start it from <b>Play the demo</b> at the top of the page - the shared panel owns that button, so there is exactly one of it.</div>     <div class="barlabel" id="tourCaption" style="display:none;margin-top:8px;color:var(--text);font-size:13px;line-height:1.5"></div>     <div class="bar" id="tourBar"><div></div></div>     <div class="barlabel" id="tourLabel"></div>@@ -336,7 +302,6 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0}     <div class="led-sub">Coming next: pulls a molecule page and opens its model in Fusion. Ask your AI: "open the molecule from wiki page owner/slug in Fusion".</div>   </div> -  </div><!-- /demosRegion -->    <h2>Surfaces: every content type, one real part</h2>   <div class="seatnote" id="surfnote">Loading the demo part...</div>@@ -345,6 +310,22 @@ dialog.settings{max-width:560px;overflow:hidden;padding:0}   <h2>Ask your AI</h2>   <div id="prompts"></div> </main>+<script src="newbie-panel.js"></script>+<script>+  // OUR WHOLE SURFACE: the phrases, the trigger noun, and what the demo button does.+  // Everything visual - card, columns, table, icons, clipboard, send path, toasts,+  // tooltips, X, ? toggle - belongs to the module, on purpose.+  AdomNewbiePanel.mount({+    mount: '#newbie',+    app: 'fusion',+    appName: 'Fusion Dashboard',+    slug: 'fusion-bridge',          // dock:<wiki page slug>, never the dock card id+    toggleButton: '#btnRecall',+    demo: { label: 'Play the demo', meta: 'the whole bridge, about 10 minutes',+            onPlay: function () { startFusionDemo(); } },+    phrases: SAY_PHRASES+  });+</script>  <dialog id="dlgSettings" class="settings" tabindex="-1" aria-label="Fusion Dashboard Settings">   <div class="dlghead">@@ -527,71 +508,12 @@ function copyText(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(){-  // say-strip contract: TWO buttons per chip - Copy (ghost, fleet toast) and Send to AI-  // (teal outline, NEW thread via POST /api/say-send; on any failure copy + error toast-  // so the user is never stranded). Clicking the phrase itself also copies.-  const host=$('#sayChips');-  $('#sayCount').textContent=SAY_PHRASES.length;-  const noun=/(fusion)/i;-  SAY_PHRASES.forEach(p=>{-    const chip=document.createElement('span');chip.className='say-chip';chip.dataset.say=p;-    const quoted='\u201C'+p.replace(noun,'<em>$1</em>')+'\u201D';-    chip.innerHTML=quoted+' <button class="copy">Copy</button><button class="send">Send to AI</button>';-    const doCopy=async()=>{await copyText(p);toast('Copied to clipboard. Paste it into any AI thread.','ok');};-    chip.addEventListener('click',doCopy);-    chip.querySelector('.copy').addEventListener('click',e=>{e.stopPropagation();doCopy();});-    chip.querySelector('.send').addEventListener('click',async e=>{-      e.stopPropagation();-      const r=await fetch('api/say-send',{method:'POST',headers:{'Content-Type':'application/json'},-        body:JSON.stringify({phrase:p})}).then(x=>x.json()).catch(()=>null);-      if(r&&r.ok){toast('Sent to a new AI thread.','ok');gradSave({aiDriven:true});applyGraduation();}-      else{await copyText(p);toast((r&&r.why)||'Could not reach the AI panel; copied instead.','err');}-    });-    host.appendChild(chip);-  });-  $('#sayDismiss').onclick=()=>{gradSave({dismissed:{...(gradState().dismissed||{}),say:true}});applyGraduation();};-  $('#sayFolded').onclick=()=>{$('#sayChips').style.display='';$('#sayFolded').style.display='none';-    document.querySelector('.say-head p').style.display='';};-})(); -// ── newbie-graduation: Fresh / Graduated / Recallable, localStorage per-app origin ──-const GRAD_KEY='adom-dash-graduation';-function gradState(){try{return JSON.parse(localStorage.getItem(GRAD_KEY))??{}}catch(e){return{}}}-function gradSave(patch){try{localStorage.setItem(GRAD_KEY,JSON.stringify({...gradState(),...patch}))}catch(e){}}-let gradRecalled=false;   // the header ? restores Fresh for this session only-function applyGraduation(){-  const g=gradState();-  const launches=g.launches||0;-  const demosFold=!gradRecalled&&((g.dismissed&&g.dismissed.start)||launches>=3||g.aiDriven);-  const sayFold=!gradRecalled&&((g.dismissed&&g.dismissed.say)||launches>=5||g.aiDriven);-  // the say strip folds LAST: it is the graduation path itself-  const strip=$('#saystrip');-  if(sayFold){$('#sayChips').style.display='none';$('#sayFolded').style.display='';-    document.querySelector('.say-head p').style.display='none';}-  else{$('#sayChips').style.display='';$('#sayFolded').style.display='none';-    document.querySelector('.say-head p').style.display='';}-  const demos=$('#demosRegion');-  if(demos)demos.style.display=demosFold?'none':'';-  const demosFolded=$('#demosFolded');-  if(demosFolded)demosFolded.style.display=demosFold?'':'none';-}-let _gradCounted=false;-function gradCountLaunch(st){-  // a launch counts as successful when the status rollup is green within the first minute-  if(_gradCounted)return;-  const leds=(st&&st.leds)||{};-  const core=['fusionRunning','addinReady'];-  if(core.every(k=>leds[k]&&leds[k].on)&&(performance.now()<60000)){-    _gradCounted=true;gradSave({launches:(gradState().launches||0)+1});-  }-}-document.addEventListener('DOMContentLoaded',()=>{applyGraduation();-  const df=$('#demosFolded');if(df)df.onclick=()=>{$('#demosRegion').style.display='';df.style.display='none';};});-// the header ? recalls all scaffolding for this session (newbie-graduation: Recallable)-document.addEventListener('DOMContentLoaded',()=>{-  $('#btnRecall').onclick=()=>{gradRecalled=true;applyGraduation();toast('Helpers are back for this session - they fold again on reload','ok');};-});++// Scaffolding state - the X, the header ? toggle, and the open/closed memory - is OWNED BY+// THE MODULE now; it is handed our #btnRecall as toggleButton. We deliberately keep no+// parallel copy, because two implementations of "is the panel open" is precisely the drift+// the shared module exists to end.  let targetsCache=[]; function statusUrl(){return 'api/status'+(curTarget?('?target='+encodeURIComponent(curTarget)):'');}@@ -653,7 +575,6 @@ function renderLeds(st){   $('#bpBridge').textContent=st.bridgeVersion||window.__lastBridgeVer||'-';   $('#bpAddin').textContent=(leds.addinReady&&leds.addinReady.version)||'-';   $('#meta').textContent=st.stale?'(busy - last verified state)':'';-  gradCountLaunch(st); } async function refresh(){try{lastStatus=await api(statusUrl());renderLeds(lastStatus);}catch(e){toast('status failed: '+e,'err');}} $('#btnRefresh').onclick=()=>{toast('refreshing status');refresh();};@@ -661,7 +582,7 @@ $('#btnStart').onclick=async()=>{toast('starting Fusion in the background');awai $('#btnStop').onclick=async()=>{toast('closing Fusion');await act('stop');setTimeout(refresh,4000);};  // Tour with a REAL progress bar (the harness contract: percent / stepLabel / done).-$('#btnTour').onclick=async()=>{+async function startFusionDemo(){   toast('starting the guided tour (background)');   const s=await act('tour');const d=s.data||s;const id=d.demoId;   if(!id){toast('tour failed to start: '+(d.error||'unknown'),'err');return;}@@ -722,7 +643,6 @@ $('#btnTour').onclick=async()=>{     if(q.done){       $('#tourTransport').style.display='none';       if(tourAudioEl)tourAudioEl.pause();tourAudioQueue=[];-      if(!q.failed){gradSave({aiDriven:true});}       $('#tourLabel').textContent=(q.failed?'tour stopped: '+(q.error||''):'tour complete')+'  -  '+shown+' views captured';       toast(q.failed?('tour failed: '+(q.error||'')):'tour complete: '+shown+' views captured',q.failed?'err':'ok');       return;@@ -958,7 +878,10 @@ const PROMPTS=[ $('#prompts').innerHTML=PROMPTS.map(p=>`<div class="prompt"><code>${p}</code><button class="btn copy" data-tip="Copy this prompt so you can paste it to your AI" onclick="navigator.clipboard.writeText('${p.replace(/'/g,"\\'")}');toast('copied','ok')">Copy</button></div>`).join('');  // ── Live channel (SSE): state pushes, AI toasts, CLI click directives ──-const CLICK_TARGETS={setup:'#btnSetup',start:'#btnStart',stop:'#btnStop',tour:'#btnTour','import-part':'#btnImportPart',settings:'#btnSettings',refresh:'#btnRefresh'};+const CLICK_TARGETS={setup:'#btnSetup',start:'#btnStart',stop:'#btnStop','import-part':'#btnImportPart',settings:'#btnSettings',refresh:'#btnRefresh'};+// `ui click tour` used to press #btnTour; the shared panel owns that button now, so drive+// the demo directly. The CLI contract is unchanged for callers.+const CLICK_FNS={tour:()=>startFusionDemo()}; let sseUp=false; function connectSSE(){   const es=new EventSource('events');@@ -983,7 +906,9 @@ function connectSSE(){   es.addEventListener('toast',e=>{try{const t=JSON.parse(e.data);toast(t.message,t.kind==='info'?'':t.kind);}catch(_){}});   es.addEventListener('click',e=>{try{     const {target}=JSON.parse(e.data);const sel=CLICK_TARGETS[target];-    if(sel&&$(sel)){$(sel).click();}else{toast('unknown click target: '+target,'err');}+    if(sel&&$(sel)){$(sel).click();}+    else if(CLICK_FNS[target]){CLICK_FNS[target]();}+    else{toast('unknown click target: '+target,'err');}   }catch(_){}});   es.onerror=()=>{sseUp=false;es.close();setTimeout(connectSSE,3000);}; }
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.201"+DASH_VERSION="1.9.202" 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.201",+  "version": "1.9.202",   "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,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.201",+  "version": "1.9.202",   "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",@@ -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]"-  }-}\ No newline at end of file+  ]+}