← Commit history

v0.9.172: THE IDLE RULE - never bounce a KiCad foreground when no verb or guard is running, so a taskbar click always wins; catch fast clicks (0x8001); show_* verbs now FAIL when their window never appeared instead of reporting success and letting the caller screenshot the wrong window

John Lauer ·bb560855a2 ·1mo ago ·parent c9d9ce9
7 files changed +80−12
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.171+0.9.172
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.171",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.171/adom-bridge-kicad-v0.9.171.zip",-  "sha256": "78d4e8ba9e0cb44bd186188733229b25ba883eb67a584ea55e1b7c944beba9ae",-  "size": 445651,+  "version": "0.9.172",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.172/adom-bridge-kicad-v0.9.172.zip",+  "sha256": "7674966eb1ef6ee04671a1cf9f8f277ea0a25b6edb7c311e27d040131b690afb",+  "size": 446638,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-18T14:40:31Z",+  "released_at": "2026-08-19T08:46:04Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.172.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.171",+  "version": "0.9.172",   "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.",
dashboard/kicad_dashboard.py+11−2
@@ -676,10 +676,19 @@ def _render(target, pick):     st = ab(target, "kicad_state", {"includeImages": True, "maxWidth": 900}, timeout=240)     ks = st.get("kicadState") or st     shots = ks.get("shots") or []-    chosen = None+    # EVIDENCE MUST BE THE THING THAT WAS ASKED FOR (John, 2026-08-19: "i had+    # asked for a footprint and the evidence you provided was just of the project+    # window"). The old fallback grabbed the last window on screen when the+    # expected one was missing, which turns a failure into a screenshot of+    # something else entirely - worse than no evidence, because it looks like+    # proof. If the requested surface is not there, say so.     if pick:         chosen = next((w for w in shots if pick in (w.get("title") or "").lower()), None)-    chosen = chosen or (shots[-1] if shots else None)+        if not chosen:+            seen = ", ".join((w.get("title") or "?")[:40] for w in shots) or "nothing"+            raise RuntimeError(f"the {pick} window never appeared - on screen instead: {seen}")+    else:+        chosen = shots[-1] if shots else None     if not chosen:         return None, None     img = (chosen.get("image") or chosen.get("screenshot")
handlers/show.py+46
@@ -45,14 +45,59 @@ 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."""     r = handle_open_symbol_editor(kicad_info, args or {})+    r = _verify_window(r, _find_symbol_editor, "Symbol Editor")     r.setdefault("surface", "symbol")     return r  +def _find_by_title(*needles) -> bool:+    """Is a window whose title contains ALL needles on screen right now?"""+    try:+        from handlers.kicad_ui import _find_all_kicad_windows+        for w in _find_all_kicad_windows() or []:+            t = (w.get("title") or "").lower()+            if all(n in t for n in needles):+                return True+    except Exception:+        pass+    return False+++def _find_symbol_editor() -> bool:+    return _find_by_title("symbol editor")+++def _find_3d_viewer() -> bool:+    return _find_by_title("3d viewer")+++def _verify_window(result: dict, finder, label: str, waits: int = 12) -> dict:+    """A show_* verb has ONE job: put THAT surface on screen. If its window is+    not there, the verb FAILS - even when every internal step reported success.++    0.9.172 (John, 2026-08-19: "i had asked for a footprint to be shown, and the+    evidence you provided was just of the project window"). The caller screenshots+    whatever the bridge says succeeded, so a false success does not just mislead,+    it manufactures wrong evidence."""+    if not isinstance(result, dict) or not result.get("success"):+        return result+    for _ in range(waits):+        if finder():+            return result+        time.sleep(0.5)+    return {**result, "success": False,+            "error": f"{label} reported success but its window never appeared",+            "errorCode": "surface_window_missing",+            "_hint": (f"The bridge could not put the {label} on screen, so there is nothing "+                      "truthful to screenshot. Retry after kicad_launch, or check the Plugin "+                      "LED: the background path needs the reverse-bridge plugin bound.")}++ def handle_show_footprint(kicad_info: dict, args: dict) -> dict:     """Show a FOOTPRINT in the Footprint Editor. Args: footprintName (+ library),     or library alone to browse."""     r = handle_open_footprint_editor(kicad_info, args or {})+    r = _verify_window(r, _find_footprint_editor, "Footprint Editor")     r.setdefault("surface", "footprint")     return r @@ -71,6 +116,7 @@ def handle_show_3d_chip(kicad_info: dict, args: dict) -> dict:                 break             time.sleep(0.5)     r = handle_open_3d_viewer(kicad_info, {**a, "editor": "fp"})+    r = _verify_window(r, _find_3d_viewer, "3D Viewer")     r.setdefault("surface", "3d_chip")     return r 
handlers/win_focus.py+16−3
@@ -1275,8 +1275,8 @@ def start_focus_sentinel() -> None:             try:                 now = _t.monotonic()                 try:-                    if (_user32.GetAsyncKeyState(VK_LBUTTON) & 0x8000) or \-                       (_user32.GetAsyncKeyState(VK_RBUTTON) & 0x8000):+                    if (_user32.GetAsyncKeyState(VK_LBUTTON) & 0x8001) or \+                       (_user32.GetAsyncKeyState(VK_RBUTTON) & 0x8001):                         last_click_t = now                     if _user32.GetAsyncKeyState(VK_MENU) & 0x8000:                         last_alt_t = now@@ -1299,6 +1299,18 @@ def start_focus_sentinel() -> None:                             _log("user-owned window (left alone)", fg, ago)                         elif _guard_count > 0:                             _log("steal-during-guard (guardian handles)", fg, ago)+                        elif _verb_inflight <= 0 and _guard_count <= 0:+                            # 0.9.172 - THE IDLE RULE (John, 2026-08-19: "when I+                            # click the taskbar icon to bring the app to the fg,+                            # you don't let me"). Bouncing only exists to stop a+                            # window flashing into his face WHILE the bridge is+                            # working. With no verb in flight and no guard armed,+                            # the bridge is not opening anything - so a KiCad+                            # window reaching the foreground is the USER, whether+                            # or not we managed to observe the click that did it+                            # (a fast taskbar click can land entirely between two+                            # 120ms sentinel polls). Leave it alone, always.+                            _log("foreground while bridge idle (left alone)", fg, ago)                         else:                             _log("self-raise (bounced to background)", fg, ago)                             _push_kicad_to_background(fg)@@ -1307,7 +1319,8 @@ def start_focus_sentinel() -> None:                             # for 10s so a repeat-raising window is refused                             # silently instead of flickering, THEN clear it so                             # user clicks work again.-                            bounce_hold[fg] = now + 10.0+                            bounce_hold[fg] = now + 3.0   # was 10s: too long to+                            # leave a window unclickable once the verb has moved on                     for h, until in list(bounce_hold.items()):                         if now >= until:                             allow_activation(h)