← Commit history

v0.9.120: per-step screenshot evidence streamed to the demo caller (#25 web-control contract) - stepShot{step,title,image<=640px} rides the first progress frame after each beat; final result.state.shots unchanged

John Lauer ·f48ffef9eb ·1mo ago ·parent 355b773
6 files changed +52−11
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.119+0.9.120
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.119",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.119/adom-bridge-kicad-v0.9.119.zip",-  "sha256": "cb4640fe59796e8355e56a175c50c31fe568596e9b6d97e8c96710f3caf58579",-  "size": 411171,+  "version": "0.9.120",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.120/adom-bridge-kicad-v0.9.120.zip",+  "sha256": "4c400e0367c333b3af41aab794eb2bc223d73cd9567d892446c42a55c8e87733",+  "size": 411776,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-16T14:27:29Z",+  "released_at": "2026-08-16T17:35:52Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.120.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.119",+  "version": "0.9.120",   "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/demo.py+42−1
@@ -441,6 +441,30 @@ def _job_update(**kw):         _JOB.update(kw)  +def _beat_shot(kicad_info: dict, step_label: str, window_title: str) -> dict | None:+    """The per-step EVIDENCE frame for the web-control page (#25, John 2026-08-16:+    the page's own guess-snaps race the tour and shoot the wrong window — only+    the bridge knows the moment a step completes and which window is its+    surface). Returns {step, title, image(base64 PNG, <=640px wide)} or None."""+    if not window_title:+        return None+    try:+        import base64 as _b64+        from handlers.kicad_ui import _find_all_kicad_windows, _screenshot_hwnd+        for w in _find_all_kicad_windows():+            if window_title.lower() in (w.get("title") or "").lower():+                shot = _screenshot_hwnd(w["hwnd"], label="stepshot", max_dim=640)+                p = shot.get("fullPath") or shot.get("savedTo")+                if shot.get("success") and p and p.lower().endswith(".png"):+                    with open(p, "rb") as f:+                        img = _b64.b64encode(f.read()).decode()+                    return {"step": step_label, "title": w.get("title"), "image": img}+                break+    except Exception:+        pass+    return None++ def _run_demo_background(kicad_info: dict) -> None:     """Thread body: the six beats with live progress accounting."""     import time as _t@@ -467,6 +491,10 @@ def _run_demo_background(kicad_info: dict) -> None:                     elapsedSec=round(_t.monotonic() - t0))         prep["warm"] = _warm_kicad(kicad_info, prep)         done_weight += _BEAT_EST["prepare"]+        shot = _beat_shot(kicad_info, "Preparing the sample project", "KiCad")+        if shot:+            with _JOB_LOCK:+                _JOB.setdefault("pendingShots", []).append(shot)         results = []         for i, s_ in enumerate(STEPS):             _job_update(step=s_, stepLabel=f"Opening {s_.replace('_', ' ')}",@@ -480,6 +508,11 @@ def _run_demo_background(kicad_info: dict) -> None:                 r["retried"] = True             r["step"] = s_             results.append(r)+            shot = _beat_shot(kicad_info, f"Opening {s_.replace('_', ' ')}",+                              r.get("title") or r.get("window") or "")+            if shot:+                with _JOB_LOCK:+                    _JOB.setdefault("pendingShots", []).append(shot)             done_weight += _BEAT_EST.get(s_, 10)         # final payload = the kicad_state shape (#22): every window, base64         state = {}@@ -517,6 +550,12 @@ def handle_demo(kicad_info: dict, args: dict) -> dict:         out = {"success": True, **{k: snap.get(k) for k in                ("active", "done", "step", "stepLabel", "beatIndex", "beatCount",                 "percent", "elapsedSec", "estimatedSec", "error")}}+        # Per-step evidence (#25): one completed-step screenshot rides the FIRST+        # progress frame after that step lands — {step, title, image<=640px}.+        with _JOB_LOCK:+            pend = _JOB.get("pendingShots") or []+            if pend:+                out["stepShot"] = pend.pop(0)         if snap.get("done") and snap.get("result") is not None:             out["result"] = snap["result"]             out["_hint"] = ("Demo complete: result.state.shots[] is every KiCad window as base64 "@@ -524,7 +563,9 @@ def handle_demo(kicad_info: dict, args: dict) -> dict:                             "kicad_close {\"force\":true} when the user is done looking.")         else:             out["_hint"] = ("Long-running job — render a real progress bar from percent/"-                            "elapsedSec/estimatedSec and show stepLabel. Poll every 2-3s.")+                            "elapsedSec/estimatedSec and show stepLabel. Poll every 2-3s. "+                            "stepShot (when present) is the completed step's own window as "+                            "base64 PNG — pin it as that step's evidence.")         return out      # ---- background start ----
handlers/kicad_ui.py+3−3
@@ -265,7 +265,7 @@ def _write_bmp(path: str, width: int, height: int, pixel_data: bytes):             f.write(bytes(row_24))  -def _screenshot_hwnd(hwnd: int, label: str = "") -> dict:+def _screenshot_hwnd(hwnd: int, label: str = "", max_dim: int = 1568) -> dict:     """Capture a window by HWND using PrintWindow. Returns {success, savedTo, sizeKB}."""     rect = _get_window_rect(hwnd)     width = rect["width"]@@ -327,7 +327,7 @@ def _screenshot_hwnd(hwnd: int, label: str = "") -> dict:         from PIL import Image         img = Image.frombytes("RGBA", (width, height), pixel_buf.raw, "raw", "BGRA")         img = img.transpose(Image.FLIP_TOP_BOTTOM)-        MAX_DIM = 1568+        MAX_DIM = max_dim         if max(width, height) > MAX_DIM:             ratio = MAX_DIM / max(width, height)             img = img.resize((int(width * ratio), int(height * ratio)), Image.LANCZOS)@@ -340,7 +340,7 @@ def _screenshot_hwnd(hwnd: int, label: str = "") -> dict:         try:             from handlers.png_stdlib import write_png_from_bgra             path = os.path.join(SCREENSHOT_DIR, f"kicad{suffix}-{timestamp}.png")-            write_png_from_bgra(path, width, height, pixel_buf.raw, max_dim=1568)+            write_png_from_bgra(path, width, height, pixel_buf.raw, max_dim=max_dim)         except Exception:             path = os.path.join(SCREENSHOT_DIR, f"kicad{suffix}-{timestamp}.bmp")             _write_bmp(path, width, height, pixel_buf.raw)