app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
← 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
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: