← Commit history

v0.9.131: John's foreground contract - NO foreground without foregroundReason (refused otherwise), every sanctioned foreground captions the reason via ab toast for 3s BEFORE the window moves, and sentinel bounces hold NOACTIVATE for 10s (10.0.5 steal storms flickered because prevention is reactive-only there)

John Lauer ·55cd782906 ·1mo ago ·parent 7c3585f
7 files changed +46−16
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.130+0.9.131
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.130",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.130/adom-bridge-kicad-v0.9.130.zip",-  "sha256": "674e14a36048e2843dde2c589e3ed8988a29561677f427a7d57164c6977ef9e4",-  "size": 413375,+  "version": "0.9.131",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.131/adom-bridge-kicad-v0.9.131.zip",+  "sha256": "c3de9c82227d4510c83e5f14eaa9afe21ab084facb2ac7883bc34a9a7a6d8113",+  "size": 413883,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-16T18:43:05Z",+  "released_at": "2026-08-16T20:02:28Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.131.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.130",+  "version": "0.9.131",   "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+1−1
@@ -262,7 +262,7 @@ def _send_ctrl_shift_e(hwnd: int):     try:         from handlers import win_focus         win_focus.announce_foreground("load the footprint via keyboard (Ctrl+Shift+E)")-        win_focus.bring_to_user(hwnd)+        win_focus.bring_to_user(hwnd, reason="Loading the footprint needs the editor visible for one keyboard command")     except Exception:         pass     time.sleep(0.3)
handlers/win_focus.py+26−7
@@ -359,6 +359,7 @@ def start_focus_sentinel() -> None:         last_fg = None         last_click_t = 0.0         last_alt_t = 0.0+        bounce_hold = {}  # hwnd -> monotonic deadline to clear NOACTIVATE         VK_LBUTTON, VK_RBUTTON, VK_MENU = 0x01, 0x02, 0x12         while True:             try:@@ -386,9 +387,16 @@ def start_focus_sentinel() -> None:                         else:                             _log("self-raise (bounced to background)", fg, ago)                             _push_kicad_to_background(fg)-                            # no guard is active: immediately clear the NOACTIVATE-                            # bit the bounce set, so a real user click still works-                            allow_activation(fg)+                            # 10.0.5: NOACTIVATE no longer PREVENTS the steal, it+                            # only helps after the bounce — hold the suppression+                            # 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+                    for h, until in list(bounce_hold.items()):+                        if now >= until:+                            allow_activation(h)+                            del bounce_hold[h]                     last_fg = fg                 _t.sleep(0.12)             except Exception:@@ -474,14 +482,25 @@ def user_wants_foreground(args) -> bool:     return bool(args.get("foreground") or args.get("showToUser") or args.get("bringToFront"))  -def bring_to_user(hwnd) -> bool:+def bring_to_user(hwnd, reason: str = "") -> bool:     """Explicit, user-requested foreground of a window (the ONLY sanctioned way a     KiCad window is brought to the front for the user — everything else stays-    background). Returns True if it took."""+    background). John's contract (2026-08-16): NO foreground without a stated+    reason, and every sanctioned foreground shows an ab caption with that reason+    for 3 seconds first. A call without a reason is REFUSED."""     if not IS_WINDOWS or not hwnd:         return False+    if not (reason or "").strip():+        try:+            print("[focus] bring_to_user REFUSED: no reason given", file=sys.stderr)+        except Exception:+            pass+        return False     try:-        expect_foreground(30.0)      # tell the sentinel this foreground is sanctioned+        announce_foreground(reason, seconds=3)+        import time as _t+        _t.sleep(1.2)                # let the caption render BEFORE the window moves+        expect_foreground(20.0)      # tell the sentinel this foreground is sanctioned         allow_activation(hwnd)       # a suppressed window can't be activated         _user32.ShowWindow(hwnd, 9)  # SW_RESTORE (in case minimized)         return _set_foreground_robust(hwnd)@@ -489,7 +508,7 @@ def bring_to_user(hwnd) -> bool:         return False  -def announce_foreground(action: str, seconds: int = 5) -> None:+def announce_foreground(action: str, seconds: int = 3) -> None:     """MANDATORY courtesy caption before any forced foreground (John 2026-08-15:     "when you HAVE to foreground cuz you have no other choice, show an ab caption     telling the user why"). Fires a native ab toast — branded, and attributed to
server.py+12−1
@@ -808,11 +808,22 @@ def dispatch_command(command: str, args: dict) -> dict:                 wins = _find_all_kicad_windows()                 if wins:                     _shown = wins[-1].get("hwnd")  # the most-recently surfaced KiCad window-                    _wf.bring_to_user(_shown)+                    _reason = (args.get("foregroundReason") or args.get("reason") or "").strip()+                    if _reason:+                        _wf.bring_to_user(_shown, reason=_reason)+                    else:+                        # John's contract (2026-08-16): NO foreground without a+                        # stated reason — refuse and tell the caller how.+                        _shown = None             except Exception:                 pass             if isinstance(result, dict):                 result["_broughtToUser"] = bool(_shown)+                if not _shown and isinstance(result, dict):+                    result.setdefault("_hint", "")+                    result["_foregroundRefused"] = ("foreground requested without foregroundReason - "+                                                   "pass foregroundReason:'<why the user needs to see this>' "+                                                   "and the bridge will caption it for 3s, then foreground.")             _user_fg = None  # don't also restore-away what the user just asked to see     except Exception:         pass