← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.178\ No newline at end of file+1.9.179\ 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.178",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.178/adom-bridge-fusion-v1.9.178.zip",-  "sha256": "9e6b0481950310332991ee3d1c60ee1a98d095c96b74c0a55264e35008e9d8be",+  "version": "1.9.179",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.179/adom-bridge-fusion-v1.9.179.zip",+  "sha256": "c1cc8f9990189dcbe592b503f7fe77cfefbb04de6e65b250e841a7bef3db59f4",   "size": 484687,   "verbPrefixes": [     "fusion_"
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.178",+  "version": "1.9.179",   "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.178"+version = "1.9.179" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.178"+version = "1.9.179" edition = "2021"  [dependencies]
dashboard/src/main.rs+72−43
@@ -1262,55 +1262,84 @@ fn url_for(port: u16) -> String { }  /// 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.+/// (127.0.0.1:47084). Lookup order per the Dock Bar guide (project-e7, measured live+/// 2026-08-23): CENSUS FIRST (GET /ai-threads - the OPEN threads; history EXCLUDES open+/// threads by design, so history-only returned empty in exactly the common case), then+/// HISTORY for closed threads (resume, then stage). run-prompt is always submit:false -+/// 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+fn hy_get(path: &str) -> Option<serde_json::Value> {     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();+        .args(["-s", "-m", "2", &format!("http://127.0.0.1:47084{}", path)])+        .output().ok()?;+    if out.stdout.is_empty() { return None; }+    serde_json::from_slice(&out.stdout).ok()+}++fn hy_post(path: &str, payload: &serde_json::Value) -> Option<serde_json::Value> {     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"})+               "-d", &payload.to_string(), &format!("http://127.0.0.1:47084{}", path)])+        .output().ok()?;+    if out.stdout.is_empty() { return None; }+    serde_json::from_slice(&out.stdout).ok()+}++fn stage_say_phrase(phrase: &str) -> serde_json::Value {+    const TAG: &str = "app:adom/fusion-bridge";+    // 1. CENSUS: open threads. Shape: {"ok":true,"groups":[{"threads":[{"tid","sid"|null,"tags":[...]}]}]}+    let census = hy_get("/ai-threads");+    if census.is_none() {+        return serde_json::json!({"ok": false,+            "why": "Hydrogen control API unreachable (not WSL2/mirrored networking?)"});+    }+    let census = census.unwrap();+    let open_row = census["groups"].as_array().into_iter().flatten()+        .flat_map(|g| g["threads"].as_array().cloned().unwrap_or_default())+        .find(|t| t["tags"].as_array().map_or(false,+            |tags| tags.iter().any(|x| x.as_str() == Some(TAG))));+    if let Some(row) = open_row {+        // Address by sid when present; tid when sid is null (thread open but no session yet).+        let mut payload = serde_json::json!({"prompt": phrase, "submit": false});+        if let Some(sid) = row["sid"].as_str() {+            payload["sid"] = serde_json::json!(sid);+        } else if let Some(tid) = row["tid"].as_str() {+            payload["tid"] = serde_json::json!(tid);+        } else {+            return serde_json::json!({"ok": false, "why": "census row has neither sid nor tid"});+        }+        return match hy_post("/ai-threads/run-prompt", &payload) {+            Some(r) if r["ok"].as_bool() != Some(false) =>+                serde_json::json!({"ok": true, "staged": true, "via": "census",+                    "_hint": "phrase staged in the open app thread's input - the user presses Enter"}),+            _ => serde_json::json!({"ok": false, "why": "run-prompt failed on the open thread"}),+        };+    }+    // 2. HISTORY: closed threads. Shape: {"ok":true,"items":[{"ai","id","title","updated_ms"}]}+    let hist = hy_get(&format!("/ai-threads/history?tag={}", TAG));+    if let Some(item) = hist.as_ref()+        .and_then(|h| h["items"].as_array())+        .and_then(|a| a.first()) {+        let (ai, id) = (item["ai"].as_str().unwrap_or("claude"),+                        item["id"].as_str().unwrap_or(""));+        if !id.is_empty() {+            // Resume reopens the thread tab - the user clicked Try, so that IS their intent.+            let resumed = hy_post("/ai-threads/resume", &serde_json::json!({"ai": ai, "id": id}));+            if resumed.map_or(false, |r| r["ok"].as_bool() != Some(false)) {+                let staged = hy_post("/ai-threads/run-prompt",+                    &serde_json::json!({"sid": id, "prompt": phrase, "submit": false}));+                if staged.map_or(false, |r| r["ok"].as_bool() != Some(false)) {+                    return serde_json::json!({"ok": true, "staged": true, "via": "history+resume",+                        "_hint": "reopened the app thread and staged the phrase - the user presses Enter"});+                }+            }+            return serde_json::json!({"ok": false,+                "why": "app thread exists but is closed and resume/stage failed"});         }-        _ => serde_json::json!({"ok": false, "why": "run-prompt call failed"}),     }+    // 3. Neither: honest copy fallback.+    serde_json::json!({"ok": false,+        "why": "no AI thread tagged app:adom/fusion-bridge yet - open the app from the dock once"}) }  /// Probe 8930-8940 for an already-serving FUSION dashboard. "dashboardVersion" alone is
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.178"+DASH_VERSION="1.9.179" 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.178",+  "version": "1.9.179",   "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.178",+  "version": "1.9.179",   "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+  ]+}