app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
Publish 1.0.57
6 files changed
+144−6
dashboard/kicad_dashboard.py+70−1@@ -49,7 +49,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlparse -VERSION = "2.1.4"+VERSION = "2.2.0" AI_THREAD = "kicad-dashboard" DEFAULT_PORT = 8931 UI_DIR = Path(__file__).resolve().parent / "ui"@@ -61,6 +61,67 @@ UI_DIR = Path(__file__).resolve().parent / "ui" # window tile and the taskbar badge all agree with KiCad itself. LOGO_PATH = UI_DIR / "kicad-logo.png" +# "Say it" (John via the dock thread, 2026-08-23): teach users how to ASK for+# this app by its noun, first and foremost, at the top of the dashboard. The+# list lives in dockbar.json (`say`) so the dock's interstitial and the+# dashboard teach the same phrases; the built-in copy is the fallback for an+# installed dashboard that ships without the manifest beside it.+_SAY_FALLBACK = [+ "open the symbol in kicad",+ "send this footprint to kicad",+ "show me the board in kicad 3d",+ "open my project in kicad",+ "install this part into kicad",+ "export gerbers from kicad",+]+++def say_phrases():+ for cand in (Path(__file__).resolve().parent.parent / "dockbar.json",+ Path.home() / ".local" / "share" / "adom" / "kicad-bridge" / "dockbar.json"):+ try:+ lst = json.loads(cand.read_text(encoding="utf-8")).get("say")+ if isinstance(lst, list) and lst:+ return [str(x) for x in lst][:6]+ except Exception:+ continue+ return list(_SAY_FALLBACK)+++HYDROGEN_CONTROL = "http://127.0.0.1:47084"+++def stage_phrase(phrase):+ """'Try it': stage the phrase (unsubmitted) into this app's own AI thread+ through the Hydrogen control API. Reachable from WSL2 under mirrored+ networking; from a cloud container it is not, and the page then falls back+ to copying. Never raises; returns {staged, reason, sid}."""+ import urllib.request+ tag = "app:adom/kicad-bridge"+ try:+ with urllib.request.urlopen(+ f"{HYDROGEN_CONTROL}/ai-threads/history?tag={tag}", timeout=2) as r:+ hist = json.loads(r.read().decode("utf-8") or "{}")+ except Exception as e:+ return {"staged": False, "reason": f"hydrogen control api unreachable ({e.__class__.__name__})"}+ threads = hist.get("threads") or hist.get("items") or hist.get("data") or []+ sid = None+ for t in threads if isinstance(threads, list) else []:+ sid = t.get("sid") or t.get("id")+ if sid:+ break+ if not sid:+ return {"staged": False, "reason": f"no AI thread tagged {tag} yet"}+ try:+ body = json.dumps({"sid": sid, "prompt": phrase, "submit": False}).encode("utf-8")+ req = urllib.request.Request(f"{HYDROGEN_CONTROL}/ai-threads/run-prompt", data=body,+ method="POST", headers={"Content-Type": "application/json"})+ with urllib.request.urlopen(req, timeout=4) as r:+ r.read()+ return {"staged": True, "sid": sid}+ except Exception as e:+ return {"staged": False, "reason": f"run-prompt failed ({e.__class__.__name__})", "sid": sid}+ # ---------------------------------------------------------------- bridge calls def ab(target, verb, args=None, timeout=140):@@ -893,6 +954,14 @@ def do_action(target, name, args): if mode not in ("progress", "flash", "both", "none"): return {"success": False, "error": f"unknown mode {mode}"} return ab(target, "kicad_set_settings", {"activityIndicator": mode}, timeout=60)+ if name == "say":+ return {"success": True, "phrases": say_phrases()}+ if name == "try_phrase":+ phrase = str(args.get("phrase") or "").strip()+ if not phrase:+ return {"success": False, "error": "phrase required"}+ r = stage_phrase(phrase)+ return {"success": True, "phrase": phrase, **r} if name == "restore_settings": return ab(target, "kicad_set_settings", {"restoreDefaults": True}, timeout=60) if name == "part_tour":
dashboard/ui/index.html+56@@ -34,6 +34,23 @@ header{height:44px;display:flex;align-items:center;gap:12px;padding:0 16px;backg .iconbtn:hover{background:var(--elevated);border-color:var(--border)} .iconbtn:active{transform:scale(.9);background:var(--overlay)} main{max-width:1080px;margin:0 auto;padding:20px 16px 60px}+/* "Say it": the first thing on the page (John, 2026-08-23: teach users how to ASK for the app by its noun, first and foremost). */+.say{background:var(--surface);border:1px solid var(--border);border-left:3px solid color-mix(in srgb,var(--accent) 45%,transparent);border-radius:12px;padding:14px 16px}+.say-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 14px;margin-bottom:10px}+.say-head h2{font-size:17px}+.say-sub{font-size:12.5px;color:var(--text-2);max-width:62ch}+.say-sub b{color:var(--text);font-weight:600}+.say-list{display:flex;flex-wrap:wrap;gap:8px}+.say-item{display:inline-flex;align-items:stretch;border:1px solid var(--border);border-radius:999px;background:var(--elevated);overflow:hidden;transition:border-color .15s ease}+.say-item:hover{border-color:var(--accent)}+.say-phrase{display:inline-flex;align-items:center;padding:7px 12px 7px 14px;background:none;border:none;color:var(--text);font:500 14px var(--brand-body);cursor:pointer;transition:background .15s ease,transform .06s ease}+.say-phrase em{font-style:normal;color:var(--accent)}+.say-phrase:hover{background:var(--overlay)}+.say-phrase:active{transform:translateY(1px) scale(.98)}+.say-try{display:inline-flex;align-items:center;padding:0 12px;border:none;border-left:1px solid var(--border);background:none;color:var(--text-2);font:600 12px var(--brand-heading);cursor:pointer;transition:background .15s ease,color .15s ease}+.say-try:hover{background:var(--overlay);color:var(--accent)}+.say-try:active{transform:translateY(1px)}+.say-phrase:focus-visible,.say-try:focus-visible{outline:2px solid var(--accent);outline-offset:-2px} h2{font-family:'Familjen Grotesk',sans-serif;font-size:17px;color:var(--text);margin:26px 0 12px} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:12px} .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px}@@ -189,6 +206,14 @@ main a:hover{border-bottom-color:var(--accent)} </div> </header> <main>+ <section class="say" id="say" aria-labelledby="sayTitle">+ <div class="say-head">+ <h2 id="sayTitle" style="margin:0">Say it to your AI</h2>+ <span class="say-sub">You do not drive this dashboard. You ask your AI, using the word <b>kicad</b>. Click a phrase to copy it; Try it stages it into this app's AI thread.</span>+ </div>+ <div class="say-list" id="sayList"></div>+ </section>+ <h2>Status</h2> <div id="targets"></div> <div class="note" id="boxnote" style="display:none">Many boxes, one bridge: pick where KiCad should run. The bridge only ever drives the KiCad instances IT launched, so your own KiCad is never touched.</div>@@ -431,6 +456,36 @@ $('#btnPartTour').onclick=async()=>{ else{toast('part installed and project generated','ok');$('#partLabel').textContent=(d.steps||[]).join(' · ');refresh(true);} }; +// "Say it": phrases come from dockbar.json `say` via the server, so the dock+// interstitial and this strip teach the same words. Copy on click; Try it+// stages the phrase into the app's AI thread when the Hydrogen control API is+// reachable, and copies with an honest toast when it is not.+async function buildSay(){+ let phrases=[];+ try{const r=await act('say');phrases=((r.data||r).phrases)||[];}catch(e){}+ const list=$('#sayList');list.innerHTML='';+ if(!phrases.length){$('#say').style.display='none';return;}+ for(const p of phrases){+ const item=document.createElement('div');item.className='say-item';+ const b=document.createElement('button');b.className='say-phrase';b.type='button';+ b.setAttribute('data-tip','Copy this phrase');+ b.innerHTML='“'+p.replace(/kicad/gi,m=>'<em>'+m+'</em>')+'”';+ b.onclick=async()=>{try{await navigator.clipboard.writeText(p);toast('copied: '+p,'ok');}catch(e){toast('could not copy: '+e,'err');}};+ const t=document.createElement('button');t.className='say-try';t.type='button';t.textContent='Try it';+ t.setAttribute('data-tip','Stage this phrase in this app\'s AI thread (not submitted, you press Enter)');+ t.onclick=async()=>{+ t.disabled=true;+ try{+ const r=await act('try_phrase',{phrase:p});const d=r.data||r;+ if(d.staged){toast('staged in your AI thread: '+p+' (press Enter there to send)','ok');}+ else{try{await navigator.clipboard.writeText(p);}catch(e){}+ toast('copied instead: '+(d.reason||'could not reach your AI thread')+'. Paste it into your AI.','');}+ }catch(e){toast('try failed: '+e,'err');}+ finally{t.disabled=false;}+ };+ item.appendChild(b);item.appendChild(t);list.appendChild(item);+ }+} // ---- Settings (settings-layout skill: modal, four dismissals, optimistic saves) ---- // The taskbar captures are native DEVICE pixels (2560-wide display at 150%). // Show them at naturalWidth / devicePixelRatio so they render 1:1 - the same@@ -704,6 +759,7 @@ ai.addEventListener('mouseleave',()=>{aiHide=setTimeout(()=>ai.classList.remove( // every box, so one wedged laptop left the whole page on "connecting..." with // no LEDs and no pills). Skeleton now, then status and targets INDEPENDENTLY. renderSkeleton();+buildSay(); buildSurfaces(); refresh(); loadTargets();
dockbar.json+9−1@@ -1,5 +1,13 @@ { "dock_manifest": "1.2.0",+ "say": [+ "open the symbol in kicad",+ "send this footprint to kicad",+ "show me the board in kicad 3d",+ "open my project in kicad",+ "install this part into kicad",+ "export gerbers from kicad"+ ], "webview": { "title": "KiCad Dashboard", "favicon": "docs/img/dock-kicad.svg"@@ -35,4 +43,4 @@ "typed": false } }-}+}
install.sh+3@@ -32,6 +32,9 @@ for asset in index.html kicad-logo.png; do done # Settings screenshots (real captures) + the shared Adom cog: the whole tree, # so a new shot never needs an installer edit to ship.+# dockbar.json beside the installed app: the dashboard reads its `say` list+# from it so the dock interstitial and the "Say it" strip teach the same words.+[ -f "$H/dockbar.json" ] && cp -f "$H/dockbar.json" "$APPDIR/dockbar.json" if [ -d "$H/dashboard/ui/assets" ]; then rm -rf "$APPDIR/dashboard/ui/assets" cp -R "$H/dashboard/ui/assets" "$APPDIR/dashboard/ui/assets"
package.json+3−2@@ -1,6 +1,6 @@ { "slug": "kicad-bridge",- "version": "1.0.56",+ "version": "1.0.57", "type": "app", "description": "Skills for your container so your AI knows how to drive the KiCad bridge. The bridge runtime itself is the release zip; Adom Bridge loads that.", "tags": [@@ -48,7 +48,8 @@ "dashboard/ui/assets/icons/settings-cog.svg", "kicad-bridge-hero-v6.png", "docs/img/dock-kicad.svg",- "tools/capture_taskbar_shots.py"+ "tools/capture_taskbar_shots.py",+ "dockbar.json" ], "hero": { "path": "kicad-bridge-hero-v6.png"
page.json+3−2@@ -4,7 +4,7 @@ "slug": "kicad-bridge", "title": "KiCad - the KiCad Bridge", "brief": "Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",- "version": "1.0.56",+ "version": "1.0.57", "tags": [ "kicad", "pcb",@@ -107,7 +107,8 @@ "dashboard/ui/assets/icons/settings-cog.svg", "kicad-bridge-hero-v6.png", "docs/img/dock-kicad.svg",- "tools/capture_taskbar_shots.py"+ "tools/capture_taskbar_shots.py",+ "dockbar.json" ], "org": "adom", "name": "kicad-bridge",