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.172: THE IDLE RULE - never bounce a KiCad foreground when no verb or guard is running, so a taskbar click always wins; catch fast clicks (0x8001); show_* verbs now FAIL when their window never appeared instead of reporting success and letting the caller screenshot the wrong window
7 files changed
+80−12
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.171+0.9.172
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.171",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.171/adom-bridge-kicad-v0.9.171.zip",- "sha256": "78d4e8ba9e0cb44bd186188733229b25ba883eb67a584ea55e1b7c944beba9ae",- "size": 445651,+ "version": "0.9.172",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.172/adom-bridge-kicad-v0.9.172.zip",+ "sha256": "7674966eb1ef6ee04671a1cf9f8f277ea0a25b6edb7c311e27d040131b690afb",+ "size": 446638, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-18T14:40:31Z",+ "released_at": "2026-08-19T08:46:04Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
adom-bridge-kicad-v0.9.172.zipadded⋯ 1 unchanged line ⋯
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.171",+ "version": "0.9.172", "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.",
dashboard/kicad_dashboard.py+11−2@@ -676,10 +676,19 @@ def _render(target, pick): st = ab(target, "kicad_state", {"includeImages": True, "maxWidth": 900}, timeout=240) ks = st.get("kicadState") or st shots = ks.get("shots") or []- chosen = None+ # EVIDENCE MUST BE THE THING THAT WAS ASKED FOR (John, 2026-08-19: "i had+ # asked for a footprint and the evidence you provided was just of the project+ # window"). The old fallback grabbed the last window on screen when the+ # expected one was missing, which turns a failure into a screenshot of+ # something else entirely - worse than no evidence, because it looks like+ # proof. If the requested surface is not there, say so. if pick: chosen = next((w for w in shots if pick in (w.get("title") or "").lower()), None)- chosen = chosen or (shots[-1] if shots else None)+ if not chosen:+ seen = ", ".join((w.get("title") or "?")[:40] for w in shots) or "nothing"+ raise RuntimeError(f"the {pick} window never appeared - on screen instead: {seen}")+ else:+ chosen = shots[-1] if shots else None if not chosen: return None, None img = (chosen.get("image") or chosen.get("screenshot")
handlers/show.py+46@@ -45,14 +45,59 @@ def handle_show_symbol(kicad_info: dict, args: dict) -> dict: """Show a schematic SYMBOL in the Symbol Editor. Args: symbolName (+ libraryName), or libraryName alone to browse a library.""" r = handle_open_symbol_editor(kicad_info, args or {})+ r = _verify_window(r, _find_symbol_editor, "Symbol Editor") r.setdefault("surface", "symbol") return r +def _find_by_title(*needles) -> bool:+ """Is a window whose title contains ALL needles on screen right now?"""+ try:+ from handlers.kicad_ui import _find_all_kicad_windows+ for w in _find_all_kicad_windows() or []:+ t = (w.get("title") or "").lower()+ if all(n in t for n in needles):+ return True+ except Exception:+ pass+ return False+++def _find_symbol_editor() -> bool:+ return _find_by_title("symbol editor")+++def _find_3d_viewer() -> bool:+ return _find_by_title("3d viewer")+++def _verify_window(result: dict, finder, label: str, waits: int = 12) -> dict:+ """A show_* verb has ONE job: put THAT surface on screen. If its window is+ not there, the verb FAILS - even when every internal step reported success.++ 0.9.172 (John, 2026-08-19: "i had asked for a footprint to be shown, and the+ evidence you provided was just of the project window"). The caller screenshots+ whatever the bridge says succeeded, so a false success does not just mislead,+ it manufactures wrong evidence."""+ if not isinstance(result, dict) or not result.get("success"):+ return result+ for _ in range(waits):+ if finder():+ return result+ time.sleep(0.5)+ return {**result, "success": False,+ "error": f"{label} reported success but its window never appeared",+ "errorCode": "surface_window_missing",+ "_hint": (f"The bridge could not put the {label} on screen, so there is nothing "+ "truthful to screenshot. Retry after kicad_launch, or check the Plugin "+ "LED: the background path needs the reverse-bridge plugin bound.")}++ def handle_show_footprint(kicad_info: dict, args: dict) -> dict: """Show a FOOTPRINT in the Footprint Editor. Args: footprintName (+ library), or library alone to browse.""" r = handle_open_footprint_editor(kicad_info, args or {})+ r = _verify_window(r, _find_footprint_editor, "Footprint Editor") r.setdefault("surface", "footprint") return r @@ -71,6 +116,7 @@ def handle_show_3d_chip(kicad_info: dict, args: dict) -> dict: break time.sleep(0.5) r = handle_open_3d_viewer(kicad_info, {**a, "editor": "fp"})+ r = _verify_window(r, _find_3d_viewer, "3D Viewer") r.setdefault("surface", "3d_chip") return r
handlers/win_focus.py+16−3@@ -1275,8 +1275,8 @@ def start_focus_sentinel() -> None: try: now = _t.monotonic() try:- if (_user32.GetAsyncKeyState(VK_LBUTTON) & 0x8000) or \- (_user32.GetAsyncKeyState(VK_RBUTTON) & 0x8000):+ if (_user32.GetAsyncKeyState(VK_LBUTTON) & 0x8001) or \+ (_user32.GetAsyncKeyState(VK_RBUTTON) & 0x8001): last_click_t = now if _user32.GetAsyncKeyState(VK_MENU) & 0x8000: last_alt_t = now@@ -1299,6 +1299,18 @@ def start_focus_sentinel() -> None: _log("user-owned window (left alone)", fg, ago) elif _guard_count > 0: _log("steal-during-guard (guardian handles)", fg, ago)+ elif _verb_inflight <= 0 and _guard_count <= 0:+ # 0.9.172 - THE IDLE RULE (John, 2026-08-19: "when I+ # click the taskbar icon to bring the app to the fg,+ # you don't let me"). Bouncing only exists to stop a+ # window flashing into his face WHILE the bridge is+ # working. With no verb in flight and no guard armed,+ # the bridge is not opening anything - so a KiCad+ # window reaching the foreground is the USER, whether+ # or not we managed to observe the click that did it+ # (a fast taskbar click can land entirely between two+ # 120ms sentinel polls). Leave it alone, always.+ _log("foreground while bridge idle (left alone)", fg, ago) else: _log("self-raise (bounced to background)", fg, ago) _push_kicad_to_background(fg)@@ -1307,7 +1319,8 @@ def start_focus_sentinel() -> None: # for 10s so a repeat-raising window is refused # silently instead of flickering, THEN clear it so # user clicks work again.- bounce_hold[fg] = now + 10.0+ bounce_hold[fg] = now + 3.0 # was 10s: too long to+ # leave a window unclickable once the verb has moved on for h, until in list(bounce_hold.items()): if now >= until: allow_activation(h)