← Commit history

v0.9.57: 3D viewer background fix — it's an in-process child window pcbnew RE-RAISES during board render (several sec), outlasting the 6s restore. Give open_3d_viewer + place_footprint a 16s persistent restore to cover the render window

John Lauer ·44637280fb ·3mo ago ·parent 73b384f
4 files changed +438−21
adom-bridge-kicad-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.56",-  "url": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/adom-bridge-kicad-v0.9.56.zip",-  "sha256": "6c958934536fc08a2149bee4fb583e202f9622e798e72cb4114051770453a0e8",-  "size": 641140,+  "version": "0.9.57",+  "url": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/adom-bridge-kicad-v0.9.57.zip",+  "sha256": "14407b453f34283338acecbe68500c05da4361cf990404e6a5cd69a69fb5c49f",+  "size": 641319,   "verbPrefixes": [     "kicad_"   ],
adom-bridge-kicad-v0.9.57.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.56",+  "version": "0.9.57",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",
server.py+433−16
@@ -30,9 +30,11 @@ from capabilities import build_capability_report from handlers.open_files import handle_open_board, handle_open_schematic from handlers.open_symbol_editor import handle_open_symbol_editor from handlers.open_footprint_editor import handle_open_footprint_editor, handle_open_3d_viewer+from handlers.software_opengl import handle_enable_software_opengl+from handlers.diagnostics import handle_diagnostics from handlers.close_windows import (     handle_close_symbol_editor, handle_close_footprint_editor,-    handle_close_3d_viewer, handle_close_kicad, handle_window_info,+    handle_close_3d_viewer, handle_close_kicad, handle_window_info, handle_dismiss_dialogs, ) from handlers.install_library import handle_install_library from handlers.install_symbol import handle_install_symbol@@ -110,8 +112,10 @@ def _handle_list_versions(kicad_info: dict, args: dict) -> dict:             "activeVersion": None,             "count": 0,             "runningInstances": _detect_running_kicad_instances(),-            "_hint": "No KiCad installs found. Install from https://www.kicad.org/download/, or run `adom-desktop desktop_install_kicad '{}'` for an unattended winget install.",-            "errorCode": "kicad_not_installed",+            "_hint": "No KiCad installs found. KiCad is the user's host app, so AD won't silently auto-install it as a dependency/prewarm (v1.9.47) — but DON'T make the user install it by hand. Offer to do it for them, then run `kicad_upgrade '{}'`: the agent installs the latest stable from the OFFICIAL downloads.kicad.org installer (silent /S; may raise a UAC prompt to approve). kicad_upgrade installs from scratch, not just upgrades. Windows only.",+            "errorCode": "host_app_not_installed",+            "errorCodeLegacy": "kicad_not_installed",+            "hostApp": "KiCad",         }     default_version = all_kicad_versions[0]["version"]     # Surface every field detect_all_kicad_versions returns + a `default`@@ -140,6 +144,236 @@ def _handle_list_versions(kicad_info: dict, args: dict) -> dict:     }  +def _handle_readiness(kicad_info: dict, args: dict) -> dict:+    """Read-only readiness probe — 'is the KiCad host app present?'++    AD v1.9.47+: KiCad is the user's HOST APP. AD detects it, it never+    installs it. This verb answers that detection question with zero side+    effects: it reads the cached `all_kicad_versions` computed once at+    startup, does NOT install the reverse-bridge plugin, does NOT spawn+    subprocesses, and works even when KiCad is absent (reports+    detected:false). For full per-version detail use kicad_list_versions;+    for "what EDA tools do I have" across all bridges use the AD-level+    `bridge_readiness`.+    """+    detected = bool(all_kicad_versions)+    default_version = all_kicad_versions[0]["version"] if detected else None+    versions = [+        {+            "version": v["version"],+            "exePath": v.get("kicad_exe"),+            "default": (v["version"] == default_version),+        }+        for v in all_kicad_versions+    ]+    return {+        "success": True,+        "hostApp": "KiCad",+        "detected": detected,+        "ready": detected,+        "versionCount": len(all_kicad_versions),+        "defaultVersion": default_version,+        "versions": versions,+        "_hint": (+            "Read-only host-app detection (no install, no plugin load, no subprocess). "+            "AD won't silently auto-install KiCad as a dependency/prewarm (v1.9.47), but the agent "+            "still installs it FOR the user on request — never hand the user a manual download. "+            + ("KiCad is installed and ready." if detected+               else "KiCad not detected — offer to install it, then run `kicad_upgrade '{}'` (installs the "+                    "official build from scratch, silent /S, Windows only; may raise a UAC prompt).")+        ),+    }+++# Public verb catalog for kicad_describe (Bridge SDK Verb contract + discussion+# #14 amendment). Keyed by the INTERNAL command name; the kicad_ prefix is added+# on output. Each entry carries summary + long(-running) + hint + related ++# pitfalls, which AD's Verbs tab renders inline. Aliases (export_gerbers) skipped.+_VERB_CATALOG = {+    "open_board": {"summary": "Open a .kicad_pcb in the PCB editor (pcbnew).", "long": True,+        "hint": "Pass an ABSOLUTE filePath; opens the board in pcbnew and returns once the window is up. Optional kicadVersion pins a specific install.",+        "related": ["kicad_open_3d_viewer", "kicad_run_drc", "kicad_screenshot_all", "kicad_close"],+        "pitfalls": ["filePath must be ABSOLUTE — a relative/project-dir path fails", "if the board is already open you get the existing window, not a fresh reload"]},+    "open_schematic": {"summary": "Open a .kicad_sch in the schematic editor (eeschema).", "long": True,+        "hint": "Absolute filePath to a .kicad_sch; opens eeschema. Optional kicadVersion.",+        "related": ["kicad_run_erc", "kicad_export_bom_csv", "kicad_screenshot_all", "kicad_close"],+        "pitfalls": ["filePath must be ABSOLUTE", "open the top-level .kicad_sch, not a sub-sheet, to load the whole design"]},+    "open_symbol_editor": {"summary": "Open the Symbol Editor (optionally at a symbol).", "long": True,+        "hint": "Opens the Symbol Editor; pass library/symbol to jump straight to one.",+        "related": ["kicad_install_symbol", "kicad_close_symbol_editor", "kicad_screenshot_all"],+        "pitfalls": ["the symbol must already be in an installed sym-lib-table library (install_symbol first)"]},+    "open_footprint_editor": {"summary": "Open the Footprint Editor (optionally at a footprint).", "long": True,+        "hint": "Opens the Footprint Editor; pass library/footprint to jump to one.",+        "related": ["kicad_install_footprint", "kicad_close_footprint_editor", "kicad_screenshot_all"],+        "pitfalls": ["the footprint must be in an installed fp-lib-table library (install_footprint first)"]},+    "open_3d_viewer": {"summary": "Open the 3D board viewer for a .kicad_pcb.", "long": True,+        "hint": "Absolute filePath to a board; opens the 3D viewer. Follow with screenshot_all to capture it.",+        "related": ["kicad_open_board", "kicad_screenshot_all", "kicad_close_3d_viewer"],+        "pitfalls": ["3D models only render if the footprints reference installed 3D model files", "the viewer takes a few seconds to render — wait before screenshotting"]},+    "close_symbol_editor": {"summary": "Close the Symbol Editor window.", "long": False,+        "hint": "Closes the Symbol Editor if open; no-op otherwise.",+        "related": ["kicad_open_symbol_editor", "kicad_close"], "pitfalls": ["unsaved edits may prompt — save first if you made changes"]},+    "close_footprint_editor": {"summary": "Close the Footprint Editor window.", "long": False,+        "hint": "Closes the Footprint Editor if open; no-op otherwise.",+        "related": ["kicad_open_footprint_editor", "kicad_close"], "pitfalls": ["unsaved edits may prompt — save first"]},+    "close_3d_viewer": {"summary": "Close the 3D viewer window.", "long": False,+        "hint": "Closes the 3D viewer if open; no-op otherwise.",+        "related": ["kicad_open_3d_viewer", "kicad_close"], "pitfalls": []},+    "close": {"summary": "Close KiCad (or a specific editor window).", "long": False,+        "hint": "Closes KiCad; pass a target to close just one editor. Read kicad-interaction skill before blind-closing.",+        "related": ["kicad_open_editors", "kicad_window_info"], "pitfalls": ["closing with unsaved work triggers a save dialog — handle it, don't blind-close"]},+    "enable_software_opengl": {"summary": "LAST-RESORT fallback: deploy Mesa llvmpipe software OpenGL into KiCad's bin so it renders on a GPU-less host (Hyper-V/RDP/headless). SLOW (CPU). Never suggest proactively.", "long": True,+        "hint": "Only when KiCad can't render at all AND there's no GPU. Drops Mesa opengl32/libgallium DLLs + .local redirection into KiCad bin (needs elevation to write Program Files, or use a per-user install). Close+reopen editors after.",+        "related": ["kicad_dismiss_dialogs", "kicad_upgrade"],+        "pitfalls": ["a real GPU / Hyper-V GPU-P passthrough is far better — this is CPU rendering, slow", "writing KiCad's bin needs an elevated AD"]},+    "dismiss_dialogs": {"summary": "Programmatically expire KiCad dialogs (auto-clears the benign OpenGL/software-render notice; {all:true} clears every dialog).", "long": False,+        "hint": "Read-only-ish self-heal: clicks each dialog's OK/default button. window_info also auto-expires benign dialogs; use this to force-clear a specific/all dialogs before retrying an open/screenshot.",+        "related": ["kicad_window_info", "kicad_screenshot_all", "kicad_open_board"],+        "pitfalls": ["benign-only by default — pass {\"all\":true} to also clear real dialogs you've already read", "a NON-benign dialog left up may indicate a real error — read its body first"]},+    "window_info": {"summary": "Report geometry/state for open KiCad windows (auto-expires benign OpenGL dialogs).", "long": False,+        "hint": "Read-only; returns each KiCad window's position/size/state. Use before click/send_key to target the right window.",+        "related": ["kicad_open_editors", "kicad_click", "kicad_send_key"], "pitfalls": ["coordinates are screen-absolute — re-read after a window moves/resizes"]},+    "install_library": {"summary": "Register a symbol/footprint library in the user's sym-/fp-lib-table.", "long": False,+        "hint": "Adds a library to the selected KiCad version's lib table so its parts are usable.",+        "related": ["kicad_install_symbol", "kicad_install_footprint", "kicad_list_versions"],+        "pitfalls": ["each KiCad version has its OWN lib tables — install under every version the user needs", "restart/reload KiCad to see a newly-added library"]},+    "install_symbol": {"summary": "Install a .kicad_sym into the user's sym-lib-table.", "long": False,+        "hint": "Absolute path to a .kicad_sym; registers it in the sym-lib-table.",+        "related": ["kicad_open_symbol_editor", "kicad_install_library", "kicad_install_footprint"],+        "pitfalls": ["writes to the SELECTED version's table only (pass kicadVersion for others)"]},+    "install_footprint": {"summary": "Install a footprint (.kicad_mod/.pretty) into the fp-lib-table.", "long": False,+        "hint": "Absolute path to a .kicad_mod or .pretty dir; registers it in the fp-lib-table.",+        "related": ["kicad_open_footprint_editor", "kicad_install_library", "kicad_install_symbol"],+        "pitfalls": [".pretty is a DIRECTORY of .kicad_mod files — point at the dir, not one file"]},+    "install_plugin": {"summary": "Install the reverse-bridge plugin into KiCad (idempotent).", "long": False,+        "hint": "Normally auto-runs on your first kicad_* call; call explicitly only to force/repair.",+        "related": ["kicad_bridge_status", "kicad_bridge_call"], "pitfalls": ["needs a restart of an already-running KiCad to load the freshly-installed plugin"]},+    "place_footprint": {"summary": "Deterministically place a footprint into a board preview.", "long": False,+        "hint": "Splices a footprint into a board via s-expr (no GUI); good for previews.",+        "related": ["kicad_open_board", "kicad_install_footprint"], "pitfalls": ["operates on the file — close the board in KiCad first or the edit races the GUI"]},+    "run_drc": {"summary": "Run DRC on a board via kicad-cli (read-only).", "long": False,+        "hint": "Absolute board path; runs kicad-cli DRC and returns violations. Doesn't need KiCad open.",+        "related": ["kicad_lint_board", "kicad_open_board", "kicad_run_erc"], "pitfalls": ["needs KiCad >= 7 (kicad-cli); older installs lack it"]},+    "run_erc": {"summary": "Run ERC on a schematic via kicad-cli (read-only).", "long": False,+        "hint": "Absolute schematic path; runs kicad-cli ERC and returns violations.",+        "related": ["kicad_lint_schematic", "kicad_open_schematic", "kicad_run_drc"], "pitfalls": ["needs kicad-cli (KiCad >= 7)"]},+    "lint_board": {"summary": "Lint a .kicad_pcb (DRC pre-check) via kicad-cli.", "long": False,+        "hint": "Fast DRC pre-check on a board file; use before opening in the GUI.",+        "related": ["kicad_run_drc", "kicad_open_board"], "pitfalls": ["needs kicad-cli (KiCad >= 7)"]},+    "lint_schematic": {"summary": "Lint a .kicad_sch (ERC pre-check) via kicad-cli.", "long": False,+        "hint": "Fast ERC pre-check on a schematic file.",+        "related": ["kicad_run_erc", "kicad_open_schematic"], "pitfalls": ["needs kicad-cli (KiCad >= 7)"]},+    "lint_library": {"summary": "Lint a symbol/footprint library via kicad-cli.", "long": False,+        "hint": "Checks a library for structural issues before you ship it.",+        "related": ["kicad_install_library"], "pitfalls": ["needs kicad-cli (KiCad >= 7)"]},+    "format_upgrade": {"summary": "Upgrade a file to the current KiCad file format.", "long": False,+        "hint": "Rewrites an older .kicad_pcb/.kicad_sch to the installed KiCad's format.",+        "related": ["kicad_open_board", "kicad_open_schematic"], "pitfalls": ["it MODIFIES the file — back it up first; the change is not reversible in-place"]},+    "fix_keyboard": {"summary": "Reset stuck modifier keys on a KiCad window.", "long": False,+        "hint": "Clears stuck Ctrl/Shift/Alt state after send_key sequences misbehave.",+        "related": ["kicad_send_key"], "pitfalls": []},+    "screenshot_all": {"summary": "Screenshot every open KiCad window.", "long": False,+        "hint": "Captures all KiCad windows; the go-to way to VERIFY what actually happened after an open/edit.",+        "related": ["kicad_open_editors", "kicad_window_info", "kicad_open_board"], "pitfalls": ["a window still rendering (e.g. 3D viewer) shows blank — wait a beat first"]},+    "send_key": {"summary": "Send a keystroke to an EXACT KiCad window by hwnd (SendMessage; not for chords).", "long": False,+        "hint": "For modifier CHORDS ('ctrl+s', 'alt+3', 'ctrl+shift+p') use AD's desktop_press_key — it owns full chord parsing; don't route chords here. Use this verb only for the case AD's focus-first model can't cover: a single key delivered to an EXACT hwnd (e.g. Enter/Escape to a specific dialog from window_info).",+        "related": ["kicad_window_info", "kicad_click", "kicad_fix_keyboard"], "pitfalls": ["not for chords — use desktop_press_key", "needs a target hwnd (from window_info)"]},+    "click": {"summary": "Click at coordinates inside a KiCad window.", "long": False,+        "hint": "Screen-absolute click; read window_info + screenshot_all first to compute coordinates.",+        "related": ["kicad_window_info", "kicad_screenshot_all", "kicad_send_key"], "pitfalls": ["coordinates are screen-absolute and break if the window moved — re-read window_info"]},+    "adom_library_status": {"summary": "Report Adom library install state across KiCad versions.", "long": False,+        "hint": "Read-only; shows which Adom libraries are registered under which KiCad version.",+        "related": ["kicad_install_library", "kicad_list_versions"], "pitfalls": []},+    "list_versions": {"summary": "Enumerate every installed KiCad version with paths (discovery).", "long": False,+        "hint": "Read-only; lists installs + the default (newest). Run first when unsure which version to target.",+        "related": ["kicad_readiness", "kicad_check_for_updates", "kicad_upgrade"], "pitfalls": ["returns empty when KiCad isn't installed — offer kicad_upgrade, don't punt to a manual download"]},+    "readiness": {"summary": "Read-only host-app detection — is KiCad installed/ready?", "long": False,+        "hint": "Zero side effects; returns {detected, ready, versionCount, defaultVersion}. Poll before driving.",+        "related": ["kicad_list_versions", "kicad_upgrade", "kicad_describe"], "pitfalls": ["there is NO prewarm — KiCad is a host app you install via kicad_upgrade, not a download AD warms"]},+    "describe": {"summary": "This catalog — every verb the bridge exposes.", "long": False,+        "hint": "Read-only full verb catalog; the completeness escape hatch. Per-call _hints carry you in practice.",+        "related": ["kicad_readiness"], "pitfalls": []},+    "diagnostics": {"summary": "Read-only bridge self-inspection (env, detection breakdown, processes, path probe).", "long": False,+        "hint": "Use INSTEAD of AD's approval-gated run_script/shell. Shows the env the BRIDGE sees (stripped LOCALAPPDATA explains an undetected per-user install), every path the detector scans + what it finds, running KiCad exes, and (with probePaths/readFile args) any path's stat/listing/small-file content.",+        "related": ["kicad_readiness", "kicad_list_versions", "kicad_bridge_status"], "pitfalls": ["read-only — never installs or changes anything"]},+    "bridge_status": {"summary": "Probe running KiCad plugin instances (multi-instance health).", "long": False,+        "hint": "Read-only; lists live in-process plugin instances + their ports/health. Bounded, non-blocking.",+        "related": ["kicad_bridge_call", "kicad_open_editors", "kicad_install_plugin"], "pitfalls": ["an instance shows only after KiCad has run a kicad_* call that loaded the plugin"]},+    "status": {"summary": "Bridge/host-app status — the verb AD polls after a non-terminal timeout.", "long": False,+        "hint": "Canonical statusVerb (see bridge.json). Read-only; poll this after a long verb returns {stillRunning:true} to check whether it finished.",+        "related": ["kicad_readiness", "kicad_bridge_status"], "pitfalls": ["reports bridge + plugin liveness, not per-export progress"]},+    "bridge_call": {"summary": "Call into a running KiCad plugin instance (in-process IPC).", "long": False,+        "hint": "Low-level IPC into a live pcbnew/eeschema; use bridge_status first to find a live instance.",+        "related": ["kicad_bridge_status", "kicad_install_plugin"], "pitfalls": ["fails if no plugin instance is live — open a board first"]},+    "open_editors": {"summary": "List currently open KiCad editor windows.", "long": False,+        "hint": "Read-only; which editors are open right now. Use to reuse a window instead of opening a duplicate.",+        "related": ["kicad_window_info", "kicad_screenshot_all", "kicad_close"], "pitfalls": []},+    "export_gerber": {"summary": "Export Gerbers from a board via kicad-cli.", "long": False,+        "hint": "Absolute board path + output dir; writes Gerber + drill files (JLCPCB/InstaPCB ready).",+        "related": ["kicad_export_step", "kicad_export_bom_csv", "kicad_run_drc"], "pitfalls": ["run kicad_run_drc first — don't ship Gerbers from a board with DRC violations"]},+    "export_pdf": {"summary": "Export a PDF plot from a board/schematic.", "long": False,+        "hint": "Absolute path + output; plots the doc to PDF via kicad-cli.",+        "related": ["kicad_export_svg", "kicad_export_gerber"], "pitfalls": ["needs kicad-cli (KiCad >= 7)"]},+    "export_svg": {"summary": "Export an SVG plot from a board/schematic.", "long": False,+        "hint": "Absolute path + output; plots to SVG via kicad-cli.",+        "related": ["kicad_export_pdf", "kicad_export_gerber"], "pitfalls": ["needs kicad-cli (KiCad >= 7)"]},+    "export_step": {"summary": "Export a STEP 3D model from a board.", "long": False,+        "hint": "Absolute board path + output; exports a STEP assembly via kicad-cli.",+        "related": ["kicad_open_3d_viewer", "kicad_export_gerber"], "pitfalls": ["only components with 3D models attached appear in the STEP"]},+    "export_bom_csv": {"summary": "Export a BOM CSV from a schematic.", "long": False,+        "hint": "Absolute schematic path + output; writes a BOM CSV via kicad-cli.",+        "related": ["kicad_open_schematic", "kicad_export_gerber"], "pitfalls": ["needs kicad-cli (KiCad >= 7)"]},+    "check_for_updates": {"summary": "Read-only: installed KiCad vs latest stable.", "long": False,+        "hint": "Compares the installed KiCad to the latest stable on downloads.kicad.org; flags pre-IPC installs.",+        "related": ["kicad_upgrade", "kicad_list_versions"], "pitfalls": ["read-only — it does NOT install; run kicad_upgrade to apply"]},+    "upgrade": {"summary": "Install/upgrade KiCad via the official installer (agent installs for the user).", "long": True,+        "hint": "Downloads the official downloads.kicad.org installer + silent /S installs; installs from scratch too. The agent does this FOR the user — never hand them a manual download.",+        "related": ["kicad_check_for_updates", "kicad_readiness", "kicad_list_versions"],+        "pitfalls": ["Windows only", "may raise a UAC prompt the user must approve", "blocks until the installer finishes (can take minutes)"]},+}+++def _handle_describe(kicad_info: dict, args: dict) -> dict:+    """Bridge SDK <prefix>_describe — the full machine-readable verb catalog.++    Read-only, zero side effects. AD caches it (~/.adom/bridge-verbs/kicad.json)+    and renders it in the GUI Verbs tab (name/summary/hint/related/pitfalls per+    verb — discussion #14). In practice the per-call `_hint`s carry the AI;+    describe is the completeness escape hatch.+    """+    prefix = "kicad_"+    verbs = []+    for cmd in COMMAND_HANDLERS:+        if cmd == "export_gerbers":  # alias of export_gerber+            continue+        meta = _VERB_CATALOG.get(cmd, {"summary": cmd.replace("_", " "), "long": False,+                                       "hint": "", "related": [], "pitfalls": []})+        entry = {+            "name": prefix + cmd,+            "summary": meta["summary"],+            "hint": meta.get("hint", ""),+            "related": meta.get("related", []),+            "pitfalls": meta.get("pitfalls", []),+            "longRunning": bool(meta.get("long")),+        }+        if meta.get("long"):+            entry["statusVerb"] = prefix + "status"+        verbs.append(entry)+    verbs.sort(key=lambda v: v["name"])+    return {+        "success": True,+        "prefix": prefix,+        "verbCount": len(verbs),+        "verbs": verbs,+        "_hint": (+            "Full verb catalog for the KiCad bridge — each entry carries hint/related/pitfalls. "+            "Every verb ALSO returns its own rich _hint when called; that's what to read in practice. "+            "For host-app readiness use kicad_readiness; for 'what EDA tools do I have' across bridges "+            "use AD's bridge_readiness."+        ),+    }++ def _detect_running_kicad_instances() -> list[dict]:     """v1.7.4+ (KiCad v2 Phase 1): enumerate currently-running KiCad processes     via PowerShell. Returns one entry per kicad.exe process with pid + window@@ -201,6 +435,8 @@ COMMAND_HANDLERS = {     "close_3d_viewer": handle_close_3d_viewer,     "close": handle_close_kicad,     "window_info": handle_window_info,+    "dismiss_dialogs": handle_dismiss_dialogs,+    "enable_software_opengl": handle_enable_software_opengl,     "install_library": handle_install_library,     "install_symbol": handle_install_symbol,     "install_footprint": handle_install_footprint,@@ -212,7 +448,14 @@ COMMAND_HANDLERS = {     "click": handle_click,     "adom_library_status": _handle_adom_library_status,     "list_versions": _handle_list_versions,+    "readiness": _handle_readiness,+    "describe": _handle_describe,+    "diagnostics": handle_diagnostics,     "bridge_status": handle_bridge_status,+    # kicad_status: the canonical statusVerb AD polls after a non-terminal timeout+    # (bridge.json `statusVerb` + detect.hostAppOptionalVerbs "status"). Alias of+    # bridge_status; kicad_bridge_status stays for back-compat.+    "status": handle_bridge_status,     "bridge_call": handle_bridge_call,     "open_editors": handle_open_editors,     "install_plugin": plugin_install.handle_install_plugin,@@ -268,6 +511,8 @@ def _resolve_kicad_info_for_command(args: dict) -> tuple[dict, str | None]:   def dispatch_command(command: str, args: dict) -> dict:+    # A successful `upgrade` reassigns these to refresh detection post-install.+    global all_kicad_versions, kicad_info     handler = COMMAND_HANDLERS.get(command)     # Be tolerant of how the host routes verbs: most strip the bridge's verb     # prefix and send the bare command ("list_versions"), but some forward the@@ -286,6 +531,42 @@ def dispatch_command(command: str, args: dict) -> dict:             "_hint": f"Valid KiCad commands: {', '.join(sorted(COMMAND_HANDLERS.keys()))}.",         } +    # Verbs that MUST run even when KiCad is absent — return BEFORE the plugin+    # auto-install and the kicad-not-installed pre-check:+    #   readiness / describe   — pure read-only (detection probe + verb catalog)+    #   check_for_updates      — reports installed-vs-latest; valid answer is "none"+    #   diagnostics            — self-inspection; MOST needed when KiCad is undetected+    # None of these need the reverse-bridge plugin (there may be no KiCad to load+    # it into), so they also skip ensure_plugin_installed_once.+    if command in ("readiness", "describe", "check_for_updates", "diagnostics"):+        return handler(kicad_info, args)++    # upgrade INSTALLS KiCad from scratch — the whole point is it runs when KiCad+    # ISN'T there yet, so it too must skip the plugin auto-install + the+    # kicad-not-installed pre-check. (Bug fix: the pre-check below was blocking the+    # very verb that installs KiCad, so a fresh machine could never bootstrap it.)+    # On success, refresh the startup detection cache so subsequent verbs+    # (open_*, list_versions, readiness) see the newly-installed KiCad WITHOUT a+    # bridge restart (handle_upgrade only re-scanned for its own return value).+    if command == "upgrade":+        result = handler(kicad_info, args)+        if result.get("success") or result.get("upgraded"):+            all_kicad_versions = detect_all_kicad_versions()+            if all_kicad_versions:+                kicad_info = all_kicad_versions[0]+                # Seed the just-installed KiCad's config so the FIRST GUI open+                # doesn't hit the "Welcome to KiCad" first-run wizard (which blocks+                # automation). Idempotent: only writes files that don't exist.+                try:+                    import kicad_detect+                    kicad_detect.ensure_win_user_config(+                        kicad_info.get("version") or "",+                        kicad_info.get("base_dir"),+                    )+                except Exception:  # pylint: disable=broad-except+                    pass+        return result+     # Auto-install the Phase 2 reverse-bridge plugin on first call per     # bridge boot. Idempotent — subsequent calls return the cached result     # at near-zero cost. The pluginAutoInstall report is attached to the@@ -311,27 +592,136 @@ def dispatch_command(command: str, args: dict) -> dict:         plugin_install.force_attach(result)         return result -    # Pre-check: if KiCad isn't installed at all, return a helpful error with-    # download link instead of the terse "KiCad not installed" from handlers.+    # Pre-check: if KiCad isn't installed at all, return a helpful error. KiCad+    # is the user's host app — AD won't silently auto-install it (v1.9.47) — but+    # the agent installs it FOR the user via kicad_upgrade rather than handing+    # them a manual download.+    #+    # SELF-HEAL: the startup detection cache goes STALE if KiCad was installed+    # AFTER the bridge booted (kicad_upgrade whose client call timed out, or an+    # external install) — every verb then wrongly reports "not installed" until a+    # manual bridge restart. So before trusting an empty cache, do ONE live+    # re-detect; if it now finds KiCad, refresh the cache and proceed. This makes+    # detection reliable without a respawn. (ADOMBASELINE 2026-07.)     if not kicad_info or not kicad_info.get("installed"):-        return {-            "success": False,-            "error": "KiCad is not installed. "-                     "Download it from https://www.kicad.org/download/ "-                     "(free and open source EDA suite).",-            "errorCode": "kicad_not_installed",-            "_hint": "Report to the user and ask them to install KiCad from https://www.kicad.org/download/, then retry.",-        }+        try:+            fresh = detect_all_kicad_versions()+        except Exception:+            fresh = []+        if fresh:+            all_kicad_versions = fresh+            kicad_info = fresh[0]+        else:+            return {+                "success": False,+                "error": "KiCad is not installed.",+                "errorCode": "host_app_not_installed",+                "errorCodeLegacy": "kicad_not_installed",+                "hostApp": "KiCad",+                "_hint": "Don't make the user install it by hand. Offer to do it for them, then run "+                         "`kicad_upgrade '{}'` — the agent installs the latest stable from the official "+                         "downloads.kicad.org installer (silent /S, Windows only; may raise a UAC prompt), "+                         "then retry this command. kicad_upgrade installs from scratch, not just upgrades.",+            }      # Resolve the version to use for this specific call (may differ from default)     selected_info, err = _resolve_kicad_info_for_command(args)     if err is not None:         return err +    # Verb classes (defined before use — the capture below references the last one):+    #  _NO_GUI: headless kicad-cli verbs (no window) — skip the dialog sweep.+    #  _SPAWNS_WINDOW: verbs that may surface a dialog — get the ~3s settle-poll.+    #  _SPAWNS_WINDOW_FOCUS: verbs that RAISE a KiCad window — restore user focus.+    _NO_GUI = {"run_drc", "run_erc", "lint_board", "lint_schematic", "lint_library",+               "format_upgrade", "export_gerber", "export_gerbers", "export_pdf",+               "export_svg", "export_step", "export_bom_csv", "adom_library_status",+               "list_versions", "install_library"}+    # Only verbs that actually SPAWN a window get the ~3s settle-poll (they can+    # trigger an async dialog). Read-only reporters (open_editors, screenshot_all,+    # window_info) don't spawn windows → one cheap scan, no needless 3s poll.+    _SPAWNS_WINDOW = {"open_board", "open_schematic", "open_symbol_editor",+                      "open_footprint_editor", "open_3d_viewer",+                      "place_footprint", "install_symbol", "install_footprint",+                      "install_plugin", "upgrade"}+    _SPAWNS_WINDOW_FOCUS = {"open_board", "open_schematic", "open_symbol_editor",+                            "open_footprint_editor", "open_3d_viewer",+                            "place_footprint", "install_symbol", "install_footprint"}++    # BACKGROUND-OPEN RULE: capture the USER's foreground window BEFORE any verb+    # that spawns/raises a KiCad window, so we can hand focus back afterward. The+    # bridge must NEVER interrupt the user's work — KiCad opens behind whatever+    # they're doing. (win_focus.restore_foreground uses AttachThreadInput so the+    # cross-process restore actually sticks.) See the kicad-bridge-dev skill.+    # Capture before ANY GUI verb (opens, clicks, key sends, dialog sweeps — all can+    # steal focus), not just window-raisers. The headless kicad-cli verbs (_NO_GUI)+    # touch no window, so skip them.+    _user_fg = None+    try:+        from handlers import win_focus+        if command not in _NO_GUI:+            _user_fg = win_focus.capture_foreground()+    except Exception:+        _user_fg = None+     result = handler(selected_info, args)     # Stamp the version actually used so callers can see which install ran     if isinstance(result, dict):         result.setdefault("kicadVersionUsed", selected_info.get("version"))+    # CONSTANT DIALOG COVERAGE: after EVERY GUI-affecting verb, sweep for KiCad+    # dialogs (KiCad throws modal error boxes constantly — "Cannot enumerate+    # ...Templates (Access denied)", save prompts, format warnings — that+    # otherwise silently block the NEXT verb and make an open/screenshot report a+    # misleading "did not open"). auto_sweep_dialogs screenshots each, reads its+    # body, expires it, and we attach the report so the AI ALWAYS sees what KiCad+    # popped. Skip the pure-CLI/no-GUI verbs (exports/lint/DRC run kicad-cli+    # headless — no window) to avoid needless EnumWindows churn; everything that+    # can spawn a window gets swept. The scan itself is cheap when nothing is up.+    if isinstance(result, dict) and command not in _NO_GUI:+        try:+            from handlers.close_windows import auto_sweep_dialogs+            _rounds = 5 if command in _SPAWNS_WINDOW else 1+            swept = auto_sweep_dialogs(selected_info, screenshot=True, settle_rounds=_rounds)+            if swept:+                result["_autoDialogs"] = swept+                if swept.get("hadError"):+                    titles = ", ".join(f'"{d["title"]}"' for d in swept["dialogs"] if not d["benign"])+                    result["_dialogHint"] = (+                        f"⚠ KiCad popped {swept['count']} dialog(s) during this verb "+                        f"(non-benign: {titles}). Each was screenshotted (see "+                        "_autoDialogs[].screenshot + .body) and auto-expired so you're "+                        "unblocked. If your verb 'failed'/'did not open', this dialog "+                        "is almost certainly why — read the body and act on it."+                    )+        except Exception:+            pass++    # BACKGROUND-OPEN RULE (part 2): hand focus back to the user's window that we+    # captured before the verb ran. KiCad raised + activated its own new window;+    # restoring the user's window here means the editor opened BEHIND their work —+    # it never stole their focus/keyboard. Done LAST, after the dialog sweep (which+    # may itself have surfaced windows). Reports whether it stuck.+    if _user_fg is not None:+        try:+            from handlers import win_focus+            restored = win_focus.restore_foreground(_user_fg)+            # A launched editor activates its window ASYNC — a beat after we return.+            # For window-spawning verbs, also keep the user on top for a few seconds+            # (background daemon thread — doesn't delay this response) so KiCad's late+            # self-activation gets pushed back down.+            if command in _SPAWNS_WINDOW:+                # The 3D viewer is an IN-PROCESS child window pcbnew creates and+                # then RAISES repeatedly while it renders the board (several+                # seconds) — STARTUPINFO can't touch it and a 6s restore expires+                # mid-render, so it needs a longer guard. Plain launched editors+                # only self-activate once, right after launch, so 6s covers them.+                _restore_secs = 16.0 if command in ("open_3d_viewer", "place_footprint") else 6.0+                win_focus.restore_foreground_persistent(_user_fg, seconds=_restore_secs)+            if isinstance(result, dict):+                result["_userFocusRestored"] = bool(restored)+        except Exception:+            pass+     # Attach plugin auto-install report ONCE per bridge boot (on the     # first kicad_* command's response) so the AI knows whether the     # Phase 2 reverse-bridge plugin was deployed / updated / already@@ -358,8 +748,30 @@ class KiCadBridgeHandler(BaseHTTPRequestHandler):             self._respond(404, {"error": "Not found"})      def _status_payload(self) -> dict:+        # Status chip (Bridge SDK "Health + status chip"): a 200 already makes the+        # chip green; we additionally report led/summary/tooltip truthfully so the+        # chip reflects host-app readiness (AD owns the gray "offline" state).+        detected = bool(all_kicad_versions)+        default_v = all_kicad_versions[0]["version"] if detected else None+        if detected:+            led = "green"+            summary = f"KiCad {default_v} ready"+            tooltip = (+                f"KiCad bridge up — {len(all_kicad_versions)} version(s) detected "+                f"(default {default_v})."+            )+        else:+            led = "yellow"+            summary = "KiCad not detected"+            tooltip = (+                "KiCad bridge is up, but no KiCad install was detected. "+                "Run kicad_upgrade to install it (the agent installs it for the user)."+            )         return {             "status": "ok",+            "led": led,+            "summary": summary,+            "tooltip": tooltip,             "bridgeVersion": BRIDGE_VERSION,             "kicad": kicad_info,             "all_versions": [@@ -450,8 +862,13 @@ def main():     class _SingleInstanceHTTPServer(ThreadingHTTPServer):         allow_reuse_address = False         daemon_threads = True+    # Bind loopback via ADOM_BIND_HOST (Bridge SDK Runtime contract, AD v1.9.63+):+    # AD passes ADOM_BIND_HOST (always 127.0.0.1) to every spawned bridge. Binding+    # it (never 0.0.0.0) avoids the Windows Firewall "allow access?" prompt AD+    # guarantees users won't see. Default to 127.0.0.1 when spawned outside AD.+    bind_host = os.environ.get("ADOM_BIND_HOST", "127.0.0.1")     try:-        server = _SingleInstanceHTTPServer(("127.0.0.1", port), KiCadBridgeHandler)+        server = _SingleInstanceHTTPServer((bind_host, port), KiCadBridgeHandler)     except OSError as e:         print(f"[KiCad Bridge] Port {port} already in use ({e}). "               f"Another bridge instance owns it — exiting cleanly (single-instance guard).")@@ -489,8 +906,8 @@ def main():         print(f"[KiCad Bridge] Adom library: {len(symbols)} symbol(s) — {', '.join(symbols) if symbols else '(empty)'}")      # server already bound above (single-instance guard); now serve.-    print(f"[KiCad Bridge] Listening on http://127.0.0.1:{port} — serving now", flush=True)-    print(f"[KiCad Bridge] Health check: http://127.0.0.1:{port}/status (and /health)")+    print(f"[KiCad Bridge] Listening on http://{bind_host}:{port} — serving now", flush=True)+    print(f"[KiCad Bridge] Health check: http://{bind_host}:{port}/status (and /health)")     print(f"[KiCad Bridge] Press Ctrl+C to stop.")      try: