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
v0.1.35: standing dialog reaper - nuisance modals never reach the user
8 files changed
+125−8
CHANGELOG.md+6@@ -1,5 +1,11 @@ # Changelog +## 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`.+- **Catalog additions with safe actions:** `Could not start Vault Explorer` (Altium's own extension failing to load — NOT the bridge: the extension is correctly registered, its payload is complete at 115 files including VaultExplorer.dll, and Altium's private .NET 8.0.15 runtime loads fine), `File type not recognised` (at startup it is Altium restoring a document it can no longer open).+- 110 verbs.+ ## 0.1.34 — 2026-09-01 - **The force-close guard now fires on INTENT, not on a blocked dialog.** 0.1.33 put it inside the blocked-by-dialog branch, so `altium_close {force:true}` sailed straight through whenever nothing happened to block — exactly the path that strands the seat. It is now checked at the top of the verb: `force:true` on a licensed instance is refused with `would_strand_licence_seat` unless `acceptSeatLoss:true`. - **`altium_instances` applies the Develop licence rule too.** 0.1.33 fixed only `licence_state()`, so the instance list still reported Altium Designer Develop as unlicensed while its agent was running and importing.
adom-bridge-altium-manifest.json+4−4@@ -1,9 +1,9 @@ { "name": "altium",- "version": "0.1.34",- "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.34/adom-bridge-altium-v0.1.34.zip",- "sha256": "3dc30ed5d47d1a9dd95dda049ab0e3aa3884c11a938d9f79068a56e57ec6e613",- "size": 132603,+ "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, "verbPrefixes": [ "altium_" ],
adom-bridge-altium-v0.1.35.zipadded⋯ 1 unchanged line ⋯
bridge.json+2−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "altium", "displayName": "Altium Designer",- "version": "0.1.34",+ "version": "0.1.35", "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",@@ -59,6 +59,7 @@ "altium_demo", "altium_describe", "altium_diagnostics",+ "altium_dialog_log", "altium_dismiss_dialogs", "altium_ensure_licensed", "altium_eval",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "altium-bridge",- "version": "0.1.34",+ "version": "0.1.35", "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.34+0.1.35
src/handlers/dialogs.py+86@@ -40,6 +40,16 @@ CATALOG = [ "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 "@@ -192,3 +202,79 @@ def close(hwnd: int) -> dict: 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")+ if not act or cat in (None, "unknown", "save_prompt"):+ continue+ 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
src/server.py+25−1@@ -2104,6 +2104,21 @@ def h_read_board(args): "data": d, "roundtripMs": r.get("roundtripMs")} ++def h_dialog_log(args):+ """What the standing dialog reaper has pre-empted.++ The reaper runs from bridge start and presses the catalog's SAFE action the instant a KNOWN+ nuisance appears — Altium's VaultExplorer startup warning, "File type not recognised", the+ script-busy notice — so the user never has to deal with them. It never touches a save prompt or+ anything not in the catalog: those are surfaced by altium_check_dialogs for a human decision."""+ return {"success": True, "reaped": dialogs.reaped_log(int(args.get("limit", 25))),+ "running": True,+ "_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."}++ # ---- licence lifecycle: status, ensure-licensed, trial setup (John 2026-08-31) ------------ def h_license_status(args):@@ -2683,6 +2698,7 @@ _VERB_CATALOG = { "close": "WM_CLOSE the main window; reports a save prompt instead of answering it; force:true taskkills.", "activate_document": "Make an ALREADY-OPEN document current by invoking its document-bar tab through UIA — no cursor, no foreground.", "layers_in_use": "How many layers the board ACTUALLY has: copper layers carrying primitives, densest first (a floor, not the stack).",+ "dialog_log": "What the standing dialog reaper has pre-empted, so nuisance modals never reach the user.", "read_board": "The raw board read (components, nets, layers, copper, rules, occupancy, outline) in ONE round trip.", "review_board": "Design review over one board read: placeholder values, single-pin nets, unused mechanical layers, off-grid parts, duplicate designators.", "read_schematic": "Components + pins of the CURRENT schematic sheet (one round trip).",@@ -2751,7 +2767,8 @@ COMMAND_HANDLERS = { "window_info": h_window_info, "state": h_state, "check_dialogs": h_check_dialogs, "dismiss_dialogs": h_dismiss_dialogs, "launch": h_launch, "close": h_close, "activate_document": h_activate_document, "layers_in_use": h_layers_in_use,- "instances": h_instances, "review_board": h_review_board, "read_board": h_read_board, "read_schematic": h_read_schematic, "cross_check": h_cross_check,+ "instances": h_instances, "review_board": h_review_board, "read_board": h_read_board,+ "dialog_log": h_dialog_log, "read_schematic": h_read_schematic, "cross_check": h_cross_check, "agent_start": h_agent_start, "agent_stop": h_agent_stop, "agent_status": h_agent_status, "agent_recover": h_agent_recover, "eval": h_eval, "run_command": h_run_command, "run_script": h_run_script,@@ -2924,6 +2941,13 @@ def main(): if "--port" in sys.argv: port = int(sys.argv[sys.argv.index("--port") + 1]) host = os.environ.get("ADOM_BIND_HOST", "127.0.0.1")+ # Start the standing dialog reaper BEFORE serving: Altium's startup warnings (VaultExplorer,+ # "File type not recognised") appear with no verb in flight, so nothing else would catch them.+ try:+ dialogs.start_reaper()+ print("[altium] standing dialog reaper started (known nuisances are pre-empted, never save prompts)")+ except Exception as e:+ print(f"[altium] dialog reaper failed to start: {e}") srv = ThreadingHTTPServer((host, port), Handler) print(f"[altium] bridge {BRIDGE_VERSION} listening on {host}:{srv.server_address[1]}") _install()