← Commit history

v0.9.105: every forced foreground announces itself first — ab notify_user caption (attributed to the calling thread) fires ~1.2s before any allowFocusSteal/allowForeground grab

John Lauer ·6544271c52 ·2mo ago ·parent 7e1b163
9 files changed +72−7
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.104+0.9.105
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "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,+  "version": "0.9.105",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.105/adom-bridge-kicad-v0.9.105.zip",+  "sha256": "20642cf4c35bc8dacb55568f86c0de5fec951cc0d212ddc29b804180b8174a8c",+  "size": 396469,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-15T10:06:09Z",+  "released_at": "2026-08-15T10:09:52Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.105.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.104",+  "version": "0.9.105",   "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/kicad_ui.py+13
@@ -570,6 +570,12 @@ def handle_send_key(kicad_info: dict, args: dict) -> dict:     time.sleep(0.05)      allow_fg = bool(args.get("allowForeground"))+    if allow_fg:+        try:+            from handlers import win_focus as _wf+            _wf.announce_foreground(f"send '{args.get('key') or 'input'}' to KiCad with real keystrokes")+        except Exception:+            pass      # DEFAULT PATH: post the key straight to KiCad's window — no focus, no screen     # theft, works while the window is buried. Chords still need the focus path@@ -781,6 +787,13 @@ def handle_click(kicad_info: dict, args: dict) -> dict:                               "user you need the screen for ~5s, then retry allowForeground:true.")}         return {"success": False, "error": "Could not post the click to the window"} +    # allowForeground granted — announce WHY before moving the user's cursor+    try:+        from handlers import win_focus as _wf+        _wf.announce_foreground("click inside KiCad with the real cursor")+    except Exception:+        pass+     # Convert to absolute coordinates for SendInput (0-65535 range)     screen_w = user32.GetSystemMetrics(0)     screen_h = user32.GetSystemMetrics(1)
handlers/open_footprint_editor.py+11
@@ -261,6 +261,7 @@ def _send_ctrl_shift_e(hwnd: int):     # here (keys must land on KiCad, never the user's window).     try:         from handlers import win_focus+        win_focus.announce_foreground("load the footprint via keyboard (Ctrl+Shift+E)")         win_focus.bring_to_user(hwnd)     except Exception:         pass@@ -946,6 +947,11 @@ def handle_open_3d_viewer(kicad_info: dict, args: dict) -> dict:         # Opt-in disruptive fallback: Alt+3 keystroke (needs the FP window foregrounded,         # so it steals focus) — only when the caller explicitly allows it.         if args.get("allowFocusSteal"):+            try:+                from handlers import win_focus as _wf+                _wf.announce_foreground("open the 3D viewer via Alt+3")+            except Exception:+                pass             VK_MENU_LOCAL = 0x12             VK_3 = 0x33             user32.SetForegroundWindow(fp_hwnd)@@ -1055,6 +1061,11 @@ def handle_open_3d_viewer(kicad_info: dict, args: dict) -> dict:         # their app's Alt+3). That steals focus, so it's OPT-IN only         # ({"allowFocusSteal": true}); by default we fail to the background path.         if args.get("allowFocusSteal"):+            try:+                from handlers import win_focus as _wf+                _wf.announce_foreground("open the board 3D viewer via Alt+3")+            except Exception:+                pass             user32.SetForegroundWindow(pcb_hwnd)             time.sleep(0.3)             VK_MENU = 0x12   # Alt key
handlers/open_symbol_editor.py+5
@@ -609,6 +609,11 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:                       "or foreground:true to bring the editor to you and load it yourself."),         } +    try:+        from handlers import win_focus as _wf+        _wf.announce_foreground(f"type the symbol search for '{search_text}'")+    except Exception:+        pass     nav_result = _navigate_to_symbol(sym_hwnd, search_text)      if nav_result["status"] == "ok":
handlers/win_focus.py+28
@@ -341,3 +341,31 @@ def bring_to_user(hwnd) -> bool:         return _set_foreground_robust(hwnd)     except Exception:         return False+++def announce_foreground(action: str, seconds: int = 5) -> 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+    the calling AI thread via the forwarded caller headers — so the user sees WHY+    their screen is about to be taken, BEFORE it happens. Fire-and-forget in a+    daemon thread: the toast failing must never fail (or delay) the verb."""+    def _fire():+        try:+            from caller_identity import ad_callback+            ad_callback("notify_user", {+                "message": f"KiCad bridge needs your screen for ~{seconds}s: {action}. "+                           "Background methods were exhausted — this was explicitly allowed "+                           "by the calling AI (allowFocusSteal/allowForeground).",+                "title": "Taking the foreground briefly",+                "level": "warning",+            }, timeout=8)+        except Exception:+            pass+    try:+        import threading+        threading.Thread(target=_fire, daemon=True).start()+        import time as _t+        _t.sleep(1.2)   # give the toast a beat to render BEFORE the grab+    except Exception:+        pass
skills/kicad-bridge-background/SKILL.md+8
@@ -84,6 +84,14 @@ the browser forward, restored stale snapshots). The rule now:   observe-only (fg_assert), it raises nothing. - The ONE exception remains foreground:true / bring_to_user — the user   explicitly asking to see a window.+- **Every forced foreground ANNOUNCES ITSELF FIRST** (John 2026-08-15: "show an+  ab caption telling the user why you have to foreground"). Rule: any+  allowFocusSteal/allowForeground branch calls `win_focus.announce_foreground(+  "<what and why>")` BEFORE the grab — it fires an ab `notify_user` toast+  (branded, attributed to the calling AI thread via forwarded caller headers)+  and waits ~1.2s so the caption renders before the screen is taken. A forced+  foreground without the announce is a bug; add the call when adding any new+  last-resort path.  ## The mechanism (v0.9.101: PREVENT, don't react)