← Commit history
bridge-rs/Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "altium-bridge"-version = "0.2.33"+version = "0.2.34" dependencies = [  "getrandom",  "serde",
bridge-rs/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.33"+version = "0.2.34" edition = "2021" description = "Adom altium-bridge — the bridge process, in Rust. Calls ab's verbs for the desktop; speaks file IPC to the resident DelphiScript agent inside Altium." license = "MIT"
bridge-rs/src/dialogs.rs+15−1
@@ -276,6 +276,12 @@ static REAPER_RUNNING: AtomicBool = AtomicBool::new(false); /// refuses to close, and the caller used to see nothing but a timeout (found on AD 26.9.1, /// 2026-09-20). agent::call watches this so the halt is reported and cleared, never silent. static LAST_SCRIPT_ERROR: Mutex<Option<Value>> = Mutex::new(None);+/// Pictures of the last 8 boxes the reaper pressed (newest last): `altium_dialog_log {shots:true}`.+static REAPED_SHOTS: Mutex<Vec<Value>> = Mutex::new(Vec::new());++pub fn reaped_shots() -> Vec<Value> {+    REAPED_SHOTS.lock().map(|g| g.clone()).unwrap_or_default()+}  fn now_ms() -> u64 {     std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|x| x.as_millis() as u64).unwrap_or(0)@@ -346,13 +352,21 @@ pub fn reap_once() -> usize {         // photograph it BEFORE pressing OK: the picture is the only copy of the error text.         let title_str = d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();         let is_script_error = cat == "single_ok_acknowledgement" && title_str.trim().eq_ignore_ascii_case("error");+        // Altium PAINTS its message text, so the picture taken here is the ONLY copy of what the box+        // said. Until 0.2.34 only "Error" boxes were photographed: an "Information" box that explained+        // why Altium refused to quit was pressed away five times in a row, unread (2026-09-21).         let mut err_shot = Value::Null;-        if is_script_error {+        {             let r = ab::call("desktop_screenshot_base64", json!({"hwnd": hwnd}), &[], 10);             if let Some(img) = r.get("image").or_else(|| r.get("base64")).and_then(|i| i.as_str()) {                 err_shot = json!(img);             }         }+        if let Ok(mut g) = REAPED_SHOTS.lock() {+            g.push(json!({"atMs": now_ms(), "title": title_str, "shot": err_shot.clone()}));+            let len = g.len();+            if len > 8 { g.drain(..len - 8); }+        }         let ok = press(hwnd, &act).get("success").and_then(|b| b.as_bool()).unwrap_or(false);         if is_script_error {             if let Ok(mut g) = LAST_SCRIPT_ERROR.lock() {
bridge-rs/src/license.rs+6
@@ -66,6 +66,12 @@ pub fn license(args: &Value) -> Value {     }     let hwnd = lic.get("hwnd").and_then(|h| h.as_i64()).unwrap_or(0);     let action = s_or(args, "action", "show");+    // An unknown action used to do nothing and answer success:true (found 2026-09-21).+    if !matches!(action.as_str(), "show" | "refresh" | "use_license" | "start_trial" | "sign_out") {+        return json!({"success": false, "errorCode": "unknown_action", "action": action,+            "error": format!("unknown licence action '{action}'"),+            "_hint": "action is one of: show (default, returns the page screenshot), refresh, use_license, start_trial, sign_out. altium_ensure_licensed does the usual sequence in one call; altium_install {stage:'license'} does it after a fresh install."});+    }     if agent::is_alive() {         agent::call_no_autostart("run_process", &[("process", "Client:LicenseSubscriptionNotification".into()), ("params", "".into())], Duration::from_secs(15));         // bring the License Management view up via its process launcher (background)
bridge-rs/src/main.rs+10−1
@@ -26,7 +26,10 @@ mod win32; use std::time::Instant; use tiny_http::{Header, Method, Response, Server}; -pub const BRIDGE_VERSION: &str = "0.2.33";+/// 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.34";  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());@@ -83,6 +86,12 @@ fn main() {                         let vargs = v.get("args").cloned().unwrap_or_else(|| serde_json::json!({}));                         let mut out = verbs::handle(&command, &vargs, &forwarded);                         if let Some(o) = out.as_object_mut() {+                            // Bridge SDK: invite the bug report IN THE OUTPUT, on every failure. The AI driving+                            // this bridge is standing on the one machine where the failure happened and holds+                            // the only copy of the evidence. Never overwrites a field a handler already set.+                            if o.get("success").and_then(|b| b.as_bool()) == Some(false) && !o.contains_key("_reportIssues") {+                                o.insert("_reportIssues".into(), serde_json::json!(REPORT_ISSUES));+                            }                             o.insert("durationMs".into(), serde_json::json!(t0.elapsed().as_millis() as u64));                             o.entry("bridgeVersion").or_insert(serde_json::json!(BRIDGE_VERSION));                         }
bridge-rs/src/misc.rs+6−2
@@ -115,7 +115,9 @@ pub fn instances_verb(_args: &Value) -> Value { /// What the standing dialog reaper has pre-empted. pub fn dialog_log(args: &Value) -> Value {     let limit = args.get("limit").and_then(|l| l.as_u64()).unwrap_or(25) as usize;-    json!({"success": true, "reaped": dialogs::reaped_log(limit), "running": true,+    let shots = if args.get("shots").and_then(|b| b.as_bool()) == Some(true) { json!(dialogs::reaped_shots()) } else { Value::Null };+    json!({"success": true, "reaped": dialogs::reaped_log(limit), "running": true, "shots": shots,+        "_shotsHint": "shots:true returns PNGs (base64) of the last 8 boxes the reaper pressed. Altium paints its message text, so the picture is the only copy of what a box SAID: read it whenever a verb is refused for no visible reason.",         "_hint": "Anything NOT listed here that reached the user is either a save prompt (never auto-answered, by design) or a dialog missing from the catalog — report it so it can be added with a safe action."}) } @@ -170,7 +172,9 @@ pub fn progress_verb(_args: &Value) -> Value {  pub fn uninstall_verb(args: &Value) -> Value {     let mut removed: Vec<String> = Vec::new();-    if agent::is_alive() {+    let dry = args.get("dryRun").and_then(|b| b.as_bool()) == Some(true);+    // A DRY run changes nothing: it used to stop the resident agent anyway (found 2026-09-21).+    if agent::is_alive() && !dry {         lifecycle::stop();     }     let d = agent::cache_dir();
bridge-rs/src/support.rs+4
@@ -193,6 +193,10 @@ fn workspace_unavailable_seen() -> bool {     }) } +pub fn workspace_unavailable_seen_pub() -> bool {+    workspace_unavailable_seen()+}+ fn is_project_path(path: &str) -> bool {     let l = path.to_lowercase();     [".prjpcb", ".prjscr", ".dsnwrk", ".libpkg", ".prjfpg", ".prjemb", ".prjmbd"].iter().any(|e| l.ends_with(e))
bridge-rs/src/surfaces.rs+3−1
@@ -15,6 +15,7 @@ pub fn open_then(path: &str, commands: &[(&str, &str)], timeout: Duration, label     let t0 = support::now();     let mut steps: Vec<Value> = Vec::new();     let r = agent::call("open", &[("path", path.to_string())], None, timeout);+    let r = support::open_verdict(path, r, std::path::Path::new(path).is_file(), support::workspace_unavailable_seen_pub());     steps.push(json!({"open": path, "result": r.get("data"), "ms": r.get("roundtripMs")}));     if r.get("success").and_then(|b| b.as_bool()) != Some(true) {         let mut out = r;@@ -211,7 +212,8 @@ pub fn activate_document(args: &Value) -> Value {         return bad;     }     let wait = timeout_of(args, 60).as_secs_f64().min(support::OPEN_BUDGET_SEC);-    let mut r = agent::call("open", &[("path", path.clone())], None, Duration::from_secs_f64(wait));+    let r0 = agent::call("open", &[("path", path.clone())], None, Duration::from_secs_f64(wait));+    let mut r = support::open_verdict(&path, r0, std::path::Path::new(&path).is_file(), support::workspace_unavailable_seen_pub());     if r.get("errorCode").and_then(|c| c.as_str()) == Some("agent_timeout") {         return json!({"success": true, "status": "in_progress", "path": path, "lane": "agent",             "windowEtiquette": "background", "waitedSec": wait,
bridge-rs/src/verbs.rs+1−1
@@ -168,7 +168,7 @@ fn describe() -> Value {         verb_doc("license", "Drive the License Management page (UIA first - no cursor, no foreground).", json!({"action": "show|refresh|use_license|start_trial|sign_out"}), shot.clone(), 60,             "One On-Demand seat serves EVERY Altium app on this machine. use_license claims a free seat.",             &["ensure_licensed", "license_status", "close"], &["NEVER kill an Altium holding the seat: the lease strands, the page shows Seats 0/1 while every claim fails. altium_close releases it."], json!({"action": "use_license"})),-        verb_doc("close", "Polite close (releases the licence seat); force needs acceptSeatLoss.", json!({"force": "bool", "acceptSeatLoss": "bool"}), json!({"closedInSec": "float"}), 30,+        verb_doc("close", "Polite close (releases the licence seat). Frees a script engine parked on a breakpoint first (Altium refuses to close over one). discardChanges:true answers Altium's save prompts with Save None for callers whose modified documents are ALL their own; force needs acceptSeatLoss.", json!({"discardChanges": "bool, default false: press Save None + confirm on 'Confirm Save Locally'", "waitSec": "float, default 20", "force": "bool", "acceptSeatLoss": "bool", "instance": "pid, when several Altiums run"}), json!({"closedInSec": "float", "engineFreed": "object|null", "errorCode": "unsaved_changes | close_blocked_by_dialog | close_timeout"}), 120,             "A save prompt on close NAMES the documents - read it; on a shared machine one may belong to another AI thread. Never defaulted.",             &["license", "launch"], &["force:true without acceptSeatLoss is refused: a kill strands the On-Demand seat."], json!({})),         verb_doc_long("install_library_bundle", "One-click install of a wiki library bundle (zip of .SchLib/.PcbLib): unpack, register, open the library, prove it with a frame.",
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.2.33",+  "version": "0.2.34",   "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",
page.json+1−1
@@ -140,4 +140,4 @@     "skills/altium-wiki-import/SKILL.md",     "uninstall.sh"   ]-}\ No newline at end of file+}
src/agent/AdomAgent.pas.tmpl+26−1
@@ -3477,6 +3477,16 @@ Begin         Result := '{' + JRaw('ok','false') + ',' + JStr('error','component not found: ' + Param(Lines, 'from')) + '}';         Exit;     End;+    If Trim(Param(Lines, 'to')) = '' Then+    Begin+        Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','name_required') + ',' + JStr('error','to (the new name) is required') + '}';+        Exit;+    End;+    If FindSchComp(SLib, Param(Lines, 'to')) <> Nil Then+    Begin+        Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','name_taken') + ',' + JStr('error','a component named ' + Param(Lines, 'to') + ' already exists in this library') + '}';+        Exit;+    End;     C.LibReference := Param(Lines, 'to');     SLib.GraphicallyInvalidate;     Result := '{' + JRaw('ok','true') + ',' + JStr('renamed', Param(Lines, 'to')) + '}';@@ -3604,6 +3614,16 @@ Begin         Result := '{' + JRaw('ok','false') + ',' + JStr('error','footprint not found: ' + Param(Lines, 'from')) + '}';         Exit;     End;+    If Trim(Param(Lines, 'to')) = '' Then+    Begin+        Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','name_required') + ',' + JStr('error','to (the new name) is required') + '}';+        Exit;+    End;+    If FindFp(PLib, Param(Lines, 'to')) <> Nil Then+    Begin+        Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','name_taken') + ',' + JStr('error','a footprint named ' + Param(Lines, 'to') + ' already exists in this library') + '}';+        Exit;+    End;     F.Name := Param(Lines, 'to');     PLib.Board.ViewManager_FullUpdate;     Result := '{' + JRaw('ok','true') + ',' + JStr('renamed', Param(Lines, 'to')) + '}';@@ -4255,16 +4275,21 @@ Var     Board : IPCB_Board;     L     : Integer;     Show  : Boolean;+    LName : String; Begin     Board := PCBServer.GetCurrentPCBBoard;     If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;     L := StrToIntDef(Param(Lines, 'layerId'), -1);     If L < 0 Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','layerId (from altium_layers) is required') + '}'; Exit; End;     Show := Param(Lines, 'displayed') <> '0';+    { an id that is not in THIS board's stack has a nil LayerObject: reading .Name off it halts the+      engine. Refuse by name instead (found by review, 2026-09-21). }+    If Board.LayerStack.LayerObject[L] = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('errorCode','unknown_layer') + ',' + JStr('error','layerId ' + IntToStr(L) + ' is not in this board; altium_layers lists the valid ids') + '}'; Exit; End;+    LName := Board.LayerStack.LayerObject[L].Name;     Board.LayerIsDisplayed[L] := Show;     Board.ViewManager_UpdateLayerTabs;     Board.ViewManager_FullUpdate;-    Result := '{' + JRaw('ok','true') + ',' + JRaw('layerId', IntToStr(L)) + ',' + JStr('name', Board.LayerStack.LayerObject[L].Name) + ',' + JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}';+    Result := '{' + JRaw('ok','true') + ',' + JRaw('layerId', IntToStr(L)) + ',' + JStr('name', LName) + ',' + JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}'; End;  { create_net_class: name=<class> nets=<A;B;C> }