← Commit history

v0.9.113: always-on focus sentinel (John saw late self-raises land in the foreground after per-verb guards expired) - bounces any KiCad fg transition not preceded by user input, honors deliberate user clicks via GetLastInputInfo, logs every transition to kicad_state.focusEvents + stderr

John Lauer ·4cafdf645b ·2mo ago ·parent 9957952
7 files changed +177−51
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.112+0.9.113
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.112",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.112/adom-bridge-kicad-v0.9.112.zip",-  "sha256": "a50bc546ef5b47f236626f0dee19126450adf2189e5b89064729f920d523e182",-  "size": 407166,+  "version": "0.9.113",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.113/adom-bridge-kicad-v0.9.113.zip",+  "sha256": "4afd53258aa6a175bd6b2a585473bb88c7e4ca111e1708a376aca3127d6af45e",+  "size": 408755,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-16T12:24:34Z",+  "released_at": "2026-08-16T13:20:09Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.113.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.112",+  "version": "0.9.113",   "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/state.py+10−1
@@ -82,9 +82,17 @@ def handle_state(kicad_info: dict, args: dict) -> dict:     except Exception:         pass +    focus_events = []+    try:+        from handlers import win_focus+        focus_events = win_focus.get_focus_events()[-40:]+    except Exception:+        pass+     return {         "success": True,         "running": running,+        "focusEvents": focus_events,         "activeVersion": kicad_info.get("version"),         "windows": [{k: w.get(k) for k in ("hwnd", "title", "className", "rect")}                     for w in windows],@@ -95,7 +103,8 @@ def handle_state(kicad_info: dict, args: dict) -> dict:             "summary": editors.get("summary"),         },         "modalDialogs": modal,-        "_hint": ("The full state of the union: every KiCad-owned window is in shots[] as inline "+        "_hint": ("focusEvents is the focus sentinel log: a self-raise (bounced) entry is a PREVENTED foreground steal; user-foregrounded means the human chose it. "+                  "The full state of the union: every KiCad-owned window is in shots[] as inline "                   "base64 PNG (render them all — one window is NOT the app). modalDialogs non-empty "                   "means something is blocking; editors.list is the plugin's logical inventory "                   "(null while the editor's Python is still binding, issue #21). Poll before/after "
handlers/win_focus.py+149−43
@@ -218,6 +218,152 @@ def _demote_below(kicad_hwnd, user_hwnd) -> None:         pass  +def _sweep_suppress_all() -> None:+    """Pre-suppress EVERY visible KiCad top-level window — so a window's FIRST+    self-activation is refused too, not just its repeat offenses. (v0.9.102: the+    0.9.101 proof run still showed exactly one visible steal per fresh window —+    the free first offense. This closes it.)"""+    try:+        import ctypes as _ct+        WNDENUMPROC = _ct.WINFUNCTYPE(_ct.c_bool, _ct.wintypes.HWND, _ct.wintypes.LPARAM)+        def cb(h, _l):+            try:+                if _user32.IsWindowVisible(h) and is_kicad_window(h):+                    suppress_activation(h)+            except Exception:+                pass+            return True+        cbp = WNDENUMPROC(cb)+        _user32.EnumWindows(cbp, 0)+    except Exception:+        pass+++def _push_kicad_to_background(h) -> None:+    """v0.9.104 (John, verbatim design): "you just always have to only deal with+    the windows you opened from kicad" — every action here targets the KICAD+    window ONLY. If it holds the foreground, MINIMIZE-BOUNCE it: minimizing the+    foreground window makes WINDOWS ITSELF activate the user's previous window+    (we never locate, choose, or raise any user window), then show it again+    unfocused, and drop it to the bottom of the z-order."""+    import time as _t+    try:+        suppress_activation(h)+        if _user32.GetForegroundWindow() == h:+            _user32.ShowWindow(h, 6)   # SW_MINIMIZE -> OS returns focus naturally+            _t.sleep(0.05)+            _user32.ShowWindow(h, 4)   # SW_SHOWNOACTIVATE -> visible again, unfocused+        HWND_BOTTOM = 1+        SWP = 0x0010 | 0x0002 | 0x0001  # NOACTIVATE|NOMOVE|NOSIZE+        _user32.SetWindowPos(h, HWND_BOTTOM, 0, 0, 0, 0, SWP)+    except Exception:+        pass+++def _seconds_since_user_input() -> float:+    """Seconds since the user's last keyboard/mouse input, system-wide."""+    try:+        import ctypes as _ct++        class LASTINPUTINFO(_ct.Structure):+            _fields_ = [("cbSize", _ct.wintypes.UINT), ("dwTime", _ct.wintypes.DWORD)]++        lii = LASTINPUTINFO()+        lii.cbSize = _ct.sizeof(LASTINPUTINFO)+        if _user32.GetLastInputInfo(_ct.byref(lii)):+            return max(0.0, (_kernel32.GetTickCount() - lii.dwTime) / 1000.0)+    except Exception:+        pass+    return 1e9+++# ---------------------------------------------------------------------------+# The ALWAYS-ON focus sentinel (0.9.113, John 2026-08-16: "i just saw some of+# your windows auto-foreground"). The per-verb guardians below PREVENT steals+# while a verb is in flight, but they expire (and deliberately clear the+# NOACTIVATE styles so the user can click into KiCad). KiCad windows self-raise+# LATE — a 3D render or a footprint-library load finishing minutes after the+# verb — and until now nothing watched that window. The sentinel does, forever:+#+#   - fg transition to a KiCad window + NO recent user input  => self-raise:+#     minimize-bounce it back down (and log it).+#   - fg transition to a KiCad window right after user input   => the USER chose+#     it (John: "always let the user choose to fg a window") — leave it alone.+#   - while a guardian is active it only logs; the guardian owns the response.+#+# Every transition lands in a ring buffer (kicad_state.focusEvents) and on+# stderr (=> ~/.adom/bridge-logs/kicad.log), so background-contract compliance+# is PROVABLE from the log, not sampled by callers.+# ---------------------------------------------------------------------------+_USER_INTENT_WINDOW_S = 1.2+_sentinel_started = False+_focus_events = None  # collections.deque, lazy+++def get_focus_events() -> list:+    """The sentinel's transition log, oldest first."""+    return list(_focus_events) if _focus_events is not None else []+++def start_focus_sentinel() -> None:+    """Start the process-lifetime sentinel thread (idempotent; called at boot)."""+    global _sentinel_started, _focus_events+    if not IS_WINDOWS or _sentinel_started:+        return+    _sentinel_started = True+    import collections+    import threading+    import time as _t+    _focus_events = collections.deque(maxlen=200)++    def _log(action, hwnd, ago):+        try:+            buf = ctypes.create_unicode_buffer(80)+            _user32.GetWindowTextW(hwnd, buf, 80)+            evt = {"t": _t.time(), "action": action, "title": buf.value,+                   "secondsSinceUserInput": round(min(ago, 9999.0), 2)}+            _focus_events.append(evt)+            print(f"[focus-sentinel] {action}: '{buf.value}' "+                  f"(user input {evt['secondsSinceUserInput']}s ago)", file=sys.stderr)+        except Exception:+            pass++    def _loop():+        last_fg = None+        blessed = set()  # KiCad hwnds the user deliberately foregrounded+        while True:+            try:+                fg = _user32.GetForegroundWindow()+                if fg != last_fg:+                    if fg and is_kicad_window(fg):+                        ago = _seconds_since_user_input()+                        if ago <= _USER_INTENT_WINDOW_S:+                            blessed.add(fg)+                            _log("user-foregrounded (left alone)", fg, ago)+                        elif _guard_count > 0:+                            _log("steal-during-guard (guardian handles)", fg, ago)+                        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)+                    else:+                        blessed.discard(last_fg)+                    last_fg = fg+                _t.sleep(0.12)+            except Exception:+                try:+                    _t.sleep(1.0)+                except Exception:+                    pass++    try:+        threading.Thread(target=_loop, daemon=True, name="kicad-focus-sentinel").start()+    except Exception:+        pass++ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float = 0.1) -> None:     """Focus guardian: for `seconds` after a window-spawning verb, make sure NO     KiCad window holds the user's foreground. KiCad activates windows@@ -239,46 +385,6 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float =     import threading     import time as _t -    def _sweep_suppress():-        """Pre-suppress EVERY visible KiCad top-level window — so a window's-        FIRST self-activation is refused too, not just its repeat offenses.-        (v0.9.102: the 0.9.101 proof run still showed exactly one visible steal-        per fresh window — the free first offense. This closes it.)"""-        try:-            import ctypes as _ct-            WNDENUMPROC = _ct.WINFUNCTYPE(_ct.c_bool, _ct.wintypes.HWND, _ct.wintypes.LPARAM)-            def cb(h, _l):-                try:-                    if _user32.IsWindowVisible(h) and is_kicad_window(h):-                        suppress_activation(h)-                except Exception:-                    pass-                return True-            cbp = WNDENUMPROC(cb)-            _user32.EnumWindows(cbp, 0)-        except Exception:-            pass--    def _push_to_background(h):-        """v0.9.104 (John, verbatim design): "you just always have to only deal-        with the windows you opened from kicad" — every action here targets the-        KICAD window ONLY. If it holds the foreground, MINIMIZE-BOUNCE it:-        minimizing the foreground window makes WINDOWS ITSELF activate the-        user's previous window (we never locate, choose, or raise any user-        window), then show it again unfocused, and drop it to the bottom of the-        z-order. Combined with NOACTIVATE it stays down."""-        try:-            suppress_activation(h)-            if _user32.GetForegroundWindow() == h:-                _user32.ShowWindow(h, 6)   # SW_MINIMIZE -> OS returns focus naturally-                _t.sleep(0.05)-                _user32.ShowWindow(h, 4)   # SW_SHOWNOACTIVATE -> visible again, unfocused-            HWND_BOTTOM = 1-            SWP = 0x0010 | 0x0002 | 0x0001  # NOACTIVATE|NOMOVE|NOSIZE-            _user32.SetWindowPos(h, HWND_BOTTOM, 0, 0, 0, 0, SWP)-        except Exception:-            pass-     def _loop():         global _guard_count         if _guard_lock:@@ -287,15 +393,15 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float =         deadline = _t.monotonic() + seconds         tick = 0         try:-            _sweep_suppress()  # pre-suppress what's already up before the verb acts+            _sweep_suppress_all()  # pre-suppress what's already up before the verb acts             while _t.monotonic() < deadline:                 try:                     tick += 1                     if tick % 5 == 0:-                        _sweep_suppress()   # catch windows CREATED mid-verb (~every 0.5s)+                        _sweep_suppress_all()   # catch windows CREATED mid-verb (~every 0.5s)                     fg = _user32.GetForegroundWindow()                     if fg and is_kicad_window(fg):-                        _push_to_background(fg)+                        _push_kicad_to_background(fg)                     # NON-KiCad foreground = the user's business. We never track,                     # never adopt, never raise it. (v0.9.103's conditional restore                     # was still user-window surgery — removed entirely.)
server.py+11
@@ -1001,6 +1001,17 @@ def main():     except Exception:         pass +    # 0.9.113: the always-on focus sentinel — after per-verb guards expire,+    # late self-raising KiCad windows (3D render / library-load completion)+    # used to land in the user's foreground unopposed. The sentinel bounces+    # any KiCad fg transition that no user input preceded, and logs every+    # transition (kicad_state.focusEvents + stderr) so quietness is provable.+    try:+        from handlers import win_focus+        win_focus.start_focus_sentinel()+    except Exception:+        pass+     port = DEFAULT_PORT     if "--port" in sys.argv:         idx = sys.argv.index("--port")