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.40: 1.0.40: the bridge no longer crashes Windows Explorer. When a KiCad window reached the foreground during a verb, the bridge sent it behind and then forced the user's previous window back to the front (thread-input attach, ShowWindow, BringWindowToTop, SetForegroundWindow). Clicking a KiCad taskbar button makes the TASKBAR that previous window, so the bridge did this to the Windows 11 taskbar and Explorer crashed inside Taskbar.View.dll (0xc0000005), twice in a row on AdomLapper and earlier on winvm. Now: a shell window (taskbar, Start, desktop, anything owned by explorer.exe) is never recorded as the user's window and never forced to the front. And the user's own click is recognised properly: any input in the last 1.5 s with the cursor on the taskbar or on the KiCad window means the user brought it up, so it is left alone; the old test polled the mouse button every 120 ms and missed quick clicks, which is why a clicked KiCad window was sometimes sent away again. This bug is in every earlier native build, including public 1.0.37.
1 file changed
+29−3
rust/crates/kicad-platform/src/win/etiquette.rs+29−3@@ -67,6 +67,8 @@ use super::{focus, hwnd, hwnd_u64, is_kicad_exe, is_window, process, responsive} // ── Tunables, every one of them a number the Python carried ────────────────── +/// User input this recent, with the cursor on the shell or on the window, is the user's own click.+const USER_INPUT_GRACE_SECS: f64 = 1.5; /// Sentinel poll period. const POLL: Duration = Duration::from_millis(120); /// A click counts as "the user brought this up" for this long after it landed.@@ -471,6 +473,19 @@ fn cursor_over(h: u64) -> bool { !root.is_invalid() && hwnd_u64(root) == h } +/// A window that belongs to the Windows shell: by class, or because explorer.exe owns it. The bridge+/// never forces one of these to the foreground and never treats one as "the user's window".+fn is_shell_window(h: u64, cache: &mut ExeCache) -> bool {+ if h == 0 {+ return false;+ }+ if SHELL_CLASSES.contains(&class_name(hwnd(h)).as_str()) {+ return true;+ }+ let exe = cache.exe(pid_of(hwnd(h))).to_lowercase();+ exe.ends_with("explorer.exe") || exe.ends_with("shellexperiencehost.exe") || exe.ends_with("startmenuexperiencehost.exe") || exe.ends_with("searchhost.exe")+}+ fn cursor_over_shell() -> bool { let root = cursor_root(); !root.is_invalid() && SHELL_CLASSES.contains(&class_name(root).as_str())@@ -624,7 +639,11 @@ fn handle_foreground(h: u64, source: Source, cache: &mut ExeCache) { let mut g = lock(); sample_input(&mut g, now); expire_guards(&mut g, now);- let clicked = within(g.last_click, CLICK_WINDOW, now);+ // A click is the polled button state (120 ms sentinel, so a quick click is missed and the+ // user's own window was bounced "sometimes") OR any user input in the last 1.5 s. Either one+ // only counts with the cursor on the shell (a taskbar button, Start) or on this window:+ // someone typing in their editor while KiCad raises itself has the cursor elsewhere.+ let clicked = within(g.last_click, CLICK_WINDOW, now) || ago < USER_INPUT_GRACE_SECS; // Only pay for the hit tests when a click is recent. let clicked_here = clicked && (cursor_over(h) || cursor_over_shell()); let t = Transition {@@ -674,7 +693,8 @@ fn handle_foreground(h: u64, source: Source, cache: &mut ExeCache) { // Hand the activation back to the user's window (restoring what they had); // without this KiCad keeps the activation and raises itself again. let user = lock().last_user_fg;- let restored = user != 0 && user != h && matches!(focus::bring_to_front(hwnd(user)), Ok(true));+ let shell = user != 0 && is_shell_window(user, &mut ExeCache::default());+ let restored = user != 0 && user != h && !shell && matches!(focus::bring_to_front(hwnd(user)), Ok(true)); lock().bounce_holds.insert(h, now + BOUNCE_HOLD); log(&format!("[bg] {h}: sent behind ({result}); user window {user} {}.", if restored { "restored" } else { "not restored" })); match source {@@ -878,7 +898,13 @@ fn sentinel_loop() { } // 2. The transition. let fg = focus::foreground();- if fg != 0 && !is_kicad_exe(&exe_of(fg, &mut cache)) {+ // NEVER a shell window. Clicking a taskbar button makes the taskbar the foreground window for+ // a moment; recording it here meant the next bounce "restored" it with bring_to_front, which+ // attaches to the target's input thread and calls ShowWindow / BringWindowToTop /+ // SetForegroundWindow on it. Done to the Windows 11 taskbar that crashed Explorer inside+ // Taskbar.View.dll (0xc0000005), twice in a row, the moment John clicked a KiCad taskbar+ // button (AdomLapper, 2026-09-21; the same thing killed Explorer on winvm the night before).+ if fg != 0 && !is_kicad_exe(&exe_of(fg, &mut cache)) && !is_shell_window(fg, &mut cache) { lock().last_user_fg = fg; } if fg != last_fg {