app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
v0.9.144: focusDebug introspection in kicad_state (sweep/hook/sentinel liveness, inflight/guard counters, parked/baseli…
abafa38
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
"""state.py — kicad_state: KiCad's full state of the union in one read-only call.
Issue #22 (John, 2026-08-14, adopted from the Bridge SDK suggested pattern):
a generic title-match screenshot returns ONE window, but KiCad is a multi-window
app — so callers (web pages, AIs that can't reach the desktop filesystem) got a
cropped sub-window as "proof". Only the bridge knows the real window set.
kicad_state returns, in one response:
- `shots[]` — a screenshot of EVERY window KiCad owns right now, captured by
real hwnd, inline as base64 PNG (`image`) plus the host file path.
- `windows[]` — hwnd/title/class/rect for each (no images when includeImages=false).
- `editors` — the plugin's logical editor inventory when a plugin is alive
(kind/frameTitle per editor), else the reason it isn't.
- `modalDialogs` + `running` + `activeVersion` — enough to decide the next move.
Read-only, non-gated, cheap to poll (images are the only cost — cap via maxWidth).
"""
import base64
import os
import sys
IS_WINDOWS = sys.platform == "win32"
def handle_state(kicad_info: dict, args: dict) -> dict:
"""Args (all optional):
includeImages: bool (default true) — inline base64 PNGs in shots[]
maxWidth: int (default 1100) — downscale cap for inline images
"""
if not IS_WINDOWS:
return {"success": False, "error": f"kicad_state not implemented on {sys.platform!r}"}
from handlers.kicad_ui import _find_all_kicad_windows, _screenshot_hwnd
include_images = args.get("includeImages", True)
max_width = int(args.get("maxWidth") or 1100)
windows = _find_all_kicad_windows()
running = len(windows) > 0
shots = []
if include_images:
for w in windows:
entry = {"title": w.get("title"), "hwnd": w.get("hwnd"),
"className": w.get("className")}
try:
shot = _screenshot_hwnd(w["hwnd"], label="state")
path = shot.get("fullPath") or shot.get("path")
if shot.get("success") and path and path.lower().endswith(".png"):
if max_width:
try:
from PIL import Image
img = Image.open(path)
if img.width > max_width:
r = max_width / img.width
img = img.resize((max_width, int(img.height * r)))
img.save(path, "PNG", optimize=True)
entry["width"], entry["height"] = img.width, img.height
except Exception:
pass
with open(path, "rb") as f:
entry["image"] = base64.b64encode(f.read()).decode()
entry["path"] = path
else:
entry["error"] = shot.get("error") or "capture failed (non-png fallback?)"
except Exception as e: # pylint: disable=broad-except
entry["error"] = f"{type(e).__name__}: {e}"
shots.append(entry)
editors: dict = {}
try:
from handlers.bridge_client import handle_open_editors
editors = handle_open_editors(kicad_info, {})
except Exception as e: # pylint: disable=broad-except
editors = {"success": False, "error": str(e)}
modal = []
try:
from handlers.close_windows import _win_scan_kicad_dialogs
modal = [{"hwnd": d.get("hwnd"), "title": d.get("title"), "body": d.get("body")}
for d in _win_scan_kicad_dialogs(kicad_info) if d.get("isDialog")]
except Exception:
pass
focus_events = []
user_idle_s = None
try:
from handlers import win_focus
focus_events = win_focus.get_focus_events()[-40:]
user_idle_s = round(win_focus._seconds_since_user_input(), 1)
except Exception:
pass
return {
"success": True,
"running": running,
"focusEvents": focus_events,
# PRESENCE GATE (John, 2026-08-17: a test suite foregrounding windows
# while he worked "MASSIVELY DISRUPTED" him). Automation that spawns
# windows MUST check this first and defer while the user is active:
# < 600 means a human touched this machine in the last 10 minutes.
"userIdleSeconds": user_idle_s,
"focusDebug": (lambda: __import__("handlers.win_focus", fromlist=["x"]).get_focus_debug())() if True else None,
"activeVersion": kicad_info.get("version"),
"windows": [{k: w.get(k) for k in ("hwnd", "title", "className", "rect")}
for w in windows],
"shots": shots,
"editors": {
"kicadRunning": editors.get("kicadRunning"),
"list": editors.get("editors"),
"summary": editors.get("summary"),
},
"modalDialogs": modal,
"_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 "
"an action to confirm it landed. includeImages:false for a cheap logical poll."),
}