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
0.9.288: Captions before a KiCad window appears now name the calling AI thread (John saw one on his laptop and could not tell which thread was opening the editor). The Symbol Editor close used by rescan goes through ab's desktop_close_window (ab 2.1.12+, graceful, never forced) with the local WM_CLOSE as fallback for older ab; responses say editorCloseVia. Bridge owns KiCad, AD owns Windows (wiki #74 item 3).
1 file changed
+46−6
handlers/open_symbol_editor.py+46−6@@ -250,15 +250,44 @@ def _stale_explanation(kicad_info: dict, name: str) -> dict: } +_LAST_CLOSE_HOW = ""++ def _close_symbol_editor(hwnd: int) -> bool: """Ask an open Symbol Editor to close, so the next open re-indexes the libraries. WM_CLOSE, not a kill: KiCad gets to run its own shutdown (and to put up a save prompt, which the caller will see in the next screenshot)."""- WM_CLOSE = 0x0010+ # Bridge owns KiCad, AD owns Windows (wiki #74 item 3): from ab 2.1.12+ # desktop_close_window does the graceful WM_CLOSE with AD's own bookkeeping+ # (window identity, ownership ledger). No forceAfterMs: a save prompt must+ # stay up for the human, never be killed. Older ab (verb unknown) falls back+ # to the local WM_CLOSE. Either way the WAIT is measured here: the frame is+ # gone when IsWindow says so, not when a verb says ok.+ global _LAST_CLOSE_HOW+ asked = False try:- user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)- except Exception:- return False+ from handlers import ad_client+ r = ad_client.call("desktop_close_window", {"hwnd": int(hwnd),+ "reason": "Close the Symbol Editor so its next open re-indexes the symbol libraries"},+ timeout=15) or {}+ out = r.get("output") if isinstance(r, dict) else None+ if isinstance(out, str):+ try:+ import json as _j+ out = _j.loads(out)+ except Exception: # pylint: disable=broad-except+ out = {}+ err = str((r.get("error") if isinstance(r, dict) else "") or "")+ asked = bool(r) and "unknown" not in err.lower() and "no installed bridge" not in err.lower()+ except Exception: # pylint: disable=broad-except+ asked = False+ _LAST_CLOSE_HOW = "ad:desktop_close_window" if asked else "local:WM_CLOSE"+ if not asked:+ WM_CLOSE = 0x0010+ try:+ user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)+ except Exception:+ return False for _ in range(24): # up to ~12s for KiCad to tear the frame down time.sleep(0.5) if not user32.IsWindow(hwnd):@@ -352,8 +381,16 @@ def _caption_new_frame(user_top: dict | None, rescan: bool = False) -> None: what = ("re-opening KiCad's Symbol Editor so it re-indexes the library (a new symbol was installed)" if rescan else "opening KiCad's Symbol Editor (it indexes every symbol library on open)") back = " It will be placed behind your current window." if user_top and not user_top.get("isKicad") else ""- ad_client.call("desktop_caption", {"text": ("Adom Bridge: " + what + back)[:160], "seconds": 6,- "reason": "Warn the user before a KiCad window appears on their screen"})+ # Name the calling thread (John, 2026-09-03: a caption on his laptop+ # could not tell him WHICH thread was about to open a window).+ try:+ import caller_identity+ who = str(caller_identity.thread_name() or "").strip()+ except Exception: # pylint: disable=broad-except+ who = ""+ prefix = f"Adom Bridge (thread {who}): " if who and who != "unknown" else "Adom Bridge: "+ ad_client.call("desktop_caption", {"text": (prefix + what + back)[:180], "seconds": 6,+ "reason": f"Warn the user before a KiCad window appears on their screen (caller: {who or 'unknown'})"}) except Exception: # pylint: disable=broad-except pass @@ -1311,6 +1348,7 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict: "navigation": dict(_LAST_NAV_TRACE), "_foreground": dict(_LAST_FG_REPORT), "rescanned": rescanned,+ "editorCloseVia": _LAST_CLOSE_HOW if rescanned else None, **_resolved_fields(_get_symbol_editor_title(), library_name), } @@ -1340,6 +1378,7 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict: "elapsedSec": int(time.monotonic() - _t0), "editorOpened": True, "rescanned": rescanned,+ "editorCloseVia": _LAST_CLOSE_HOW if rescanned else None, "navigation": dict(_LAST_NAV_TRACE), "_foreground": dict(_LAST_FG_REPORT), **_resolved_fields(_get_symbol_editor_title(), library_name),@@ -1363,6 +1402,7 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict: "attempts": 2, **_resolved_fields(_get_symbol_editor_title(), library_name), "rescanned": rescanned,+ "editorCloseVia": _LAST_CLOSE_HOW if rescanned else None, } if not args.get("allowFocusSteal"):