← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.67\ No newline at end of file+1.13.68\ No newline at end of file
CHANGELOG.md+8
@@ -3,6 +3,14 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.13.68 (2026-11-03)++`fusion_project_create` makes a design with its sheet and board linked (`linked: true`), through Fusion's own EAGLE import, so a part placed on the sheet goes on the board. It used to open two unlinked documents, which is now only the fallback when the import can't run (`linked: false`, `whyNotLinked`). If Adom Bridge is waiting for an OK, it answers `stillRunning` with a `runId`.++A script that opens a design and meets Fusion's "will be opened with warnings" notice no longer hangs until the call times out: the bridge answers the notice with OK and says so (`noticesAnswered`). From AI Flow.++Add-in 1.0.33: its text no longer names the retired `fusion_send_key` and `fusion_click_fusion`. Text only; Fusion needs one restart to load it.+ ## 1.13.67 (2026-11-03)  The `fusion_open_board` and `fusion_open_schematic` refusal handling from 1.13.66 now runs on the path those verbs really take: in 1.13.66 the modal still hung the call.
addin/AdomBridge/AdomBridge.manifest+1−1
@@ -6,7 +6,7 @@     "description": {         "": "Adom Desktop Conduit bridge add-in. Exposes a localhost HTTP API for external automation of Fusion 360."     },-    "version": "1.0.32",+    "version": "1.0.33",     "runOnStartup": true,     "supportedOS": "windows" }\ No newline at end of file
addin/AdomBridge/commands/close_document.py+2−2
@@ -158,7 +158,7 @@ def handle_close_document(app: adsk.core.Application, args: dict) -> dict:         return {             "success": False,             "error": f"Failed to close document: {e}",-            "_hint": "Retry with {\"save\": false} to discard changes, or call fusion_screenshot_fusion to check for a blocking Save dialog and dismiss it with fusion_send_key.",+            "_hint": "Retry with {\"save\": false} to discard changes, or call fusion_screenshot_fusion to check for a blocking Save dialog and answer it with fusion_dismiss_blocking_dialogs.",         }  @@ -202,7 +202,7 @@ def handle_close_all_documents(app: adsk.core.Application, args: dict) -> dict:         return {             "success": False,             "error": f"Failed to enumerate documents: {e}",-            "_hint": "Retry with {\"save\": false} to discard changes, or call fusion_screenshot_fusion to check for a blocking Save dialog and dismiss it with fusion_send_key.",+            "_hint": "Retry with {\"save\": false} to discard changes, or call fusion_screenshot_fusion to check for a blocking Save dialog and answer it with fusion_dismiss_blocking_dialogs.",         }      for doc in snapshot:
addin/AdomBridge/commands/cloud_documents.py+4−4
@@ -937,7 +937,7 @@ def handle_open_cloud_file(app: adsk.core.Application, args: dict) -> dict:         return {             "success": False,             "error": f"Failed to open '{target.name}' after {len(attempts)} attempts: {last_err}",-            "_hint": "Call fusion_screenshot to check for a recovery dialog or other modal, then fusion_send_key with {\"key\":\"escape\"} or click to dismiss it, then retry.",+            "_hint": "Call fusion_screenshot to check for a recovery dialog or other modal, then fusion_dismiss_blocking_dialogs to answer it, then retry.",             "data": {"fileId": target.id, "attempts": attempts},         } @@ -1017,14 +1017,14 @@ def handle_open_cloud_file(app: adsk.core.Application, args: dict) -> dict:             "steps": [                 "Run desktop_list_windows to find the Fusion HWND",                 "Run desktop_screenshot_window with that HWND",-                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"} or fusion_click_fusion on the Cancel/X button",+                "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs",                 "Screenshot again to confirm the dialog is gone",             ],             "commonDialogs": [-                {"name": "What to design?", "dismiss": "fusion_send_key {\"key\": \"escape\"} or click Cancel"},+                {"name": "What to design?", "dismiss": "fusion_dismiss_blocking_dialogs"},                 {"name": "PCB out of date", "dismiss": "Click Update or X to dismiss"},                 {"name": "Recovered Documents", "dismiss": "fusion_dismiss_recovery"},-                {"name": "Save changes?", "dismiss": "fusion_send_key {\"key\": \"escape\"} to cancel"},+                {"name": "Save changes?", "dismiss": "fusion_dismiss_blocking_dialogs"},             ],         },     }
addin/AdomBridge/commands/export.py+1−1
@@ -23,7 +23,7 @@ _HINT_FORMAT_UNAVAILABLE = ( ) _HINT_EXPORT_FAILED = (     "If this persists, call fusion_screenshot_fusion to check for a blocking dialog, "-    "then fusion_send_key with 'escape' or 'enter' to dismiss it. "+    "then fusion_dismiss_blocking_dialogs to answer it. "     "Wrong-workspace errors: call fusion_show_3d_board first." ) _HINT_FILE_LOCKED = (
addin/AdomBridge/commands/import_file.py+2−2
@@ -81,7 +81,7 @@ def handle_import_file(app: adsk.core.Application, args: dict) -> dict:             return {                 "success": False,                 "error": f"Import returned null for {path.name}",-                "_hint": "Call fusion_screenshot_fusion to check for a blocking dialog (e.g. unit prompt), then fusion_send_key with 'enter' to accept defaults, then retry.",+                "_hint": "Call fusion_screenshot_fusion to check for a blocking dialog (e.g. unit prompt), then fusion_dismiss_blocking_dialogs, then retry.",             }          # Activate the newly imported document so subsequent commands work on it@@ -103,5 +103,5 @@ def handle_import_file(app: adsk.core.Application, args: dict) -> dict:         return {             "success": False,             "error": f"Import failed: {e}",-            "_hint": "Call fusion_screenshot to check for a modal dialog, dismiss with fusion_send_key escape, then retry.",+            "_hint": "Call fusion_screenshot to check for a modal dialog, answer with fusion_dismiss_blocking_dialogs, then retry.",         }
addin/AdomBridge/commands/open_electronics_file.py+6−6
@@ -173,7 +173,7 @@ def _open_electronics(app: adsk.core.Application, file_path: str, file_type: str         return {             "success": False,             "error": f"Failed to open {file_type}: {e}",-            "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, then fusion_send_key escape to dismiss it, then retry.",+            "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, then fusion_dismiss_blocking_dialogs to answer it, then retry.",             "data": {                 "filePath": file_path,                 "hint": ("PATH WITH SPACES? This verb now quotes the path and falls back to the "@@ -232,7 +232,7 @@ def _open_electronics(app: adsk.core.Application, file_path: str, file_type: str             "steps": [                 "Run desktop_list_windows to find the Fusion HWND",                 "Run desktop_screenshot_window with that HWND",-                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"} or fusion_click_fusion on the Cancel/X button",+                "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs",                 "Screenshot again to confirm the dialog is gone",             ],         },@@ -376,7 +376,7 @@ def handle_show_3d_board(app: adsk.core.Application, args: dict) -> dict:         "data": {             "activeWorkspace": new_ws,             "nextSteps": [-                "FIRST: Screenshot Fusion window to check for blocking dialogs โ€” dismiss with fusion_send_key escape",+                "FIRST: Screenshot Fusion window to check for blocking dialogs โ€” answer with fusion_dismiss_blocking_dialogs",                 "Use fusion_take_screenshot to capture the 3D board view",                 "Use fusion_show_2d_board to return to 2D layout",                 "Use fusion_close_document to close without saving when done",@@ -387,7 +387,7 @@ def handle_show_3d_board(app: adsk.core.Application, args: dict) -> dict:             "steps": [                 "Run desktop_list_windows to find the Fusion HWND",                 "Run desktop_screenshot_window with that HWND",-                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",+                "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs",                 "Screenshot again to confirm the dialog is gone",             ],         },@@ -521,7 +521,7 @@ def handle_import_electronics(app: adsk.core.Application, args: dict) -> dict:             "steps": [                 "Run desktop_list_windows to find the Fusion HWND",                 "Run desktop_screenshot_window with that HWND",-                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",+                "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs",                 "Screenshot again to confirm the dialog is gone",             ],         },@@ -652,7 +652,7 @@ def handle_show_schematic(app: adsk.core.Application, args: dict) -> dict:             "required": True,             "steps": [                 "Run desktop_screenshot_window on the Fusion HWND",-                "If a dialog is visible, dismiss it with fusion_send_key {\"key\": \"escape\"}",+                "If a dialog is visible, answer it with fusion_dismiss_blocking_dialogs",             ],         },     }
addin/AdomBridge/commands/open_lbr.py+1−1
@@ -67,7 +67,7 @@ def handle_open_lbr(app: adsk.core.Application, args: dict) -> dict:         return {             "success": False,             "error": f"Document.newDesignFromLocal failed: {e}",-            "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, dismiss with fusion_send_key escape, then retry.",+            "_hint": "Call fusion_screenshot to check for a modal dialog blocking Fusion, answer with fusion_dismiss_blocking_dialogs, then retry.",             "data": {"filePath": file_path},         } 
addin/AdomBridge/http_server.py+1−1
@@ -400,7 +400,7 @@ class AdomBridgeHandler(BaseHTTPRequestHandler):                     "Add-in is busy with a long-running command. Do NOT retry add-in commands. "                     "Do NOT press Escape โ€” the add-in is working, not stuck on a dialog. "                     "Commands that still work: fusion_state, fusion_screenshot_fusion, "-                    "fusion_click_fusion, fusion_send_key, fusion_close_window."+                    "fusion_close_window."                 ),             }             if walk:
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.67",+  "version": "1.13.68",   "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.",
contracts/describe.json+1−1
@@ -20734,7 +20734,7 @@         "steps": "[str]"       },       "statusVerb": null,-      "summary": "A new, empty electronics design (the standard's project_create): an empty schematic and board written as EAGLE source in <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as the schematic's global attributes) and opened in Fusion as a new design, dialog-free. Save it to the cloud with fusion_save_to_cloud.",+      "summary": "A new, empty electronics design (the standard's project_create): an empty schematic and board written as EAGLE source in <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as the schematic's global attributes) and imported into Fusion as one design with its sheet and board linked, so a part placed on the sheet goes on the board (linked:true, about 30 s, through Fusion's own import in the background; Adom Bridge may ask for an OK, and then it answers stillRunning with a runId for fusion_eagle_import_status). If the import can't run, it opens the two files as unlinked documents and says why (linked:false, whyNotLinked). Save it to the cloud with fusion_save_to_cloud.",       "timeoutSeconds": 180     },     {
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.13.67"+version = "1.13.68" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.67"+version = "1.13.68" edition = "2021"  [dependencies]
describe.py+1−1
@@ -394,7 +394,7 @@ _T = [      {"expectDocument": "the design's name (default the document in front)", "label": "added to the folder name", "outputDir": "default Documents/Adom Snapshots/<design>", "pdfs": "bool, default true"},      {"folder": "path", "files": "[path]", "pdfs": "{made: [path], skipped}", "notes": "[str]"},      300, None, False, {"expectDocument": "BQ25792", "label": "before-reroute"}),-    ('fusion_project_create', "A new, empty electronics design (the standard's project_create): an empty schematic and board written as EAGLE source in <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as the schematic's global attributes) and opened in Fusion as a new design, dialog-free. Save it to the cloud with fusion_save_to_cloud.",+    ('fusion_project_create', "A new, empty electronics design (the standard's project_create): an empty schematic and board written as EAGLE source in <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as the schematic's global attributes) and imported into Fusion as one design with its sheet and board linked, so a part placed on the sheet goes on the board (linked:true, about 30 s, through Fusion's own import in the background; Adom Bridge may ask for an OK, and then it answers stillRunning with a runId for fusion_eagle_import_status). If the import can't run, it opens the two files as unlinked documents and says why (linked:false, whyNotLinked). Save it to the cloud with fusion_save_to_cloud.",      {"parentDir": "a folder on the Fusion machine (required)", "name": "the design's name (required)", "title": "str", "layers": "2|4|6|8, default 2", "textVariables": "{'REV': 'A'}", "fabRules": "a fab from adom/pcb-design-rules, recorded for later", "overwrite": "bool", "dryRun": "bool"},      {"project": "folder", "files": "[sch, brd]", "layers": "int", "layerSetup": "str", "steps": "[str]"},      180, None, False, {"parentDir": "C:/Users/me/Documents/Boards", "name": "Sensor-Hub", "layers": 4, "title": "Sensor hub", "textVariables": {"REV": "A"}, "dryRun": True}),
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.13.67"+DASH_VERSION="1.13.68" 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.13.67",+  "version": "1.13.68",   "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+2−2
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.67",+  "version": "1.13.68",   "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": {@@ -152,4 +152,4 @@   "confirmed_user_skills": [     "fusion-design-efficiency"   ]-}\ No newline at end of file+}
rust/Cargo.lock+5−5
@@ -19,7 +19,7 @@ dependencies = [  [[package]] name = "adom-fusion-bridge"-version = "1.13.67"+version = "1.13.68" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.67"+version = "1.13.68" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.67"+version = "1.13.68" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.67"+version = "1.13.68" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.67"+version = "1.13.68" dependencies = [  "fusion-core",  "windows-sys 0.59.0",
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.13.67"+version = "1.13.68" edition = "2021" license = "MIT" 
rust/bridge/src/addin.rs+4
@@ -322,6 +322,10 @@ pub fn pass_through(cmd: &str, args: &Value, ctx: &Ctx) -> Value {     if matches!(cmd, "open_board" | "open_schematic") {         return crate::orchestrations::open_file_guarded(ctx, cmd, args, &|v: &str, a: Value| pass_through_checked(v, &a, ctx));     }+    if cmd == "run_modeling_script" {+        let watch = crate::small_verbs::watch_for_open_notice(&ctx.caller, &ctx.pids);+        return crate::small_verbs::finish_notice_watch(watch, pass_through_checked(cmd, args, ctx));+    }     pass_through_checked(cmd, args, ctx) } 
rust/bridge/src/small_verbs.rs+46
@@ -490,6 +490,46 @@ pub fn finish_picker_watch(watch: Option<PickerWatch>, mut r: Value) -> Value {     r } +/// While a script runs: Fusion's "board file has errors and will be opened with warnings" notice. A script+/// that opens such a design doesn't return until it's answered, and a call between machines gives up at 120 s+/// with nothing to say why (AI Flow, 2026-11-03). OK is its only answer, so the bridge gives it. A script+/// shorter than the first look costs nothing.+pub struct NoticeWatch { stop: std::sync::Arc<std::sync::atomic::AtomicBool>, answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>> }++pub fn watch_for_open_notice(caller: &fusion_core::ab::Caller, fusion_pids: &[u32]) -> Option<NoticeWatch> {+    let ab = fusion_core::ab::Loopback::from_env()?;+    let (stop, answered) = (std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), std::sync::Arc::new(std::sync::Mutex::new(Vec::new())));+    let (stop2, answered2, caller, pids) = (stop.clone(), answered.clone(), caller.clone(), fusion_pids.to_vec());+    std::thread::spawn(move || {+        use fusion_core::ab::AbClient;+        use std::sync::atomic::Ordering;+        let pause = |secs: u64| -> bool { for _ in 0..(secs * 2) { if stop2.load(Ordering::SeqCst) { return false; } std::thread::sleep(std::time::Duration::from_millis(500)); } true };+        if !pause(6) { return; }+        loop {+            for d in classify_blocking_dialogs(&ab, &caller, &pids) {+                if d["category"] != "board_opened_with_warnings" { continue; }+                if let Some(h) = d["hwnd"].as_i64() {+                    let clicked = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK", "role": "Button"}), &caller, 15).map(|v| v["success"].as_bool() != Some(false)).unwrap_or(false);+                    answered2.lock().unwrap().push(json!({"title": d["title"], "answeredWith": "OK", "clicked": clicked}));+                }+            }+            if !pause(3) { return; }+        }+    });+    Some(NoticeWatch { stop, answered })+}++pub fn finish_notice_watch(watch: Option<NoticeWatch>, mut r: Value) -> Value {+    let Some(w) = watch else { return r };+    w.stop.store(true, std::sync::atomic::Ordering::SeqCst);+    let answered = w.answered.lock().unwrap().clone();+    if !answered.is_empty() && r.is_object() {+        r["noticesAnswered"] = json!(answered);+        r["_hintNotice"] = json!("Fusion opened a board file with errors and said so; the bridge answered its notice with OK. Read the board before trusting it: fusion_routing_state lists any invalidViaNets, and fusion_set_layer_count at the same count repairs them.");+    }+    r+}+ /// Fusion's "<file> has errors and cannot be opened" modal, by its text: (hwnd, what it says). pub fn file_refused_dialog(ab: &dyn fusion_core::ab::AbClient, caller: &fusion_core::ab::Caller, fusion_pids: &[u32]) -> Option<(i64, String)> {     let mut args = json!({});@@ -765,6 +805,12 @@ mod image_delivery_tests {         assert_eq!(r["pickerClosed"], true);         assert!(r["error"].as_str().unwrap().contains("C:/a/l.lbr and C:/b/l.lbr"));     }+    #[test] fn an_answered_notice_is_said_in_the_reply() {+        let w = Some(NoticeWatch { stop: Default::default(), answered: std::sync::Arc::new(std::sync::Mutex::new(vec![json!({"answeredWith": "OK"})])) });+        let r = finish_notice_watch(w, json!({"success": true}));+        assert_eq!(r["noticesAnswered"][0]["answeredWith"], "OK");+        assert!(finish_notice_watch(None, json!({"success": true}))["noticesAnswered"].is_null());+    }     #[test] fn no_picker_leaves_the_reply_alone() {         assert_eq!(finish_picker_watch(watch(None), json!({"success": true}))["success"], true);         assert_eq!(finish_picker_watch(None, json!({"success": true}))["success"], true);
rust/bridge/src/stdshapes.rs+1−1
@@ -441,7 +441,7 @@ pub fn update_pcb(args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {     json!({"success": true, "executed": execute && !behind, "changeOrder": [],         "preview": {"addParts": add, "removeParts": remove, "sheetParts": cc["schematicComponents"], "boardParts": cc["boardComponents"]},         "sheet": cc["sheet"], "board": cc["board"],-        "_hint": if behind { "addParts are on the sheet and not on the board. A supply symbol or frame has no package and stays that way; a part with a footprint means this design's board is not linked to its sheet (a fusion_project_create design): start from fusion_board_seed and fusion_new_electronics_from_eagle, which link them." } else { "Fusion keeps the board in step with the sheet as you edit, so there is nothing to run. removeParts are only on the board and stay there." }})+        "_hint": if behind { "addParts are on the sheet and not on the board. A supply symbol or frame has no package and stays that way; a part with a footprint means this design's board is not linked to its sheet (opened from loose EAGLE files, or a fusion_project_create that answered linked:false): fusion_project_create, or fusion_board_seed with fusion_new_electronics_from_eagle, make a linked one." } else { "Fusion keeps the board in step with the sheet as you edit, so there is nothing to run. removeParts are only on the board and stay there." }}) }  /// `fusion_select_items` (the standard's select_items): select parts by designator, or clear the selection.
rust/bridge/src/testproject.rs+38−4
@@ -317,8 +317,8 @@ pub(crate) fn board_layers() -> String {  /// The standard's project_create: an empty schematic and board as EAGLE source in /// <parentDir>/<name>/ (the board's copper count in its layer setup; title and text variables as-/// the schematic's global attributes), opened in Fusion as a new design through the dialog-free-/// Document.newDesignFromLocal. Saving it to the cloud is fusion_save_to_cloud.+/// the schematic's global attributes), imported into Fusion as a linked design, or opened as two unlinked+/// documents when the import can't run (linked:false). Saving it to the cloud is fusion_save_to_cloud. pub fn project_create(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Value {     let name = args["name"].as_str().unwrap_or("").trim().to_string();     let parent = args["parentDir"].as_str().unwrap_or("").replace('\\', "/");@@ -342,6 +342,40 @@ pub fn project_create(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Val     let brd = format!("{HEAD}{brd_layers}<board><plain/><libraries/><attributes/><variantdefs/><classes><class number=\"0\" name=\"default\" width=\"0\" drill=\"0\"/></classes><designrules name=\"default\"><param name=\"layerSetup\" value=\"{setup}\"/></designrules><autorouter/><elements/><signals/></board></drawing></eagle>\n");     if let Err(e) = std::fs::create_dir_all(&dir) { return json!({"success": false, "errorCode": "write_failed", "error": format!("{dir}: {e}")}); }     if let Err(e) = std::fs::write(&sch_path, &sch).and_then(|_| std::fs::write(&brd_path, &brd)) { return json!({"success": false, "errorCode": "write_failed", "error": e.to_string()}); }+    // Fusion's EAGLE import makes a design whose sheet and board are linked: a part placed on the sheet is on+    // the board (live 2026-11-03, 25 s on an empty project). Opening the two files with newDesignFromLocal+    // makes two unlinked documents, so that's only the fallback when the import can't run.+    let why_not_linked;+    if args["linked"].as_bool() != Some(false) {+        let started = crate::orchestrations::new_electronics_from_eagle(ctx, &json!({"schPath": sch_path, "brdPath": brd_path}), pass);+        if let Some(run) = started["runId"].as_str().filter(|_| started["success"].as_bool() == Some(true)) {+            let mut stage = Value::Null;+            let mut record = Value::Null;+            for _ in 0..30 {+                std::thread::sleep(Duration::from_secs(3));+                record = crate::orchestrations::eagle_import_status(ctx, &json!({"runId": run}));+                stage = record["stage"].clone();+                if stage == "done" || stage == "failed" { break; }+            }+            if stage == "done" {+                crate::chains::note_bridge_doc_pub(&ctx.state_dir, &name);+                let doc = pass("get_app_state", json!({}))["data"]["activeDocument"].clone();+                return json!({"success": true, "mutated": true, "linked": true, "documentName": doc, "project": dir, "files": [sch_path, brd_path], "layers": layers, "layerSetup": setup,+                    "fabRules": args["fab"].clone().as_str().or(args["fabRules"].as_str()).map(|f| json!({"recorded": f, "apply": "fusion_set_design_rules {fab} once the board has content"})),+                    "_hint": "An empty electronics design with its sheet and board linked: a part placed on the sheet goes on the board. Fusion names it by its own rules (documentName). Save it to the cloud with fusion_save_to_cloud {name, projectName, folderPath}; the EAGLE files stay in project as the source."});+            }+            if stage != "failed" {+                return json!({"success": true, "mutated": true, "stillRunning": true, "started": true, "runId": run, "statusVerb": "fusion_eagle_import_status", "project": dir, "files": [sch_path, brd_path], "layers": layers, "layerSetup": setup,+                    "needsApproval": started["needsApproval"], "tellUser": started["tellUser"],+                    "_hint": "Fusion's import is still running, likely waiting on Adom Bridge's approval to fill its Open dialog. Poll fusion_eagle_import_status {runId}."});+            }+            why_not_linked = json!(record["result"]["error"].as_str().unwrap_or("Fusion's EAGLE import failed"));+        } else {+            why_not_linked = json!(started["error"].as_str().unwrap_or("Fusion's EAGLE import could not start"));+        }+    } else {+        why_not_linked = json!("linked:false was asked for");+    }     let mut steps: Vec<String> = Vec::new();     for (label, fpath) in [("board", &brd_path), ("schematic", &sch_path)] {         let fp = crate::fusionpath::for_fusion(&ctx.state_dir, fpath).unwrap_or_else(|_| fpath.clone());@@ -355,7 +389,7 @@ pub fn project_create(ctx: &Ctx, args: &Value, pass: &dyn Fn(&str, Value) -> Val         std::thread::sleep(Duration::from_secs(2));     }     crate::chains::note_bridge_doc_pub(&ctx.state_dir, &name);-    json!({"success": true, "mutated": true, "project": dir, "files": [sch_path, brd_path], "layers": layers, "layerSetup": setup, "steps": steps,+    json!({"success": true, "mutated": true, "linked": false, "whyNotLinked": why_not_linked, "project": dir, "files": [sch_path, brd_path], "layers": layers, "layerSetup": setup, "steps": steps,            "fabRules": args["fab"].clone().as_str().or(args["fabRules"].as_str()).map(|f| json!({"recorded": f, "apply": "fusion_set_design_rules {fab} once the board has content"})),-           "_hint": "An empty schematic and board, open in Fusion as a new design. Save it to the cloud with fusion_save_to_cloud {name, projectName, folderPath}; the EAGLE files stay in project as the source."})+           "_hint": "An empty schematic and board, open as two documents that are NOT linked (whyNotLinked): parts placed on the sheet stay off the board. Save it to the cloud with fusion_save_to_cloud {name, projectName, folderPath}; the EAGLE files stay in project as the source."}) }
tests/open_refusal_drill.py+9
@@ -49,4 +49,13 @@ g = call('fusion_get_app_state'); check('Fusion still answers', g.get('success') r = call('fusion_sch_place_part', {'library': a.lib, 'symbol': 'RES', 'designator': 'R9', 'x': 3000, 'y': 4000}); check('another library still places', r.get('success'), r.get('error')) for n in docs():     if n != 'Untitled' or docs().count('Untitled') > 1: call('fusion_close_document', {'name': n})+print('--- fusion_project_create is linked')+for layers in (2, 4):+    r = call('fusion_project_create', {'name': 'OLink%d' % layers, 'parentDir': D, 'layers': layers, 'overwrite': True}); check('%d layers: linked' % layers, r.get('linked') is True and r.get('documentName'), {k: r.get(k) for k in ('success', 'linked', 'whyNotLinked', 'error', 'stillRunning')})+    time.sleep(2); no_dialogs('after it')+    call('fusion_show_schematic', {}); r = call('fusion_sch_place_part', {'library': a.lib, 'symbol': 'RES', 'designator': 'R1', 'x': 3000, 'y': 3000}); check('a part goes on the sheet', r.get('success'), r.get('error'))+    call('fusion_show_2d_board', {}); s = call('fusion_board_summary', {}); names = [c.get('designator') for c in (s.get('summary') or s).get('components') or []]+    check('and is on the board', 'R1' in names, {k: s.get(k) for k in ('success', 'errorCode', 'error')})+    d0 = call('fusion_get_app_state').get('data', {}).get('activeDocument'); n = len(call('fusion_routing_state', {'expectDocument': d0}).get('copperLayers') or []); check('with %d copper layers' % layers, n == layers, n)+    call('fusion_close_document', {'name': d0}) print('%d PASS, %d FAIL' % (sum(results), len(results) - sum(results))); sys.exit(0 if all(results) else 1)