← Commit history

v0.1.36: read Altium's painted dialog text via UIA - the reaper was blind

John Lauer ·7e5df6f22a ·1mo ago ·parent 072eb47
7 files changed +45−9
CHANGELOG.md+5
@@ -1,5 +1,10 @@ # Changelog +## 0.1.36 — 2026-09-01+- **The bridge can finally READ Altium's dialog messages.** Altium PAINTS its dialog text instead of using Static controls, so `WM_GETTEXT` returned nothing but the BUTTON labels — `Could not start Vault Explorer…` came back as a body of just `"OK"`. Every such dialog therefore classified as `unknown`, which is why the standing reaper (correctly, per its own rules) refused to touch the exact warning it was built for.+- `_body_of()` now strips the button labels and, when what is left is too short to be a message, asks UIA (`desktop_ui_text`) for the painted text. Classification, the reaper, `check_dialogs` and every `_hint` that quotes a dialog all get the real message.+- This was the missing half of 0.1.35: the reaper was correct, its input was blind.+ ## 0.1.35 — 2026-09-01 - **A standing dialog reaper, running from bridge start.** John: *"and 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. The reaper polls every 1.5 s and presses the catalog's SAFE action the instant a KNOWN nuisance appears, whether or not any verb is in flight. That is the only thing that can catch Altium's STARTUP warnings (`Could not start Vault Explorer`, `File type not recognised`), which appear with nothing running. - **Hard rules, so it can never answer for the user:** only dialogs whose catalog entry declares a safe action; never `unknown`; never a save prompt (those name documents that may belong to another AI thread and are surfaced for a human decision). Every press is recorded and readable via the new `altium_dialog_log`.
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.35",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.35/adom-bridge-altium-v0.1.35.zip",-  "sha256": "4f050195f6dac158b7ab893e6b9da02d09bc40a0e874f72255a96bc73179fcb8",-  "size": 134461,+  "version": "0.1.36",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.36/adom-bridge-altium-v0.1.36.zip",+  "sha256": "8f549ccaf75788c179fe76112f17243a6a74ab83f6180ee1519f059eae8f5b94",+  "size": 134889,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.36.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.35",+  "version": "0.1.36",   "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",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.1.35",+  "version": "0.1.36",   "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": [
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.35+0.1.36
src/handlers/dialogs.py+33−2
@@ -94,6 +94,36 @@ def _text_of(hwnd: int) -> str:     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 []@@ -152,10 +182,11 @@ def scan(main_pid_only: bool = False) -> list[dict]:         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-        body = _text_of(w["hwnd"])+        _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": _buttons_of(w["hwnd"]),+            "body": body, "buttons": _btns,             "ownedByMainInstance": (main_pid is not None and w["pid"] == main_pid),             "rect": w["rect"],         }