app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
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.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
//! The window etiquette around an add-in verb, native. Ported from server.py's
//! `_window_etiquette_pre` / `_window_etiquette_post` / `_SavePromptSentinel` and their
//! helpers: the document registry (what we open is ours to clean up), the foreground rule
//! (a reason and a 3s heads-up, else the background envelope), the save-prompt sentinel that
//! discards ONLY bridge documents, the post-verb chokepoint sweep, and the etiquette note.
//! Desktop-shaped work goes through ab verbs (desktop_find_control, desktop_ui_click,
//! desktop_close_window, desktop_taskbar); the Win32 envelope is `HostProcess::background_envelope`.
use serde_json::{json, Value};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use fusion_core::ab::{AbClient, Caller, Loopback};
static BADGED: AtomicBool = AtomicBool::new(false);
/// Every Fusion assertion the bridge clicked OK on (the background sentinel included), with when:
/// a verb's reply names the ones raised while it ran (issue #135: a fatal PlacePartWorker assert was
/// clicked away silently and the run went on).
static ASSERTS_SEEN: std::sync::Mutex<Vec<(f64, String)>> = std::sync::Mutex::new(Vec::new());
fn now_secs() -> f64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0) }
/// The routing assertion Fusion raises when a LINE completes a net's last airwire (pusher.cpp
/// inLocator) is informational; any other one means the command did not do its job.
pub fn assert_is_harmless(body: &str) -> bool { body.to_lowercase().contains("pusher.cpp") }
static BADGED_HWND: std::sync::Mutex<i64> = std::sync::Mutex::new(0);
/// `set_driving_badge`: the Adom mark on Fusion's taskbar button while Adom drives it.
pub fn set_badge(caller: &Caller, hwnd: i64) -> bool {
// The SAME badge the Python bridge painted (handlers/taskbar_overlay.py): our own 32 px PNG as
// base64, not ab's built-in "adom" badge, which draws larger (John, 2026-09-05).
static OVERLAY_PNG: &[u8] = include_bytes!("../../../resources/adom-overlay-32.png");
let ok = Loopback::from_env().map(|ab| ab.call("desktop_taskbar", json!({"hwnd": hwnd, "overlay": {"icon": b64_pub(OVERLAY_PNG), "tooltip": "Adom is driving Fusion"}}), caller, 10).is_ok()).unwrap_or(false);
if ok { BADGED.store(true, Ordering::Relaxed); *BADGED_HWND.lock().unwrap() = hwnd; }
ok
}
static BADGE_AT: std::sync::Mutex<Option<Instant>> = std::sync::Mutex::new(None);
/// set_badge, but a repeat on the same window within 60 s is a no-op (per-verb callers must not
/// spam desktop_taskbar), exactly the Python bridge's throttle.
pub fn set_badge_throttled(caller: &Caller, hwnd: i64) -> bool {
let same = *BADGED_HWND.lock().unwrap() == hwnd && BADGE_AT.lock().unwrap().map(|t| t.elapsed() < Duration::from_secs(60)).unwrap_or(false);
if same { return true; }
let ok = set_badge(caller, hwnd);
if ok { *BADGE_AT.lock().unwrap() = Some(Instant::now()); }
ok
}
/// `clear_driving_badge`: remove the taskbar badge from whichever window we badged last.
pub fn clear_badge(caller: &Caller) -> bool {
let h = *BADGED_HWND.lock().unwrap();
if h == 0 { return false; }
let ok = Loopback::from_env().map(|ab| ab.call("desktop_taskbar", json!({"hwnd": h, "overlay": {"badge": "none"}}), caller, 10).is_ok()).unwrap_or(false);
if ok { BADGED.store(false, Ordering::Relaxed); *BADGED_HWND.lock().unwrap() = 0; }
ok
}
static LAST_DRIVE_AT: std::sync::Mutex<f64> = std::sync::Mutex::new(0.0);
pub fn badge_active() -> bool { BADGED.load(Ordering::Relaxed) }
pub fn last_drive_at() -> Option<f64> { let v = *LAST_DRIVE_AT.lock().unwrap(); if v > 0.0 { Some(v) } else { None } }
pub fn b64_pub(b: &[u8]) -> String { b64(b) }
pub const SHOW_SURFACE_VERBS: &[&str] = &["show_symbol", "show_footprint", "show_3d_package", "show_library", "show_project", "show_schematic", "show_2d_board", "show_3d_board"];
const DONT_SAVE_LABELS: &[&str] = &["Don't Save", "Don\u{2019}t Save"];
const BRIDGE_DOC_BUILTINS: &[&str] = &["adom-test-", "adomdemosample", "adom-fusion-wrap", "ads8588sipm", "0603waf"];
pub struct Pre { pub args: Value, pub ran_backgrounded: bool, pub refusal: Option<Value>, pub docs_before: Option<Vec<String>>, pub fg: bool }
/// `_note_focus_event`: the quietness audit log, mirrored to focus-events.jsonl.
pub fn note_focus_event(state_dir: &Path, verb: &str, outcome: &str, reason: &str) {
let v = if verb.starts_with("fusion_") { verb.to_string() } else { format!("fusion_{verb}") };
let rec = json!({"at": chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(), "verb": v, "outcome": outcome, "reason": reason.chars().take(200).collect::<String>()});
use std::io::Write;
let _ = std::fs::create_dir_all(state_dir);
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(state_dir.join("focus-events.jsonl")) { let _ = writeln!(f, "{rec}"); }
}
/// `_snapshot_open_docs`: lowercase names of the documents open right now (empty on failure).
pub fn snapshot_open_docs(call: &dyn Fn(&str, Value, u64) -> Value) -> Vec<String> {
let r = call("document_info", json!({}), 5);
if r["success"].as_bool() != Some(true) { return vec![]; }
let mut v: Vec<String> = r["data"]["openDocuments"].as_array().map(|a| a.iter().filter_map(|d| d["name"].as_str()).filter(|n| !n.is_empty()).map(|n| n.trim_end_matches('*').trim().to_lowercase()).collect()).unwrap_or_default();
v.sort(); v.dedup(); v
}
/// Each open document's lowercase name and whether it is a saved cloud document (empty on failure).
fn snapshot_open_docs_detail(call: &dyn Fn(&str, Value, u64) -> Value) -> Vec<(String, bool)> {
let r = call("document_info", json!({}), 5);
if r["success"].as_bool() != Some(true) { return vec![]; }
r["data"]["openDocuments"].as_array().map(|a| a.iter().filter_map(|d| {
let n = d["name"].as_str().filter(|n| !n.is_empty())?.trim_end_matches('*').trim().to_lowercase();
Some((n, d["isCloudDocument"].as_bool() == Some(true)))
}).collect()).unwrap_or_default()
}
/// Is a document that appeared while `cmd` ran the bridge's own scratch? A saved cloud document
/// that appeared was OPENED (open_by_urn, a script's documents.open): it is its owner's work, and
/// registering it let the save-prompt sentinel answer Don't Save for it and the restart discard
/// it. So were the documents Fusion opens at launch (recovered work). Only a document the verb
/// made, or a Save As the bridge itself did, is scratch.
pub fn appeared_doc_is_scratch(cmd: &str, cloud: bool) -> bool { cmd != "start" && (!cloud || cmd == "save_to_cloud") }
fn bridge_docs(state_dir: &Path) -> Vec<String> {
crate::small_verbs::get_pref(state_dir, "bridgeDocs").and_then(|v| v.as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).filter(|s| s.len() >= 3).collect())).unwrap_or_default()
}
/// `_is_bridge_doc_text`: the bridge-doc name the text mentions, if any.
pub fn bridge_doc_in(state_dir: &Path, text: &str) -> Option<String> {
let t = text.to_lowercase();
bridge_docs(state_dir).into_iter().find(|n| t.contains(n.as_str())).or_else(|| BRIDGE_DOC_BUILTINS.iter().find(|p| t.contains(*p)).map(|s| s.to_string()))
}
/// `_find_controls_any`: {best, controls} from anywhere in ab's answer.
pub fn find_controls_any(v: &Value, depth: u8) -> Value {
if depth > 5 { return json!({}); }
if let Some(s) = v.as_str() { if s.starts_with('{') || s.starts_with('[') { if let Ok(p) = serde_json::from_str::<Value>(s) { return find_controls_any(&p, depth + 1); } } return json!({}); }
if let Some(o) = v.as_object() {
if o.get("best").map(|b| b.is_object()).unwrap_or(false) || o.get("controls").map(|c| c.is_array()).unwrap_or(false) {
return json!({"best": o.get("best").cloned().unwrap_or(json!({})), "controls": o.get("controls").cloned().unwrap_or(json!([]))});
}
for (_, x) in o { let r = find_controls_any(x, depth + 1); if r.is_object() && !r.as_object().unwrap().is_empty() { return r; } }
}
if let Some(a) = v.as_array() { for x in a { let r = find_controls_any(x, depth + 1); if r.is_object() && !r.as_object().unwrap().is_empty() { return r; } } }
json!({})
}
/// `_find_save_prompt`: is the unsaved-doc Save prompt up? {found, hwnd, button, text}.
pub fn find_save_prompt(ab: &dyn AbClient, caller: &Caller, pids: &[u32]) -> Value {
let dialogs = {
let mut a = json!({}); if !pids.is_empty() { a["pids"] = json!(pids); }
match ab.call("desktop_enum_windows", a, caller, 10) { Ok(v) => fusion_core::dialogs::qt_dialog_rows(&v["windows"].as_array().or_else(|| v["data"]["windows"].as_array()).cloned().unwrap_or_default()), Err(_) => return json!({"found": false}) }
};
for d in dialogs {
let Some(h) = d["hwnd"].as_i64().filter(|h| *h != 0) else { continue };
let title = d["title"].as_str().unwrap_or("").trim().to_lowercase();
for label in DONT_SAVE_LABELS {
let r = ab.call("desktop_find_control", json!({"hwnd": h, "name": label}), caller, 10).map(|v| find_controls_any(&v, 0)).unwrap_or(json!({}));
let best = &r["best"];
if best["name"].as_str().map(|n| !n.is_empty()).unwrap_or(false) && best["invokable"].as_bool().unwrap_or(false) {
let mut text = String::new();
if let Ok(t) = ab.call("desktop_find_control", json!({"hwnd": h, "name": "save"}), caller, 10) {
let t = find_controls_any(&t, 0);
let mut cands = vec![t["best"].clone()]; cands.extend(t["controls"].as_array().cloned().unwrap_or_default());
for c in cands { let nm = c["name"].as_str().unwrap_or(""); if nm.len() > text.len() { text = nm.to_string(); } }
}
return json!({"found": true, "hwnd": h, "button": best["name"], "text": text});
}
}
if ["save", "save changes", "unsaved changes"].contains(&title.as_str()) { return json!({"found": true, "hwnd": h, "button": Value::Null, "text": ""}); }
}
json!({"found": false})
}
/// Fusion Electronics' own "unexpected condition" assertion (pusher.cpp inLocator, seen when a
/// LINE completes a net's last airwire): OK is its only button and is informational, the copper
/// has already landed. Click it and hand back the body text so the caller can checkpoint.
pub fn dismiss_unexpected_condition(ab: &dyn AbClient, caller: &Caller, pids: &[u32]) -> Option<String> {
for d in crate::small_verbs::classify_blocking_dialogs(ab, caller, pids) {
if d["category"].as_str() != Some("fusion_unexpected_condition") { continue; }
let Some(h) = d["hwnd"].as_i64() else { continue };
let body = ab.call("desktop_find_controls", json!({"hwnd": h}), caller, 10).ok().map(|r| { let dd = if r["data"].is_object() { r["data"].clone() } else { r }; dd["controls"].as_array().or(dd["matches"].as_array()).cloned().unwrap_or_default().iter().filter_map(|c| c["name"].as_str()).filter(|n| n.to_lowercase().contains("unexpected condition")).map(str::to_string).next().unwrap_or_default() }).unwrap_or_default();
let clicked = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12).map(|r| r["invoked"].as_bool().unwrap_or(false) || r["success"].as_bool().unwrap_or(false)).unwrap_or(false);
eprintln!("[fusion-bridge] Fusion Electronics assertion dialog {}: {}", if clicked { "dismissed (OK)" } else { "seen, OK did not land" }, body.chars().take(160).collect::<String>());
if let Ok(mut v) = ASSERTS_SEEN.lock() { v.push((now_secs(), body.clone())); if v.len() > 50 { v.remove(0); } }
return Some(if body.is_empty() { "Fusion Electronics has detected an unexpected condition".into() } else { body });
}
None
}
/// `_dismiss_save_prompt_if_bridge_doc`: Don't Save for OUR documents only; a user's prompt is never touched.
pub fn dismiss_save_prompt_if_bridge_doc(state_dir: &Path, caller: &Caller, pids: &[u32], main_title: &str) -> Value {
let Some(ab) = Loopback::from_env() else { return json!({"handled": false, "why": "no ab loopback"}) };
let prompt = find_save_prompt(&ab, caller, pids);
if prompt["found"].as_bool() != Some(true) { return json!({"handled": false, "why": "no save prompt"}); }
let hwnd = prompt["hwnd"].as_i64().unwrap_or(0);
let text = prompt["text"].as_str().unwrap_or("").to_string();
let matched = bridge_doc_in(state_dir, &text).or_else(|| bridge_doc_in(state_dir, main_title));
let Some(matched) = matched else {
// an unattributed prompt: only if the main thread is actually blocked, and only after
// two sightings, answer Cancel (WM_CLOSE: non-destructive, the doc stays open unsaved)
let st = crate::addin::addin_status_probe(Duration::from_secs(2));
let blocked = st.as_ref().map(|s| s["busy"].as_bool().unwrap_or(false)).unwrap_or(true);
if !blocked { return json!({"handled": false, "why": "unattributed dialog but the main thread is responsive - zombie window, leaving it alone", "hwnd": hwnd}); }
let n = { let mut g = STRIKES.lock().unwrap(); let e = g.entry(hwnd).or_insert(0); *e += 1; *e };
if n >= 2 {
if ab.call("desktop_close_window", json!({"hwnd": hwnd}), caller, 15).is_ok() {
eprintln!("[fusion-bridge] save-prompt sentinel: cancelled an unattributed prompt after {n} sightings (WM_CLOSE - non-destructive, main thread freed)");
STRIKES.lock().unwrap().remove(&hwnd);
return json!({"handled": true, "why": format!("escaped unattributed prompt (Cancel) after {n} sightings")});
}
}
return json!({"handled": false, "why": format!("unattributed save prompt - strike {n} of 2 before Escape"), "hwnd": hwnd});
};
if let Some(b) = prompt["button"].as_str() {
if let Ok(r) = ab.call("desktop_ui_click", json!({"hwnd": hwnd, "name": b}), caller, 15) {
if r["invoked"].as_bool().unwrap_or(false) || r["success"].as_bool().unwrap_or(false) {
eprintln!("[fusion-bridge] save-prompt sentinel: clicked {b:?} for bridge doc {matched:?} (background)");
return json!({"handled": true, "why": format!("clicked '{b}' for bridge doc '{matched}'")});
}
}
}
if let Ok(r) = ab.call("desktop_ui_click", json!({"hwnd": hwnd, "name": "Don't Save"}), caller, 15) {
if r["invoked"].as_bool().unwrap_or(false) || r["success"].as_bool().unwrap_or(false) {
crate::fusion_prefs::action_ledger(state_dir, "save_prompt_dismiss", json!({"doc": matched, "how": "dont_save"}));
return json!({"handled": true, "why": format!("clicked Don't Save for bridge doc '{matched}'")});
}
}
if ab.call("desktop_close_window", json!({"hwnd": hwnd}), caller, 15).is_ok() {
crate::fusion_prefs::action_ledger(state_dir, "save_prompt_dismiss", json!({"doc": matched, "how": "escape"}));
return json!({"handled": true, "why": format!("escaped (Cancel) for bridge doc '{matched}'")});
}
json!({"handled": false, "why": "dismiss attempts did not land", "hwnd": hwnd})
}
static STRIKES: std::sync::Mutex<std::collections::BTreeMap<i64, u32>> = std::sync::Mutex::new(std::collections::BTreeMap::new());
/// `_SavePromptSentinel`: poll for the Save prompt while a doc-switching call runs.
pub struct Sentinel { stop: Arc<AtomicBool>, deadline: Arc<std::sync::Mutex<Instant>> }
impl Sentinel {
pub fn start(state_dir: std::path::PathBuf, caller: Caller, pids: Vec<u32>, max_seconds: f64) -> Sentinel {
let stop = Arc::new(AtomicBool::new(false));
let deadline = Arc::new(std::sync::Mutex::new(Instant::now() + Duration::from_secs_f64(max_seconds.max(5.0))));
let (s2, d2) = (stop.clone(), deadline.clone());
std::thread::spawn(move || {
while !s2.load(Ordering::Relaxed) && Instant::now() < *d2.lock().unwrap() {
let _ = dismiss_save_prompt_if_bridge_doc(&state_dir, &caller, &pids, "");
if let Some(ab) = Loopback::from_env() { let _ = dismiss_unexpected_condition(&ab, &caller, &pids); }
std::thread::sleep(Duration::from_secs(1));
}
});
Sentinel { stop, deadline }
}
/// keep covering the just-returned call for a short grace (the post-timeout prompt is the wedge)
pub fn soft_stop(&self, grace: f64) { let mut d = self.deadline.lock().unwrap(); let g = Instant::now() + Duration::from_secs_f64(grace); if g < *d { *d = g; } }
#[allow(dead_code)]
pub fn hard_stop(&self) { self.stop.store(true, Ordering::Relaxed); }
}
const BACKGROUND_NOTE: &str = "Window opened/changed in the BACKGROUND - the user's focus was NOT touched; do NOT foreground anything to compensate. Render results via fusion_state. If the USER asks to watch, re-call with {\"foreground\": true, \"foregroundReason\": \"<why, shown to the user as a 3s caption>\"}.";
/// `_window_etiquette_pre` plus the document snapshot and the long-running pre-dismiss.
pub fn pre(host: &dyn fusion_core::HostProcess, state_dir: &Path, caller: &Caller, cmd: &str, args: &Value, pids: &[u32], call: &dyn Fn(&str, Value, u64) -> Value) -> Pre {
let window_raising = crate::addin::WINDOW_RAISING.contains(&cmd);
let docs_before = if window_raising { Some(snapshot_open_docs(call)) } else { None };
let fg = args["foreground"].as_bool().unwrap_or(false) || (args["foreground"].is_string() && !args["foreground"].as_str().unwrap_or("").is_empty());
let mut out = Pre { args: args.clone(), ran_backgrounded: false, refusal: None, docs_before, fg };
if window_raising {
if fg {
let reason = args["foregroundReason"].as_str().unwrap_or("").trim().to_string();
if reason.is_empty() {
note_focus_event(state_dir, cmd, "refused", "");
out.refusal = Some(json!({"success": false, "errorCode": "foreground_reason_required",
"error": "foreground:true needs foregroundReason (a short user-facing sentence).",
"_hint": "Fusion verbs ALWAYS run in the background so the user is never interrupted mid-typing. If the user genuinely wants to WATCH, re-call with foreground:true AND foregroundReason:\"<why, in words the user will read>\" - it is shown as an ab caption for 3 seconds before the window comes forward, so they can yield."}));
return out;
}
note_focus_event(state_dir, cmd, "foreground", &reason);
if let Some(ab) = Loopback::from_env() {
let _ = ab.call("notify_user", json!({"title": "Fusion is coming to the foreground", "body": format!("{reason} (Requested by your AI - Fusion verbs otherwise always run in the background.)"), "level": "info", "target": "all"}), caller, 10);
std::thread::sleep(Duration::from_secs(3));
}
if let Some(o) = out.args.as_object_mut() { o.remove("foreground"); o.remove("foregroundReason"); }
} else {
let secs = crate::addin::timeout_secs_or(cmd, 60) as f64 + 20.0;
host.background_envelope(secs, cmd == "start");
if crate::small_verbs::get_pref(state_dir, "showDrivingBadge").map(|v| v != json!(false)).unwrap_or(true) {
// the SAME small PNG badge as set_badge (John, 2026-09-07: this path painted ab's
// built-in "adom" badge, which draws far larger than pup's overlays and than the
// Python bridge ever did), throttled like the Python bridge's set_driving_badge
if let Some(ab) = Loopback::from_env() { if let Some(h) = fusion_core::windows_facts::facts_from_ab(&ab, caller).main_hwnd { set_badge_throttled(caller, h); } }
*LAST_DRIVE_AT.lock().unwrap() = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs_f64()).unwrap_or(0.0);
}
out.ran_backgrounded = true;
}
}
if ["walk_cloud_tree", "search_cloud_files"].contains(&cmd) {
if let Some(ab) = Loopback::from_env() {
let dialogs = crate::small_verbs::classify_blocking_dialogs(&ab, caller, pids);
for d in &dialogs { if let Some(h) = d["hwnd"].as_i64() { let _ = ab.call("desktop_close_window", json!({"hwnd": h}), caller, 10); } }
if !dialogs.is_empty() { std::thread::sleep(Duration::from_millis(500)); }
}
}
out
}
/// `_window_etiquette_post` plus the recent-actions ledger on a failed slow verb.
pub fn post(state_dir: &Path, caller: &Caller, cmd: &str, args: &Value, mut res: Value, pre: &Pre, since: f64, pids: &[u32], call: &dyn Fn(&str, Value, u64) -> Value) -> Value {
let window_raising = crate::addin::WINDOW_RAISING.contains(&cmd);
let failed = res["success"].as_bool() == Some(false);
if failed && (window_raising || ["walk_cloud_tree", "search_cloud_files"].contains(&cmd)) {
let acts = ledger_entries_since(state_dir, since, 12);
if !acts.is_empty() && res.is_object() {
if res.get("recentActions").is_none() { res["recentActions"] = json!(acts); }
if res.get("_ledgerHint").is_none() { res["_ledgerHint"] = json!("recentActions = every background action the bridge took on the host app while this verb ran (newest last); fusion_action_ledger {sinceSeconds} for more."); }
}
}
// Fusion's own router assertion modal blocks the main thread; clear it BEFORE the document
// snapshot below asks the add-in anything (2026-09-07: that snapshot hung 19 s per verb and
// the routing test read as an add-in crash)
if window_raising {
if let Some(ab) = Loopback::from_env() {
// the assertion lands a moment AFTER the command returns: look for it a few times
let mut found = None;
for i in 0..4 { found = dismiss_unexpected_condition(&ab, caller, pids); if found.is_some() { break; } if i < 3 { std::thread::sleep(Duration::from_millis(300)); } }
let _ = found.take();
// every assertion clicked while this verb ran (the sentinel's too): a harmless routing one is
// noted; any other one fails the verb (#135: PlacePartWorker assetAdded = the ADD did not land)
let seen: Vec<String> = ASSERTS_SEEN.lock().map(|v| v.iter().filter(|(t, _)| *t >= since).map(|(_, b)| b.clone()).collect()).unwrap_or_default();
let fatal: Vec<&String> = seen.iter().filter(|b| !assert_is_harmless(b)).collect();
if !fatal.is_empty() && res.is_object() {
res["success"] = json!(false);
if res["errorCode"].is_null() || res["errorCode"] == json!("") { res["errorCode"] = json!("fusion_assert"); }
res["fusionAsserts"] = json!(seen);
let e = res["error"].as_str().unwrap_or("").to_string();
res["error"] = json!(format!("Fusion raised an internal error during this verb ({}); the bridge clicked OK so Fusion is not blocked, but the command did not complete. {e}", fatal[0].chars().take(160).collect::<String>()).trim().to_string());
}
let found = seen.into_iter().last();
if let Some(body) = found { if res.is_object() { res["fusionUnexpectedCondition"] = json!(body); let h = res["_hint"].as_str().unwrap_or("").to_string(); res["_hint"] = json!(format!("{h} Fusion Electronics raised its 'unexpected condition' assertion during this verb and the bridge clicked OK; checkpoint the board now (fusion_export_eagle_source) and verify the copper with fusion_routing_state.").trim().to_string()); } }
}
}
if let Some(before) = &pre.docs_before {
// a short, undiagnosed add-in hop: a hung main thread must cost 3 s here, not the 5 s
// call plus a 14 s timeout diagnosis (2026-09-07, the routing test's 60 s relay window)
let quick = |verb: &str, a: Value, _t: u64| crate::addin::addin_http(verb, &a, 3);
let _ = call;
for (name, cloud) in snapshot_open_docs_detail(&quick) {
if before.contains(&name) || name.is_empty() || name == "untitled" { continue; }
if appeared_doc_is_scratch(cmd, cloud) { crate::chains::note_bridge_doc_pub(state_dir, &name); eprintln!("[fusion-bridge] registered {name:?} as a bridge doc (it appeared while we drove)"); }
else { eprintln!("[fusion-bridge] {name:?} appeared during {cmd} but is an existing document someone opened, not bridge scratch"); }
}
}
if SHOW_SURFACE_VERBS.contains(&cmd) {
for k in ["name", "deviceset", "libraryPath", "fileName", "query"] { if let Some(v) = args[k].as_str().filter(|s| !s.is_empty()) { crate::chains::note_bridge_doc_pub(state_dir, v); } }
if args["capture"].as_bool() == Some(true) && res["success"].as_bool() != Some(false) { res["shot"] = capture_surface_shot(caller, cmd, args, call); }
}
if window_raising {
let main_title = Loopback::from_env().and_then(|ab| fusion_core::windows_facts::facts_from_ab(&ab, caller).main_title).unwrap_or_default();
let sw = dismiss_save_prompt_if_bridge_doc(state_dir, caller, pids, &main_title);
if sw["handled"].as_bool() == Some(true) && res.is_object() { res["savePromptCleared"] = sw["why"].clone(); }
if let Some(ab) = Loopback::from_env() {
for d in crate::small_verbs::classify_blocking_dialogs(&ab, caller, pids) {
let (cat, h) = (d["category"].as_str().unwrap_or(""), d["hwnd"].as_i64().unwrap_or(0));
match cat {
"upload_in_progress" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "No"}), caller, 12); if res.is_object() { res["uploadCloseDeclined"] = json!(true); } eprintln!("[fusion-bridge] answered the Hub upload-close confirm with No (work-preserving; close retried after jobs drain)"); }
"create_confirm" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "No"}), caller, 12); if res.is_object() { res["createPromptDeclined"] = json!(true); } eprintln!("[fusion-bridge] declined a standing Create-new prompt at the chokepoint (raised by a raw EDIT; Yes would fabricate an empty artifact)"); }
"launch_error" | "lbr_open_error" | "fusion_unexpected_condition" | "board_opened_with_warnings" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12); eprintln!("[fusion-bridge] dismissed a standing {cat} dialog at the chokepoint"); }
_ => {}
}
}
}
if res.is_object() {
if pre.fg { if res.get("windowEtiquette").is_none() { res["windowEtiquette"] = json!("foreground"); } }
else {
if res.get("windowEtiquette").is_none() { res["windowEtiquette"] = json!("background"); }
let h = res["_hint"].as_str().map(str::to_string);
if !h.as_deref().map(|h| h.contains("focus was NOT touched")).unwrap_or(false) { res["_hint"] = json!(match h { Some(h) => format!("{h} {BACKGROUND_NOTE}"), None => BACKGROUND_NOTE.to_string() }); }
}
}
}
res
}
/// `_ledger_entries_since`.
pub fn ledger_entries_since(state_dir: &Path, t0: f64, limit: usize) -> Vec<Value> {
let Ok(t) = std::fs::read_to_string(state_dir.join("actions.jsonl")) else { return vec![] };
let lines: Vec<&str> = t.lines().collect();
let tail = &lines[lines.len().saturating_sub(400)..];
let out: Vec<Value> = tail.iter().filter_map(|l| serde_json::from_str::<Value>(l).ok()).filter(|e| e["epoch"].as_f64().unwrap_or(0.0) >= t0 - 1.0).collect();
out[out.len().saturating_sub(limit)..].to_vec()
}
/// `_capture_surface_shot`: settle on the add-in going idle, then the frame ab already resized.
fn capture_surface_shot(caller: &Caller, verb: &str, args: &Value, call: &dyn Fn(&str, Value, u64) -> Value) -> Value {
let label = match verb { "show_symbol" => "Symbol", "show_footprint" => "Footprint", "show_3d_package" => "3D package", "show_library" => "Library", "show_project" => "Project", "show_schematic" => "Schematic", "show_2d_board" => "2D board", "show_3d_board" => "3D board", v => v };
let deadline = Instant::now() + Duration::from_secs_f64(args["captureTimeoutSeconds"].as_f64().unwrap_or(120.0));
while Instant::now() < deadline { if call("get_app_state", json!({}), 6)["success"].as_bool() == Some(true) { break; } std::thread::sleep(Duration::from_secs(3)); }
std::thread::sleep(Duration::from_secs_f64(args["capturePaintDelay"].as_f64().unwrap_or(2.5)));
let shot = crate::small_verbs::screenshot_fusion(&json!({}), caller);
let Some(path) = shot["savedTo"].as_str().filter(|_| shot["success"].as_bool() == Some(true)).map(str::to_string) else { return json!({"title": label, "image": Value::Null, "error": shot["error"].as_str().unwrap_or("capture failed")}) };
let max_w = (args["captureMaxWidth"].as_i64().unwrap_or(640)).clamp(120, 1600);
// the ~640px frame Python resamples is served here as ab's own safe-size copy (<=1400px)
let small = shot["safePath"].as_str().map(str::to_string).unwrap_or_else(|| path.clone());
let img = std::fs::read(&small).ok().map(|b| b64(&b));
json!({"title": label, "image": img, "path": path, "maxWidth": max_w, "thumbnailDataUrl": img.as_ref().map(|i| format!("data:image/png;base64,{i}"))})
}
fn b64(bytes: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4);
for c in bytes.chunks(3) {
let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
out.push(T[(n >> 18) as usize & 63] as char); out.push(T[(n >> 12) as usize & 63] as char);
out.push(if c.len() > 1 { T[(n >> 6) as usize & 63] as char } else { '=' }); out.push(if c.len() > 2 { T[n as usize & 63] as char } else { '=' });
}
out
}
#[cfg(test)]
mod tests {
#[test]
fn bridge_doc_text_and_ledger_window() {
let d = std::env::temp_dir().join(format!("fb-etq-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
crate::small_verbs::set_pref(&d, "bridgeDocs", serde_json::json!(["demo_board", "lan7800"]));
assert_eq!(super::bridge_doc_in(&d, "Save changes to Demo_Board?").as_deref(), Some("demo_board"));
assert_eq!(super::bridge_doc_in(&d, "ADOM-TEST-42 has unsaved changes").as_deref(), Some("adom-test-"));
assert_eq!(super::bridge_doc_in(&d, "MyRobotArm v3"), None);
crate::fusion_prefs::action_ledger(&d, "x", serde_json::json!({"k": 1}));
assert_eq!(super::ledger_entries_since(&d, 0.0, 12).len(), 1);
assert_eq!(super::ledger_entries_since(&d, 4e9, 12).len(), 0);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn opened_documents_are_not_bridge_scratch() {
use super::appeared_doc_is_scratch as scratch;
// 2026-10-28: four of the user's assemblies, reopened with open_by_urn, were registered
assert!(!scratch("open_by_urn", true));
assert!(!scratch("run_modeling_script", true));
// Fusion's own launch (recovered documents) is never the bridge's
assert!(!scratch("start", false));
// what a verb made is scratch, and so is the bridge's own Save As
assert!(scratch("import_step", false));
assert!(scratch("make_3d_package", false));
assert!(scratch("save_to_cloud", true));
}
#[test]
fn b64_matches_python() { assert_eq!(super::b64(b"hello"), "aGVsbG8="); assert_eq!(super::b64(b""), ""); assert_eq!(super::b64(b"ab"), "YWI="); }
}