← Commit history

v0.9.63: kicad_demo — first-class showcase tour (symbol->footprint->3D->schematic->2D board->3D board), generates its own assets, offers to install KiCad if missing

John Lauer ·4310925370 ·3mo ago ·parent c6f4ac4
6 files changed +430−9
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.62+0.9.63
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.62",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.62/adom-bridge-kicad-v0.9.62.zip",-  "sha256": "0d12a8ce22f54893548c6030fbe7e25cd4d3bcfa94c5f20c12f093234540a282",-  "size": 343743,+  "version": "0.9.63",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.63/adom-bridge-kicad-v0.9.63.zip",+  "sha256": "c2c1249e3c49c9668ecc59397fdb88f87944406a7c595e554b42cf727b8d9264",+  "size": 351315,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-19T22:58:48Z",+  "released_at": "2026-07-20T11:09:33Z",   "hero": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.63.zipadded
⋯ 1 unchanged line ⋯
bridge.json+4−3
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.62",+  "version": "0.9.63",   "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.",@@ -61,7 +61,8 @@     "kicad_export_step",     "kicad_export_bom_csv",     "kicad_check_for_updates",-    "kicad_upgrade"+    "kicad_upgrade",+    "kicad_demo"   ],   "statusVerb": "kicad_status",   "timeouts": {@@ -147,4 +148,4 @@     "PowerShell"   ],   "releasedAt": "2026-06-26T16:00:00Z"-}+}\ No newline at end of file
handlers/demo.pyadded+414
@@ -0,0 +1,414 @@+"""kicad_demo — the bridge's first-class, self-contained showcase.++Adom Desktop demos this bridge during Hydrogen Desktop's install, so an AI with+no context must be able to run ONE verb and narrate a convincing tour of what+the bridge can do. Six steps, in the order a hardware person thinks:++    symbol -> footprint -> 3D of that part -> schematic -> 2D board -> 3D board++Design constraints that shaped this file:++* **Ships zero assets.** The runtime zip is auto-bundled into AD, so it carries+  no sample projects (see the kicad-bridge-publish skill). Every symbol,+  footprint, schematic and board below is GENERATED here as text, a few KB of+  code instead of ~500 KB of files.+* **Real 3D for free.** The generated footprints point at KiCad's OWN bundled+  `share/kicad/3dmodels/*.3dshapes` models, resolved from the detected install+  at runtime. Nothing to download, and the 3D viewer shows a real part. If the+  model is missing we still place the pads and say so rather than failing.+* **Narration, not just status.** Every step returns `say` (a line the AI can+  speak), `pointOut` (what is actually on screen worth mentioning) and the+  window title to screenshot. The AI reads verb OUTPUT, not our docs.+* **KiCad may not exist yet.** During HD install that is the likely case, so+  the not-installed path is a first-class, actionable answer — not an error.+"""++import base64+import os+from pathlib import Path++from .install_symbol import handle_install_symbol+from .install_footprint import handle_install_footprint+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_detect import detect_kicad++DEMO_LIB = "Adom"+IC_SYM, IC_FP = "AdomDemo_IC", "AdomDemo_SOIC8"+R_SYM, R_FP = "AdomDemo_R", "AdomDemo_R0805"++STEPS = ["symbol", "footprint", "footprint3d", "schematic", "board", "board3d"]+++# ---------------------------------------------------------------- 3D models --+def _model_path(kicad_info: dict, lib: str, name: str):+    """Absolute path to one of KiCad's OWN bundled 3D models, or None.++    Layout is stable across KiCad 6-10: <base>/share/kicad/3dmodels/<lib>/<name>.+    We probe .wrl then .step so the demo degrades gracefully instead of writing+    a dangling (model ...) that makes the 3D viewer look broken.+    """+    base = kicad_info.get("base_dir")+    if not base:+        return None+    root = Path(base) / "share" / "kicad" / "3dmodels" / lib+    for ext in (".wrl", ".step"):+        p = root / (name + ext)+        if p.exists():+            return str(p).replace("\\", "/")+    return None+++# ------------------------------------------------------------ asset writers --+def _sym_block(name: str) -> str:+    """The bare `(symbol "<name>" ...)` block.++    Returned on its own (not wrapped in a kicad_symbol_lib) because it is needed+    in TWO places: the .kicad_sym we install, and the schematic's `lib_symbols`.+    Slicing it back out of the wrapper is how you silently ship a schematic with+    no symbols in it — so it is generated once, here.+    """+    if name == IC_SYM:+        pins = []+        names = ["VDD", "IN1", "IN2", "GND", "OUT2", "OUT1", "EN", "NC"]+        for i in range(4):  # left 1-4, top-down+            pins.append(f'      (pin bidirectional line (at -12.7 {5.08 - i * 2.54:.2f} 0) (length 2.54) '+                        f'(name "{names[i]}") (number "{i + 1}"))')+        for i in range(4):  # right 5-8, bottom-up, like a real SOIC+            pins.append(f'      (pin bidirectional line (at 12.7 {-3.81 + i * 2.54:.2f} 180) (length 2.54) '+                        f'(name "{names[4 + i]}") (number "{i + 5}"))')+        return f'''  (symbol "{IC_SYM}"+    (pin_names (offset 1.016)) (exclude_from_sim no) (in_bom yes) (on_board yes)+    (property "Reference" "U" (at -10.16 9.53 0) (effects (font (size 1.27 1.27)) (justify left)))+    (property "Value" "{IC_SYM}" (at -10.16 -9.53 0) (effects (font (size 1.27 1.27)) (justify left)))+    (property "Footprint" "{DEMO_LIB}:{IC_FP}" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))+    (property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))+    (symbol "{IC_SYM}_0_1"+      (rectangle (start -10.16 7.62) (end 10.16 -7.62)+        (stroke (width 0.254) (type default)) (fill (type background)))+    )+    (symbol "{IC_SYM}_1_1"+{chr(10).join(pins)}+    )+  )+'''+    return f'''  (symbol "{R_SYM}"+    (pin_numbers (hide yes)) (pin_names (offset 0) (hide yes))+    (exclude_from_sim no) (in_bom yes) (on_board yes)+    (property "Reference" "R" (at 2.54 1.27 0) (effects (font (size 1.27 1.27)) (justify left)))+    (property "Value" "10k" (at 2.54 -1.27 0) (effects (font (size 1.27 1.27)) (justify left)))+    (property "Footprint" "{DEMO_LIB}:{R_FP}" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))+    (symbol "{R_SYM}_0_1"+      (rectangle (start -1.016 2.54) (end 1.016 -2.54)+        (stroke (width 0.254) (type default)) (fill (type none)))+    )+    (symbol "{R_SYM}_1_1"+      (pin passive line (at 0 3.81 270) (length 1.27) (name "~") (number "1"))+      (pin passive line (at 0 -3.81 90) (length 1.27) (name "~") (number "2"))+    )+  )+'''+++def _sym_lib(name: str) -> str:+    """Installable .kicad_sym = the wrapper + the block."""+    return ('(kicad_symbol_lib (version 20231120) (generator "adom-demo")\n'+            + _sym_block(name) + ')\n')+++def _fp_soic8(model: str | None, at: str = "") -> str:+    pads = []+    ys = [1.905, 0.635, -0.635, -1.905]+    for i, y in enumerate(ys):  # 1-4 left+        pads.append(f'  (pad "{i + 1}" smd roundrect (at -2.475 {y}) (size 1.95 0.6) '+                    f'(layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))')+    for i, y in enumerate(reversed(ys)):  # 5-8 right+        pads.append(f'  (pad "{i + 5}" smd roundrect (at 2.475 {y}) (size 1.95 0.6) '+                    f'(layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))')+    model_block = (f'  (model "{model}" (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)))\n'+                   if model else "")+    return f'''(footprint "{IC_FP}"+  (version 20240108) (generator "adom-demo") (layer "F.Cu"){at}+  (attr smd)+  (property "Reference" "U**" (at 0 -3.7 0) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.12))))+  (property "Value" "{IC_FP}" (at 0 3.7 0) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))+  (fp_line (start -1.95 -2.45) (end 1.95 -2.45) (stroke (width 0.12) (type solid)) (layer "F.SilkS"))+  (fp_line (start -1.95 2.45) (end 1.95 2.45) (stroke (width 0.12) (type solid)) (layer "F.SilkS"))+  (fp_circle (center -2.6 2.6) (end -2.45 2.6) (stroke (width 0.2) (type solid)) (fill none) (layer "F.SilkS"))+  (fp_rect (start -3.7 -2.7) (end 3.7 2.7) (stroke (width 0.05) (type solid)) (fill none) (layer "F.CrtYd"))+{chr(10).join(pads)}+{model_block})+'''+++def _fp_r0805(model: str | None, at: str = "") -> str:+    model_block = (f'  (model "{model}" (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)))\n'+                   if model else "")+    return f'''(footprint "{R_FP}"+  (version 20240108) (generator "adom-demo") (layer "F.Cu"){at}+  (attr smd)+  (property "Reference" "R**" (at 0 -1.6 0) (layer "F.SilkS") (effects (font (size 0.6 0.6) (thickness 0.1))))+  (property "Value" "10k" (at 0 1.6 0) (layer "F.Fab") (effects (font (size 0.6 0.6) (thickness 0.1))))+  (fp_rect (start -1.7 -0.9) (end 1.7 0.9) (stroke (width 0.05) (type solid)) (fill none) (layer "F.CrtYd"))+  (pad "1" smd roundrect (at -0.9125 0) (size 1.025 1.4) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))+  (pad "2" smd roundrect (at 0.9125 0) (size 1.025 1.4) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))+{model_block})+'''+++def _uuid(n: int) -> str:+    return f"00000000-0000-4000-8000-{n:012d}"+++def _schematic() -> str:+    """Modern (v7+) schematic. The `(instances ...)` block is MANDATORY — without+    it KiCad 8/9/10 opens the sheet but renders NO symbols (silently)."""+    sheet = _uuid(1)+    parts = [(IC_SYM, "U1", 101.6, 76.2), (R_SYM, "R1", 152.4, 63.5), (R_SYM, "R2", 152.4, 88.9)]+    # lib_symbols entries are keyed by the FULL lib id ("Adom:Name") — the instances+    # below reference exactly that, or KiCad renders an empty sheet.+    libs = (_sym_block(IC_SYM).replace(f'(symbol "{IC_SYM}"', f'(symbol "{DEMO_LIB}:{IC_SYM}"', 1)+            + _sym_block(R_SYM).replace(f'(symbol "{R_SYM}"', f'(symbol "{DEMO_LIB}:{R_SYM}"', 1))+    inst = ""+    for i, (sym, ref, x, y) in enumerate(parts):+        u = _uuid(200 + i)+        inst += f'''  (symbol (lib_id "{DEMO_LIB}:{sym}") (at {x} {y} 0) (unit 1)+    (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid "{u}")+    (property "Reference" "{ref}" (at {x + 12} {y - 2.54} 0) (effects (font (size 1.27 1.27)) (justify left)))+    (property "Value" "{sym}" (at {x + 12} {y + 2.54} 0) (effects (font (size 1.27 1.27)) (justify left)))+    (instances (project "adom-demo" (path "/{sheet}" (reference "{ref}") (unit 1))))+  )+'''+    return f'''(kicad_sch (version 20231120) (generator "eeschema") (generator_version "8.0")+  (uuid "{sheet}") (paper "A4")+  (title_block (title "Adom KiCad Bridge — Demo") (company "Adom"))+  (lib_symbols+{libs}  )+{inst}  (sheet_instances (path "/" (page "1")))+)+'''+++def _board(kicad_info: dict) -> str:+    soic = _model_path(kicad_info, "Package_SO.3dshapes", "SOIC-8_3.9x4.9mm_P1.27mm")+    r08 = _model_path(kicad_info, "Resistor_SMD.3dshapes", "R_0805_2012Metric")+    fps = (_fp_soic8(soic, "\n  (at 120 80)").replace("\n", "\n  ")+           + "\n  " + _fp_r0805(r08, "\n  (at 145 70)").replace("\n", "\n  ")+           + "\n  " + _fp_r0805(r08, "\n  (at 145 90)").replace("\n", "\n  "))+    edge = ""+    pts = [(100, 60), (170, 60), (170, 100), (100, 100), (100, 60)]+    for i in range(len(pts) - 1):+        (x1, y1), (x2, y2) = pts[i], pts[i + 1]+        edge += (f'  (gr_line (start {x1} {y1}) (end {x2} {y2}) '+                 f'(stroke (width 0.15) (type solid)) (layer "Edge.Cuts") (uuid "{_uuid(300 + i)}"))\n')+    return f'''(kicad_pcb (version 20240108) (generator "pcbnew") (generator_version "8.0")+  (general (thickness 1.6)) (paper "A4")+  (layers+    (0 "F.Cu" signal) (31 "B.Cu" signal)+    (36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen")+    (38 "B.Mask" user) (39 "F.Mask" user) (40 "B.Paste" user) (41 "F.Paste" user)+    (44 "Edge.Cuts" user) (45 "Margin" user) (49 "F.Fab" user) (50 "B.Fab" user)+    (46 "B.CrtYd" user "B.Courtyard") (47 "F.CrtYd" user "F.Courtyard")+  )+  (setup (pad_to_mask_clearance 0))+{edge}  {fps}+)+'''+++# ------------------------------------------------------------------ the verb --+def _demo_dir() -> Path:+    d = Path.home() / "Documents" / "adom-kicad-demo"+    d.mkdir(parents=True, exist_ok=True)+    return d+++def _b64(s: str) -> str:+    return base64.b64encode(s.encode("utf-8")).decode()+++def _prepare(kicad_info: dict) -> dict:+    """Install the two demo parts into the Adom library + write the project."""+    out = {"installed": [], "warnings": []}+    for fname, sym, content in ((f"{IC_SYM}.kicad_sym", IC_SYM, _sym_lib(IC_SYM)),+                                (f"{R_SYM}.kicad_sym", R_SYM, _sym_lib(R_SYM))):+        r = handle_install_symbol(kicad_info, {"fileName": fname, "symbolName": sym,+                                               "fileContent": _b64(content), "quietInstall": True})+        (out["installed"] if r.get("success") else out["warnings"]).append(+            sym if r.get("success") else f"{sym}: {r.get('error')}")++    soic = _model_path(kicad_info, "Package_SO.3dshapes", "SOIC-8_3.9x4.9mm_P1.27mm")+    r08 = _model_path(kicad_info, "Resistor_SMD.3dshapes", "R_0805_2012Metric")+    out["models3d"] = {"soic8": soic, "r0805": r08}+    if not (soic and r08):+        out["warnings"].append(+            "KiCad's bundled 3D models were not found — pads/board still render, "+            "component bodies will be missing in the 3D views.")+    for fname, fp, content in ((f"{IC_FP}.kicad_mod", IC_FP, _fp_soic8(soic)),+                               (f"{R_FP}.kicad_mod", R_FP, _fp_r0805(r08))):+        r = handle_install_footprint(kicad_info, {"fileName": fname, "footprintName": fp,+                                                  "fileContent": _b64(content), "quietInstall": True})+        (out["installed"] if r.get("success") else out["warnings"]).append(+            fp if r.get("success") else f"{fp}: {r.get('error')}")++    d = _demo_dir()+    (d / "adom-demo.kicad_sch").write_text(_schematic(), encoding="utf-8")+    (d / "adom-demo.kicad_pcb").write_text(_board(kicad_info), encoding="utf-8")+    (d / "adom-demo.kicad_pro").write_text('{"board":{},"meta":{"filename":"adom-demo.kicad_pro","version":1},"schematic":{},"sheets":[]}', encoding="utf-8")+    out["projectDir"] = str(d)+    out["schematic"] = str(d / "adom-demo.kicad_sch")+    out["board"] = str(d / "adom-demo.kicad_pcb")+    return out+++def _run_step(kicad_info: dict, step: str, prep: dict) -> dict:+    """Run one tour step. Returns narration-ready info, never raises."""+    has3d = bool((prep.get("models3d") or {}).get("soic8"))+    if step == "symbol":+        r = handle_open_symbol_editor(kicad_info, {"libraryName": DEMO_LIB, "symbolName": IC_SYM})+        return {"ok": bool(r.get("success")), "window": "Symbol Editor",+                "title": "1/6 · Schematic symbol",+                "say": (f"This is the {IC_SYM} symbol, in the user's own KiCad symbol editor. "+                        "The bridge installed it into their Adom library a second ago — no file "+                        "copying, no dialogs."),+                "pointOut": ["8 named pins (VDD, IN1/IN2, GND, OUT1/OUT2, EN, NC)",+                             "the body rectangle and pin numbering",+                             f"it is registered in the '{DEMO_LIB}' library, not a scratch file"],+                "raw": r}+    if step == "footprint":+        r = handle_open_footprint_editor(kicad_info, {"libraryName": DEMO_LIB, "footprintName": IC_FP})+        return {"ok": bool(r.get("success")), "window": "Footprint Editor",+                "title": "2/6 · The footprint for that symbol",+                "say": (f"Same part, now its land pattern: {IC_FP}. This is what actually gets "+                        "soldered — the symbol is the idea, the footprint is the copper."),+                "pointOut": ["8 SMD pads on 1.27 mm pitch", "silkscreen outline + the pin-1 dot",+                             "the courtyard rectangle (keep-out for the assembler)"],+                "raw": r}+    if step == "footprint3d":+        r = handle_open_3d_viewer(kicad_info, {"editor": "footprint"})+        return {"ok": bool(r.get("success")), "window": "3D Viewer",+                "title": "3/6 · That part in 3D",+                "say": ("And here is the same footprint in three dimensions" ++                        (", with the real SOIC-8 body from KiCad's own 3D model library."+                         if has3d else " — pads only; KiCad's bundled 3D models weren't found.")),+                "pointOut": (["the SOIC-8 package body sitting on its pads", "gold pad plating"]+                             if has3d else ["the bare pads (no bundled 3D model on this machine)"]),+                "raw": r}+    if step == "schematic":+        r = handle_open_schematic(kicad_info, {"filePath": prep["schematic"]})+        return {"ok": bool(r.get("success")), "window": "Schematic Editor",+                "title": "4/6 · A schematic using it",+                "say": ("Now a real schematic sheet: the demo IC as U1 with two 10k resistors, "+                        "R1 and R2. The bridge generated and opened this project itself."),+                "pointOut": ["U1 plus R1/R2, each properly annotated",+                             "the title block: 'Adom KiCad Bridge — Demo'"],+                "raw": r}+    if step == "board":+        r = handle_open_board(kicad_info, {"filePath": prep["board"]})+        return {"ok": bool(r.get("success")), "window": "PCB Editor",+                "title": "5/6 · The 2D board layout",+                "say": ("The PCB side: the same three parts placed on a 70 by 40 millimetre "+                        "board outline, copper, silkscreen and courtyards all real."),+                "pointOut": ["the SOIC-8 land pattern and two 0805 resistors",+                             "the Edge.Cuts board outline",+                             "'Pads 12' in the status bar — proof the footprints loaded"],+                "raw": r}+    if step == "board3d":+        r = handle_open_3d_viewer(kicad_info, {"editor": "pcb"})+        return {"ok": bool(r.get("success")), "window": "3D Viewer",+                "title": "6/6 · The whole board in 3D",+                "say": ("And the finished board in 3D — this is the payoff: from a symbol to a "+                        "rendered assembly, every step driven from the cloud on the user's own "+                        "machine."),+                "pointOut": (["all three component bodies on the green board"] if has3d+                             else ["the board and copper (component bodies need KiCad's 3D models)"])+                            + ["you can orbit it with the mouse — it is their real KiCad"],+                "raw": r}+    return {"ok": False, "error": f"unknown step '{step}'", "validSteps": STEPS}+++def handle_demo(kicad_info: dict, args: dict) -> dict:+    """Run the bridge's showcase tour (all six steps, or one at a time)."""+    # KiCad absent is the LIKELY case during an HD install demo. Never dead-end the+    # user with "install it yourself" — OFFER, and install it for them on the spot.+    if not kicad_info.get("installed"):+        if not args.get("installKiCad"):+            return {+                "success": False,+                "error": "KiCad is not installed",+                "data": {+                    "offer": "I can install KiCad for you right now — it takes a few minutes.",+                    "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)."+                ),+            }+        # They said yes — install it, then re-detect and carry on into the tour.+        up = handle_upgrade(kicad_info, {})+        fresh = detect_kicad() or {}+        if not fresh.get("installed"):+            return {+                "success": False,+                "error": "KiCad install did not complete",+                "data": {"upgrade": up},+                "_hint": ("The silent install ran but KiCad still isn't detected. Tell the user "+                          "plainly, then check kicad_upgrade {\"diagnoseOnly\":true} — the usual "+                          "causes are SmartScreen/AV blocking the downloaded installer or a "+                          "pending reboot. Don't pretend the demo succeeded."),+            }+        kicad_info = fresh+        installed_now = True+    else:+        installed_now = False++    step = args.get("step")+    if step and step not in STEPS:+        return {"success": False, "error": f"unknown step '{step}'", "data": {"validSteps": STEPS}}++    prep = _prepare(kicad_info)+    steps = [step] if step else STEPS+    results, failed = [], []+    for s in steps:+        r = _run_step(kicad_info, s, prep)+        r["step"] = s+        results.append(r)+        if not r.get("ok"):+            failed.append(s)++    done = [r["step"] for r in results if r.get("ok")]+    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 "")),+        "data": {+            "steps": results,+            "projectDir": prep.get("projectDir"),+            "installedParts": prep.get("installed"),+            "models3d": prep.get("models3d"),+            "warnings": prep.get("warnings"),+            "installedKiCadJustNow": installed_now,+            "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 — each entry in data.steps 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}."+        ),+    }
server.py+6
@@ -59,6 +59,8 @@ from handlers.export import ( # v0.9.61+: one-shot desktop half of the Adom molecule pipeline (STEP + silk # renders + sibling sch/pro discovery + container next-steps manifest). from handlers.export_molecule import handle_export_molecule+# v0.9.63+: first-class showcase tour (AD demos this bridge during HD install).+from handlers.demo import handle_demo # v0.9.10+: check/upgrade KiCad via the OFFICIAL downloads.kicad.org installer # (NOT winget, which lags the real release). check_for_updates is read-only; # upgrade silent-installs the official signed NSIS build.@@ -323,6 +325,9 @@ _VERB_CATALOG = {     "export_step": {"summary": "Export a STEP 3D model from a board.", "long": False,         "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.",+        "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.",         "related": ["kicad_export_step", "kicad_export_svg"], "pitfalls": ["adom-molecule requires glb+pcb+sch+pro — check data.missing before running the container half", "silk renders need KiCad 9+ (kicad-cli pcb render); older KiCads skip them gracefully"]},@@ -480,6 +485,7 @@ COMMAND_HANDLERS = {     "export_svg": handle_export_svg,     "export_step": handle_export_step,     "export_molecule": handle_export_molecule,+    "demo": handle_demo,     "export_bom_csv": handle_export_bom_csv,     # v0.9.10+: KiCad version check + upgrade via the official downloads.kicad.org     # installer (read-only check; silent /S install). Exposed as kicad_check_for_updates