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.
main
John Lauer
v0.1.37: lone-OK dialogs carry no decision - reaper clears them without needing the text
c56dc01
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
"""dialogs.py — find, classify, read and dismiss Altium's dialogs BY PID, in the background.
Why by PID: a title match is a prefilter at best (a browser tab titled "Altium …"
would match). Every window here is confirmed to belong to an X2.exe process.
Classes seen on AD 26.9.1 (2026-08-29):
TMessageForm "X2" script runtime error: "Unknown identifier … Continue execution?" [OK|Cancel]
TMessageForm "Error" script compile error / "Another script executing now" [OK]
TMessageForm "Information" "There is another instance of running, but it is busy" [OK] (owned by a HELPER X2, not the main one)
TMessageForm "Confirm" save / overwrite prompts [Yes|No|Cancel]
#32770 Win32 common dialogs (file open/save)
Dismissal is a posted button click (BM_CLICK to the button hwnd found by text) —
no focus steal, no cursor. UIA via ab is the fallback (server.py has the callback).
"""
from __future__ import annotations
import ctypes
import sys
import time
from ctypes import wintypes
from handlers import altium_detect
IS_WINDOWS = sys.platform == "win32"
DIALOG_CLASSES = ("TMessageForm", "#32770", "TForm", "TMessageDialog", "TForm_RunScriptParameters")
# title/body substring -> (category, safe default action, meaning)
CATALOG = [
("another script executing", "script_busy", "OK",
"A -R script request arrived while the agent loop (or another script) was resident. Dismiss; route work through the agent."),
("continue execution?", "script_runtime_error", "Cancel",
"A script hit a runtime error. Cancel stops it cleanly; OK continues past the error."),
("undeclared identifier", "script_compile_error", "OK",
"The script failed to compile; the engine is not halted after OK."),
("unknown identifier", "script_compile_error", "OK", "Compile error in RunScriptText text (statements only, no Var)."),
("select item to run", "run_script_picker", "Cancel",
"Altium could not resolve the -R RunScript target (unit did not compile, or wrong ProcName) and opened its Run Script picker. Cancel it, fix the script."),
("another instance of", "second_instance_busy", "OK",
"A helper X2 could not hand its -R request to the running instance because that instance is busy (a modal is open)."),
("do you want to save", "save_prompt", None, "Unsaved changes on close. Decide: Yes saves, No discards. Never default this."),
("save changes", "save_prompt", None, "Unsaved changes. Decide explicitly."),
("could not start vault explorer", "altium_extension_broken", "OK",
"Altium's own VaultExplorer extension fails to load at startup. NOT caused by the bridge: the "
"extension is correctly registered and its payload is complete (VaultExplorer.dll + 114 files), "
"and Altium's private .NET 8.0.15 runtime loads fine. Informational — dismissing it costs "
"nothing, and the standing reaper does so automatically."),
("file type not recognised", "unsupported_document", "OK",
"Altium was handed a path it does not open. From the bridge that means a bad open call (the "
"bridge now refuses unsupported extensions up front); at STARTUP it means Altium is restoring a "
"document from the last session that it can no longer open. Safe to dismiss."),
("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."),
("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",
"Altium's own crash-report dialog, usually raised when a window's state changed while it was "
"focusing a document view (e.g. a restore/minimize cycle during a background capture). Its "
"only button SENDS a report to Altium — try the title-bar X first if the user's data matters."),
("unsaved changes", "save_prompt", None,
"Names the document(s) at risk — READ IT. On a shared machine the document may belong to "
"ANOTHER AI thread; Save preserves their work, Don't Save destroys it. Never default this."),
("license", "licence", None, "Licence prompt — read it; usually informational."),
("trial", "licence", None, "Trial notice."),
]
def _text_of(hwnd: int) -> str:
"""All Static/label text under a dialog (WM_GETTEXT on children), joined."""
if not IS_WINDOWS:
return ""
user32 = ctypes.windll.user32
parts = []
@ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
def cb(h, _):
n = user32.GetWindowTextLengthW(h)
if n:
buf = ctypes.create_unicode_buffer(n + 1)
user32.GetWindowTextW(h, buf, n + 1)
cls = ctypes.create_unicode_buffer(64)
user32.GetClassNameW(h, cls, 64)
parts.append((cls.value, buf.value))
return True
user32.EnumChildWindows(hwnd, cb, 0)
texts = [t for c, t in parts if c.lower() not in ("button", "tbutton", "txpbutton")]
return " ".join(t.strip() for t in texts if t.strip())
def _uia_text_of(hwnd: int) -> str:
"""Altium PAINTS its dialog messages instead of using Static controls, so WM_GETTEXT returns
nothing but the BUTTON labels — 'Could not start Vault Explorer…' came back as just "OK", every
such dialog classified as `unknown`, and the standing reaper (correctly) refused to touch it.
UIA can see the painted text, so fall back to it whenever the local read is useless."""
try:
import caller_identity
r = caller_identity.ad_callback("desktop_ui_text", {"hwnd": hwnd}, on_behalf=False, timeout=8) or {}
except Exception:
return ""
v = r.get("text") or r.get("texts") or ""
if isinstance(v, list):
v = " ".join(str(x) for x in v if x)
return str(v or "")
def _body_of(hwnd: int, buttons: list) -> str:
"""The dialog's MESSAGE, with the button labels stripped — what classification needs."""
body = _text_of(hwnd)
labels = {str((b or {}).get("text") or "").strip().lower() for b in (buttons or [])}
stripped = " ".join(w for w in body.split() if w.strip().lower() not in labels).strip()
if len(stripped) < 12: # nothing but button labels: ask UIA
ui = _uia_text_of(hwnd)
if len(ui) > len(stripped):
return ui
return body
def _buttons_of(hwnd: int) -> list[dict]:
if not IS_WINDOWS:
return []
user32 = ctypes.windll.user32
out = []
@ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
def cb(h, _):
cls = ctypes.create_unicode_buffer(64)
user32.GetClassNameW(h, cls, 64)
# Altium's OK is a WinXPButtons.TXPBitBtn (measured) — match btn|button
if "button" in cls.value.lower() or "btn" in cls.value.lower():
n = user32.GetWindowTextLengthW(h)
buf = ctypes.create_unicode_buffer(n + 1)
user32.GetWindowTextW(h, buf, n + 1)
out.append({"hwnd": int(h), "text": buf.value.replace("&", ""), "className": cls.value,
"enabled": bool(user32.IsWindowEnabled(h))})
return True
user32.EnumChildWindows(hwnd, cb, 0)
return out
def classify(title: str, body: str) -> dict:
hay = f"{title} {body}".lower()
for needle, cat, action, meaning in CATALOG:
if needle in hay:
return {"category": cat, "safeAction": action, "meaning": meaning}
return {"category": "unknown", "safeAction": None,
"meaning": "Not in the catalog — read the text and decide; never blind-dismiss."}
def scan(main_pid_only: bool = False) -> list[dict]:
"""Every dialog window owned by any X2.exe (helpers included, flagged).
SCOPED to the selected instance when one is set (John, 2026-09-01): with Designer and Designer
Develop both running, an unscoped sweep lets one AI thread see — and `dismiss_dialogs` CLEAR —
a modal belonging to the other thread's Altium."""
wins = altium_detect.altium_windows()
try:
from handlers import agent as _agent
sel = _agent.current_instance()
except Exception:
sel = None
if sel and sel.get("pid"):
wins = [w for w in wins if w.get("pid") == sel["pid"]]
main = next((w for w in wins if w["kind"] == "main"), None)
main_pid = main["pid"] if main else None
out = []
main_disabled = bool(main) and not main.get("enabled", True)
for w in wins:
if w["kind"] == "main":
continue
# Any enabled top-level window while the MAIN window is disabled IS the modal,
# whatever its class (measured: TForm_RunScriptParameters "Select Item To Run").
modal_by_state = main_disabled and w.get("enabled") and w["pid"] == main_pid and w["className"] not in ("TClientBarControl", "TThemedNotificationPopup", "TAnimatedNotificationControl")
if w["className"] not in DIALOG_CLASSES and w["kind"] not in ("dialog", "script_error_modal") and not modal_by_state:
continue
_btns = _buttons_of(w["hwnd"])
body = _body_of(w["hwnd"], _btns)
d = {
"hwnd": w["hwnd"], "pid": w["pid"], "title": w["title"], "className": w["className"],
"body": body, "buttons": _btns,
"ownedByMainInstance": (main_pid is not None and w["pid"] == main_pid),
"rect": w["rect"],
}
d.update(classify(w["title"], body))
if main_pid_only and not d["ownedByMainInstance"]:
continue
out.append(d)
return out
def press(hwnd: int, button_text: str) -> dict:
"""Click a dialog button by its text — background, two ways:
1. BM_CLICK posted to the button hwnd (TXPBitBtn / Button classes);
2. ab's UIA InvokePattern (desktop_ui_click) — Altium's TMessageForm exposes
'OK'/'Cancel' as invokable Buttons even when Win32 posting is ignored."""
if not IS_WINDOWS:
return {"success": False, "error": "windows only"}
user32 = ctypes.windll.user32
for b in _buttons_of(hwnd):
if b["text"].strip().lower() == button_text.strip().lower():
BM_CLICK = 0x00F5
user32.PostMessageW(b["hwnd"], BM_CLICK, 0, 0)
time.sleep(0.4)
if not user32.IsWindow(hwnd):
return {"success": True, "clicked": b["text"], "buttonHwnd": b["hwnd"], "via": "BM_CLICK"}
break
try:
import caller_identity
r = caller_identity.ad_callback("desktop_ui_click", {"hwnd": hwnd, "name": button_text, "role": "button"})
time.sleep(0.4)
if not user32.IsWindow(hwnd):
return {"success": True, "clicked": button_text, "via": "uia", "ab": r}
return {"success": False, "error": f"dialog {hwnd} still open after UIA click", "ab": r,
"buttons": [b["text"] for b in _buttons_of(hwnd)]}
except Exception as e:
return {"success": False, "error": f"no button '{button_text}' on hwnd {hwnd} ({e})",
"buttons": [b["text"] for b in _buttons_of(hwnd)]}
def close(hwnd: int) -> dict:
"""WM_CLOSE = the dialog's Cancel/X path. Background."""
if not IS_WINDOWS:
return {"success": False, "error": "windows only"}
WM_CLOSE = 0x0010
ctypes.windll.user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)
return {"success": True, "posted": "WM_CLOSE", "hwnd": hwnd}
# ---- the standing reaper (John, 2026-09-01: "always pre-empt these from ever showing up") ------
#
# Clearing a dialog AFTER a verb notices it is not pre-emption — the user has already seen it. This
# runs continuously from bridge start and presses the catalog's SAFE action the moment one of our
# known nuisances appears, whether or not any verb is running. Altium's own startup warnings
# (VaultExplorer, "File type not recognised") are exactly the case: no verb is in flight when they
# appear, so nothing else would ever catch them.
#
# HARD RULES:
# * only dialogs whose CATALOG entry declares a safeAction — never `unknown`, never a save prompt
# (safeAction None), never anything naming a document
# * every press is recorded, and surfaced by altium_dialog_log / altium_status
# * one press per hwnd; a dialog that re-fires (License Renewal Failed) is logged once per hwnd so
# the log stays readable
_REAPED: list = []
_REAPED_HWNDS: set = set()
_REAPER_STOP = None
def reaped_log(limit: int = 25) -> list:
return _REAPED[-limit:]
def _reap_once() -> int:
n = 0
for d in scan():
act = (d.get("safeAction") or d.get("classification", {}).get("safeAction")
if isinstance(d.get("classification"), dict) else d.get("safeAction"))
if not act:
c = classify(d.get("title") or "", d.get("body") or "")
act, cat = c.get("safeAction"), c.get("category")
else:
cat = d.get("category")
# Altium PAINTS its dialog messages: they are in neither WM_GETTEXT nor the UIA tree (the
# subtree of the VaultExplorer warning is literally Window "Warning" -> panes -> Button
# "OK"). So text classification cannot see them. What CAN be trusted is the button set:
#
# A dialog whose ONLY button is OK carries NO DECISION. Dismissing it can acknowledge,
# never choose. Every dialog that asks something of the user has >= 2 buttons — a save
# prompt (Save / Don't Save / Cancel), a licence chooser (Choose a license / Continue), a
# confirm (Yes / No). So a lone OK is safe to press even when the text is unreadable.
labels = [str((b or {}).get("text") or "").strip().lower() for b in (d.get("buttons") or [])]
lone_ok = labels == ["ok"]
if not act and lone_ok:
act, cat = "OK", "single_ok_acknowledgement"
if not act or cat in (None, "unknown", "save_prompt"):
continue
if cat != "single_ok_acknowledgement" and len(labels) > 1 and act not in labels:
continue # the catalog's action is not even on this dialog — do not improvise
hw = d.get("hwnd")
if hw in _REAPED_HWNDS:
continue
try:
import caller_identity
r = caller_identity.ad_callback("desktop_ui_click",
{"hwnd": hw, "name": act, "role": "button"},
on_behalf=False, timeout=8) or {}
ok = bool(r.get("success"))
except Exception:
ok = False
_REAPED_HWNDS.add(hw)
_REAPED.append({"at": time.time(), "title": d.get("title"), "category": cat,
"pressed": act, "ok": ok,
"note": "pre-empted by the standing reaper — the user never had to deal with it"})
del _REAPED[:-200]
n += 1
return n
def start_reaper(poll_sec: float = 1.5):
"""Start the standing reaper. Idempotent."""
global _REAPER_STOP
if _REAPER_STOP is not None:
return False
import threading
_REAPER_STOP = threading.Event()
def _loop():
while not _REAPER_STOP.is_set():
try:
_reap_once()
except Exception:
pass
_REAPER_STOP.wait(poll_sec)
threading.Thread(target=_loop, daemon=True, name="altium-dialog-reaper").start()
return True