← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.10.19\ No newline at end of file+1.10.20\ No newline at end of file
bridge.json+3−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.10.19",+  "version": "1.10.20",   "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.",@@ -81,7 +81,8 @@     "fusion_route_net",     "fusion_routing_drc",     "fusion_routing_state",-    "fusion_plan_route"+    "fusion_plan_route",+    "fusion_schematic"   ],   "dependencies": {},   "category": "CAD",
contracts/describe.json+33−2
@@ -1,6 +1,6 @@ {   "contract": "fusion-bridge verb catalog",-  "frozen": "2026-09-19",+  "frozen": "2026-09-20",   "note": "The Python bridge and the Rust bridge must both serve this catalog byte-for-byte in meaning: same names, same args, same result shapes, same timeouts. Regenerate ONLY from describe.py and diff in CI.",   "source": "describe.py",   "verbs": [@@ -1966,7 +1966,7 @@         "success": "bool"       },       "statusVerb": null,-      "summary": "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",+      "summary": "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D. PREFLIGHT (1.10.20): refuses with eagle_files_missing before touching any dialog when either file is not on the Fusion machine; if the Open dialog does not accept the file the bridge cancels the dialog itself (open_dialog_stuck), so nothing is ever left on the user's screen. Prefer fusion_schematic {action:\"open\"}, which picks this route only when a .brd exists next to the .sch.",       "timeoutSeconds": 200     },     {@@ -2470,6 +2470,37 @@       "summary": "Save the ACTIVE document to Autodesk cloud storage with an explicit name and destination. Dialog-free precisely because you supply what the Save box would ask for - use it before closing or restarting when a document has never been saved (fusion_close_all_documents refuses such a document with unsaved_named_document rather than discarding it).",       "timeoutSeconds": 180     },+    {+      "example": {+        "action": "open",+        "fileName": "BQ25792 Assembly",+        "projectName": "Main"+      },+      "input": {+        "action": "required str: open | create | show",+        "brdPath": "optional local .brd (open; defaults to the sibling)",+        "capture": "optional bool: inline screenshot proof after the switch",+        "fileName": "cloud design name (open)",+        "filePath": "local .fsch (open)",+        "name": "optional str (create)",+        "projectName": "optional str (open, with fileName)",+        "query": "find-and-open by name (open)",+        "schPath": "local EAGLE .sch on the Fusion machine (open)",+        "urn": "cloud lineage urn (open)",+        "withBoard": "optional bool (open; false = schematic only, dialog-free)"+      },+      "longRunning": true,+      "name": "fusion_schematic",+      "output": {+        "documentName": "str (create)",+        "schPath": "str (create)",+        "success": "bool",+        "via": "str: the verb that did the work"+      },+      "statusVerb": "fusion_get_app_state",+      "summary": "START HERE for a schematic. ONE verb, three actions. action:\"open\" lands in the schematic editor from whatever names the design: fileName (+projectName) or urn or query for a CLOUD design (opens it, then switches to the schematic view), schPath for a local EAGLE .sch (its sibling .brd is imported with it so parts instantiate, unless withBoard:false; a .sch alone opens dialog-free), or filePath for a Fusion-native .fsch. action:\"create\" writes a blank EAGLE schematic on the box and opens it as a NEW local electronics design with the schematic editor active, no dialog anywhere. action:\"show\" switches the already-open design to its schematic view. Every reply says which underlying verb it used (via). Remember: a schematic is a VIEW of an electronics design, not a file of its own, which is why there is no verb called open_a_schematic_file.",+      "timeoutSeconds": 240+    },     {       "example": {},       "input": {
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.10.19"+version = "1.10.20" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.10.19"+version = "1.10.20" edition = "2021"  [dependencies]
describe.py+4−1
@@ -43,7 +43,7 @@ _T = [     ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.",      {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}), -    ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",+    ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D. PREFLIGHT (1.10.20): refuses with eagle_files_missing before touching any dialog when either file is not on the Fusion machine; if the Open dialog does not accept the file the bridge cancels the dialog itself (open_dialog_stuck), so nothing is ever left on the user's screen. Prefer fusion_schematic {action:\"open\"}, which picks this route only when a .brd exists next to the .sch.",      {"schPath": "required str (Windows path to .sch)", "brdPath": "optional str (defaults to sibling .brd)"},      {"success": "bool", "imported": "list"}, 200, None, False,      {"schPath": "C:/Users/me/adom-lib/MyBoard.sch", "brdPath": "C:/Users/me/adom-lib/MyBoard.brd"}),@@ -357,6 +357,9 @@ _T = [      {"filePath": "required str (WINDOWS path to the .brd, C:/...; must exist on the Fusion machine)"},      {"success": "bool", "filePath": "str", "fileType": "str", "activeWorkspace": "str", "activeDocument": "str", "nextSteps": "[str]", "postOpenCheck": "{required, steps}", "screenshots": "[{type,savedTo,sizeKB}] (sidecar shell) or postOpenScreenshot (exe shell)"},      60, None, True, {"filePath": "C:/Temp/TJA1050.brd"}),+    ("fusion_schematic", "START HERE for a schematic. ONE verb, three actions. action:\"open\" lands in the schematic editor from whatever names the design: fileName (+projectName) or urn or query for a CLOUD design (opens it, then switches to the schematic view), schPath for a local EAGLE .sch (its sibling .brd is imported with it so parts instantiate, unless withBoard:false; a .sch alone opens dialog-free), or filePath for a Fusion-native .fsch. action:\"create\" writes a blank EAGLE schematic on the box and opens it as a NEW local electronics design with the schematic editor active, no dialog anywhere. action:\"show\" switches the already-open design to its schematic view. Every reply says which underlying verb it used (via). Remember: a schematic is a VIEW of an electronics design, not a file of its own, which is why there is no verb called open_a_schematic_file.",+     {"action": "required str: open | create | show", "fileName": "cloud design name (open)", "projectName": "optional str (open, with fileName)", "urn": "cloud lineage urn (open)", "query": "find-and-open by name (open)", "schPath": "local EAGLE .sch on the Fusion machine (open)", "brdPath": "optional local .brd (open; defaults to the sibling)", "withBoard": "optional bool (open; false = schematic only, dialog-free)", "filePath": "local .fsch (open)", "name": "optional str (create)", "capture": "optional bool: inline screenshot proof after the switch"},+     {"success": "bool", "via": "str: the verb that did the work", "documentName": "str (create)", "schPath": "str (create)"}, 240, _STATUS, True, {"action": "open", "fileName": "BQ25792 Assembly", "projectName": "Main"}),     ("fusion_open_schematic", "Open a LOCAL legacy EAGLE .sch in Fusion's Schematic Editor (dialog-free Document.newDesignFromLocal; a background open). NAV RULE: the schematic, 2D board and 3D board are VIEWS of ONE electronics design, not standalone files. For a team design open the DESIGN document (fusion_open_cloud_file / fusion_open_by_urn) and switch views with fusion_show_schematic; this verb is for a .sch that exists on the Fusion machine (push it there first). Drive the opened sheet with fusion_electron_run (WINDOW FIT, DISPLAY ALL). The bridge captures a post-open screenshot because Fusion can raise a blocking dialog the API cannot see.",      {"filePath": "required str (WINDOWS path to the .sch, C:/...; must exist on the Fusion machine)"},      {"success": "bool", "filePath": "str", "fileType": "str", "activeWorkspace": "str", "activeDocument": "str", "nextSteps": "[str]", "postOpenCheck": "{required, steps}", "screenshots": "[{type,savedTo,sizeKB}] (sidecar shell) or postOpenScreenshot (exe shell)"},
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.10.19"+DASH_VERSION="1.10.20" 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.10.19",+  "version": "1.10.20",   "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−27
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.10.19",+  "version": "1.10.20",   "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,28 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "agent_permissions": {-    "allow_bash": [],-    "allow": [-      "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",-      "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."-    ],-    "soft_deny": [-      "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",-      "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",-      "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",-      "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."-    ],-    "environment": [-      "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",-      "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."-    ]-  },-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.10.19"+version = "1.10.20" edition = "2021" license = "MIT" 
rust/bridge/src/lifecycle.rs+24−5
@@ -191,6 +191,24 @@ pub fn dialog_inventory(ctx: &Ctx) -> Vec<Value> {     let rows = match ab.call("desktop_enum_windows", args, &ctx.caller, 10) { Ok(v) => v["windows"].as_array().or_else(|| v["data"]["windows"].as_array()).cloned().unwrap_or_default(), Err(_) => return vec![] };     let classified = crate::small_verbs::classify_blocking_dialogs(&ab, &ctx.caller, &ctx.pids);     let mut out = Vec::new();+    // native Windows file dialogs (class #32770: Open, Save As) are not Qt dialogs; a+    // dialog-driven import (ImportSCHAndBRDCmd) can leave one up, and it blocks Fusion just the same+    for w in &rows {+        let class = w["className"].as_str().or(w["class"].as_str()).unwrap_or("");+        if !class.contains("#32770") || w["visible"].as_bool() == Some(false) { continue; }+        let h = w["hwnd"].as_i64().unwrap_or(0);+        let (mut text, mut buttons): (Vec<String>, Vec<String>) = (vec![], vec![]);+        if let Ok(r) = ab.call("desktop_find_controls", json!({"hwnd": h}), &ctx.caller, 10) {+            let dd = if r["data"].is_object() { r["data"].clone() } else { r };+            for c in dd["controls"].as_array().or(dd["matches"].as_array()).cloned().unwrap_or_default() {+                let name = c["name"].as_str().unwrap_or("").trim().to_string(); if name.is_empty() { continue; }+                let ty = c["controlType"].as_str().or(c["type"].as_str()).or(c["role"].as_str()).unwrap_or("").to_lowercase();+                if ty.contains("button") { buttons.push(name); } else if text.len() < 12 { text.push(name); }+            }+        }+        out.push(json!({"hwnd": h, "title": w["title"], "category": "file_dialog", "resolution": "a native Windows Open/Save dialog (left by a dialog-driven import or a manual action): cancelled safely with WM_CLOSE, nothing is saved or discarded",+            "text": text.join(" ").chars().take(300).collect::<String>(), "buttons": buttons}));+    }     for d in fusion_core::dialogs::qt_dialog_rows(&rows) {         let h = d["hwnd"].as_i64().unwrap_or(0);         let (mut text, mut buttons): (Vec<String>, Vec<String>) = (vec![], vec![]);@@ -222,7 +240,7 @@ pub fn expire_dialogs(ctx: &Ctx, before: &[Value]) -> (Vec<Value>, Vec<Value>) {         let r = crate::small_verbs::dismiss_blocking_dialogs(&ctx.state_dir, &json!({}), &ctx.caller, &ctx.pids, &|_| {});         tried.push(json!({"step": "fusion_dismiss_blocking_dialogs (known rules)", "dismissed": r["dismissed"]}));     }-    for d in before.iter().filter(|d| d["category"].as_str() == Some("unknown")) {+    for d in before.iter().filter(|d| matches!(d["category"].as_str(), Some("unknown") | Some("file_dialog"))) {         if let Some(h) = d["hwnd"].as_i64() {             let r = ab.call("desktop_close_window", json!({"hwnd": h}), &ctx.caller, 10);             tried.push(json!({"step": "WM_CLOSE (Cancel/X semantics)", "hwnd": h, "title": d["title"], "ok": r.is_ok()}));@@ -243,13 +261,14 @@ pub enum DialogGate { Clear, Expired(Vec<Value>, Vec<Value>), Blocked(Value) }  pub fn blocked_by_dialog_reply(ctx: &Ctx, cmd: &str) -> DialogGate {     let before = dialog_inventory(ctx);-    let shot0 = crate::small_verbs::screenshot_fusion(&json!({}), &ctx.caller);-    if before.is_empty() && shot0["ownedPopupCount"].as_u64().unwrap_or(0) == 0 { return DialogGate::Clear; }+    // only real dialogs count: Fusion's own tool windows (Data panel, message tray) are owned+    // popups too, and counting them refused every verb for hours on 2026-09-19+    if before.is_empty() { return DialogGate::Clear; }     let (tried, after) = expire_dialogs(ctx, &before);     let shot = crate::small_verbs::screenshot_fusion(&json!({}), &ctx.caller);     let popups = shot["ownedPopupCount"].as_u64().unwrap_or(0);     crate::fusion_prefs::action_ledger(&ctx.state_dir, "blocked_by_dialog", json!({"forCommand": cmd, "before": before.iter().map(|d| d["title"].clone()).collect::<Vec<_>>(), "tried": tried, "after": after.iter().map(|d| d["title"].clone()).collect::<Vec<_>>(), "ownedPopups": popups}));-    if after.is_empty() && popups == 0 {+    if after.is_empty() {         eprintln!("[fusion-bridge] {cmd}: expired {} dialog(s) programmatically before running", before.len());         return DialogGate::Expired(before, tried);     }@@ -397,7 +416,7 @@ pub fn diagnose_addin_timeout(ctx: &Ctx, command: &str, original: &Value) -> Val                 let inv = dialog_inventory(ctx);                 let (tried, after) = expire_dialogs(ctx, &inv);                 let popups = crate::small_verbs::screenshot_fusion(&json!({}), &ctx.caller);-                if !inv.is_empty() && after.is_empty() && popups["ownedPopupCount"].as_u64().unwrap_or(0) == 0 {+                if !inv.is_empty() && after.is_empty() {                     crate::fusion_prefs::action_ledger(&ctx.state_dir, "dialog_expired_after_timeout", json!({"forCommand": command, "dialogs": inv.iter().map(|d| d["title"].clone()).collect::<Vec<_>>(), "tried": tried}));                     return json!({"success": false, "errorCode": "dialog_expired_retry", "dialogsExpired": inv, "expiryTried": tried,                         "error": format!("'{command}' timed out behind a dialog ({}); the bridge expired it programmatically (rules, then WM_CLOSE). Re-run the verb.", inv.iter().map(|d| d["title"].as_str().unwrap_or("").to_string()).collect::<Vec<_>>().join(", ")),
rust/bridge/src/main.rs+1−1
@@ -408,7 +408,7 @@ fn main() {                         r["shell"] = json!("rust"); r },                     "fusion_start" | "start" | "launch" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, "start", &caller); let mut r = launch::start(&ctx, &parsed["args"]); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, "start", t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     "fusion_open_lbr" | "open_lbr" if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, "open_lbr", &caller); let mut r = orchestrations::open_lbr(&ctx, &parsed["args"], &|v, a| addin::pass_through(v, &a, &ctx)); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, "open_lbr", t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },-                    v @ ("fusion_show_symbol" | "show_symbol" | "fusion_show_footprint" | "show_footprint" | "fusion_show_library" | "show_library" | "fusion_show_project" | "show_project" | "fusion_show_schematic" | "show_schematic" | "fusion_show_2d_board" | "show_2d_board" | "fusion_show_3d_board" | "show_3d_board" | "fusion_capture_library_views" | "capture_library_views" | "fusion_open_cloud_file" | "open_cloud_file" | "fusion_save_lbr" | "save_lbr" | "fusion_cleanup_cloud_files" | "cleanup_cloud_files" | "fusion_inspect_bodies" | "inspect_bodies" | "fusion_board_stackup" | "board_stackup" | "fusion_export_optimized_glb" | "export_optimized_glb" | "fusion_fetch_optimized_glb" | "fetch_optimized_glb" | "fusion_set_design_rules" | "set_design_rules" | "fusion_make_test_project" | "make_test_project" | "fusion_open_design" | "open_design" | "fusion_new_electronics_from_eagle" | "new_electronics_from_eagle" | "fusion_aps_open" | "aps_open") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, cmd, &caller); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "show_symbol" => orchestrations::show_symbol(&ctx, &parsed["args"], &pass), "show_footprint" => orchestrations::show_footprint(&ctx, &parsed["args"], &pass), "show_library" => orchestrations::show_library(&ctx, &parsed["args"], &pass), "show_project" => match orchestrations::show_project(&ctx, &parsed["args"], &pass) { Some(r) => r, None => side("show_project", parsed["args"].clone(), 240) }, "capture_library_views" => small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &parsed["args"], &pass), &caller, &pids), "open_cloud_file" => orchestrations::open_cloud_file(&ctx, &parsed["args"], &pass), "save_lbr" => small_verbs::merge_dialog_array(orchestrations::save_lbr(&ctx, &parsed["args"], &pass), &caller, &pids), "cleanup_cloud_files" => orchestrations::cleanup_cloud_files(&parsed["args"], &pass), "inspect_bodies" => orchestrations::inspect_bodies(&parsed["args"], &pass), "board_stackup" => services::board_stackup(&parsed["args"], &pass), "export_optimized_glb" => if parsed["args"]["async"].as_bool() == Some(true) { services::start_glb_export_async(&ctx, &parsed["args"]) } else { services::export_optimized_glb(&parsed["args"], &pass) }, "fetch_optimized_glb" => if parsed["args"]["exportId"].is_string() { services::glb_export_async_status(&parsed["args"]) } else { services::fetch_optimized_glb(&parsed["args"]) }, "set_design_rules" => services::set_design_rules(&ctx, &parsed["args"], &pass), "make_test_project" => small_verbs::merge_dialog_array(testproject::make_test_project(&ctx, &parsed["args"], &pass), &caller, &pids), "open_design" => orchestrations::open_design(&ctx, &parsed["args"], &pass), "new_electronics_from_eagle" => orchestrations::new_electronics_from_eagle(&ctx, &parsed["args"], &pass), "aps_open" => orchestrations::aps_open(&ctx, &parsed["args"], &pass), other => orchestrations::open_with_screenshot(&ctx, other, &parsed["args"], &pass) }; let mut r = small_verbs::merge_dialog_array(r0, &caller, &pids); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },+                    v @ ("fusion_show_symbol" | "show_symbol" | "fusion_show_footprint" | "show_footprint" | "fusion_show_library" | "show_library" | "fusion_show_project" | "show_project" | "fusion_show_schematic" | "show_schematic" | "fusion_show_2d_board" | "show_2d_board" | "fusion_show_3d_board" | "show_3d_board" | "fusion_capture_library_views" | "capture_library_views" | "fusion_open_cloud_file" | "open_cloud_file" | "fusion_save_lbr" | "save_lbr" | "fusion_cleanup_cloud_files" | "cleanup_cloud_files" | "fusion_inspect_bodies" | "inspect_bodies" | "fusion_board_stackup" | "board_stackup" | "fusion_export_optimized_glb" | "export_optimized_glb" | "fusion_fetch_optimized_glb" | "fetch_optimized_glb" | "fusion_set_design_rules" | "set_design_rules" | "fusion_make_test_project" | "make_test_project" | "fusion_open_design" | "open_design" | "fusion_new_electronics_from_eagle" | "new_electronics_from_eagle" | "fusion_aps_open" | "aps_open" | "fusion_schematic" | "schematic") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); let ind = timings::Indicator::start(&sd, cmd, &caller); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "show_symbol" => orchestrations::show_symbol(&ctx, &parsed["args"], &pass), "show_footprint" => orchestrations::show_footprint(&ctx, &parsed["args"], &pass), "show_library" => orchestrations::show_library(&ctx, &parsed["args"], &pass), "show_project" => match orchestrations::show_project(&ctx, &parsed["args"], &pass) { Some(r) => r, None => side("show_project", parsed["args"].clone(), 240) }, "capture_library_views" => small_verbs::apply_failure_dialogs(orchestrations::capture_library_views(&ctx, &parsed["args"], &pass), &caller, &pids), "open_cloud_file" => orchestrations::open_cloud_file(&ctx, &parsed["args"], &pass), "save_lbr" => small_verbs::merge_dialog_array(orchestrations::save_lbr(&ctx, &parsed["args"], &pass), &caller, &pids), "cleanup_cloud_files" => orchestrations::cleanup_cloud_files(&parsed["args"], &pass), "inspect_bodies" => orchestrations::inspect_bodies(&parsed["args"], &pass), "board_stackup" => services::board_stackup(&parsed["args"], &pass), "export_optimized_glb" => if parsed["args"]["async"].as_bool() == Some(true) { services::start_glb_export_async(&ctx, &parsed["args"]) } else { services::export_optimized_glb(&parsed["args"], &pass) }, "fetch_optimized_glb" => if parsed["args"]["exportId"].is_string() { services::glb_export_async_status(&parsed["args"]) } else { services::fetch_optimized_glb(&parsed["args"]) }, "set_design_rules" => services::set_design_rules(&ctx, &parsed["args"], &pass), "make_test_project" => small_verbs::merge_dialog_array(testproject::make_test_project(&ctx, &parsed["args"], &pass), &caller, &pids), "open_design" => orchestrations::open_design(&ctx, &parsed["args"], &pass), "new_electronics_from_eagle" => orchestrations::new_electronics_from_eagle(&ctx, &parsed["args"], &pass), "schematic" => orchestrations::schematic(&ctx, &parsed["args"], &pass), "aps_open" => orchestrations::aps_open(&ctx, &parsed["args"], &pass), other => orchestrations::open_with_screenshot(&ctx, other, &parsed["args"], &pass) }; let mut r = small_verbs::merge_dialog_array(r0, &caller, &pids); ind.stop(r["success"].as_bool() == Some(false)); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     "fusion_state" | "state" if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: h.bridge_state_dir(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let mut r = orchestrations::fusion_state(&ctx, &parsed["args"]); r["shell"] = json!("rust"); r },                     v @ ("fusion_make_3d_package" | "make_3d_package" | "fusion_build_library_3d" | "build_library_3d" | "fusion_bind_library_3d" | "bind_library_3d" | "fusion_prove_library" | "prove_library" | "fusion_attach_3d_package" | "attach_3d_package" | "fusion_show_3d_package" | "show_3d_package") if addin_owner_is_exe(&**h) => { let (sc, ca, ca2) = (sidecar.clone(), caller.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let sd = h.bridge_state_dir(); let pids: Vec<u32> = h.fusion_processes().iter().map(|p| p.pid).collect(); let dlg = |r: Value| small_verbs::merge_dialog_array(r, &ca2, &pids); let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: sd.clone(), caller: caller.clone(), pids: pids.clone(), sidecar: &side, dialogs: &dlg }; let cmd = v.strip_prefix("fusion_").unwrap_or(v); let t0 = Instant::now(); *LONG_OP.lock().unwrap() = Some(cmd.to_string()); *LONG_OP_SINCE.lock().unwrap() = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0); let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let r0 = match cmd { "make_3d_package" => mint::make_3d_package(&ctx, &parsed["args"], &pass), "bind_library_3d" => mint::bind_library_3d(&ctx, &parsed["args"], &pass), "prove_library" => prove::prove_library(&ctx, &parsed["args"], &pass), "attach_3d_package" => mint::attach_3d_package(&ctx, &parsed["args"], &pass), "show_3d_package" => mint::show_3d_package(&ctx, &parsed["args"], &pass), _ => mint::build_library_3d(&ctx, &parsed["args"], &pass) }; *LONG_OP.lock().unwrap() = None; let mut r = small_verbs::apply_failure_dialogs(r0, &caller, &pids); timings::log_run(&sd, cmd, t0.elapsed().as_secs_f64(), r["success"].as_bool() != Some(false)); r["shell"] = json!("rust"); r },                     v @ ("fusion_apply_fusion_update" | "apply_fusion_update" | "fusion_install_fusion" | "install_fusion" | "fusion_uninstall" | "uninstall" | "fusion_prefs_open" | "prefs_open" | "fusion_prefs_close" | "prefs_close" | "fusion_generate_package" | "generate_package" | "fusion_mcp_status" | "mcp_status" | "fusion_mcp_tools" | "mcp_tools" | "fusion_mcp_call" | "mcp_call" | "fusion_mcp_resources" | "mcp_resources" | "fusion_mcp_enable" | "mcp_enable" | "fusion_library_manager" | "library_manager" | "fusion_managed_library_create" | "managed_library_create" | "fusion_managed_library_link" | "managed_library_link" | "fusion_managed_library_pull" | "managed_library_pull" | "fusion_managed_library_push" | "managed_library_push" | "fusion_managed_library_unlink" | "managed_library_unlink" | "fusion_signin" | "signin" | "fusion_signin_2fa" | "signin_2fa" | "fusion_web_signin" | "web_signin" | "fusion_aps_signin" | "aps_signin" | "fusion_demo" | "demo" | "fusion_demo_progress" | "demo_progress" | "fusion_demo_control" | "demo_control" | "fusion_electronics_query" | "electronics_query" | "fusion_view" | "view" | "fusion_interactive" | "interactive" | "fusion_narrate" | "narrate") if addin_owner_is_exe(&**h) => { let (sc, ca) = (sidecar.clone(), caller.clone()); let side = move |verb: &str, a: Value, t: u64| proxy_with_timeout(&sc, &json!({"command": verb, "args": a}).to_string(), &ca, t); let dlg = |r: Value| r; let ctx = addin::Ctx { host: &**h, fusion_exe: h.fusion_executable(), short_path: |p| host().short_path(p), state_dir: h.bridge_state_dir(), caller: caller.clone(), pids: h.fusion_processes().iter().map(|p| p.pid).collect(), sidecar: &side, dialogs: &dlg }; let pass = |verb: &str, a: Value| addin::pass_through(verb, &a, &ctx); let cmd = v.strip_prefix("fusion_").unwrap_or(v); let mut r = match cmd { "apply_fusion_update" => maintenance::apply_fusion_update(&ctx, &pass), "install_fusion" => maintenance::install_fusion(&ctx), "prefs_open" => maintenance::prefs_open(&ctx, &parsed["args"], &pass), "prefs_close" => maintenance::prefs_close(&ctx, &parsed["args"]), "generate_package" => epg::generate_package(&parsed["args"], VERSION, &pass),
rust/bridge/src/orchestrations.rs+113−1
@@ -844,10 +844,109 @@ pub fn open_design(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) }  /// `fusion_new_electronics_from_eagle`: Fusion's own EAGLE importer, its two Open dialogs driven through ab.+/// A blank EAGLE schematic: one empty sheet, the schematic layers, nothing else. Opened through+/// Document.newDesignFromLocal it becomes a new local electronics design with the schematic+/// editor active, with no dialog anywhere (the create path of `fusion_schematic`).+const BLANK_SCH: &str = r#"<?xml version="1.0" encoding="utf-8"?>+<!DOCTYPE eagle SYSTEM "eagle.dtd">+<eagle version="9.6.2">+<drawing>+<settings><setting alwaysvectorfont="no"/><setting verticaltext="up"/></settings>+<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1" display="no" altdistance="0.01" altunitdist="inch" altunit="inch"/>+<layers>+<layer number="88" name="SimResults" color="9" fill="1" visible="yes" active="yes"/>+<layer number="89" name="SimProbes" color="9" fill="1" visible="yes" active="yes"/>+<layer number="90" name="Modules" color="5" fill="1" visible="yes" active="yes"/>+<layer number="91" name="Nets" color="2" fill="1" visible="yes" active="yes"/>+<layer number="92" name="Busses" color="1" fill="1" visible="yes" active="yes"/>+<layer number="93" name="Pins" color="2" fill="1" visible="no" active="yes"/>+<layer number="94" name="Symbols" color="4" fill="1" visible="yes" active="yes"/>+<layer number="95" name="Names" color="7" fill="1" visible="yes" active="yes"/>+<layer number="96" name="Values" color="7" fill="1" visible="yes" active="yes"/>+<layer number="97" name="Info" color="7" fill="1" visible="yes" active="yes"/>+<layer number="98" name="Guide" color="6" fill="1" visible="yes" active="yes"/>+</layers>+<schematic xreflabel="%F%N/%S.%C%R" xrefpart="/%S.%C%R">+<libraries/>+<attributes/>+<variantdefs/>+<classes><class number="0" name="default" width="0" drill="0"/></classes>+<parts/>+<sheets><sheet><plain/><instances/><busses/><nets/></sheet></sheets>+</schematic>+</drawing>+</eagle>+"#;++/// `fusion_schematic`: THE front door for a schematic. `action:"open"` takes whatever names+/// the design (a cloud fileName / urn / query, a local .sch with or without its .brd, a+/// Fusion-native .fsch) and lands in the schematic editor; `action:"create"` writes a blank+/// EAGLE schematic and opens it as a new local design, dialog-free; `action:"show"` switches+/// the open design to its schematic view. Every path says which verb it used (`via`).+pub fn schematic(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {+    let action = args["action"].as_str().unwrap_or("").trim().to_lowercase();+    let a = |k: &str| args[k].as_str().map(|s| s.trim().replace('\\', "/")).filter(|s| !s.is_empty());+    let capture = args["capture"].clone();+    let menu = "fusion_schematic takes action:\"open\" with ONE of: fileName (+projectName) for a cloud design, urn for a cloud design by lineage, query to find-and-open by name, schPath for a local EAGLE .sch (its sibling .brd is imported too unless withBoard:false), or filePath for a Fusion-native .fsch; action:\"create\" with an optional name for a new blank schematic (dialog-free); action:\"show\" to switch the open design to its schematic view. A schematic is a VIEW of an electronics design, never a file on its own.";+    match action.as_str() {+        "show" => { let mut r = pass("show_schematic", json!({"capture": capture})); r["via"] = json!("show_schematic"); r }+        "create" => {+            let name: String = a("name").unwrap_or_else(|| format!("adom-schematic-{}", (now_f() as u64) % 100000)).chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').collect();+            let root = match crate::fusionpath::stage_root(&ctx.state_dir) { Ok(r) => r.join("new"), Err(e) => return json!({"success": false, "errorCode": "stage_failed", "error": e}) };+            let _ = std::fs::create_dir_all(&root);+            let path = root.join(format!("{name}.sch")).to_string_lossy().replace('\\', "/");+            if let Err(e) = std::fs::write(&path, BLANK_SCH) { return json!({"success": false, "errorCode": "write_failed", "error": format!("cannot write {path}: {e}")}); }+            let mut r = pass("open_schematic", json!({"filePath": path}));+            r["via"] = json!("blank EAGLE .sch written on the box, then open_schematic (Document.newDesignFromLocal, no dialog)");+            r["schPath"] = json!(path); r["documentName"] = json!(name);+            if r["success"].as_bool() == Some(true) {+                r["_hint"] = json!(format!("A new electronics design named {name} is open with its (empty) schematic editor active. NEXT: put circuitry on it. Interactive editor commands (ADD, MOVE) are blocked because they wait for a mouse; author the schematic as an EAGLE .sch offline (adom-schematic / adom-lbr generate real parts and nets), then fusion_schematic {{action:\"open\", schPath}} re-opens it, or fusion_new_electronics_from_eagle {{schPath, brdPath}} when a placed board should come with it. fusion_export_eagle_source saves what is open; fusion_save_to_cloud puts it in the hub."));+            }+            r+        }+        "open" => {+            if let Some(p) = a("schPath").or_else(|| a("filePath")).filter(|p| p.to_lowercase().ends_with(".sch")) {+                let sibling = format!("{}.brd", &p[..p.len() - 4]);+                let brd = a("brdPath").or_else(|| if std::path::Path::new(&sibling).is_file() { Some(sibling.clone()) } else { None });+                if args["withBoard"].as_bool() != Some(false) {+                    if let Some(b) = brd {+                        let mut r = new_electronics_from_eagle(ctx, &json!({"schPath": p, "brdPath": b}), pass);+                        r["via"] = json!("new_electronics_from_eagle (schematic + board, parts instantiated; this one drives Fusion's own Open dialogs in the background because Autodesk offers no API for it)");+                        return r;+                    }+                }+                let mut r = pass("open_schematic", json!({"filePath": p})); r["via"] = json!("open_schematic (Document.newDesignFromLocal, dialog-free, schematic only)"); return r;+            }+            if let Some(p) = a("filePath").filter(|p| p.to_lowercase().ends_with(".fsch")) { let mut r = pass("import_electronics", json!({"filePath": p})); r["via"] = json!("import_electronics (Fusion-native .fsch)"); return r; }+            let opened = if let Some(u) = a("urn") { Some(("open_by_urn", pass("open_by_urn", json!({"urn": u, "intent": "electronics"})))) }+                else if let Some(f) = a("fileName") { Some(("open_cloud_file", open_cloud_file(ctx, &json!({"fileName": f, "projectName": args["projectName"], "folderPath": args["folderPath"]}), pass))) }+                else if let Some(q) = a("query") { Some(("aps_open", aps_open(ctx, &json!({"query": q, "wait": true}), pass))) }+                else { None };+            let Some((how, r)) = opened else { return json!({"success": false, "errorCode": "schematic_source_required", "error": "Nothing names the design to open.", "_hint": menu}); };+            if r["success"].as_bool() != Some(true) { let mut r = r; r["via"] = json!(how); return r; }+            std::thread::sleep(Duration::from_secs(2));+            let mut s = pass("show_schematic", json!({"capture": capture}));+            s["opened"] = json!({"via": how, "output": r["output"], "hint": r["_hint"]}); s["via"] = json!(format!("{how} + show_schematic"));+            if s["success"].as_bool() != Some(true) { s["_hint"] = json!(format!("The design opened ({how}) but the switch to its schematic view failed: {}. If the open landed on a mechanical design (DesignProductType) it has no schematic; check fusion_get_app_state and open the electronics DESIGN document instead.", s["error"].as_str().unwrap_or("unknown"))); }+            s+        }+        _ => json!({"success": false, "errorCode": "schematic_action_required", "error": format!("action must be open, create or show (got '{action}')."), "_hint": menu}),+    }+}+ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {     let sch = args["schPath"].as_str().or(args["filePath"].as_str()).unwrap_or("").replace('\\', "/");     if sch.is_empty() || !sch.to_lowercase().ends_with(".sch") { return json!({"success": false, "error": "schPath (a .sch file on the Windows host) is required.", "_hint": "Author the EAGLE .sch + paired .brd (same basename, same folder), stage both to Windows (send_files), then call with {schPath, brdPath}."}); }     let brd = args["brdPath"].as_str().map(|s| s.replace('\\', "/")).filter(|s| !s.is_empty()).unwrap_or_else(|| format!("{}.brd", &sch[..sch.len() - 4]));+    // Preflight (John, 2026-09-20): this verb drives Fusion's own Open dialogs, so a wrong path+    // used to leave a real Open dialog on the user's screen. Both files must exist on this box+    // before any dialog is touched.+    let missing: Vec<&str> = [(&sch, "schPath"), (&brd, "brdPath")].iter().filter(|(p, _)| !std::path::Path::new(p.as_str()).is_file()).map(|(_, k)| *k).collect();+    if !missing.is_empty() {+        return json!({"success": false, "errorCode": "eagle_files_missing", "schPath": sch, "brdPath": brd, "missing": missing,+            "error": format!("Not started: {} not found on the Fusion machine. This verb drives Fusion's Open dialogs, so it refuses before touching them.", missing.join(" and ")),+            "_hint": "Push both files to the box first (send_files, a space-free folder is best) and pass their absolute Windows paths. No dialog was opened. If you only have a .sch, fusion_schematic {action:\"open\", schPath, withBoard:false} opens it dialog-free (schematic only); if the .brd already carries placed parts, fusion_open_board {filePath} instantiates them with no dialog either."});+    }     let Some(ab) = fusion_core::ab::Loopback::from_env() else {         return json!({"success": false, "errorCode": "use_open_board_instead", "error": "ab direct API unavailable - and this dialog-driven import FOREGROUNDS Fusion anyway.",             "_hint": format!("Don't drive the Open dialogs. If the parts are placed in the .brd (elements + embedded library), call `fusion_open_board {{filePath:\"{brd}\"}}` - it instantiates them via newDesignFromLocal with NO dialogs and NO foreground (the background way). For 3D bodies, embed <packages3d> + per-element package3d_urn in the .brd (see this verb's docstring), then fusion_show_3d_board. Only if you truly have a bare .sch that Fusion must import: fire 'Commands.Start ImportSCHAndBRDCmd' via fusion_execute_text_command and drive the two Open dialogs (foreground; last resort).")});@@ -855,6 +954,8 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V     use fusion_core::ab::AbClient;     let find_open_dialog = || -> Option<i64> { ab.call("desktop_list_windows", json!({}), &ctx.caller, 10).ok().and_then(|v| v["data"]["windows"].as_array().or_else(|| v["windows"].as_array()).cloned()).unwrap_or_default().iter().find(|w| w["title"].as_str().unwrap_or("").trim().to_lowercase() == "open").and_then(|w| w["hwnd"].as_i64()) };     let (sch_base, brd_base) = (sch.rsplit('/').next().unwrap_or("").to_string(), brd.rsplit('/').next().unwrap_or("").to_string());+    let mut stale_cancelled = false;+    if let Some(h) = find_open_dialog() { let _ = ab.call("desktop_close_window", json!({"hwnd": h}), &ctx.caller, 10); stale_cancelled = true; std::thread::sleep(Duration::from_secs(1)); }     let _ = pass("execute_text_command", json!({"command": "Commands.Start ImportSCHAndBRDCmd"}));     let mut picked: Vec<String> = Vec::new();     for (want, base) in [("sch", &sch_base), ("brd", &brd_base)] {@@ -865,6 +966,17 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V             return json!({"success": false, "error": format!("The '{want}' Open dialog never appeared."), "_hint": "Screenshot the Fusion hwnd + READ screenshots[] for a blocking modal; the importer may have errored on the EAGLE source. Verify the .sch/.brd are valid EAGLE.", "data": {"picked": picked}});         };         let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": base}), &ctx.caller, 15); std::thread::sleep(Duration::from_secs(1)); let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "Open"}), &ctx.caller, 15);+        // the dialog must be gone now; if the file was not in the folder the dialog showed, the+        // click missed and the dialog stays. Never leave it on the user's screen.+        let mut gone = false;+        for _ in 0..8 { std::thread::sleep(Duration::from_secs(1)); if find_open_dialog().map(|x| x != h).unwrap_or(true) { gone = true; break; } }+        if !gone {+            let _ = ab.call("desktop_close_window", json!({"hwnd": h}), &ctx.caller, 10);+            crate::fusion_prefs::action_ledger(&ctx.state_dir, "eagle_import_dialog_cancelled", json!({"wanted": base, "kind": want}));+            return json!({"success": false, "errorCode": "open_dialog_stuck", "wanted": base, "imported": picked,+                "error": format!("Fusion's Open dialog did not accept '{base}' (it was not in the folder the dialog showed, or the name did not match); the bridge cancelled the dialog so nothing is left on screen."),+                "_hint": "Nothing was imported. Put the .sch and .brd together in one space-free folder, pass both absolute paths, and retry; or use the dialog-free routes: fusion_schematic {action:\"open\", schPath, withBoard:false} for the schematic alone, fusion_open_board {filePath} for a .brd with placed parts."});+        }         picked.push(base.clone());         std::thread::sleep(Duration::from_secs(2));     }@@ -875,7 +987,7 @@ pub fn new_electronics_from_eagle(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, V         let d = &st["data"];         if d["isElectronics"].as_bool() == Some(true) && ["pcb editor", "schematic editor", "board layout", "3d pcb"].contains(&d["activeWorkspace"].as_str().unwrap_or("").to_lowercase().as_str()) { ready = true; break; }     }-    json!({"success": ready, "imported": picked, "statusVerb": "fusion_board_info",+    json!({"success": ready, "imported": picked, "staleDialogCancelled": stale_cancelled, "dialogsDriven": "Fusion's own Open dialogs (sch, then brd), driven in the background by accessible name; this is Autodesk's only route to a populated board from EAGLE source", "statusVerb": "fusion_board_info",         "_hint": if ready { "EAGLE design imported via Fusion's own ImportSCHAndBRDCmd (both Open dialogs driven in the background). It is now an electronics design in the PCB editor. NEXT: fusion_show_2d_board, then RATSNEST + 'AUTO ;' (fusion_electron_run) to autoroute, fusion_show_3d_board for the 3D. NOTE: board_info can read 0 right after import (wrong-view query) even though the board is populated - verify by screenshotting the Fusion hwnd (read screenshots[]) or fusion_show_2d_board + WINDOW FIT. If 3D bodies are missing, the placed parts' package3d urns must resolve in THIS project - build the library's 3D into this project with fusion_build_library_3d (do NOT reuse another library's urns; they don't resolve cross-project)." } else { "Import fired + both Open dialogs driven, but the design did not confirm as electronics within the budget (a slow VM import can run longer). Poll fusion_get_app_state, and screenshot the Fusion hwnd reading screenshots[] for a blocking import dialog." }}) } 
skills/fusion-electronics/SKILL.md+7
@@ -5,6 +5,13 @@ description: How to drive Fusion 360 Electronics (the EAGLE-based schematic + PC  # Fusion Electronics - drive the schematic, board, and 3D PCB +> **Opening or creating a schematic: `fusion_schematic`.** `{action:"open", fileName|urn|query}` opens a+> cloud design and switches to its schematic; `{action:"open", schPath}` opens a local EAGLE .sch (with+> its .brd when one sits beside it, so parts instantiate); `{action:"create", name}` writes a blank+> schematic and opens it as a new local design with no dialog; `{action:"show"}` switches the open+> design to the schematic view. The reply's `via` names the verb it used. Everything below is the+> detail behind that front door.+ ## 1. ALWAYS open the PROJECT, never a child file  A Fusion electronics design is NOT one file. It is a parent/child chain of separate files, each a
tools/check-naming.sh

  
tools/check-portable.sh

  
tools/promote.sh

  
tools/release.sh