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
Update 6 file(s)
6 files changed
+123−13
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.192+0.9.193
adom-bridge-kicad-manifest.json+5−5@@ -1,15 +1,15 @@ { "manifest_version": 1, "name": "kicad",- "version": "0.9.192",- "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.192/adom-bridge-kicad-v0.9.192.zip",- "sha256": "13bb28e8564364c640416ee08911286a6423213cead03066b378a186394108e5",- "size": 460359,+ "version": "0.9.193",+ "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.193/adom-bridge-kicad-v0.9.193.zip",+ "sha256": "d22381f4e66f2699baa2d1d0d19102ed64e4fc2b7099859bb29403e8b7685775",+ "size": 462071, "verbPrefixes": [ "kicad_" ], "healthEndpoint": "/status",- "released_at": "2026-08-19T21:40:00Z",+ "released_at": "2026-08-19T21:55:00Z", "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png", "languages": [ "Python",
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.192",+ "version": "0.9.193", "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/install_symbol.py+26@@ -124,6 +124,23 @@ def handle_install_symbol(kicad_info: dict, args: dict) -> dict: # hands the caller a success it did not ask for. Say so out loud instead. requested_lib = (args.get("libraryName") or "").strip() ignored_lib = requested_lib and requested_lib != ADOM_LIB_NAME++ # A Symbol Editor indexes its libraries once, when it opens. If one was+ # already open when we added these symbols, it cannot see them, and the next+ # kicad_show_symbol will fail in a way that looks like a search bug rather+ # than a stale index (0.9.192, found while verifying wiki issue #37). Say it+ # here, at the moment the staleness is created, not three calls later.+ stale_editor = ""+ if not editor_opened:+ try:+ from handlers.open_symbol_editor import _find_symbol_editor+ if _find_symbol_editor():+ stale_editor = (+ "a Symbol Editor was already open when these symbols were added, so it "+ "cannot see them until it reopens"+ )+ except Exception:+ pass if editor_opened: output_parts.append(f"Symbol Editor: {editor_output}") elif open_editor:@@ -134,6 +151,8 @@ def handle_install_symbol(kicad_info: dict, args: dict) -> dict: f"NOTE: libraryName '{requested_lib}' was ignored - every Adom symbol " f"installs into the single global '{ADOM_LIB_NAME}' library" )+ if stale_editor:+ output_parts.append(f"NOTE: {stale_editor}") out = { "success": True,@@ -143,6 +162,13 @@ def handle_install_symbol(kicad_info: dict, args: dict) -> dict: "libraryName": ADOM_LIB_NAME, "editorOpened": editor_opened, }+ if stale_editor:+ out["staleEditorOpen"] = True+ out["_staleHint"] = (+ "The open Symbol Editor's library index predates this install. To show one of "+ "these symbols, call kicad_show_symbol with rescan:true (it closes and reopens "+ "the editor so KiCad re-indexes), or close the editor first."+ ) if ignored_lib: out["ignoredArgs"] = {"libraryName": requested_lib} out["_hint"] = (
handlers/open_symbol_editor.py+84−4@@ -37,6 +37,44 @@ KICAD_SYMBOL_EDITOR_CMD_ID = 20011 # Project Manager > Tools > Symbol Editor SW_RESTORE = 9 GW_OWNER = 4 ++def _symbol_on_disk_but_not_in_editor(kicad_info: dict, name: str) -> bool:+ """True when `name` IS in the Adom library file but a Symbol Editor that+ opened earlier cannot see it.++ A Symbol Editor builds its library tree once, at open. Symbols added to+ Adom.kicad_sym after that are invisible to it until it reopens, which looks+ exactly like a failed background search and is the reason 0.9.192 stopped+ reporting it as one. Only the disk half is checked here: the caller reaches+ this after both background attempts already failed to find it in the tree."""+ if not name:+ return False+ try:+ import adom_library+ paths = adom_library._get_paths(kicad_info)+ if not paths:+ return False+ _table, lib_path = paths+ return name.lower() in {s.lower() for s in adom_library.list_symbols(lib_path)}+ except Exception:+ return False+++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+ 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):+ return True+ return False+ if IS_WINDOWS: user32 = ctypes.windll.user32 kernel32 = ctypes.windll.kernel32@@ -391,8 +429,10 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: symbol_name = args.get("symbolName", "") library_name = args.get("libraryName", "") - # Check if the symbol is already loaded in the Symbol Editor- if symbol_name:+ # Check if the symbol is already loaded in the Symbol Editor.+ # rescan:true skips this: the caller is asking for a re-index precisely+ # because they do not trust what this editor currently has.+ if symbol_name and not args.get("rescan"): sym_hwnd = _find_symbol_editor() if sym_hwnd: windows = _find_windows_by_title("Symbol Editor")@@ -408,6 +448,20 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: sym_hwnd = _find_symbol_editor() pathways_tried: list[str] = []+ rescanned = False++ # 0.9.192: rescan:true is the remedy for errorCode library_cache_stale. An+ # editor only indexes its libraries at open, so the ONLY way to make it see a+ # newly installed symbol is to close it and let the flow below open a fresh+ # one. Opt-in, because it closes a window the user may be working in and+ # costs the cold library-index time (#35).+ if sym_hwnd and args.get("rescan"):+ if _close_symbol_editor(sym_hwnd):+ rescanned = True+ sym_hwnd = 0+ pathways_tried.append("rescan-closed-stale-editor")+ else:+ pathways_tried.append("rescan-close-failed") if not sym_hwnd: # Phase 3 Tier 1: try the reverse bridge first. Two sub-paths@@ -636,6 +690,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: "success": True, "output": f"Symbol '{search_text}' loaded (background win32 path): {_get_symbol_editor_title()}", "via": "win32-background",+ "rescanned": rescanned, } # BACKGROUND-FIRST CONTRACT (John, 2026-08-14): the win32 tier above is the@@ -657,17 +712,42 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict: f"{_get_symbol_editor_title()}", "via": "win32-background", "attempts": 2,+ "rescanned": rescanned, } if not args.get("allowFocusSteal"):+ # 0.9.192: before blaming the search tier, ask whether the symbol is even+ # in the editor's world. A Symbol Editor indexes its libraries ONCE when+ # it opens; a symbol added to Adom.kicad_sym afterwards is on disk but+ # not in that editor's tree, so no amount of filtering will ever find it.+ # Reproduced live: RP2040 (present when the editor opened) loads first+ # try; COL0-TEST, installed minutes later, fails forever. Reporting that+ # as "didn't load in the background" sends the caller to retry a call+ # that cannot succeed.+ stale = _symbol_on_disk_but_not_in_editor(kicad_info, symbol_name or search_text)+ if stale:+ return {+ "success": False, "editorOpened": True, "symbolLoaded": False,+ "attempts": 2,+ "errorCode": "library_cache_stale",+ "error": f"'{search_text}' exists in the Adom library on disk but the open "+ f"Symbol Editor cannot see it: that editor indexed its libraries when "+ f"it opened, before this symbol was added "+ f"(title: {_get_symbol_editor_title() or 'unknown'})",+ "_hint": ("This is not a search-timing failure and retrying will never fix it. "+ "The Symbol Editor has to re-index, which only happens when it opens. "+ "Close the Symbol Editor and call again, or pass rescan:true to have "+ "the bridge close and reopen it for you (that costs the cold "+ "library-index time, roughly a minute - wiki issue #35)."),+ } return { "success": False, "editorOpened": True, "symbolLoaded": False, "attempts": 2, "error": f"Symbol Editor open but '{search_text}' didn't load in the background " f"after two attempts (title: {_get_symbol_editor_title() or 'unknown'})", "_hint": ("Two background attempts did not confirm, so a third is unlikely to "- "help - check that the symbol really exists in this library "- "(kicad_list_symbols). The keystroke fallback "+ "help - check that the symbol really exists in this library. "+ "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."),
handlers/show.py+6−2@@ -69,8 +69,12 @@ 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.""" a = args or {}- already = _open_editor_showing("symbol editor", a.get("symbolName") or "",- a.get("libraryName") or a.get("library") or "")+ # rescan:true means "I do not trust this editor's library index" (the remedy+ # for errorCode library_cache_stale), so the warm-reuse shortcut must not+ # swallow it - reuse is exactly what the caller is asking us to stop doing.+ already = None if a.get("rescan") else _open_editor_showing(+ "symbol editor", a.get("symbolName") or "",+ a.get("libraryName") or a.get("library") or "") if already: return {"success": True, "surface": "symbol", "reused": True, "window": already.get("title"),