← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.193\ No newline at end of file+0.9.194\ No newline at end of file
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "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,+  "version": "0.9.194",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.194/adom-bridge-kicad-v0.9.194.zip",+  "sha256": "eac65abb0e01fa6fca01404fed453ed9d0e1c330e39351dda33de587c666e098",+  "size": 463369,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-19T21:55:00Z",+  "released_at": "2026-08-19T23:10: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.193",+  "version": "0.9.194",   "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/open_symbol_editor.py+54−6
@@ -38,6 +38,51 @@ SW_RESTORE = 9 GW_OWNER = 4  +def _parse_editor_title(title: str) -> tuple[str, str]:+    """Split a Symbol Editor title into (library, symbol).++    KiCad titles the frame "<library>:<symbol> — Symbol Editor", with a leading+    '*' when there are unsaved changes. Returns ("", "") when the title is not+    in that shape (no part loaded yet)."""+    t = (title or "").strip()+    for suffix in ("— Symbol Editor", "- Symbol Editor", "Symbol Editor"):+        if t.endswith(suffix):+            t = t[: -len(suffix)].strip()+            break+    t = t.lstrip("*").strip()+    if ":" not in t:+        return ("", "")+    lib, _, sym = t.partition(":")+    return (lib.strip(), sym.strip())+++def _title_confirms(title: str, symbol: str, library: str = "") -> bool:+    """Does this title prove THE REQUESTED part is loaded?++    0.9.194: this used to be `requested.lower() in title.lower()`, and a+    substring test is not proof. Reproduced live on ConfRoomROG: asking for+    symbol RP2040 in library Adom filtered the tree to "Adom RP2040", KiCad+    selected the first symbol of a DIFFERENT library that happened to be called+    AdomRP2040, and the title "AdomRP2040:ADOM_MECHANICAL_PIN — Symbol Editor"+    contains "rp2040" - so the bridge reported success for a part that does not+    exist in the requested library, with an unrelated symbol on screen. A caller+    then screenshots that as evidence. Compare the parsed library:symbol+    exactly instead."""+    lib, sym = _parse_editor_title(title)+    if not sym:+        return False+    if "no symbol loaded" in (title or "").lower():+        return False+    if sym.lower() != (symbol or "").lower():+        return False+    # The library is only checked when the caller named one. KiCad reports the+    # library the part was actually loaded from, so a mismatch means we landed+    # in a different library with a same-named part - still not what was asked.+    if library and lib.lower() != library.lower():+        return False+    return True++ 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.@@ -235,7 +280,8 @@ def _plugin_background_load(symbol_name: str, library_name: str = "",         return {"loaded": False, "_error": f"{type(e).__name__}: {e}"}  -def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str) -> bool:+def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str,+                               verify_lib: str = "") -> bool:     """v0.9.83 (#20/#21): plugin-free BACKGROUND symbol load. WM_SETTEXT into the     LIB_TREE's filter Edit (fires EN_CHANGE -> KiCad filters and auto-selects the     best match on its debounce timer), then Enter posted to the Edit (KiCad's@@ -283,8 +329,10 @@ def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str) ->         return cands      def _title_ok():-        t = (_get_symbol_editor_title() or "").lower()-        return verify in t and "no symbol loaded" not in t+        # The sentinel means "no title check applies" (library-browse mode).+        if verify == "\u0000never":+            return False+        return _title_confirms(_get_symbol_editor_title() or "", verify, verify_lib)      # Round 1: Enter posted straight to each tree candidate (the payload's own     # in-process technique, aimed from outside).@@ -674,7 +722,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:     if str(_pl.get("loaded")) in ("True", "unverified"):         time.sleep(0.6)         _title = _get_symbol_editor_title() if "_get_symbol_editor_title" in globals() else ""-        if _pl.get("loaded") is True or (_verify in (_title or "").lower()):+        if _pl.get("loaded") is True or _title_confirms(_title or "", _verify, library_name):             return {                 "success": True,                 "output": f"Symbol '{search_text}' loaded (background plugin path): {_title or _pl.get('frameTitle','')}",@@ -685,7 +733,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:     # v0.9.83: plugin-free win32 tier — filter via WM_SETTEXT + Enter, background.     # This is the workhorse while eeschema's Python is cold (issue #21).     _filter = f"{library_name} {symbol_name}".strip() if library_name else search_text-    if _background_navigate_win32(sym_hwnd, _filter, _verify):+    if _background_navigate_win32(sym_hwnd, _filter, _verify, library_name):         return {             "success": True,             "output": f"Symbol '{search_text}' loaded (background win32 path): {_get_symbol_editor_title()}",@@ -705,7 +753,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:     # own retry loop out through the API. Do it here, once, before failing.     # The first attempt is what warms the tree, so this is a fresh-tree retry,     # not a duplicate of a hopeless call.-    if _background_navigate_win32(sym_hwnd, _filter, _verify):+    if _background_navigate_win32(sym_hwnd, _filter, _verify, library_name):         return {             "success": True,             "output": f"Symbol '{search_text}' loaded (background win32 path, 2nd attempt): "
handlers/plugin_diagnose.py+29−2
@@ -118,13 +118,40 @@ def handle_plugin_diagnose(kicad_info: dict, args: dict) -> dict:     except Exception as e:         report["live"] = {"error": str(e)[:120]} +    # 0.9.194: the loader log is written by ANY process that runs the bootstrap,+    # including the bridge's own short-lived `python.exe -c ...` probes. Judging+    # "did the plugin run" from those lines is how this verb told a live box+    # "IMPORTED BUT DID NOT START" while no KiCad was running at all - a+    # confident answer to a question nobody could answer yet. Split the log by+    # process and only count GUI lines as evidence about KiCad.+    gui_names = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor", "bitmap2component")+    gui_log, probe_log = [], []+    for line in report["loaderLog"]:+        low = line.lower()+        (gui_log if any(f"in {n}.exe" in low for n in gui_names) else probe_log).append(line)+    report["loaderLogGui"] = gui_log+    report["loaderLogProbes"] = probe_log++    try:+        from handlers.kicad_ui import _find_all_kicad_windows+        kicad_running = bool(_find_all_kicad_windows())+    except Exception:+        kicad_running = bool(report.get("instances"))+    report["kicadRunning"] = kicad_running+     # the verdict, so a human does not have to correlate five sections by hand     deployed = any(d["loader"].get("exists") for d in pdirs)     imported = any(d["importedByKiCad"] for d in pdirs)-    ran = bool(report["loaderLog"])+    ran = bool(gui_log)          # GUI evidence only, never a probe process     alive = (report.get("live") or {}).get("alive") or 0     if alive:         verdict = "WORKING: the plugin is bound and answering."+    elif not kicad_running:+        verdict = ("KICAD NOT RUNNING: there is nothing for the plugin to load into "+                   "yet, so this says nothing about whether it works. Launch KiCad "+                   "and open a board editor, then re-run. "+                   + ("The loader IS deployed." if deployed else+                      "The loader is NOT deployed - run kicad_install_plugin."))     elif not deployed:         verdict = ("NOT DEPLOYED: adom_bridge_plugin.py is missing from KiCad's "                    "scripting/plugins. Run kicad_install_plugin.")@@ -135,7 +162,7 @@ def handle_plugin_diagnose(kicad_info: dict, args: dict) -> dict:                    "has been opened and this still says never imported, this KiCad "                    "build does not scan that directory and the hook has to change.")     elif ran and not alive:-        verdict = ("IMPORTED BUT DID NOT START: read loaderLog below - it names the "+        verdict = ("IMPORTED BUT DID NOT START: read loaderLogGui below - it names the "                    "exact failure. 'No module named X' means KiCad's embedded stdlib "                    "is incomplete (see embeddedStdlib); a repair install fixes it.")     else: