← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.194\ No newline at end of file+0.9.195\ 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.194",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.194/adom-bridge-kicad-v0.9.194.zip",-  "sha256": "eac65abb0e01fa6fca01404fed453ed9d0e1c330e39351dda33de587c666e098",-  "size": 463369,+  "version": "0.9.195",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.195/adom-bridge-kicad-v0.9.195.zip",+  "sha256": "60cc891dbb286f577796cf6a6e847670d698fcdf111267d630503fd00fda04a8",+  "size": 466617,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-19T23:10:00Z",+  "released_at": "2026-08-20T00:05:00Z",   "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.194",+  "version": "0.9.195",   "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/open_symbol_editor.py+21
@@ -38,6 +38,21 @@ SW_RESTORE = 9 GW_OWNER = 4  ++def _step(label: str, percent=None) -> None:+    """Report a SUB-STEP of the running show/open phase.++    Wiki issue #35 ask 2 (Colby): "sub-phase timings splitting launch vs+    library-scan vs navigate". One opaque 24-second bar cannot tell a slow+    library index from a wedged window; these labels can. Best-effort by+    design - progress reporting must never be able to fail a real verb."""+    try:+        from handlers import progress as _pg+        for ph in ("show_symbol", "cold_start", "warm_start", "show_library"):+            _pg.step(ph, label, percent)+    except Exception:+        pass+ def _parse_editor_title(title: str) -> tuple[str, str]:     """Split a Symbol Editor title into (library, symbol). @@ -497,6 +512,8 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:     sym_hwnd = _find_symbol_editor()     pathways_tried: list[str] = []     rescanned = False+    _step("Symbol Editor already open" if sym_hwnd+          else "opening the Symbol Editor (KiCad indexes every symbol library here)", 10)      # 0.9.192: rescan:true is the remedy for errorCode library_cache_stale. An     # editor only indexes its libraries at open, so the ONLY way to make it see a@@ -675,6 +692,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:             }          # Wait for Symbol Editor to fully initialize+        _step("Symbol Editor is up, letting it settle", 55)         time.sleep(1.5)      # --- Phase 2: Navigate to specific symbol (if requested) ---@@ -718,6 +736,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:         }      _verify = (symbol_name or search_text).lower()+    _step(f"locating '{search_text}' via the reverse-bridge plugin", 70)     _pl = _plugin_background_load(search_text, library_name, wait_plugin_s=4.0)     if str(_pl.get("loaded")) in ("True", "unverified"):         time.sleep(0.6)@@ -733,6 +752,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:     # v0.9.83: plugin-free win32 tier — filter via WM_SETTEXT + Enter, background.     # This is the workhorse while eeschema's Python is cold (issue #21).     _filter = f"{library_name} {symbol_name}".strip() if library_name else search_text+    _step(f"navigating the library tree to '{search_text}'", 80)     if _background_navigate_win32(sym_hwnd, _filter, _verify, library_name):         return {             "success": True,@@ -753,6 +773,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:     # own retry loop out through the API. Do it here, once, before failing.     # The first attempt is what warms the tree, so this is a fresh-tree retry,     # not a duplicate of a hopeless call.+    _step(f"first pass did not confirm; retrying now the tree is warm", 90)     if _background_navigate_win32(sym_hwnd, _filter, _verify, library_name):         return {             "success": True,
handlers/progress.py+68
@@ -189,3 +189,71 @@ def finish(phase: str, started_at: float, start_kind: str = "warm", ok: bool = T         b["stepLabel"] = step_label         b.pop("measured", None)     return b+++# ── LIVE, IN-FLIGHT progress (0.9.195) ──────────────────────────────────────+# John's contract to the team (2026-08-19): "ongoing streaming feedback so+# progress bars could be shown in the front-end" and "show live streaming of the+# progress in your text status line". Until now `block()` was only ever built+# ONCE, on the terminal frame - which is a receipt, not a progress bar. A caller+# watching a 24s cold footprint open saw nothing at all until it finished.+#+# So a running verb publishes its current frame here, and anything that can+# poll a verb (ab's `watch` wrapper, the dashboard's SSE hub, a status line)+# can read it while the work is still happening. In-memory on purpose: this is+# liveness, and a stale frame surviving a bridge restart would be a lie.+_live: dict = {}+_live_seq = 0+++def begin(phase: str, start_kind: str = "warm", step_label: str = "starting") -> None:+    """Mark a phase as in flight. Safe to call for phases nobody watches."""+    global _live_seq+    with _lock:+        _live_seq += 1+        _live[phase] = {"startedAt": time.time(), "startKind": start_kind,+                        "stepLabel": step_label, "percent": None,+                        "blockedBy": None, "seq": _live_seq}+++def step(phase: str, step_label: str, percent=None, blocked_by: str = None) -> None:+    """Advance the visible sub-step of an in-flight phase.++    This is what turns one opaque 24-second bar into "launching KiCad" ->+    "indexing symbol libraries" -> "navigating to IN-S42ATR", which is the+    sub-phase breakdown Colby asked for on wiki issue #35."""+    with _lock:+        cur = _live.get(phase)+        if not cur:+            return+        cur["stepLabel"] = step_label+        cur["blockedBy"] = blocked_by+        if percent is not None:+            # never let a bar go backwards: a bar that retreats reads as broken+            cur["percent"] = max(percent, cur.get("percent") or 0)+++def end(phase: str) -> None:+    with _lock:+        _live.pop(phase, None)+++def live() -> list:+    """Every phase currently in flight, as full progress blocks.++    Percent/ETA are recomputed at READ time from this machine's own history, so+    a poller gets a bar that keeps moving even if the verb has not reported a+    new sub-step since its last one."""+    out = []+    with _lock:+        snapshot = list(_live.items())+    for phase, cur in snapshot:+        try:+            b = block(phase, cur["stepLabel"], cur["startedAt"],+                      percent=cur.get("percent"), start_kind=cur.get("startKind", "warm"),+                      blocked_by=cur.get("blockedBy"))+            b["seq"] = cur.get("seq")+            out.append(b)+        except Exception:+            pass+    return out
handlers/show.py+75−7
@@ -76,8 +76,10 @@ def handle_show_symbol(kicad_info: dict, args: dict) -> dict:         "symbol editor", a.get("symbolName") or "",         a.get("libraryName") or a.get("library") or "")     if already:+        _ev = _capture_evidence(already, "symbol-editor-reused")         return {"success": True, "surface": "symbol", "reused": True,                 "window": already.get("title"),+                **({"evidence": _ev} if _ev else {}),                 "output": f"Symbol Editor is already showing {already.get('title')} - "                           f"reused it (no relaunch, no library rescan).",                 "_hint": "Returned the existing editor instead of running the cold path. "@@ -90,24 +92,79 @@ def handle_show_symbol(kicad_info: dict, args: dict) -> dict:     return r  -def _find_by_title(*needles) -> bool:-    """Is a window whose title contains ALL needles on screen right now?"""+def _find_by_title(*needles):+    """The window whose title contains ALL needles, or None.++    0.9.195: this used to return a bare bool. The window DICT is what lets+    _verify_window capture the thing it just verified, so evidence comes from+    the same lookup that decided the verb succeeded - not a second, possibly+    different, window found later."""     try:         from handlers.kicad_ui import _find_all_kicad_windows         for w in _find_all_kicad_windows() or []:             t = (w.get("title") or "").lower()             if all(n in t for n in needles):-                return True+                return w     except Exception:         pass-    return False+    return None+++def _window_title(hwnd: int) -> str:+    try:+        from handlers.kicad_ui import _get_window_title+        return _get_window_title(hwnd) or ""+    except Exception:+        return ""+++def _capture_evidence(win, label: str) -> dict | None:+    """Screenshot the window a show_* verb just opened, in the BACKGROUND.++    John's contract to the team (2026-08-19): "you should always get a+    screenshot of the kicad or fusion window after the item opens in it so you+    can show that in a thumbnail". Before this, the verbs only TOLD the caller+    to go screenshot something - which is how a caller ends up screenshotting+    the project manager and captioning it as a footprint (0.9.172). Capturing+    here, from the verified hwnd, means the evidence and the success verdict+    come from the same window and cannot disagree.++    PrintWindow captures an occluded window, so this never foregrounds."""+    try:+        # The finders are not uniform: show.py's own return a window dict, while+        # _find_footprint_editor / _find_pcb_editor return a bare hwnd. Accept+        # both, or the footprint and board surfaces - the ones most likely to be+        # mistaken for the project window - would be the only ones with no proof.+        if isinstance(win, dict):+            hwnd, title = win.get("hwnd"), win.get("title")+        else:+            hwnd, title = win, _window_title(win)+        if not hwnd:+            return None+        from handlers.kicad_ui import _screenshot_hwnd+        shot = _screenshot_hwnd(hwnd, label=label)+        if not shot or not shot.get("success"):+            return None+        return {+            "window": title,+            "hwnd": hwnd,+            "screenshotPath": shot.get("fullPath") or shot.get("savedTo"),+            "sizeKB": shot.get("sizeKB"),+            "capturedInBackground": True,+            "_hint": ("Proof this surface is really on screen, captured from the same "+                      "window that satisfied the verb. ab auto-pulls fullPath into the "+                      "caller's container, so it can be read or shown as a thumbnail "+                      "without a separate pull_file."),+        }+    except Exception:+        return None  -def _find_symbol_editor() -> bool:+def _find_symbol_editor():     return _find_by_title("symbol editor")  -def _find_3d_viewer() -> bool:+def _find_3d_viewer():     return _find_by_title("3d viewer")  @@ -122,7 +179,16 @@ def _verify_window(result: dict, finder, label: str, waits: int = 12) -> dict:     if not isinstance(result, dict) or not result.get("success"):         return result     for _ in range(waits):-        if finder():+        win = finder()+        if win:+            # Evidence is part of the contract, not a nicety - but a failed+            # capture must never turn a real success into a failure, so it is+            # reported as absent rather than raised.+            ev = _capture_evidence(win, label)+            if ev:+                result["evidence"] = ev+            else:+                result["evidenceUnavailable"] = "window verified but the capture failed"             return result         time.sleep(0.5)     return {**result, "success": False,@@ -209,8 +275,10 @@ def handle_show_footprint(kicad_info: dict, args: dict) -> dict:     already_fp = _open_editor_showing("footprint editor", a2.get("footprintName") or "",                                       a2.get("library") or a2.get("libraryName") or "")     if already_fp:+        _ev = _capture_evidence(already_fp, "footprint-editor-reused")         return {"success": True, "surface": "footprint", "reused": True,                 "window": already_fp.get("title"),+                **({"evidence": _ev} if _ev else {}),                 "output": f"Footprint Editor is already showing {already_fp.get('title')} - "                           f"reused it (no relaunch, no library rescan).",                 "_hint": "Returned the existing editor instead of running the cold path "
server.py+46−1
@@ -540,6 +540,36 @@ def _detect_running_kicad_instances() -> list[dict]:         return []  ++def _handle_progress(kicad_info: dict, args: dict) -> dict:+    """kicad_progress — what is this bridge doing RIGHT NOW, as progress blocks.++    0.9.195. The measured-progress contract (#32) only ever produced a terminal+    frame, which is a receipt: a caller watching a 24s cold footprint open saw+    nothing until it was over. This verb is the live half, so a front-end can+    draw a real bar and a text status line can stream sub-steps while the work+    is still happening. Read-only, cheap, and safe to poll every second."""+    try:+        from handlers import progress as _pg+        frames = _pg.live()+    except Exception as e:+        return {"success": False, "error": f"progress unavailable: {e}"[:140]}+    return {+        "success": True,+        "busy": bool(frames),+        "active": frames,+        "history": _pg.history(),+        "_hint": ("Poll this while a long verb runs (1s is fine). `active` holds one "+                  "progress block per in-flight phase: stepLabel is the human line for "+                  "a status bar, percent/etaSec drive the bar, and confidence says "+                  "whether the estimate is measured on THIS machine or still a default. "+                  "An empty `active` with busy:false means the bridge is idle - it does "+                  "NOT mean a verb failed. blockedBy names a human-controlled wait (a "+                  "modal dialog), where a bar should stop animating and say so."),+        "_next": ["kicad_status — bridge/plugin liveness",+                  "kicad_describe — the progressContract this conforms to"],+    }+ COMMAND_HANDLERS = {     "open_board": handle_open_board,     "open_schematic": handle_open_schematic,@@ -587,6 +617,7 @@ COMMAND_HANDLERS = {     "uninstall": handle_uninstall,     "install_plugin": plugin_install.handle_install_plugin,     "plugin_diagnose": handle_plugin_diagnose,+    "progress": _handle_progress,     # v1.7.12+: headless lint pre-checks via kicad-cli     "run_erc": handle_run_erc,     "lint_board": handle_lint_board,@@ -776,7 +807,7 @@ def dispatch_command(command: str, args: dict) -> dict:     _NO_GUI = {"run_drc", "run_erc", "lint_board", "lint_schematic", "lint_library",                "format_upgrade", "export_gerber", "export_gerbers", "export_pdf",                "export_svg", "export_step", "export_bom_csv", "adom_library_status",-               "list_versions", "install_library"}+               "list_versions", "install_library", "progress"}     # Only verbs that actually SPAWN a window get the ~3s settle-poll (they can     # trigger an async dialog). Read-only reporters (open_editors, screenshot_all,     # window_info) don't spawn windows → one cheap scan, no needless 3s poll.@@ -824,6 +855,10 @@ def dispatch_command(command: str, args: dict) -> dict:             _prog_kind = _pg.start_kind_for_kicad()             if _prog_phase == "cold_start" and _prog_kind == "warm":                 _prog_phase = "warm_start"   # cold vs warm differ by 10-50x+            # 0.9.195: publish a LIVE frame for the whole duration, not just a+            # receipt at the end. This is what a front-end progress bar and a+            # status line read while the verb is still running.+            _pg.begin(_prog_phase, _prog_kind, step_label=f"{command} starting")         except Exception:             pass @@ -852,6 +887,16 @@ def dispatch_command(command: str, args: dict) -> dict:                 _wfg.end_gui_verb()         except Exception:             pass+        # Retire the live progress frame HERE, in the one block that runs even+        # when the handler raises. Anywhere later is unreachable on the error+        # path, and a leaked frame shows a poller a bar that never completes for+        # a call that is long over - worse than showing no bar at all.+        try:+            if _prog_phase:+                from handlers import progress as _pg3+                _pg3.end(_prog_phase)+        except Exception:+            pass     # Stamp the version actually used so callers can see which install ran, and     # OUR OWN version — 0.9.159: BRIDGE_VERSION only ever reached /health, so no     # caller (the dashboard included) could ask which bridge code is running, and