app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
bridge-rs: scope dialog scanning by pid (title matching caught foreign RemoteApp windows)
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);