app
KiCad - the KiCad Bridge
Public Made by Adomby adom
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.
← Commit history
v0.9.97: background-first hard rule — keystroke fallbacks gated behind allowFocusSteal (were leaking keys to the user's window via neutered _force_foreground); foreground:true user opt-in to bring a window to the user; principle in dev skill
9 files changed
+127−8
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.96+0.9.97
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.96",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.96/adom-bridge-kicad-v0.9.96.zip",- "sha256": "d26cca2b9dadc9472d4a10f24e73b4dc343cdd9442acd8fc5036ae3ce442f6d5",- "size": 388470,+ "version": "0.9.97",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.97/adom-bridge-kicad-v0.9.97.zip",+ "sha256": "cb5b58d558700f4947c851f12d9926e7b3a74d1e90cf58410384ed1a02dbdf3d",+ "size": 389789, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-15T02:01:57Z",+ "released_at": "2026-08-15T02:19:49Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.97.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.96",+ "version": "0.9.97", "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-bridge", "author": "Adom Inc.",
handlers/open_footprint_editor.py+21−1@@ -257,7 +257,13 @@ def _send_ctrl_shift_e(hwnd: int): VK_CONTROL = 0x11 VK_SHIFT = 0x10 VK_E = 0x45- _force_foreground(hwnd)+ # Only reachable via allowFocusSteal now — so a REAL foreground is correct+ # here (keys must land on KiCad, never the user's window).+ try:+ from handlers import win_focus+ win_focus.bring_to_user(hwnd)+ except Exception:+ pass time.sleep(0.3) user32.keybd_event(VK_CONTROL, 0, 0, 0) user32.keybd_event(VK_SHIFT, 0, 0, 0)@@ -721,6 +727,20 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict: "via": "win32-background", } + # BACKGROUND-FIRST (John, 2026-08-14): the win32 tier above is the background+ # path; the SendKeys/Ctrl+Shift+E fallback needs a foregrounded window (rude,+ # and leaks keys to the user's window if we DON'T foreground). Opt-in only.+ if not args.get("allowFocusSteal"):+ return {+ "success": False, "editorOpened": True, "footprintLoaded": False,+ "error": f"Footprint Editor open but '{search_text}' didn't load in the background "+ f"(title: {_get_footprint_editor_title() or 'unknown'})",+ "_hint": ("Background load unconfirmed. Retry once (the win32 tier usually lands warm). "+ "The keystroke fallback is off by default (it must foreground the window — "+ "rude while you type); pass allowFocusSteal:true to accept a focus grab, or "+ "foreground:true to bring the editor to you."),+ }+ nav_result = _navigate_to_footprint(fp_hwnd, search_text) if nav_result["status"] == "ok":
handlers/open_symbol_editor.py+19@@ -590,6 +590,25 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: "via": "win32-background", } + # BACKGROUND-FIRST CONTRACT (John, 2026-08-14): the win32 tier above is the+ # background path. The PowerShell SendKeys fallback below fires GLOBAL+ # keystrokes that need the window foregrounded — which would type into the+ # USER's active window if we don't steal focus, and stealing focus is rude.+ # So it is OPT-IN only ({"allowFocusSteal":true}); by default we return the+ # honest failure and let the caller retry (the win32 tier usually lands on a+ # 2nd try once eeschema's tree is warm).+ if not args.get("allowFocusSteal"):+ return {+ "success": False, "editorOpened": True, "symbolLoaded": False,+ "error": f"Symbol Editor open but '{search_text}' didn't load in the background "+ f"(title: {_get_symbol_editor_title() or 'unknown'})",+ "_hint": ("The background load didn't confirm. Retry this same call (the win32 tier "+ "usually lands 2nd try once the library tree is warm). The keystroke fallback "+ "is disabled by default because it needs to foreground the window (rude while "+ "you're typing) — pass allowFocusSteal:true ONLY if you accept a focus grab, "+ "or foreground:true to bring the editor to you and load it yourself."),+ }+ nav_result = _navigate_to_symbol(sym_hwnd, search_text) if nav_result["status"] == "ok":
handlers/win_focus.py+22@@ -189,3 +189,25 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = threading.Thread(target=_loop, daemon=True).start() except Exception: pass+++def user_wants_foreground(args) -> bool:+ """True only when the CALLER explicitly asked to SEE the window — the user's+ own choice to foreground (John: 'always let the user choose to fg a window').+ Never inferred; must be an explicit foreground/showToUser/bringToFront flag."""+ if not isinstance(args, dict):+ return False+ return bool(args.get("foreground") or args.get("showToUser") or args.get("bringToFront"))+++def bring_to_user(hwnd) -> bool:+ """Explicit, user-requested foreground of a window (the ONLY sanctioned way a+ KiCad window is brought to the front for the user — everything else stays+ background). Returns True if it took."""+ if not IS_WINDOWS or not hwnd:+ return False+ try:+ _user32.ShowWindow(hwnd, 9) # SW_RESTORE (in case minimized)+ return _set_foreground_robust(hwnd)+ except Exception:+ return False
server.py+24@@ -770,6 +770,30 @@ def dispatch_command(command: str, args: dict) -> dict: # 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.+ # USER CHOICE (John, 2026-08-14): a caller may explicitly want to SEE the+ # window — foreground:true / showToUser:true. That is the ONE sanctioned way a+ # KiCad window comes to the front. When set, we SKIP the background-restore and+ # bring the freshly-opened KiCad window to the user instead.+ try:+ from handlers import win_focus as _wf+ if _wf.user_wants_foreground(args) and command in _SPAWNS_WINDOW:+ import time as _t+ _t.sleep(1.0) # let the window exist+ _shown = None+ try:+ from handlers.kicad_ui import _find_all_kicad_windows+ wins = _find_all_kicad_windows()+ if wins:+ _shown = wins[-1].get("hwnd") # the most-recently surfaced KiCad window+ _wf.bring_to_user(_shown)+ except Exception:+ pass+ if isinstance(result, dict):+ result["_broughtToUser"] = bool(_shown)+ _user_fg = None # don't also restore-away what the user just asked to see+ except Exception:+ pass+ if _user_fg is not None: try: from handlers import win_focus
skills/kicad-bridge-background/SKILL.md+34@@ -13,6 +13,40 @@ clicks, key sends, demo beats — happens in the background while the user keeps working.** This is a hard product requirement for the public release, not a politeness preference. +## The principle, in full (John, 2026-08-14, verbatim intent)++"ALL of your verbs should do their work in the background if they can. It's rude+to force-foreground a KiCad window, because a user driving your bridge is doing+OTHER AI-thread prompts and typing while you work. Try SUPER hard to do it in the+background. Foreground is only for UI that can ONLY be done in the fg, and even+then it's the worst case, last resort. And ALWAYS let the user CHOOSE to+foreground a window if they want to look at what you're doing."++Three obligations this puts on every windowed verb:++1. **Background-first, and try hard.** Exhaust the background paths (plugin RPC,+ the win32 tier: WM_SETTEXT/PostMessage to a specific hwnd, UIA background+ Invoke — none need focus) BEFORE any focus-dependent path.+2. **Foreground = opt-in last resort, and it must ACTUALLY foreground.** Global+ keystrokes (`keybd_event`, SendKeys, Alt+3) go to whatever window has focus —+ so if you fire them WITHOUT foregrounding, they leak into the user's window+ (found live 2026-08-14: `_send_ctrl_shift_e` did exactly this because+ `_force_foreground` is neutered). Rule: a keystroke fallback is gated behind+ `allowFocusSteal:true` AND, when it runs, does a real `win_focus.bring_to_user`+ first. Off by default → return the honest `success:false` + retry hint instead+ of leaking keys.+3. **The user may say "show me."** `foreground:true` (or `showToUser` /+ `bringToFront`) on any window-spawning verb is the ONE sanctioned foreground+ path: the dispatcher then brings the freshly-opened KiCad window TO the user+ (`win_focus.bring_to_user`) instead of restoring their window, and reports+ `_broughtToUser`. `win_focus.user_wants_foreground(args)` is the only place+ that intent is read — never infer it.++Audit rule when you touch window code: `grep` for `SetForegroundWindow`,+`keybd_event`, `SendInput`, `SendKeys`. Every hit must be EITHER inside an+`allowFocusSteal`/`user_wants_foreground` branch that truly foregrounds, or gone.+An ungated one is a focus-steal bug.+ ## Why this is hard (the three failure modes) 1. **KiCad activates windows asynchronously.** `SW_SHOWNOACTIVATE` on the