app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
← Commit history
1.0.31: NATIVE (Rust) build. 1.0.31: the foreground toast is debounced (foregroundToastMinutes setting, default 10, 0 = off); the on-screen caption on the driven box is unchanged.
4 files changed
+34−8
rust/Cargo.lock+3−3@@ -244,7 +244,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.30"+version = "1.0.31" dependencies = [ "kicad-core", "kicad-platform",@@ -255,7 +255,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.30"+version = "1.0.31" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -281,7 +281,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.30"+version = "1.0.31" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.30"+version = "1.0.31" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-bridge/src/verbs_libraries.rs+1−1@@ -62,7 +62,7 @@ pub static VERBS: &[Verb] = &[ mechanism: Mechanism::File, risk: "config", timeout_sec: 10, input: "{\"overlayBadges\"?: bool, \"activityIndicator\"?: \"progress|flash|both|none\", \"restoreDefaults\"?: bool}", example: "kicad_set_settings {\"overlayBadges\": false}",- hint: "kicad_set_settings {\"overlayBadges\": false}.",+ hint: "kicad_set_settings {\"overlayBadges\": false}. foregroundToastMinutes (default 10) is how often a foreground the calling AI asked for may announce itself with a toast, which ab mirrors to the user's other boxes; 0 turns the toast off, the 3 s on-screen caption on the driven box stays.", related: &["kicad_get_settings"], pitfalls: &["live re-badging of open KiCad windows arrives with the window verbs (phase 3); the setting persists now"], },
rust/crates/kicad-bridge/src/verbs_windows.rs+29−3@@ -804,8 +804,14 @@ fn bring_to_user(hwnd: u64, reason: &str) -> bool { if reason.trim().is_empty() { return false; }- let msg = format!("KiCad bridge needs your screen for ~3s: {reason}. Background methods were exhausted; this was explicitly allowed by the calling AI (foreground:true).");- let _ = ab::notify_user("Taking the foreground briefly", &msg, "warning");+ // The toast is debounced (settings.foregroundToastMinutes, 0 = never): ab mirrors it to+ // the user's other boxes, and one per verb from a busy thread was worse than the+ // foreground itself. The caption is local to the driven box and always shown.+ let toast = foreground_toast_due();+ if toast {+ let msg = format!("KiCad bridge needs your screen for ~3s: {reason}. Background methods were exhausted; this was explicitly allowed by the calling AI (foreground:true).");+ let _ = ab::notify_user("Taking the foreground briefly", &msg, "warning");+ } let _ = ab::desktop_caption("kicad-foreground", reason, 3000, "Announce the foreground the calling AI asked for before the KiCad window moves"); sleep_ms(1200); // let the caption render BEFORE the window moves native().etiquette_sanction(hwnd, 20.0); // the loop must not bounce a foreground the AI asked for@@ -814,10 +820,30 @@ fn bring_to_user(hwnd: u64, reason: &str) -> bool { Err(e) if is_not_impl(&e) => ab::desktop_bring_to_front(hwnd, reason).is_ok(), Err(_) => false, };- record_focus_event(json!({"event": "user-foregrounded", "hwnd": hwnd, "reason": reason, "ok": ok, "at": kicad_common::now_utc_iso()}));+ record_focus_event(json!({"event": "user-foregrounded", "hwnd": hwnd, "reason": reason, "ok": ok, "toast": toast, "at": kicad_common::now_utc_iso()})); ok } +static LAST_FOREGROUND_TOAST: Mutex<Option<Instant>> = Mutex::new(None);++/// One foreground toast per `foregroundToastMinutes`; 0 means never. Records the time+/// when it says yes.+fn foreground_toast_due() -> bool {+ let minutes = kicad_core::settings::get_settings().get("foregroundToastMinutes").and_then(Value::as_u64).unwrap_or(10);+ if minutes == 0 {+ return false;+ }+ let mut last = match LAST_FOREGROUND_TOAST.lock() {+ Ok(g) => g,+ Err(p) => p.into_inner(),+ };+ let due = last.map(|t| t.elapsed() >= Duration::from_secs(minutes * 60)).unwrap_or(true);+ if due {+ *last = Some(Instant::now());+ }+ due+}+ // ── Dialogs (close_windows.py) ──────────────────────────────────────────────── #[derive(Clone, Debug, Default)]