← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.177\ No newline at end of file+1.9.178\ No newline at end of file
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]"-  }-}\ No newline at end of file+  ]+}