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
0.2.42 (STAGED, no tier): bounded altium_panel with named answers + altium_demo carries the failing beat's error (issue 13); main.rs version stamp re-synced (0.2.40/41 pushes had omitted it)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
12 files changed
+160−52
CHANGELOG.md+7@@ -1,5 +1,12 @@ # Changelog +## 0.2.42 (2026-09-23), STAGED for the licensed battery, not on any tier+Fixes the two defects John's licensed live test of the held 0.2.41 found (adom/altium-bridge#13, arav-rog, AD 26.10.1).++- **`altium_panel` answers inside 24 s with a named result, every time.** It was not a loop that never ended: it made up to five sequential UIA searches (read, select, read, click, read) with 10+15+10+15+10 s timeouts, exactly ab's 60 s budget, and each search of Altium's large window tree ran into its timeout, so the verb ended in ab's generic `timed_out` with the abandoned searches still running (the likely cause of the collateral `run_drc` timeout that followed it in the battery). Now one search reads the tab's existence and selection together, then at most one select and one verify, inside a 24 s budget; the answers are `already`, `ui_select`, `panel_not_found`, `panel_search_timeout` (with the stage), `ui_select_gated`, `panel_not_selected`. The click fallback is gone (a click does not switch panel tabs), and when ab's select falls back to its raising SendInput click the reply says `lane: ui_select_raised` instead of hiding a foreground steal.+- **`altium_demo` carries the failing beat's own error.** A failed beat came back as a bare `success:false` (John saw it on Altium Designer Develop). The reason was real and discarded: the tour opens Altium's bundled AD26 example projects, which a Develop install does not have (`file not found`). The reply now carries the beat's `errorCode`/`error`/`_hint`, names this case `demo_examples_missing`, and falls back to `demo_beat_failed`.++ ## Unreleased (docs, 2026-09-21) 0.2.39 was promoted to the PUBLIC tier (John Lauer's call and his words; public had been on 0.2.9, so 0.2.13-0.2.39 all landed at once), then re-verified by installing from the public manifest as a stranger would and running the full battery against that artifact: 121 pass, 0 fail, 1 safety skip.
bridge-rs/src/main.rs+1−1@@ -29,7 +29,7 @@ use tiny_http::{Header, Method, Response, Server}; /// Attached to every failed reply (see the /command handler). const REPORT_ISSUES: &str = "If this looks like an altium-bridge BUG (not a refusal you expected), report it: check for a duplicate with `adom-wiki issue list adom/altium-bridge`, then `adom-wiki issue create adom/altium-bridge --title \"<verb>: <one-line symptom>\" --category bug-report --body \"<what you ran, what you expected, what happened, the VERBATIM response JSON (and errorShot if present), bridgeVersion, the Altium version from altium_status, the OS>\"`. A relay, routing, toast or lifecycle problem belongs on adom/adom-bridge instead. Reports from real machines are how this bridge gets correct: three different Altium builds behaved three different ways in one weekend."; -pub const BRIDGE_VERSION: &str = "0.2.39";+pub const BRIDGE_VERSION: &str = "0.2.42"; fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> { let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
bridge-rs/src/misc.rs+81−42@@ -112,70 +112,109 @@ pub fn instances_verb(_args: &Value) -> Value { support::seat_hints(out, None, n) } -/// Activate a docked panel TAB in the background (#12: the "ganged-up view" - symbol on canvas,-/// 3D chip in the Editor preview, footprint in the SCH Library panel - needed a human click).-/// No server process activates a tab: `Client:CustomizeResources Action=Show|ObjectKind=Panel`-/// only makes sure the panel EXISTS. So this is UIA on the dock tab: SelectionItemPattern first,-/// InvokePattern as the fallback, and the tab's own selection state read back as the verdict.+/// Activate a docked panel TAB in the background (the "ganged-up view": symbol on canvas, 3D chip+/// in the Editor preview, footprint in the SCH Library panel). No server process activates a tab:+/// `Client:CustomizeResources Action=Show|ObjectKind=Panel` only makes sure the panel EXISTS.+///+/// Bounded by construction. A UIA search by name walks Altium's whole window tree and can take+/// longer than the per-call timeout on a licensed box, so the first cut (read, select, read,+/// click, read at 10+15+10+15+10 s) spent exactly ab's 60 s budget and answered ab's generic+/// timeout, and the searches it abandoned kept ab busy for the next verb. Now: ONE discovery+/// call that reads existence and selection together, at most ONE select, ONE verify, all inside+/// PANEL_BUDGET, and every exit is a named answer. No click fallback: a click does not switch a+/// panel tab. If ab's select falls back to its raising SendInput click, the reply says so.+const PANEL_BUDGET_SEC: u64 = 24;++fn find_tab(hwnd: i64, name: &str, secs: u64) -> Result<Option<Value>, ()> {+ let r = ab::call("desktop_find_controls", json!({"hwnd": hwnd, "name": name, "maxResults": 5}), &[], secs.max(1));+ if r.get("success").and_then(|b| b.as_bool()) == Some(false) && r.get("matches").is_none() {+ return Err(());+ }+ let matches = r.get("matches").and_then(|m| m.as_array()).cloned().unwrap_or_default();+ // prefer the element UIA calls a tab; else the first exact-name match+ let tab = matches.iter().find(|m| m.get("role").and_then(|r| r.as_str())+ .map(|r| r.to_ascii_lowercase().contains("tab")).unwrap_or(false))+ .or_else(|| matches.first()).cloned();+ Ok(tab)+}++fn tab_selected(m: &Value) -> Option<bool> {+ m.get("selected").or_else(|| m.get("isSelected")).and_then(|b| b.as_bool())+}+ pub fn panel(args: &Value) -> Value { let name = match s(args, "name") { Some(n) if !n.is_empty() => n,- _ => return json!({"success": false, "error": "name is required - the dock tab's caption exactly as painted, e.g. 'SCH Library', 'PCB Library', 'Components', 'Projects', 'Properties'"}),+ _ => return json!({"success": false, "errorCode": "name_required", "error": "name is required - the dock tab's caption exactly as painted, e.g. 'SCH Library', 'PCB Library', 'Components', 'Projects', 'Properties'"}), }; let lic = instance::licence_state(); let hwnd = match lic.get("hwnd").and_then(|h| h.as_i64()) { Some(h) if lic.get("running").and_then(|b| b.as_bool()) == Some(true) => h, _ => return json!({"success": false, "errorCode": "altium_not_running"}), };- // Optional: make sure the panel exists first (its tab cannot be selected before Altium has- // created it). Costs an agent round trip, so only on request.+ let t0 = std::time::Instant::now();+ let left = || PANEL_BUDGET_SEC.saturating_sub(t0.elapsed().as_secs()); let mut ensured = Value::Null; if let Some(id) = s(args, "ensureId") { let r = agent::call("run_process", &[("command", "Client:CustomizeResources".into()), ("params", format!("Action=Show|ObjectKind=Panel|ID={id}"))],- None, Duration::from_secs(30));+ None, Duration::from_secs(10)); ensured = json!({"id": id, "ok": r.get("success").and_then(|b| b.as_bool()).unwrap_or(false)}); }- let read_selected = || -> Option<bool> {- ab::call("desktop_ui_get", json!({"hwnd": hwnd, "name": name}), &[], 10)- .get("selected").and_then(|b| b.as_bool())+ let timed_out = |stage: &str, t0: std::time::Instant| json!({"success": false, "errorCode": "panel_search_timeout",+ "panel": name, "stage": stage, "elapsedSec": t0.elapsed().as_secs_f64(), "ensured": ensured,+ "_hint": "Altium's UIA tree did not answer inside the panel budget. The tab may be undocked, hidden, or the caption spelled differently (case matters). Pass ensureId (e.g. TSchLibPanel) if the panel may not exist yet; the search is abandoned, nothing was clicked."});+ // 1. discovery: existence + selection in one search+ let tab = match find_tab(hwnd, &name, left().min(10)) {+ Err(_) => return timed_out("find", t0),+ Ok(None) => return json!({"success": false, "errorCode": "panel_not_found", "panel": name, "ensured": ensured,+ "elapsedSec": t0.elapsed().as_secs_f64(),+ "_hint": "No control with that caption on Altium's main window. Check the spelling against the painted tab, and pass ensureId if the panel was never opened in this session."}),+ Ok(Some(m)) => m, };- let already = read_selected();- if already == Some(true) {+ if tab_selected(&tab) == Some(true) { return json!({"success": true, "panel": name, "selected": true, "lane": "already",- "ensured": ensured, "windowEtiquette": "background"});+ "ensured": ensured, "elapsedSec": t0.elapsed().as_secs_f64()});+ }+ if left() < 4 {+ return timed_out("select", t0); }- let mut attempted = Vec::new();- // SelectionItemPattern is the correct pattern for a tab; a plain click does not switch- // panel tabs (measured 2026-09-02).+ // 2. one select let sel = ab::call("desktop_ui_select", json!({"hwnd": hwnd, "name": name,- "reason": format!("activate the '{name}' panel tab in the background (altium_panel)")}), &[], 15);- let sel_ok = sel.get("success").and_then(|b| b.as_bool()).unwrap_or(false);- attempted.push(json!({"lane": "ui_select", "ok": sel_ok,- "error": sel.get("error").and_then(|e| e.as_str()).map(|e| e.chars().take(160).collect::<String>())}));- if sel_ok || read_selected() == Some(true) {- return json!({"success": true, "panel": name, "selected": true, "lane": "ui_select",- "attempted": attempted, "ensured": ensured, "windowEtiquette": "background"});+ "reason": format!("activate the '{name}' panel tab in the background (altium_panel)")}), &[], left().min(10));+ let err = sel.get("error").and_then(|e| e.as_str()).unwrap_or("").to_string();+ if err.contains("approval") || err.contains("SHELL") {+ return json!({"success": false, "errorCode": "ui_select_gated", "panel": name, "ensured": ensured,+ "elapsedSec": t0.elapsed().as_secs_f64(),+ "_hint": "ab gates desktop_ui_select behind the shell approval grant (Adom Bridge footer 'Auto' button / Settings > Shell Commands > Auto mode). Turn it on, or select the tab by hand."});+ }+ if sel.get("raised").and_then(|b| b.as_bool()) == Some(true) {+ // ab fell back to a SendInput click, which foregrounds Altium: report it, never hide it+ return json!({"success": sel.get("success").and_then(|b| b.as_bool()).unwrap_or(false),+ "panel": name, "lane": "ui_select_raised", "windowEtiquette": "foreground",+ "_hint": "The tab exposes no SelectionItemPattern, so ab clicked it with synthetic input, which raised Altium. The dispatch wrapper hands the user's window back, but this tab cannot be switched silently."});+ }+ // 3. verify+ if left() < 3 {+ return json!({"success": sel.get("success").and_then(|b| b.as_bool()).unwrap_or(false),+ "panel": name, "lane": "ui_select", "verified": false, "elapsedSec": t0.elapsed().as_secs_f64()}); }- let clk = ab::call("desktop_ui_click", json!({"hwnd": hwnd, "name": name,- "reason": format!("activate the '{name}' panel tab (InvokePattern fallback, altium_panel)")}), &[], 15);- let clk_ok = clk.get("success").and_then(|b| b.as_bool()).unwrap_or(false);- attempted.push(json!({"lane": "ui_click", "ok": clk_ok}));- let after = read_selected();- if after == Some(true) {- return json!({"success": true, "panel": name, "selected": true, "lane": "ui_click",- "attempted": attempted, "ensured": ensured, "windowEtiquette": "background"});+ match find_tab(hwnd, &name, left().min(8)) {+ Ok(Some(m)) if tab_selected(&m) == Some(true) =>+ json!({"success": true, "panel": name, "selected": true, "lane": "ui_select", "ensured": ensured,+ "elapsedSec": t0.elapsed().as_secs_f64()}),+ Ok(Some(m)) if tab_selected(&m).is_none() =>+ // the tree exposes no selection state for this tab: trust the pattern call, say so+ json!({"success": sel.get("success").and_then(|b| b.as_bool()).unwrap_or(false), "panel": name,+ "lane": "ui_select", "verified": false, "elapsedSec": t0.elapsed().as_secs_f64(),+ "_hint": "The tab reports no selection state, so the select could not be verified; check the returned frame of the next show_* verb."}),+ Err(_) => timed_out("verify", t0),+ _ => json!({"success": false, "errorCode": "panel_not_selected", "panel": name, "ensured": ensured,+ "selectReply": {"success": sel.get("success"), "error": err.chars().take(160).collect::<String>()},+ "elapsedSec": t0.elapsed().as_secs_f64(),+ "_hint": "The tab exists but did not become selected. It may be a floating panel's title rather than a dock tab."}), }- let gated = attempted.iter().any(|a| a.get("error").and_then(|e| e.as_str())- .map(|e| e.contains("approval") || e.contains("SHELL")).unwrap_or(false));- json!({"success": false, "errorCode": if gated { "ui_select_gated" } else { "panel_not_selected" },- "panel": name, "selected": after, "attempted": attempted, "ensured": ensured,- "_hint": if gated {- "ab gates desktop_ui_select behind the shell approval grant (Adom Bridge footer 'Auto' button / Settings > Shell Commands > Auto mode). Turn it on, or select the tab by hand - the verb reports honestly either way (adom/adom-bridge#123 tracks the gating)."- } else {- "The tab's UIA element did not select. Check the caption spelling against the painted tab (case matters), and pass ensureId (e.g. TSchLibPanel) if the panel may not exist yet."- }}) } /// What the standing dialog reaper has pre-empted.
bridge-rs/src/surfaces.rs+19−2@@ -327,12 +327,29 @@ pub fn demo(args: &Value) -> Value { Some(img) => json!([{"label": label, "image": img}]), None => json!([]), };- json!({"success": ok, "done": false, "stage": stage, "label": label, "narrate": narrate,+ let mut out = json!({"success": ok, "done": false, "stage": stage, "label": label, "narrate": narrate, "screenshots": shots, "steps": r.get("steps"), "dialogs": r.get("dialogs"), "failedSteps": if ok { json!([]) } else { json!([label]) }, "nextCall": {"verb": "altium_demo", "args": {"stage": stage + 1}}, "_hint": format!("Beat {}/{} ({label}). Narrate, show the screenshot, then call altium_demo {{stage:{}}}.",- stage + 1, DEMO_BEATS.len(), stage + 1)})+ stage + 1, DEMO_BEATS.len(), stage + 1)});+ if !ok {+ // A failed beat used to come back as a bare success:false (seen on Altium Designer+ // Develop, whose install has no AD26 example projects): carry the beat's own answer.+ for k in ["errorCode", "error", "_hint"] {+ if let Some(v) = r.get(k) {+ out[k] = v.clone();+ }+ }+ let missing = r.get("errorCode").and_then(|c| c.as_str()) == Some("file_not_found")+ || r.get("error").and_then(|e| e.as_str()).map(|e| e.contains("file not found")).unwrap_or(false);+ if missing {+ out["errorCode"] = json!("demo_examples_missing");+ out["_hint"] = json!(format!("The demo tours Altium's bundled example projects ({}), which this install does not have (Altium Designer Develop ships without them). Point show_* verbs at your own documents instead.", crate::support::EXAMPLES_DIR));+ }+ out.as_object_mut().map(|o| o.entry("errorCode").or_insert(json!("demo_beat_failed")));+ }+ out } // ---- the live show lanes (John, 2026-09-02: "for all the stuff you need for the demo, you
bridge-rs/src/verbs.rs+3−3@@ -89,10 +89,10 @@ fn describe() -> Value { json!({"success": "bool"}), 60, "Params are pipe-joined key=value pairs. altium_list_commands searches the catalog.", &["eval", "list_commands"], &["Some processes open a modal dialog and block the loop until it is answered."], json!({"command": "PCB:Zoom", "params": {"Action": "All"}})), verb_doc("panel", "Activate a docked panel TAB (SCH Library, PCB Library, Components, Projects, Properties) in the BACKGROUND - the 'ganged-up view' needs the right panels in front, and no Altium server process switches a tab.", json!({"name": "string - the tab caption exactly as painted, e.g. 'SCH Library'", "ensureId": "string optional - panel ID for Client:CustomizeResources (e.g. TSchLibPanel) when the panel may not exist yet"}),- json!({"selected": "bool", "lane": "already | ui_select | ui_click", "attempted": "[]"}), 30,- "UIA SelectionItemPattern on the dock tab, InvokePattern fallback, verified by reading the tab's selection state - no foreground steal either way.",+ json!({"selected": "bool", "lane": "already | ui_select | ui_select_raised", "verified": "bool (false when the tab exposes no selection state)", "elapsedSec": "float"}), 30,+ "One UIA search reads the tab's existence AND selection, then at most one SelectionItemPattern select and one verify, all inside a 24 s budget: every exit is a named answer (panel_not_found, panel_search_timeout, ui_select_gated, panel_not_selected), never ab's generic timeout.", &["show_symbol", "show_footprint", "run_command"],- &["ab gates desktop_ui_select behind the shell approval grant; without it the verb reports ui_select_gated instead of pretending.", "A plain click does not switch panel tabs - that is why this verb exists."],+ &["ab gates desktop_ui_select behind the shell approval grant; without it the verb reports ui_select_gated instead of pretending.", "A plain click does not switch panel tabs - that is why this verb exists.", "lane ui_select_raised means ab fell back to a synthetic click that foregrounded Altium; the dispatch wrapper hands the user's window back."], json!({"name": "SCH Library"})), verb_doc("read_board", "ONE round trip: components, nets, layers, copper, rules, occupancy, outline.", json!({"sections": "string csv - optional", "limit": "int"}), board.clone(), 120, "The primitive every board view derives from; board_summary / rules_by_kind / layers_in_use are shaped from the same read.",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.2.41",+ "version": "0.2.42", "type": "app", "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.", "tags": [
page.json+1−1@@ -4,7 +4,7 @@ "type": "app", "title": "Altium - the Altium Bridge", "brief": "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.",- "version": "0.2.41",+ "version": "0.2.42", "hero": { "type": "image", "path": "screenshots/hero.png"
scripts/releases/0.2.40.jsonadded+15@@ -0,0 +1,15 @@+{+ "name": "altium",+ "version": "0.2.40",+ "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.2.40/adom-bridge-altium-v0.2.40.zip",+ "sha256": "b0224ec421041352e2233556367d5cf023ac13a87cb6323be9514606e6287042",+ "size": 1633877,+ "verbPrefixes": [+ "altium_"+ ],+ "healthEndpoint": "/health",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/altium-bridge/files/adom-bridge-altium-manifest.json",+ "insidersManifestUrl": "https://wiki.adom.inc/api/v1/pages/insiders/files/altium-bridge/version.json",+ "released_at": "2026-09-21T15:00:42.573993Z",+ "tier": "insiders"+}
scripts/releases/0.2.41.jsonadded+15@@ -0,0 +1,15 @@+{+ "name": "altium",+ "version": "0.2.41",+ "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.2.41/adom-bridge-altium-v0.2.41.zip",+ "sha256": "d796533f06d55a39a9408f3d120cb762f63fd739168e9d40d4d54909d1a8a9ca",+ "size": 1634462,+ "verbPrefixes": [+ "altium_"+ ],+ "healthEndpoint": "/health",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/altium-bridge/files/adom-bridge-altium-manifest.json",+ "insidersManifestUrl": "https://wiki.adom.inc/api/v1/pages/insiders/files/altium-bridge/version.json",+ "released_at": "2026-09-21T15:09:23.049456Z",+ "tier": "insiders"+}
scripts/releases/0.2.42.jsonadded+15@@ -0,0 +1,15 @@+{+ "name": "altium",+ "version": "0.2.42",+ "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.2.42/adom-bridge-altium-v0.2.42.zip",+ "sha256": "6c75cbaceb04115f5d6ac861a3ada19b6fd9aaca36354c33181b761c5644f7f9",+ "size": 1638541,+ "verbPrefixes": [+ "altium_"+ ],+ "healthEndpoint": "/health",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/altium-bridge/files/adom-bridge-altium-manifest.json",+ "insidersManifestUrl": "https://wiki.adom.inc/api/v1/pages/insiders/files/altium-bridge/version.json",+ "released_at": "2026-09-23T20:07:50.064472Z",+ "tier": "insiders"+}
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.2.41+0.2.42
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.2.41",+ "version": "0.2.42", "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.", "homepage": "https://wiki.adom.inc/adom/altium-bridge", "docs": "https://wiki.adom.inc/adom/altium-bridge",