← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.206\ No newline at end of file+0.9.207\ 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.206",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.206/adom-bridge-kicad-v0.9.206.zip",-  "sha256": "5fe9ac7c74cff82499faac9138ef9a36ced7ab012f040af0eb386cc585191054",-  "size": 474127,+  "version": "0.9.207",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.207/adom-bridge-kicad-v0.9.207.zip",+  "sha256": "060fb8c7fb95348be3b82e9bef760019c98da26efb9f6d0fb7171236a1455fda",+  "size": 476698,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-20T12:40:00Z",+  "released_at": "2026-08-20T13:05: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.206",+  "version": "0.9.207",   "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_footprint.py+57
@@ -222,3 +222,60 @@ def handle_install_footprint(kicad_info: dict, args: dict) -> dict:         "editorOpened": editor_opened,         "model3d": model_installed or None,     }+++def handle_list_footprints(kicad_info: dict, args: dict) -> dict:+    """kicad_list_footprints — what footprints actually exist, per library.++    Wiki issue #42 item 3 (John): a failure hint told callers to run+    kicad_list_footprints, and no such verb existed. Calling it returned the+    bridge's "valid commands" list, so a caller chasing a real failure lost a+    round trip and had every reason to think the bridge was broken. Rather than+    delete the hint, make it true - "does this footprint exist?" is the first+    question anyone asks when a load fails, and nothing else answered it.++    Read-only. Args: library (optional, defaults to every registered .pretty).+    """+    from pathlib import Path as _P+    import os as _os++    want = (args.get("library") or args.get("libraryName") or "").strip()+    user_dir = kicad_info.get("user_dir")+    if not user_dir:+        try:+            import kicad_detect+            _c, _u = kicad_detect.ensure_win_user_config(+                kicad_info.get("version") or "", kicad_info.get("base_dir"))+            user_dir = str(_u) if _u else None+        except Exception:+            user_dir = None+    if not user_dir:+        return {"success": False, "error": "KiCad user directory not found",+                "_hint": "Launch KiCad once so it creates Documents/KiCad/<ver>/."}++    roots = [_P(user_dir) / "footprints"]+    libraries, total = {}, 0+    for root in roots:+        if not root.is_dir():+            continue+        for pretty in sorted(root.glob("*.pretty")):+            name = pretty.stem+            if want and name.lower() != want.lower():+                continue+            names = sorted(p.stem for p in pretty.glob("*.kicad_mod"))+            libraries[name] = {"path": str(pretty).replace("\\", "/"),+                               "count": len(names), "footprints": names}+            total += len(names)++    out = {"success": True, "libraryCount": len(libraries), "footprintCount": total,+           "libraries": libraries}+    if want and not libraries:+        out["_hint"] = (f"No library named '{want}' on this machine. Libraries present: "+                        f"{', '.join(sorted(libraries)) or 'none'}. Install one with "+                        f"kicad_install_footprint, which creates Adom.pretty on first use.")+    else:+        out["_hint"] = ("The footprint NAME is the .kicad_mod file stem, which is what "+                        "kicad_show_footprint expects - it is often NOT the name declared "+                        "inside the file. If a show_footprint failed, check the name here "+                        "before assuming the editor is at fault.")+    return out
handlers/open_footprint_editor.py+93−26
@@ -301,7 +301,54 @@ def _get_footprint_editor_title() -> str:         return ""  -def _background_navigate_win32_fp(fp_hwnd: int, filter_text: str, verify: str) -> bool:++def _parse_fp_title(title: str) -> tuple:+    """Split a Footprint Editor title into (library, footprint).++    KiCad titles the frame "<library>:<footprint> - Footprint Editor", with a+    leading '*' for unsaved changes."""+    t = (title or "").strip()+    for suffix in ("\u2014 Footprint Editor", "- Footprint Editor", "Footprint Editor"):+        if t.endswith(suffix):+            t = t[: -len(suffix)].strip()+            break+    t = t.lstrip("*").strip()+    if ":" not in t:+        return ("", "")+    lib, _, fp = t.partition(":")+    return (lib.strip(), fp.strip())+++def _fp_title_confirms(title: str, footprint: str, library: str = "") -> bool:+    """Does this title PROVE the requested footprint is loaded?++    0.9.207, wiki issue #42 (John, P0). Two separate holes, both here:++      * the background tier used `verify in title` - a substring test, the same+        one that certified a wrong part for symbols in 0.9.194 and was fixed+        there but never here;+      * the allowFocusSteal keystroke tier confirmed NOTHING at all. It returned+        success on the basis of having typed, so two different requests both+        reported success while `Adom:s10b-ph-sm4-tb` stayed on screen.++    A caller that trusts success:true renders whatever window is up as evidence+    for the part it asked for, which is how a user got a green "Done" over+    screenshots of a different component. Compare the parsed library:footprint+    exactly instead."""+    lib, fp = _parse_fp_title(title)+    if not fp:+        return False+    low = (title or "").lower()+    if "no footprint loaded" in low:+        return False+    if fp.lower() != (footprint or "").lower():+        return False+    if library and lib.lower() != library.lower():+        return False+    return True++def _background_navigate_win32_fp(fp_hwnd: int, filter_text: str, verify: str,+                                  verify_lib: str = "") -> bool:     """v0.9.89 (#21): plugin-free BACKGROUND footprint load — the same tier that     fixed the Symbol Editor (0.9.86). WM_SETTEXT into the LIB_TREE filter, Down     through the filter box to select the match, Enter posted to the tree@@ -340,8 +387,7 @@ def _background_navigate_win32_fp(fp_hwnd: int, filter_text: str, verify: str) -         return cands      def _title_ok():-        t = (_get_footprint_editor_title() or "").lower()-        return verify in t and "no footprint loaded" not in t+        return _fp_title_confirms(_get_footprint_editor_title() or "", verify, verify_lib)      for hwnd, _r in _tree_candidates():         user32.PostMessageW(hwnd, WM_KEYDOWN, VK_RETURN, 0)@@ -783,7 +829,7 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict:     # v0.9.89: plugin-free background load first (the 0.9.86 symbol-editor tier).     _filter = f"{library_name} {footprint_name}".strip() if library_name else search_text     _step(f"navigating the library tree to '{search_text}'", 80)-    if _background_navigate_win32_fp(fp_hwnd, _filter, _verify):+    if _background_navigate_win32_fp(fp_hwnd, _filter, _verify, library_name):         return {             "success": True,             "output": f"Footprint '{search_text}' loaded (background win32 path): {_get_footprint_editor_title()}",@@ -797,7 +843,7 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict:     # what warms the library tree, so a second one is a genuinely different shot,     # not a repeat of a hopeless call. Same change as open_symbol_editor.     _step("first pass did not confirm; retrying now the tree is warm", 90)-    if _background_navigate_win32_fp(fp_hwnd, _filter, _verify):+    if _background_navigate_win32_fp(fp_hwnd, _filter, _verify, library_name):         return {             "success": True,             "output": f"Footprint '{search_text}' loaded (background win32 path, 2nd attempt): "@@ -819,31 +865,52 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict:                       "foreground:true to bring the editor to you."),         } -    nav_result = _navigate_to_footprint(fp_hwnd, search_text)--    if nav_result["status"] == "ok":-        return {-            "success": True,-            "output": f"Footprint '{search_text}' loaded in the Footprint Editor: {nav_result['title']}",-        }-    elif nav_result["status"] == "partial":+    # P0 (#42): the keystroke tier used to report success from its OWN verdict -+    # "I typed, and the editor says it loaded something" - without ever checking+    # that the something was what the caller asked for. Two different requests+    # both returned success:true with `Adom:s10b-ph-sm4-tb` on screen. The+    # post-condition below is the same one the background tier already applies:+    # the TITLE must name the requested part, or this is a failure.+    def _steal_result(nav, attempt: int):+        title = _get_footprint_editor_title() or nav.get("title") or ""+        if _fp_title_confirms(title, footprint_name or search_text, library_name):+            return {+                "success": True,+                "output": f"Footprint '{search_text}' loaded in the Footprint Editor: {title}",+                "via": "keystroke-focus-steal",+                "attempts": attempt,+            }         return {-            "success": True,-            "output": f"Footprint Editor loaded: {nav_result['title']}",+            "success": False,+            "editorOpened": True,+            "footprintLoaded": False,+            "errorCode": "wrong_footprint_on_screen",+            "attempts": attempt,+            "error": (f"The keystroke path ran but the Footprint Editor is not showing "+                      f"'{search_text}' (title: {title or 'unknown'})."),+            "_hint": ("Reported as a FAILURE deliberately: the editor is displaying a "+                      "different part, and a caller that trusted success here would "+                      "screenshot the wrong footprint as evidence. Retry, or confirm the "+                      "library:footprint pair exists with kicad_list_footprints."),         }-    elif nav_result["status"] == "noload":++    nav_result = _navigate_to_footprint(fp_hwnd, search_text)++    if nav_result["status"] in ("ok", "partial"):+        r = _steal_result(nav_result, 1)+        if r["success"]:+            return r+        # fall through to the retry below rather than failing on the first pass+        nav_result = {"status": "noload"}++    if nav_result["status"] == "noload":         time.sleep(0.5)         nav_result2 = _navigate_to_footprint(fp_hwnd, search_text)-        if nav_result2["status"] == "ok":-            return {-                "success": True,-                "output": f"Footprint '{search_text}' loaded in the Footprint Editor: {nav_result2['title']}",-            }-        elif nav_result2["status"] == "partial":-            return {-                "success": True,-                "output": f"Footprint Editor loaded: {nav_result2['title']}",-            }+        if nav_result2["status"] in ("ok", "partial"):+            r2 = _steal_result(nav_result2, 2)+            if r2["success"]:+                return r2+            return r2         else:             return {                 "success": False,
handlers/show.py+36
@@ -349,6 +349,42 @@ def handle_show_3d_chip(kicad_info: dict, args: dict) -> dict:     r = handle_open_3d_viewer(kicad_info, {**a, "editor": "fp"})     r = _verify_window(r, _find_3d_viewer, "3D Viewer")     r.setdefault("surface", "3d_chip")++    # Wiki issue #42 item 4 (John): "the 3D Viewer window is titled just+    # '3D Viewer', with no part identity anywhere, so nothing downstream can+    # confirm which part is on screen - we open it and simply cannot check."+    #+    # KiCad owns that title and we are not going to rewrite it, but the 3D chip+    # view is rendered FROM the Footprint Editor, and that window's title does+    # name the part. So echo the part this viewer was opened from, and say where+    # the claim comes from rather than asserting it out of thin air.+    want = (a.get("footprintName") or a.get("footprint") or "").strip()+    if isinstance(r, dict) and r.get("success"):+        src_title = ""+        try:+            h = _find_footprint_editor()+            if h:+                src_title = _window_title(h)+        except Exception:+            pass+        from handlers.open_footprint_editor import _parse_fp_title+        lib, fp = _parse_fp_title(src_title)+        r["shows"] = {+            "requested": want or None,+            "footprint": fp or None,+            "library": lib or None,+            "sourceWindow": src_title or None,+            "verified": bool(want and fp and fp.lower() == want.lower()),+            "_hint": ("The 3D Viewer's own title carries no part identity, so this is read "+                      "from the Footprint Editor the view was rendered from. verified:true "+                      "means that editor is on the part you asked for; false means it is "+                      "not, and the 3D view is therefore of something else."),+        }+        if want and not r["shows"]["verified"]:+            r["success"] = False+            r["errorCode"] = "wrong_part_in_3d_view"+            r["error"] = (f"3D Viewer opened, but it was rendered from '{fp or 'unknown'}' "+                          f"rather than the requested '{want}'.")     return r  
server.py+3−2
@@ -41,7 +41,7 @@ from handlers.close_windows import ( ) from handlers.install_library import handle_install_library from handlers.install_symbol import handle_install_symbol-from handlers.install_footprint import handle_install_footprint+from handlers.install_footprint import handle_install_footprint, handle_list_footprints from handlers.place_footprint import handle_place_footprint from handlers.run_drc import handle_run_drc from handlers.fix_keyboard import handle_fix_keyboard@@ -727,6 +727,7 @@ COMMAND_HANDLERS = {     "uninstall": handle_uninstall,     "install_plugin": plugin_install.handle_install_plugin,     "plugin_diagnose": handle_plugin_diagnose,+    "list_footprints": handle_list_footprints,     "progress": _handle_progress,     # v1.7.12+: headless lint pre-checks via kicad-cli     "run_erc": handle_run_erc,@@ -917,7 +918,7 @@ def dispatch_command(command: str, args: dict) -> dict:     _NO_GUI = {"run_drc", "run_erc", "lint_board", "lint_schematic", "lint_library",                "format_upgrade", "export_gerber", "export_gerbers", "export_pdf",                "export_svg", "export_step", "export_bom_csv", "adom_library_status",-               "list_versions", "install_library", "progress"}+               "list_versions", "install_library", "progress", "list_footprints"}     # Only verbs that actually SPAWN a window get the ~3s settle-poll (they can     # trigger an async dialog). Read-only reporters (open_editors, screenshot_all,     # window_info) don't spawn windows → one cheap scan, no needless 3s poll.