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.105: every forced foreground announces itself first — ab notify_user caption (attributed to the calling thread) fires ~1.2s before any allowFocusSteal/allowForeground grab
9 files changed
+72−7
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.104+0.9.105
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.104",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.104/adom-bridge-kicad-v0.9.104.zip",- "sha256": "41ce2eaf45d88264eb0016c1b9100aeccacb24f428f1031eafade119ba673778",- "size": 395740,+ "version": "0.9.105",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.105/adom-bridge-kicad-v0.9.105.zip",+ "sha256": "20642cf4c35bc8dacb55568f86c0de5fec951cc0d212ddc29b804180b8174a8c",+ "size": 396469, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-15T10:06:09Z",+ "released_at": "2026-08-15T10:09:52Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.105.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.104",+ "version": "0.9.105", "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/kicad_ui.py+13@@ -570,6 +570,12 @@ def handle_send_key(kicad_info: dict, args: dict) -> dict: time.sleep(0.05) allow_fg = bool(args.get("allowForeground"))+ if allow_fg:+ try:+ from handlers import win_focus as _wf+ _wf.announce_foreground(f"send '{args.get('key') or 'input'}' to KiCad with real keystrokes")+ except Exception:+ pass # DEFAULT PATH: post the key straight to KiCad's window — no focus, no screen # theft, works while the window is buried. Chords still need the focus path@@ -781,6 +787,13 @@ def handle_click(kicad_info: dict, args: dict) -> dict: "user you need the screen for ~5s, then retry allowForeground:true.")} return {"success": False, "error": "Could not post the click to the window"} + # allowForeground granted — announce WHY before moving the user's cursor+ try:+ from handlers import win_focus as _wf+ _wf.announce_foreground("click inside KiCad with the real cursor")+ except Exception:+ pass+ # Convert to absolute coordinates for SendInput (0-65535 range) screen_w = user32.GetSystemMetrics(0) screen_h = user32.GetSystemMetrics(1)
handlers/open_footprint_editor.py+11@@ -261,6 +261,7 @@ def _send_ctrl_shift_e(hwnd: int): # here (keys must land on KiCad, never the user's window). try: from handlers import win_focus+ win_focus.announce_foreground("load the footprint via keyboard (Ctrl+Shift+E)") win_focus.bring_to_user(hwnd) except Exception: pass@@ -946,6 +947,11 @@ def handle_open_3d_viewer(kicad_info: dict, args: dict) -> dict: # Opt-in disruptive fallback: Alt+3 keystroke (needs the FP window foregrounded, # so it steals focus) — only when the caller explicitly allows it. if args.get("allowFocusSteal"):+ try:+ from handlers import win_focus as _wf+ _wf.announce_foreground("open the 3D viewer via Alt+3")+ except Exception:+ pass VK_MENU_LOCAL = 0x12 VK_3 = 0x33 user32.SetForegroundWindow(fp_hwnd)@@ -1055,6 +1061,11 @@ def handle_open_3d_viewer(kicad_info: dict, args: dict) -> dict: # their app's Alt+3). That steals focus, so it's OPT-IN only # ({"allowFocusSteal": true}); by default we fail to the background path. if args.get("allowFocusSteal"):+ try:+ from handlers import win_focus as _wf+ _wf.announce_foreground("open the board 3D viewer via Alt+3")+ except Exception:+ pass user32.SetForegroundWindow(pcb_hwnd) time.sleep(0.3) VK_MENU = 0x12 # Alt key
handlers/open_symbol_editor.py+5@@ -609,6 +609,11 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: "or foreground:true to bring the editor to you and load it yourself."), } + try:+ from handlers import win_focus as _wf+ _wf.announce_foreground(f"type the symbol search for '{search_text}'")+ except Exception:+ pass nav_result = _navigate_to_symbol(sym_hwnd, search_text) if nav_result["status"] == "ok":
handlers/win_focus.py+28@@ -341,3 +341,31 @@ def bring_to_user(hwnd) -> bool: return _set_foreground_robust(hwnd) except Exception: return False+++def announce_foreground(action: str, seconds: int = 5) -> None:+ """MANDATORY courtesy caption before any forced foreground (John 2026-08-15:+ "when you HAVE to foreground cuz you have no other choice, show an ab caption+ telling the user why"). Fires a native ab toast — branded, and attributed to+ the calling AI thread via the forwarded caller headers — so the user sees WHY+ their screen is about to be taken, BEFORE it happens. Fire-and-forget in a+ daemon thread: the toast failing must never fail (or delay) the verb."""+ def _fire():+ try:+ from caller_identity import ad_callback+ ad_callback("notify_user", {+ "message": f"KiCad bridge needs your screen for ~{seconds}s: {action}. "+ "Background methods were exhausted — this was explicitly allowed "+ "by the calling AI (allowFocusSteal/allowForeground).",+ "title": "Taking the foreground briefly",+ "level": "warning",+ }, timeout=8)+ except Exception:+ pass+ try:+ import threading+ threading.Thread(target=_fire, daemon=True).start()+ import time as _t+ _t.sleep(1.2) # give the toast a beat to render BEFORE the grab+ except Exception:+ pass
skills/kicad-bridge-background/SKILL.md+8@@ -84,6 +84,14 @@ the browser forward, restored stale snapshots). The rule now: observe-only (fg_assert), it raises nothing. - The ONE exception remains foreground:true / bring_to_user — the user explicitly asking to see a window.+- **Every forced foreground ANNOUNCES ITSELF FIRST** (John 2026-08-15: "show an+ ab caption telling the user why you have to foreground"). Rule: any+ allowFocusSteal/allowForeground branch calls `win_focus.announce_foreground(+ "<what and why>")` BEFORE the grab — it fires an ab `notify_user` toast+ (branded, attributed to the calling AI thread via forwarded caller headers)+ and waits ~1.2s so the caption renders before the screen is taken. A forced+ foreground without the announce is a bug; add the call when adding any new+ last-resort path. ## The mechanism (v0.9.101: PREVENT, don't react)