app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← Commit history
Merge PR #414: macOS: find the PCB editor by its title when the KiCad bridge names no kind
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);+ }+}