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.41: licence_expired catalog entry + native WM_CLOSE fallback for UIA-refusing OK boxes
Co-Authored-By: Claude Fable 5 <[email protected]>
3 files changed
+28−3
bridge-rs/src/dialogs.rs+11−1@@ -54,6 +54,8 @@ const CATALOG: &[(&str, &str, &str, &str)] = &[ ("file type not recognized", "unsupported_document", "OK", "Same as 'not recognised' (US spelling)."), ("license maximum usage reached", "licence_seat_stranded", "OK", "The seat is held by an instance that never released it — almost always an Altium that was KILLED rather than closed. The licence page will still show Seats 0/1, which looks free and is not. FIX: close every other Altium POLITELY (altium_close, never process_kill), then altium_license {action:'use_license'}. Waiting does not clear it."),+ ("has expired or been deactivated", "licence_expired", "OK",+ "The licence (Develop entitlement or a lease) is dead server-side: Altium runs but refuses scripting and can wedge in a modal loop on close. The bridge cannot renew an entitlement - the human renews or re-activates it in the Altium workspace. NOTE: Develop shows NO title licence tail in this state either, so the absent-tail 'Licensed' inference is WRONG while this box is up (measured 2026-09-21)."), ("license renewal failed", "licence_seat_stranded", "OK", "Altium could not renew the On-Demand lease. Same cause and fix as 'maximum usage reached': a killed instance is still holding the seat. This dialog RE-FIRES on a timer until the seat is reclaimed, so clearing it is not the fix."), ("cannot focus a disabled or invisible window", "altium_internal", "Send and Close",@@ -224,10 +226,18 @@ fn is_licence_chooser(btns: &[String]) -> bool { l.iter().any(|b| b == "choose a license") && l.iter().any(|b| b == "continue") } -/// Press a named button on a dialog through UIA (no focus steal, no cursor).+/// Press a named button on a dialog through UIA (no focus steal, no cursor). When the button is+/// an acknowledgement (OK) and its UIA Invoke refuses - some Information boxes expose no pattern+/// (measured 2026-09-21 on "licence has expired or been deactivated") - fall back to a native+/// WM_CLOSE on the dialog: the same dismissal its titlebar X performs. Never for any other+/// button: closing a question box would silently answer it with its cancel path. pub fn press(hwnd: i64, button: &str) -> Value { let r = ab::call("desktop_ui_click", json!({"hwnd": hwnd, "name": button, "role": "button"}), &[], 15); let ok = r.get("success").and_then(|b| b.as_bool()).unwrap_or(false);+ if !ok && button.eq_ignore_ascii_case("ok") && crate::win32::post_message_close(hwnd) {+ return json!({"success": true, "clicked": button, "hwnd": hwnd, "via": "wm_close_fallback",+ "note": "the OK exposed no UIA Invoke; dismissed with a native WM_CLOSE (same as its titlebar X)", "ab": r});+ } json!({"success": ok, "clicked": button, "hwnd": hwnd, "via": "uia", "ab": r}) }
bridge-rs/src/win32.rs+16−1@@ -28,9 +28,19 @@ mod imp { fn IsHungAppWindow(hwnd: Hwnd) -> i32; fn IsWindowEnabled(hwnd: Hwnd) -> i32; fn PostThreadMessageW(tid: u32, msg: u32, wparam: usize, lparam: isize) -> i32;+ fn PostMessageW(hwnd: Hwnd, msg: u32, wparam: usize, lparam: isize) -> i32; } const WM_QUIT: u32 = 0x0012;+ const WM_CLOSE: u32 = 0x0010;++ /// Post WM_CLOSE to one window — the dismissal its titlebar X performs, POSTED so it cannot+ /// inherit a hang. The dialog fallback lane: some Information boxes carry an OK that exposes+ /// no UIA Invoke (measured 2026-09-21 on the expired-licence box), and closing the box IS the+ /// acknowledgement.+ pub fn post_message_close(hwnd: i64) -> bool {+ unsafe { PostMessageW(hwnd as Hwnd, WM_CLOSE, 0, 0) != 0 }+ } /// Ask a UI thread to leave its message loop — Delphi's `Application.Terminate`, reached from /// outside. POSTED (never sent), so it cannot inherit a hang, and unlike TerminateProcess the@@ -174,7 +184,7 @@ mod proc_imp { } #[cfg(windows)]-pub use imp::{post_quit_to_thread, visible_windows, WinRow};+pub use imp::{post_message_close, post_quit_to_thread, visible_windows, WinRow}; #[cfg(windows)] pub use proc_imp::{kill, pids_of}; @@ -188,6 +198,11 @@ pub fn kill(_pid: u32) -> bool { false } +#[cfg(not(windows))]+pub fn post_message_close(_hwnd: i64) -> bool {+ false+}+ #[cfg(not(windows))] pub fn post_quit_to_thread(_tid: u32) -> bool { false
page.json+1−1@@ -140,4 +140,4 @@ "skills/altium-wiki-import/SKILL.md", "uninstall.sh" ]-}+}