← Commit history

Merge PR #414: macOS: find the PCB editor by its title when the KiCad bridge names no kind

John Lauer ·02e4db27c1 · ·parent 7c38229
1 file changed +80−3
crates/aiflow-bridge/src/lib.rs+80−3
@@ -293,6 +293,34 @@ fn read_socket_args(verb: &str, args: &Value, socket: Option<&str>) -> Value {     result } +/// The PCB editor's window in a `kicad_window_info` reply: the editor whose `kind` is `pcb_editor`.+/// The macOS KiCad bridge (0.11.x) names no kind at all (`{app, hwnd, pid, rect, title}`), so there,+/// and only when no editor carries a kind, it is the first window titled `<board> — PCB Editor`.+/// Before this, a Mac run fell back to the last window it had filmed, which+/// after `tour fields` or `tour 3d` is the closed Fields or 3D Viewer window: the net walkthrough+/// then framed and screenshotted nothing, and the next step's clip did not start.+pub fn pcb_editor_of(reply: &Value) -> Option<i64> {+    let d = reply.get("data").cloned().unwrap_or(reply.clone());+    let eds = d.get("editors").and_then(|e| e.as_array())?;+    let hwnd = |w: &Value| w.get("hwnd").and_then(|h| h.as_i64());+    if let Some(w) = eds.iter().find(|w| w.get("kind").and_then(|k| k.as_str()) == Some("pcb_editor")) {+        return hwnd(w);+    }+    // a bridge that names kinds and lists no PCB editor has none open: unchanged+    if eds.iter().any(|w| w.get("kind").map_or(false, |k| !k.is_null())) {+        return None;+    }+    eds.iter().find(|w| w.get("title").and_then(|t| t.as_str()).map_or(false, titled_pcb_editor)).and_then(hwnd)+}++/// "scratch_strip — PCB Editor" or "*board — PCB Editor" (unsaved), never the bare+/// "PCB Editor" strip the macOS bridge also lists, nor the "3D Viewer".+fn titled_pcb_editor(title: &str) -> bool {+    title.trim().strip_suffix("PCB Editor").map_or(false, |head| {+        !head.trim_end_matches(|c: char| c.is_whitespace() || matches!(c, '—' | '–' | '-' | '|')).trim_start_matches('*').trim().is_empty()+    })+}+ #[derive(Clone)] pub struct Bridge {     pub ai_thread: String,@@ -469,9 +497,7 @@ impl Bridge {     }      pub fn pcb_editor_hwnd(&self) -> Option<i64> {-        let r = self.call("kicad_window_info", &json!({}));-        let d = r.get("data").cloned().unwrap_or(r.clone());-        d.get("editors").and_then(|e| e.as_array()).and_then(|a| a.iter().find(|w| w.get("kind").and_then(|k| k.as_str()) == Some("pcb_editor"))).and_then(|w| w.get("hwnd")).and_then(|h| h.as_i64())+        pcb_editor_of(&self.call("kicad_window_info", &json!({})))     }      pub fn record_start(&self, reason: &str) -> Result<String, Value> {@@ -1050,3 +1076,54 @@ mod edge_overhang_tests {         assert!(fine.get("offendingRefs").is_none());     } }++#[cfg(test)]+mod pcb_editor_tests {+    use super::*;++    // kicad_window_info on the Mac (KiCad bridge 0.11.3, KiCad 10.0.7) with the 3D Viewer open, as the+    // Rev C strip run read it on 2026-11-02 (.aiflow-run-revc-edge-board/stop/kicad-window-info-3d-viewer-open.json)+    fn mac_reply() -> Value {+        json!({"data": {"autoDismissed": [], "editors": [+            {"app": "PCB Editor", "hwnd": 2893, "pid": 52411, "rect": {"height": 720, "width": 1280}, "title": "3D Viewer"},+            {"app": "PCB Editor", "hwnd": 2858, "pid": 52411, "rect": {"height": 44, "width": 1470}, "title": "PCB Editor"},+            {"app": "PCB Editor", "hwnd": 2846, "pid": 52411, "rect": {"height": 923, "width": 1470}, "title": "edge_board — PCB Editor"}],+            "hasModalDialogs": false, "modalDialogs": [], "projectManager": null},+            "output": "3 editor(s), 0 dialog(s)", "status": "ok", "success": true})+    }++    #[test]+    fn the_mac_editor_is_found_by_its_title() {+        assert_eq!(pcb_editor_of(&mac_reply()), Some(2846));+        // unsaved board: KiCad marks the title with a star+        let star = json!({"data": {"editors": [{"hwnd": 7, "title": "PCB Editor"}, {"hwnd": 8, "title": "*board — PCB Editor"}]}});+        assert_eq!(pcb_editor_of(&star), Some(8));+    }++    #[test]+    fn never_the_viewer_or_the_bare_strip() {+        let only_viewer = json!({"data": {"editors": [{"hwnd": 2893, "title": "3D Viewer"}, {"hwnd": 2858, "title": "PCB Editor"}]}});+        assert_eq!(pcb_editor_of(&only_viewer), None);+        assert!(!titled_pcb_editor("PCB Editor"));+        assert!(!titled_pcb_editor(" — PCB Editor"));+        assert!(!titled_pcb_editor("3D Viewer"));+        assert!(!titled_pcb_editor("board — Schematic Editor"));+        assert!(titled_pcb_editor("scratch_strip — PCB Editor"));+    }++    #[test]+    fn a_bridge_that_names_kinds_is_read_as_before() {+        // the Windows bridge: kind decides, the title is never used+        let win = json!({"data": {"editors": [+            {"kind": "schematic_editor", "hwnd": 1, "title": "board — Schematic Editor"},+            {"kind": "pcb_editor", "hwnd": 2, "title": "board — PCB Editor"}]}});+        assert_eq!(pcb_editor_of(&win), Some(2));+        // kinds named and no PCB editor among them: none, even with a PCB-like title+        let none = json!({"data": {"editors": [{"kind": "3d_viewer", "hwnd": 3, "title": "board — PCB Editor"}]}});+        assert_eq!(pcb_editor_of(&none), None);+        // the reply without a data wrapper, and no editors at all+        assert_eq!(pcb_editor_of(&json!({"editors": [{"kind": "pcb_editor", "hwnd": 9}]})), Some(9));+        assert_eq!(pcb_editor_of(&json!({"status": "error"})), None);+        assert_eq!(pcb_editor_of(&json!({"data": {"editors": []}})), None);+    }+}