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.156: geometry-poison prevention (unpark before close verbs, atexit unpark, boot heal of off-screen-stranded windows) + background doctrine skill updated with the ownership contract
7 files changed
+129−10
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.155+0.9.156
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.155",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.155/adom-bridge-kicad-v0.9.155.zip",- "sha256": "abd0989f6d447c3b3dac9b8f444442880cf5a8a4c1b4f4970e25b3e11f98b003",- "size": 436974,+ "version": "0.9.156",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.156/adom-bridge-kicad-v0.9.156.zip",+ "sha256": "504ef46dd51e4e7addcc45f4de7cb99a58bae307c827f94640db98d05ac9ed5a",+ "size": 437802, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-17T21:13:28Z",+ "released_at": "2026-08-17T21:17:20Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.156.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.155",+ "version": "0.9.156", "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/win_focus.py+43−3@@ -383,8 +383,8 @@ _park_sweep_started = False # path leaves it completely alone. There is deliberately NO "adopt what exists" # seeding: unknown windows default to user-owned. _owned_pids: set = set()-_KICAD_EXES = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor",- "pcb_calculator", "bitmap2component")+_OWNED_EXE_PREFIXES = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor",+ "pcb_calculator", "bitmap2component") def _ledger_path():@@ -425,7 +425,7 @@ def _pid_exe_base(pid) -> str: def _pid_is_kicad(pid) -> bool: base = _pid_exe_base(pid)- return any(base.startswith(e) for e in _KICAD_EXES)+ return any(base.startswith(e) for e in _OWNED_EXE_PREFIXES) def _save_ledger() -> None:@@ -549,6 +549,46 @@ def is_bridge_window(hwnd) -> bool: return False +def heal_offscreen_windows() -> int:+ """Boot repair (John, 2026-08-17: "when the user goes to open kicad normally+ ... it starts way offscreen and kicad doesn't fix itself"): if a parked+ window was closed (or the bridge died) before unparking, KiCad saved the+ off-screen geometry, and the next launch restores there. Nothing but our+ parking puts a KiCad window at <= -20000, so any window found there — OURS+ OR THE USER'S — is our poison to heal: move it to a sane on-screen spot,+ no activation, size preserved."""+ healed = 0+ if not IS_WINDOWS:+ return healed+ try:+ for i, h in enumerate(sorted(_enum_kicad_hwnds())):+ try:+ if _user32.IsIconic(h):+ continue+ wp = _WINDOWPLACEMENT()+ wp.length = ctypes.sizeof(_WINDOWPLACEMENT)+ if not _user32.GetWindowPlacement(h, ctypes.byref(wp)):+ continue+ if wp.rcNormalPosition.left > -20000:+ continue+ width = wp.rcNormalPosition.right - wp.rcNormalPosition.left+ height = wp.rcNormalPosition.bottom - wp.rcNormalPosition.top+ x, y = 120 + 48 * (i % 5), 90 + 36 * (i % 5)+ wp.rcNormalPosition.left = x+ wp.rcNormalPosition.top = y+ wp.rcNormalPosition.right = x + max(width, 400)+ wp.rcNormalPosition.bottom = y + max(height, 300)+ wp.showCmd = 8 # SW_SHOWNA — visible, no activation+ _user32.SetWindowPlacement(h, ctypes.byref(wp))+ healed += 1+ print(f"[focus] healed off-screen window {h} -> ({x},{y})", file=sys.stderr)+ except Exception:+ pass+ except Exception:+ pass+ return healed++ def _enum_kicad_hwnds() -> set: out = set() try:
server.py+23@@ -770,6 +770,16 @@ def dispatch_command(command: str, args: dict) -> dict: if command not in _NO_GUI: from handlers import win_focus as _wfg _wfg.begin_gui_verb()+ # 0.9.156 (John's catch): a window CLOSED while parked off-screen+ # makes KiCad persist the off-screen geometry, so the user's next+ # normal launch opens invisibly. Restore true positions before any+ # close verb runs.+ if command in ("close", "close_symbol_editor",+ "close_footprint_editor", "close_3d_viewer"):+ try:+ _wfg.unpark_all()+ except Exception:+ pass except Exception: pass try:@@ -1087,6 +1097,19 @@ def main(): win_focus.load_owned_ledger() except Exception: pass+ # 0.9.156: heal any window stranded at parking coordinates (a crash or a+ # close-while-parked poisons KiCad's saved geometry, and the next NORMAL+ # launch opens invisibly off-screen — John's catch).+ try:+ win_focus.heal_offscreen_windows()+ except Exception:+ pass+ # Bridge shutdown must never strand parked windows either.+ try:+ import atexit+ atexit.register(win_focus.unpark_all)+ except Exception:+ pass try: from handlers.win_taskbar import badge_existing_on_boot badge_existing_on_boot()
skills/kicad-bridge-background/SKILL.md+56@@ -252,6 +252,62 @@ BEFORE the window moves. Sentinel bounces hold NOACTIVATE for 10s afterward (10.0.5 prevention is reactive-only; without the hold, repeat-steal storms flicker - the '10 windows blipping' John saw was exactly this during test runs). +## THE OWNERSHIP CONTRACT (0.9.155, John 2026-08-17 — supersedes everything above where they conflict)++Caught live: John launched his own KiCad from the taskbar and the sentinel+bounced him 8 times, including a File Open Warning dialog he never saw. Root+causes: (a) click-on-window intent detection misses taskbar/Start clicks (the+cursor is over the SHELL, not the raised window, when the transition lands);+(b) nothing distinguished HIS windows from OURS.++**The contract: the bridge may only park, bounce, badge, or identity-stamp+windows it OWNS. Ownership is decided by PROCESS TREE, recorded at spawn.**++- Every kicad-family pid the bridge spawns is registered+ (`win_focus.register_owned_pid`, called at every `Popen` site — when you add+ a spawn site, ADD THE CALL). Children count via parent-chain walk (the PM+ spawns eeschema.exe/pcbnew.exe as separate processes).+- The set persists to `%USERPROFILE%/.adom/kicad-bridge-owned.json` and is+ revived at boot (`load_owned_ledger`) with liveness+exe validation, so a+ bridge restart neither orphans our windows nor mis-adopts the user's.+- There is deliberately NO "adopt whatever exists" seeding. Unknown = user's.+- The sentinel, the SetWinEventHook path, the parking sweep, overlay badges,+ and AUMID identity stamps ALL check `_is_owned_window()` first. A user-owned+ window is logged ("user-owned window (left alone)") and never touched.+- User intent now includes clicks on the shell (taskbar / Start / desktop+ classes in `_SHELL_CLASSES`) — clicking a taskbar button IS the canonical+ "bring this window up" gesture.+- Two instances coexist by design: KiCad has no single-instance lock (only+ per-project .lck warnings). The bridge drives its own projects in its own+ processes alongside the user's instance and never opens the user's projects.++## Geometry-poison prevention (0.9.156, John's catch)++Parking moves owned windows off-screen (~-32000). KiCad persists window+geometry on close — so a window CLOSED while parked (or stranded by a bridge+crash) writes the off-screen position into KiCad's config, and the user's next+NORMAL launch opens invisibly. Three defenses, all mandatory:++1. The dispatcher calls `unpark_all()` BEFORE any close verb runs+ (close / close_symbol_editor / close_footprint_editor / close_3d_viewer).+2. `atexit.register(unpark_all)` — bridge shutdown never strands parked windows.+3. Boot runs `heal_offscreen_windows()`: any KiCad window found at+ left <= -20000 (nothing but our parking puts one there) is moved to a sane+ on-screen spot without activation — this heals crash-poisoned geometry even+ on windows the user re-launched themselves.++## Taskbar identity + badges (0.9.153-0.9.155, see handlers/win_taskbar.py)++In-bridge ITaskbarList3::SetOverlayIcon is IN-PROCESS-ONLY (returns S_OK+cross-process, shell ignores it — pup and fusion both paid for this lesson+first). Badges go through AD-core `desktop_taskbar` via `handlers/ad_client.py`+(ported verbatim from fusion-bridge), using fusion's exact 32px badge asset+(`resources/adom-overlay-32.png` — mark anchored bottom-right of a transparent+canvas; a full-bleed badge renders oversized). Blank white eeschema/pcbnew+buttons = poisoned shell icon cache for those exe paths; bypassed by stamping+per-exe AUMIDs (`desktop_set_window_identity`) with .ico files extracted from+the exes. All of it OWNED WINDOWS ONLY, gated by the `overlayBadges` setting.+ ## The presence gate (0.9.136, John 2026-08-17) A live incident settles it: the daily verb suite ran while John was working at