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
Publish 1.0.10
3 files changed
+53−2
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-desktop-kicad-bridge",- "version": "1.0.9",+ "version": "1.0.10", "type": "app", "description": "KiCad bridge for Adom Desktop.", "tags": [
page.json+1−1@@ -4,7 +4,7 @@ "slug": "adom-desktop-kicad-bridge", "title": "Adom Desktop - KiCad Bridge", "brief": "Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",- "version": "1.0.9",+ "version": "1.0.10", "tags": [ "kicad", "pcb",
skills/kicad-bridge-dev/SKILL.md+51@@ -19,6 +19,57 @@ A reverse bridge AD spawns on the user's laptop (`spawn.kind: python`, `entrypoi 3. **Embedded-Python reverse bridge** (`plugin_payload/adom_bridge.py`) — `usercustomize.py` auto-injects at KiCad's USER_SITE → runs an HTTP server INSIDE each KiCad process, writes a `%TEMP%` discovery file; container-side `handlers/bridge_client.py` POSTs JSON-RPC to `/rpc`, dispatched onto KiCad's wx UI thread via `wx.CallAfter`. Methods: `ping`, `list_frames`, `get_menu_ids`, `wm_command`. Verbs: `bridge_status`, `bridge_call`. 4. **Win32 / UIA / SendKeys** (`handlers/kicad_ui.py`, ctypes/user32) — the last resort: `WM_COMMAND` (open editors), `SendInput`/`keybd_event` (`alt+3`, `ctrl+s`), `PrintWindow`+`EnumWindows` (screenshots), `GetWindowRect`. For opening the 3D viewer, screenshots, clicks, keystrokes, window management. +## 🪟 NON-NEGOTIABLE: KiCad windows ALWAYS open in the BACKGROUND++**The bridge must NEVER steal the user's focus or interrupt their work.** A user may+be typing in another app when the AI opens a board — the KiCad window must appear+*behind* their active window, not yank their keyboard/foreground. This is a product+rule, not a nicety. (Requested by John, 2026-07.)++**How it's enforced (central, in `dispatch_command`):** for **EVERY GUI verb** (everything+except the headless `_NO_GUI` kicad-cli set — opens, clicks, key sends, dialog sweeps, all+of it) the dispatcher:+1. `win_focus.capture_foreground()` — records the USER's active window **before** the handler runs.+2. runs the handler.+3. `win_focus.restore_foreground(user_hwnd)` **after** the handler + dialog sweep — hands+ focus back. Uses **AttachThreadInput** so the cross-process SetForegroundWindow actually+ sticks (a plain call from a background process is blocked by the foreground lock).+ The response carries `_userFocusRestored: true/false`.++**Mid-operation matters too, not just the end state.** Restoring focus *after* still lets a+window FLASH forward mid-verb, which the user sees and hates. So the handlers themselves must+not foreground: `_force_foreground` in open_symbol/footprint_editor was gutted to+`ShowWindow(SW_SHOWNOACTIVATE)` (visible, not activated). The two fallbacks that genuinely+can't avoid it are **OPT-IN, off by default**:+- the launcher-icon **click** (moves the user's mouse via `SetCursorPos`) → needs `{"allowCursorClick": true}`.+- the **Alt+3** keystroke for the 3D viewer (needs pcbnew foregrounded) → needs `{"allowFocusSteal": true}`.+Default behavior is the **background plugin path** (WM_COMMAND via the embedded reverse bridge —+no focus, no cursor). If it can't (plugin not loaded), the verb fails gracefully pointing at+"open a board first so the plugin loads," rather than hijacking the desktop.++**Background fallback = UIA, not the cursor.** Between the plugin path and the opt-in disruptive+fallbacks sits **Tier 1.5: `handlers/uia.py`** — it shells to PowerShell's `System.Windows.Automation`+and **Invokes** the "Footprint Editor" / "3D Viewer" control by accessible name. UIA Invoke is+programmatic: no foreground, no focus steal, no cursor move. KiCad (wxWidgets) exposes an invokable+UIA tree, so this is the reliable background way to open editors when the WM_COMMAND plugin path+isn't loaded. `uia.uia_invoke(hwnd, contains=...)` / `uia.uia_set_value(...)`. This is the bridge's OWN+subprocess (like kicad-cli) — NOT AD's gated `run_script`.++**Rules when you touch UI code:**+- A new window-raising verb → add it to **`_SPAWNS_WINDOW_FOCUS`** in `server.py`. That's the+ whole opt-in; the dispatcher does the rest.+- Prefer **`win_focus.show_without_activating(hwnd)`** (SW_SHOWNOACTIVATE) over+ `ShowWindow(SW_RESTORE)+SetForegroundWindow+BringWindowToTop` when a window just needs to be+ visible. The legacy `_force_foreground` in open_symbol/footprint_editor is used for+ SendInput-nav that genuinely needs transient focus — the dispatcher restore covers its cost,+ but don't add NEW gratuitous foregrounding.+- Deliver input by **`PostMessage`/`SendMessage` to an exact hwnd** where you can (no focus+ needed) instead of `SendInput` (global, needs foreground). `kicad_click`/`kicad_send_key`+ still foreground+SendInput for the hard cases; that's fine as long as the dispatcher restore+ runs after.+- Recording/demo doctrine agrees: **NEVER foreground a window to fix a blank frame.** Background+ capture (WGC) + background windows is the standard.+ ## Adding a verb (3 places, keep in sync) 1. Write `handle_<verb>` in a handler; return a dict with `success` + a rich **`_hint`** (mandatory — the AI reads verb OUTPUT, not this skill).