← Commit history

v0.9.104 FINAL background design (John): guardian touches ONLY KiCad windows — minimize-bounce the offender (OS returns focus naturally) + NOACTIVATE + HWND_BOTTOM; ALL user-window capture/restore/raise deleted from dispatcher, guardian, and suite

John Lauer ·7e1b163879 ·2mo ago ·parent 257ad86
8 files changed +60−24
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.103+0.9.104
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.103",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.103/adom-bridge-kicad-v0.9.103.zip",-  "sha256": "90822ed96ed63ac17a05144838472188f3dd591fa785b8ff520d622892adc7da",-  "size": 395356,+  "version": "0.9.104",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.104/adom-bridge-kicad-v0.9.104.zip",+  "sha256": "41ce2eaf45d88264eb0016c1b9100aeccacb24f428f1031eafade119ba673778",+  "size": 395740,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-15T10:04:17Z",+  "released_at": "2026-08-15T10:06:09Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.104.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.103",+  "version": "0.9.104",   "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/win_focus.py+25−12
@@ -259,12 +259,31 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float =         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:             with _guard_lock:                 _guard_count += 1-        protect = hwnd         deadline = _t.monotonic() + seconds         tick = 0         try:@@ -275,17 +294,11 @@ def restore_foreground_persistent(hwnd, seconds: float = 6.0, interval: float =                     if tick % 5 == 0:                         _sweep_suppress()   # catch windows CREATED mid-verb (~every 0.5s)                     fg = _user32.GetForegroundWindow()-                    if fg and fg != protect:-                        if is_kicad_window(fg):-                            # PREVENT the next steal (NOACTIVATE), then repair this-                            # one: user back on top, thief tucked under.-                            suppress_activation(fg)-                            if _user32.IsWindow(protect):-                                _set_foreground_robust(protect)-                                _demote_below(fg, protect)-                        else:-                            # The USER moved to another window — protect that instead.-                            protect = fg+                    if fg and is_kicad_window(fg):+                        _push_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.)                 except Exception:                     pass                 _t.sleep(interval)
server.py+4−1
@@ -803,7 +803,10 @@ def dispatch_command(command: str, args: dict) -> dict:     if _user_fg is not None:         try:             from handlers import win_focus-            restored = win_focus.restore_foreground(_user_fg)+            # v0.9.104: NO user-window restore — the guardian handles KiCad+            # windows only (minimize-bounce + NOACTIVATE). We never raise the+            # user's window; Windows re-activates it naturally.+            restored = False             # A launched editor activates its window ASYNC — a beat after we return.             # For window-spawning verbs, also keep the user on top for a few seconds             # (background daemon thread — doesn't delay this response) so KiCad's late
skills/kicad-bridge-background/SKILL.md+22
@@ -63,6 +63,28 @@ An ungated one is a focus-steal bug.    dialog auto-dismiss that pokes a window, `_force_foreground` calls left in    old handlers. New code copies old code; old code steals focus. +## THE FINAL DESIGN (v0.9.104 — John, verbatim): touch ONLY KiCad's windows++"You should only ever make sure the windows that auto-come to the fg, you then+push back to the bg — but NOT by finding this browser window we're chatting+from. You just always have to only deal with the windows YOU opened from KiCad."++This deletes an entire class of bugs the earlier designs had: capturing the+user's foreground window, restoring it, re-raising it, adopting it — ALL of it+was user-window surgery, and every variant eventually fought the user (yanked+the browser forward, restored stale snapshots). The rule now:++- The guardian NEVER locates, raises, restores, or tracks any user window.+- It sweeps KICAD windows only: WS_EX_NOACTIVATE on every visible one (first+  offense prevented), and if one holds the foreground, MINIMIZE-BOUNCE it —+  SW_MINIMIZE (Windows itself then re-activates the user's previous window;+  the OS picks, we never do) → SW_SHOWNOACTIVATE (visible again, unfocused) →+  SetWindowPos HWND_BOTTOM. Every API call targets the KiCad hwnd.+- The dispatcher's old capture-fg/restore-fg is GONE. The suite's stealth is+  observe-only (fg_assert), it raises nothing.+- The ONE exception remains foreground:true / bring_to_user — the user+  explicitly asking to see a window.+ ## The mechanism (v0.9.101: PREVENT, don't react)  v0.9.79's guardian only REACTED — poll 250ms, yank focus back after KiCad had
skills/kicad-bridge-test/run_verb_tests.py+2−4
@@ -339,10 +339,8 @@ def main():         if top and not any(k in tt for k in _kw):             _user_win = top          # user moved on; protect their new window             return                   # already on top — nothing to do-        # KiCad is on top: raise the user's window back over it-        hw = _user_win.get("hwnd")-        if hw:-            ab(a.target, "desktop_raise_window", {"hwnd": hw}, timeout=20)+        # v0.9.104 (John): NEVER raise the user's window from here. The bridge's+        # own guardian pushes KiCad windows back; the suite only OBSERVES.      def fg_assert(verb):         """After every GUI verb: the OS foreground window must NOT be KiCad.