← Commit history

bridge-rs: scope dialog scanning by pid (title matching caught foreign RemoteApp windows)

John Lauer ·622cf76979 ·1mo ago ·parent b38384f
2 files changed +48−30
CHANGELOG.md+4
@@ -1,5 +1,9 @@ # Changelog +## Unreleased — bridge-rs only (no change to the shipped Python runtime)+- **`check_dialogs` no longer sees other applications' windows.** The first cut matched window TITLES containing "Error"/"Warning" and picked up four *"RemoteApp security warning"* windows on John's desktop that had nothing to do with Altium — which `dismiss_dialogs` would then have clicked OK on. Pressing buttons in someone else's application is exactly what this bridge must never do. Now scoped by PID: `process_list` for Altium's pids, `desktop_enum_windows {pids}` for only those windows, then class + "not the main window". Also 300× faster (one enumeration instead of six title searches): **1 ms**.+- Note for the Python bridge: it was never affected — `handlers/dialogs.py` has always attributed dialogs by pid.+ ## 0.1.16 — 2026-09-01 - **Renamed `dump_board` → `read_board`** (agent command and Rust verb), before anything depended on it. John, on the 0.1.15 naming: *"that's not a great verb name ... we should probably pick a better verb thats more like a read* or write*"*. He is right — "dump" names the implementation rather than the caller's intent, and it has no counterpart, so it breaks the symmetry the moment writes arrive. The convention is now recorded in the dev skill and the Rust README: **`read_*` to get data out, `write_*` / `apply_*` to put it back.** Rust module `dump.rs` → `board.rs`, `BoardDump` → `BoardData`, fixture `read_board.json`. - For the record, since it came up: the name was mine, not inherited — no verb names were copied from any third-party Altium MCP server.
bridge-rs/src/dialogs.rs+44−30
@@ -4,43 +4,57 @@ use crate::ab; use serde_json::{json, Value}; -/// Window titles Altium uses for script/engine modals, plus the generic ones worth surfacing.-const MODAL_TITLES: [&str; 6] = ["Error", "Information", "Confirm", "Warning", "Altium Designer", "Script"];+/// Dialog window classes, from the Python bridge's measured list. NOTE `#32770` is the generic+/// Win32 dialog class — half the desktop uses it — so class alone is NEVER enough to decide a+/// window is Altium's. See `scan()`.+const DIALOG_CLASSES: [&str; 5] = ["TMessageForm", "#32770", "TForm", "TMessageDialog", "TForm_RunScriptParameters"]; -/// Windows that merely CONTAIN these are the app itself / our own tooling, never a modal.-fn is_main_window(title: &str) -> bool {-    crate::health::parse_title(title).is_some()-}--/// Enumerate blocking dialogs. Returns [] when the screen is clean.+/// Every dialog owned by an Altium process.+///+/// Scoped by PID, not by title. The first cut of this matched window titles containing "Error" /+/// "Warning" and immediately picked up four **"RemoteApp security warning"** windows that had+/// nothing to do with Altium (2026-09-01, on John's desktop) — which `dismiss_dialogs` would then+/// have clicked OK on. Pressing buttons in someone else's application is exactly the kind of thing+/// this bridge must never do, so: ask ab which pids are Altium (`process_list`), enumerate only+/// those windows (`desktop_enum_windows {pids}`), and let class + "not the main window" decide. pub fn scan() -> Vec<Value> {-    let mut out: Vec<Value> = Vec::new();-    let mut seen: Vec<i64> = Vec::new();-    for t in MODAL_TITLES {-        let r = ab::call("desktop_find_window", json!({"titleContains": t}), &[], 10);-        let wins = r-            .get("windows")-            .and_then(|a| a.as_array())-            .cloned()-            .unwrap_or_default();-        for w in wins {-            let hwnd = match w.get("hwnd").and_then(|h| h.as_i64()) {-                Some(h) => h,-                None => continue,-            };-            let title = w.get("title").and_then(|s| s.as_str()).unwrap_or("").to_string();-            if seen.contains(&hwnd) || is_main_window(&title) || title.is_empty() {-                continue;-            }-            seen.push(hwnd);-            out.push(json!({"hwnd": hwnd, "title": title,-                            "text": dialog_text(hwnd),-                            "buttons": buttons(hwnd)}));+    let pids: Vec<u64> = ab::call("process_list", json!({"nameContains": "X2"}), &[], 15)+        .get("processes")+        .and_then(|a| a.as_array())+        .map(|a| a.iter().filter_map(|p| p.get("pid").and_then(|v| v.as_u64())).collect())+        .unwrap_or_default();+    if pids.is_empty() {+        return Vec::new(); // Altium is not running — nothing of ours can be on screen+    }+    let r = ab::call("desktop_enum_windows", json!({"pids": pids}), &[], 20);+    let wins = r.get("windows").and_then(|a| a.as_array()).cloned().unwrap_or_default();+    let mut out = Vec::new();+    for w in wins {+        let title = w.get("title").and_then(|s| s.as_str()).unwrap_or("").to_string();+        let class = w.get("className").and_then(|s| s.as_str()).unwrap_or("").to_string();+        let hwnd = match w.get("hwnd").and_then(|h| h.as_i64()) {+            Some(h) => h,+            None => continue,+        };+        let visible = w.get("visible").and_then(|b| b.as_bool()).unwrap_or(true);+        if !visible || title.is_empty() || is_main_window(&title) {+            continue;         }+        if !DIALOG_CLASSES.iter().any(|c| c.eq_ignore_ascii_case(&class)) {+            continue;+        }+        out.push(json!({"hwnd": hwnd, "title": title, "className": class,+                        "pid": w.get("pid").cloned().unwrap_or(Value::Null),+                        "text": dialog_text(hwnd), "buttons": buttons(hwnd)}));     }     out } +/// Windows that merely CONTAIN the app name are the app itself, never a modal.+fn is_main_window(title: &str) -> bool {+    crate::health::parse_title(title).is_some()+}+ /// The message body — this is what tells a save prompt apart from a compile error. fn dialog_text(hwnd: i64) -> Value {     let r = ab::call("desktop_ui_text", json!({"hwnd": hwnd}), &[], 10);