← Commit history

v0.9.65: align kicad_demo to the fusion_demo contract (stage/done/narrate/screenshots/steps), capture its own screenshots, never end on an error screen

John Lauer ·b44e91c39d ·3mo ago ·parent 3d3e314
6 files changed +101−35
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.64+0.9.65
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.64",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.64/adom-bridge-kicad-v0.9.64.zip",-  "sha256": "a73eea1edd4a215a77a9badc396f639a724f1fa3e12e88a8714dc54bee7d8b66",-  "size": 354935,+  "version": "0.9.65",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.65/adom-bridge-kicad-v0.9.65.zip",+  "sha256": "0e8377d14bfb77ae628424ede412553477f0976240e4cfeecc727b86ad91312f",+  "size": 356208,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-20T11:39:29Z",+  "released_at": "2026-07-20T12:31:37Z",   "hero": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.65.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.64",+  "version": "0.9.65",   "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-desktop",   "author": "Adom Inc.",
handlers/demo.py+93−27
@@ -33,6 +33,7 @@ from .open_symbol_editor import handle_open_symbol_editor from .open_footprint_editor import handle_open_footprint_editor, handle_open_3d_viewer from .open_files import handle_open_board, handle_open_schematic from .upgrade import handle_upgrade+from .kicad_ui import handle_screenshot_all from kicad_detect import detect_kicad  DEMO_LIB = "Adom"@@ -41,6 +42,32 @@ R_SYM, R_FP = "AdomDemo_R", "AdomDemo_R0805"  STEPS = ["symbol", "footprint", "footprint3d", "schematic", "board", "board3d"] +# Screenshot labels. `schematic` / `board_2d` / `board_3d` deliberately match the+# labels fusion_demo returns, so an AI driving BOTH bridges (HD's installer does)+# sees one vocabulary instead of two.+STEP_LABEL = {"symbol": "symbol", "footprint": "footprint", "footprint3d": "part_3d",+              "schematic": "schematic", "board": "board_2d", "board3d": "board_3d"}+++def _capture(step: str, window_title: str) -> dict | None:+    """Capture the window this beat just opened and return {label, path}.++    fusion_demo hands back `screenshots:[{label,path}]` rather than making the+    caller hunt for windows; matching that means the AI shows the tour the same+    way for both bridges.+    """+    try:+        shots = handle_screenshot_all({}, {}) or {}+        for sh in (shots.get("data") or {}).get("screenshots") or shots.get("screenshots") or []:+            if window_title.lower() in (sh.get("title") or "").lower():+                p = sh.get("fullPath") or sh.get("localPath") or sh.get("savedTo")+                if p:+                    return {"label": STEP_LABEL.get(step, step), "path": p,+                            "title": sh.get("title")}+    except Exception:+        pass+    return None+  # ---------------------------------------------------------------- 3D models -- def _model_path(kicad_info: dict, lib: str, name: str):@@ -368,20 +395,29 @@ def handle_demo(kicad_info: dict, args: dict) -> dict:         if not args.get("installKiCad"):             return {                 "success": False,+                "stage": "not_installed",+                "done": False,+                "narrate": (+                    "You don't have KiCad yet — want me to install it for you? It's the free, "+                    "fully open-source EDA suite: no trial clock, no personal-use tier, no feature "+                    "limits. Everything in this demo works forever, on any machine you own. "+                    "It's a large download, so give it a few minutes."),+                "screenshots": [],                 "error": "KiCad is not installed",                 "data": {-                    "offer": "I can install KiCad for you right now — it takes a few minutes.",+                    "offer": "I can install KiCad for you right now.",                     "acceptWith": "kicad_demo {\"installKiCad\": true}",                     "installVerb": "kicad_upgrade",                     "steps": STEPS,                 },                 "_hint": (-                    "ASK THE USER: \"You don't have KiCad yet — want me to install it for you? \" "-                    "Never hand them a download link or ask them to click through an installer. "-                    "On yes, call kicad_demo {\"installKiCad\": true} — it installs the official "-                    "build silently (per-user, no UAC) and then runs the whole tour in the same "-                    "call. Watching the AI install their EDA tool is itself the best demo beat, "-                    "so narrate it. Takes a few minutes on a fresh machine (large download)."+                    "ASK, then do it for them — never hand a first-time user a download page or an "+                    "installer to click through. Speak `narrate`, and on yes call "+                    "kicad_demo {\"installKiCad\": true}: it installs the official build silently "+                    "(per-user, no UAC) and then runs the tour in the SAME call. Watching the AI "+                    "install their EDA tool is itself the best demo beat, so narrate it rather than "+                    "going quiet for several minutes. Unlike Fusion there is no trial or tier "+                    "caveat to disclose — KiCad is free and unrestricted, which is worth saying."                 ),             }         # They said yes — install it, then re-detect and carry on into the tour.@@ -436,35 +472,65 @@ def handle_demo(kicad_info: dict, args: dict) -> dict:     last = steps[-1]     nxt = STEPS[STEPS.index(last) + 1] if last in STEPS and STEPS.index(last) + 1 < len(STEPS) else None -    done = [r["step"] for r in results if r.get("ok")]+    # Capture what each beat put on screen, so the caller can SHOW the tour+    # instead of hunting for windows (same contract as fusion_demo).+    shots = []+    for r in results:+        if r.get("ok") and r.get("window"):+            c = _capture(r["step"], r["window"])+            if c:+                shots.append(c)+                r["screenshot"] = c["path"]++    # Narration. A failed beat must STILL say something useful and hand the user+    # forward — "never end a demo on an error screen" (fusion-demo, rule).+    lines = []+    for r in results:+        if r.get("ok"):+            lines.append(r.get("say", ""))+        else:+            lines.append(+                f"{r.get('title', r['step'])} didn't open on this machine, so I'll skip it rather "+                f"than leave you staring at nothing — the rest of the tour still works.")+    if installed_now:+        lines.insert(0, "I installed KiCad for you first — that's the bridge doing real work on "+                        "your machine, not a video.")+    narrate = " ".join(x for x in lines if x)+     return {-        "success": not failed,-        "output": (f"Demo tour: {len(done)}/{len(steps)} steps opened "-                   f"({', '.join(done) or 'none'})" + (f"; failed: {', '.join(failed)}" if failed else "")),+        "success": True,   # a skipped beat is not a failed demo; `failedSteps` carries the detail+        "stage": last,+        "done": nxt is None,+        "narrate": narrate,+        "screenshots": shots,+        "steps": [{"step": r["step"], "title": r.get("title"), "ok": r.get("ok"),+                   "pointOut": r.get("pointOut"), "window": r.get("window"),+                   "retried": r.get("retried", False)} for r in results],+        "output": (f"Demo {last}: {len([r for r in results if r.get('ok')])}/{len(results)} opened"+                   + (f"; skipped {', '.join(failed)}" if failed else "")),         "data": {-            "steps": results,+            "nextCall": (f'kicad_demo {{"step": "{nxt}"}}' if nxt else None),+            "remainingSteps": STEPS[STEPS.index(last) + 1:] if last in STEPS else [],+            "failedSteps": failed,+            "installedKiCadJustNow": installed_now,+            "warmedUp": {k: (prep.get("warm") or {}).get(k) for k in ("warmed", "eeschema", "pcbnew")},             "projectDir": prep.get("projectDir"),             "installedParts": prep.get("installed"),             "models3d": prep.get("models3d"),             "warnings": prep.get("warnings"),-            "installedKiCadJustNow": installed_now,-            "warmedUp": {k: (prep.get("warm") or {}).get(k) for k in ("warmed", "eeschema", "pcbnew")},-            "nextCall": (f'kicad_demo {{"step": "{nxt}"}}' if nxt else None),-            "remainingSteps": STEPS[STEPS.index(last) + 1:] if last in STEPS else [],-            "screenshotVerb": "kicad_screenshot_all",             "cleanup": "kicad_close {\"force\":true}",         },         "_hint": (             ("You just installed KiCad for them — say so, it lands well. " if installed_now else "")-            + "This is a NARRATED tour, ONE BEAT PER CALL. Speak data.steps[0].say, screenshot the "-            "window named in .window, mention .pointOut, then call data.nextCall for the next "-            "beat. Each entry carries `title`, `say` (speak it, "-            "don't read JSON), `pointOut` (what's actually on screen) and `window` (match that "-            "title in kicad_screenshot_all to show it). Best flow: call kicad_demo with one "-            "`step` at a time (" + " -> ".join(STEPS) + "), say the line, screenshot that window, "-            "then move on — a single bulk call opens all six at once and the user can't follow. "-            "Windows open in the BACKGROUND by design; screenshot them rather than stealing focus. "-            "Everything is generated by the bridge (no shipped sample files) and installs into the "-            "user's real KiCad. Tear down with kicad_close {\"force\":true}."+            + "STAGED + RESUMABLE, same contract as fusion_demo: speak `narrate` (it's written for a "+            "newcomer, not an engineer), SHOW every entry in `screenshots` ({label,path} — already "+            "captured for you), then call `data.nextCall` and repeat until `done` is true. Mention "+            "`steps[].pointOut` while the image is up. Everything is generated by the bridge (no "+            "shipped sample files) and installs into the user's real KiCad. Windows open in the "+            "BACKGROUND and all input is window-targeted, so the user can keep working throughout — "+            "say that, it's a genuine differentiator. If a beat was skipped, don't dwell: say what "+            "you tried and move on. Finish by offering next steps in THEIR words ('export the "+            "Gerbers', 'run DRC', 'make a part with my number etched on it') — never verb names. "+            "Tear down with kicad_close {\"force\":true}."         ),     }
server.py+1−1
@@ -326,7 +326,7 @@ _VERB_CATALOG = {         "hint": "Absolute board path + output; exports a STEP assembly via kicad-cli.",         "related": ["kicad_open_3d_viewer", "kicad_export_gerber"], "pitfalls": ["only components with 3D models attached appear in the STEP"]},     "demo": {"summary": "Run the bridge's showcase tour: symbol -> footprint -> 3D part -> schematic -> 2D board -> 3D board.", "long": True,-        "hint": "The demo verb. Generates its own parts + project (ships no sample files), installs them into the user's real KiCad, and opens each view. Every step returns `say` (speak it), `pointOut` (what's on screen) and `window` (match in kicad_screenshot_all). Pass {\"step\":\"symbol\"} to pace it one beat at a time; no args runs all six.",+        "hint": "The demo verb — STAGED + RESUMABLE, same contract as fusion_demo (stage/done/narrate/screenshots/steps). Speak narrate, show screenshots[{label,path}] (captured for you), call data.nextCall until done:true. Generates its own parts + project (ships no sample files), installs them into the user's real KiCad, and opens each view. Every step returns `say` (speak it), `pointOut` (what's on screen) and `window` (match in kicad_screenshot_all). Pass {\"step\":\"symbol\"} to pace it one beat at a time; no args runs all six.",         "related": ["kicad_screenshot_all", "kicad_close", "kicad_upgrade"], "pitfalls": ["if KiCad isn't installed it returns an actionable error — call kicad_upgrade, don't send the user to a download page", "windows open in the BACKGROUND; screenshot them instead of stealing focus", "a bulk run opens all six windows at once — step-by-step narrates better"]},     "export_molecule": {"summary": "One-shot molecule export pack: STEP + silk renders + sch/pro discovery for the Adom molecule pipeline.", "long": True,         "hint": "Absolute board path; exports STEP (+ silkscreen top/bottom PNGs when kicad-cli pcb render exists), finds the sibling .kicad_sch/.kicad_pro, and returns data.nextSteps — the exact container commands (step2glb → adom-molecule create) to finish.",