← Commit history

Update 40 file(s)

John Lauer ·a956db4e0e ·1mo ago ·parent 4022071
4 files changed +143−7
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.264\ No newline at end of file+0.9.265\ No newline at end of file
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.264",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.264/adom-bridge-kicad-v0.9.264.zip",-  "sha256": "6f7179349c1454ad103f2de90b8ad4f87492207b03d960b854e9de7007b8f39b",-  "size": 1030598,+  "version": "0.9.265",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.265/adom-bridge-kicad-v0.9.265.zip",+  "sha256": "743ae641dec347f38e814dee3027008ffff1b070418e930d4564e6e058b2a408",+  "size": 1033495,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-09-01T01:48:44Z",+  "released_at": "2026-09-01T02:23:49Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.264",+  "version": "0.9.265",   "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/foreground_guard.pyadded+136
@@ -0,0 +1,136 @@+"""Foreground etiquette for actions that raise a KiCad window.++Two helpers, `warn_before_foreground` and `guarded_foreground`, that every verb+which is KNOWN to raise a window should use. Added 2026-09-01 on John's+instruction, after he watched this bridge repeatedly steal his foreground window.++WHY THIS EXISTS AT ALL+----------------------+KiCad is wxWidgets, and wx ACTIVATES ITS OWN TOP-LEVEL WINDOW whenever its UI+state changes. Measured on AdomLapper against KiCad 10.0 by comparing z-order+before and after each call (see skills/kicad-uia):++    desktop_ui_tree   (read-only)          z unchanged+    desktop_ui_expand (uia-pattern)        KiCad z 4 -> 0+    desktop_ui_select (uia-pattern)        editor  z 1 -> 0+    desktop_ui_click  (Invoke)             editor  z 1 -> 0++Note the middle two took the pure UIA pattern path — Adom Bridge never touched+SendInput — and the window STILL came forward. So this is not something a+different ab verb or code path can avoid: you cannot change a wx control's state+without the app raising itself. UI Automation is a way to READ KiCad precisely+and to act without coordinates. It is NOT a way to act invisibly, and it must+never be described to a user as if it were.++THE 200ms RULE (the important part)+-----------------------------------+When we cause a raise, we hand the user's window straight back, so the raise is a+blip instead of a theft. But the guard gets EXACTLY ONE LOOK, within ~200ms, and+then stops forever for that action.++That bound is the safety property, not a performance tweak. A KiCad window+arriving 50ms after our call is ours. A KiCad window arriving two seconds later+is THE USER — they clicked its taskbar button or alt-tabbed to it. A guard that+keeps watching would fight them: they foreground KiCad, we shove it back, and the+machine feels broken. John's words, 2026-09-01: "after that you need to be hands+off in case the adom user wants to click the windows taskbar icon to foreground+that window or alt-tab to it."++So restoring requires BOTH:+  1. we are still inside the window (default 200ms), AND+  2. the window now on top is the one WE raised — not merely "not the user's".+Fail either and do nothing at all.++CAPTION FIRST+-------------+Anything known to foreground announces itself on screen first, via+desktop_caption, so a window appearing is explained rather than surprising. ab+requires a `reason` (>=10 chars) which it prints on the caption beside the thread+name, so the user can always see who is drawing on their screen. Keep the text to+one clause: ab measures the rendered box and warns `too_wide_for_screen`.+"""++from __future__ import annotations++import time++GUARD_WINDOW_MS = 200+++def _top_window(ab_call) -> int | None:+    """hwnd currently at the top of the z-order, or None if unreadable."""+    try:+        res = ab_call("desktop_list_windows", {})+        wins = (res or {}).get("windows") or []+        if not wins:+            return None+        return sorted(wins, key=lambda w: w.get("z", 10**9))[0].get("hwnd")+    except Exception:  # pylint: disable=broad-except+        return None+++def warn_before_foreground(ab_call, text: str, reason: str, seconds: int = 5) -> None:+    """Tell the user on screen WHY a window is about to jump to the front.++    Best-effort: a caption failure must never block the real work, and must never+    turn into an error the user sees twice."""+    try:+        ab_call("desktop_caption", {"text": text[:120], "seconds": seconds,+                                    "reason": reason})+    except Exception:  # pylint: disable=broad-except+        pass+++def guarded_foreground(ab_call, action, *, caption: str | None = None,+                       reason: str = "", guard_ms: int = GUARD_WINDOW_MS) -> dict:+    """Run `action` (which is expected to raise a KiCad window) and give the+    user's window back — once, immediately, and only if we are the cause.++    Returns the action's result with a `_foreground` report attached, so a caller+    can SAY what happened instead of guessing. Never raises on guard failure: the+    user's window placement is a courtesy, not a correctness requirement."""+    if caption:+        warn_before_foreground(ab_call, caption,+                               reason or "Warn before an action that raises a KiCad window")++    user_win = _top_window(ab_call)+    t0 = time.monotonic()+    result = action()+    elapsed_ms = (time.monotonic() - t0) * 1000.0++    report = {"userWindowBefore": user_win, "elapsedMs": round(elapsed_ms),+              "restored": False, "why": ""}++    # Condition 1: are we still inside the guard window? If the action itself+    # took longer than the guard, we have ALREADY missed our chance — anything+    # on top now is as likely to be the user as us, so we do not touch it.+    if elapsed_ms > guard_ms:+        report["why"] = (f"action took {report['elapsedMs']}ms (> {guard_ms}ms guard); "+                         "too late to tell our raise from a deliberate user switch")+        result["_foreground"] = report+        return result++    now_top = _top_window(ab_call)+    report["topAfter"] = now_top++    # Condition 2: is the window on top the one WE raised? "Not the user's+    # window" is NOT good enough — a third app may have come forward on its own,+    # and shoving it aside would be the same rudeness in a different direction.+    if user_win is None or now_top is None:+        report["why"] = "could not read z-order; leaving the user's screen alone"+    elif now_top == user_win:+        report["why"] = "nothing was raised"+    else:+        try:+            ab_call("desktop_raise_window", {+                "hwnd": user_win,+                "reason": "Restore the window that was in front before this thread raised a KiCad window"})+            report["restored"] = True+            report["why"] = "restored the user's window within the guard window"+        except Exception as e:  # pylint: disable=broad-except+            report["why"] = f"restore failed: {type(e).__name__}"++    # No second look. Ever. From here the foreground belongs to the user: if they+    # alt-tab to KiCad a moment from now, that is their choice and we leave it.+    result["_foreground"] = report+    return result