← Commit history

v0.9.89: footprint editor gets the plugin-free win32 background load tier + honest failures (#21 remainder); suite fp test is now a real e2e stock-footprint load

John Lauer ·f631f28c22 ·2mo ago ·parent ed15b7d
6 files changed +112−15
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.88+0.9.89
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.88",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.88/adom-bridge-kicad-v0.9.88.zip",-  "sha256": "061ac097595f6b62b72faa9591a022bd0a1b80baef20338abe02af0ec42b887a",-  "size": 383168,+  "version": "0.9.89",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.89/adom-bridge-kicad-v0.9.89.zip",+  "sha256": "b87fcfd194b84814d8530441d0cf827bc13741275e98207159558419c5044f70",+  "size": 384185,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-14T18:14:16Z",+  "released_at": "2026-08-14T18:35:39Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.89.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.88",+  "version": "0.9.89",   "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_footprint_editor.py+101−7
@@ -267,6 +267,84 @@ def _send_ctrl_shift_e(hwnd: int):     user32.keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0)  +def _get_footprint_editor_title() -> str:+    """Current Footprint Editor title ('' if not found) — load ground truth."""+    try:+        hwnd = _find_footprint_editor()+        if not hwnd:+            return ""+        buf = ctypes.create_unicode_buffer(512)+        user32.GetWindowTextW(hwnd, buf, 512)+        return buf.value or ""+    except Exception:  # pylint: disable=broad-except+        return ""+++def _background_navigate_win32_fp(fp_hwnd: int, filter_text: str, verify: 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+    candidates + PostMessage dblclick fallback. Zero focus, no plugin needed."""+    edit = _find_child_by_class_and_parent(fp_hwnd, "Edit", "searchCtrl")+    if not edit:+        return False+    WM_SETTEXT = 0x000C+    WM_KEYDOWN, WM_KEYUP, VK_RETURN, VK_DOWN = 0x0100, 0x0101, 0x0D, 0x28+    try:+        user32.SendMessageW(edit, WM_SETTEXT, 0, ctypes.create_unicode_buffer(filter_text))+    except Exception:+        return False+    time.sleep(1.4)+    user32.PostMessageW(edit, WM_KEYDOWN, VK_DOWN, 0)+    user32.PostMessageW(edit, WM_KEYUP, VK_DOWN, 0)+    time.sleep(0.5)++    def _tree_candidates():+        cands = []+        try:+            pane = user32.GetParent(user32.GetParent(edit)) or user32.GetParent(edit)+            WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND,+                                             ctypes.wintypes.LPARAM)+            def cb(hwnd, _l):+                r = ctypes.wintypes.RECT()+                user32.GetWindowRect(hwnd, ctypes.byref(r))+                if (r.bottom - r.top) > 200 and hwnd != edit:+                    cands.append((hwnd, r))+                return True+            cbp = WNDENUMPROC(cb)+            _callbacks.append(cbp)+            user32.EnumChildWindows(pane, cbp, 0)+        except Exception:+            pass+        return cands++    def _title_ok():+        t = (_get_footprint_editor_title() or "").lower()+        return verify in t and "no footprint loaded" not in t++    for hwnd, _r in _tree_candidates():+        user32.PostMessageW(hwnd, WM_KEYDOWN, VK_RETURN, 0)+        user32.PostMessageW(hwnd, WM_KEYUP, VK_RETURN, 0)+    for _ in range(6):+        time.sleep(0.7)+        if _title_ok():+            return True+    WM_LBUTTONDOWN, WM_LBUTTONUP, WM_LBUTTONDBLCLK, MK_LBUTTON = 0x0201, 0x0202, 0x0203, 0x0001+    for hwnd, r in _tree_candidates():+        w = r.right - r.left+        x, y = max(10, w // 3), 58+        lp = (y << 16) | (x & 0xFFFF)+        user32.PostMessageW(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, lp)+        user32.PostMessageW(hwnd, WM_LBUTTONUP, 0, lp)+        user32.PostMessageW(hwnd, WM_LBUTTONDBLCLK, MK_LBUTTON, lp)+        user32.PostMessageW(hwnd, WM_LBUTTONUP, 0, lp)+    for _ in range(8):+        time.sleep(0.7)+        if _title_ok():+            return True+    return False++ def _navigate_to_footprint(fp_hwnd: int, footprint_name: str, load_after: bool = True) -> dict:     """Call the PowerShell script to navigate to a specific footprint. @@ -581,6 +659,16 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict:         return {"success": True, "output": "Footprint Editor opened successfully."}      search_text = footprint_name or library_name+    _verify = (footprint_name or search_text).lower()++    # 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+    if _background_navigate_win32_fp(fp_hwnd, _filter, _verify):+        return {+            "success": True,+            "output": f"Footprint '{search_text}' loaded (background win32 path): {_get_footprint_editor_title()}",+            "via": "win32-background",+        }      nav_result = _navigate_to_footprint(fp_hwnd, search_text) @@ -609,16 +697,22 @@ def handle_open_footprint_editor(kicad_info: dict, args: dict) -> dict:             }         else:             return {-                "success": True,-                "output": f"Footprint Editor opened and searched for '{search_text}'. "-                          f"Use Ctrl+Shift+E to load the footprint if it didn't auto-load.",+                "success": False,+                "editorOpened": True,+                "footprintLoaded": False,+                "error": f"Footprint Editor opened but '{search_text}' did not load "+                         f"(title: {_get_footprint_editor_title() or 'unknown'})",+                "_hint": ("All load paths ran without confirming the footprint. Verify the "+                          "library/footprint pair exists (fp-lib-table row + .pretty content); "+                          "a retry usually lands if pcbnew just cold-started."),             }     else:-        _force_foreground(fp_hwnd)         return {-            "success": True,-            "output": f"Footprint Editor opened but navigation failed: {nav_result.get('error', 'unknown')}. "-                      f"Search for '{search_text}' manually.",+            "success": False,+            "editorOpened": True,+            "footprintLoaded": False,+            "error": f"Footprint Editor opened but navigation errored: {nav_result.get('error', 'unknown')}",+            "_hint": f"Retry once; kicad_state shows the editor. Requested: '{search_text}'.",         }  
skills/kicad-bridge-test/run_verb_tests.py+4−1
@@ -124,7 +124,10 @@ def matrix(w: dict) -> dict:         "open_symbol_editor": dict(phase=3, args={"symbolName": "R", "libraryName": "Device"},                                    check="symbol_verified", timeout=160,                                    note="the #20 acceptance test: verified Device:R load"),-        "open_footprint_editor": dict(phase=3, args={}, timeout=160),+        "open_footprint_editor": dict(phase=3,+                                  args={"footprintName": "R_0603_1608Metric", "library": "Resistor_SMD"},+                                  timeout=160,+                                  note="e2e load of a stock footprint (needs the 0.9.84 lib self-heal)"),         "open_3d_viewer":    dict(phase=3, args={"reason": "daily verb test"}, timeout=200),         "open_editors":      dict(phase=3, args={}, check="editors_listed"),         "state":             dict(phase=3, args={"maxWidth": 640}, check="state_shots", timeout=160,