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.73
3 files changed
+68−41
dashboard/kicad_dashboard.py+66−39@@ -49,7 +49,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlparse -VERSION = "3.1.0"+VERSION = "3.2.0" AI_THREAD = "kicad-dashboard" DEFAULT_PORT = 8931 UI_DIR = Path(__file__).resolve().parent / "ui"@@ -117,54 +117,81 @@ def _hydrogen_control_url(): return None -# EMERGENCY PARK (dock thread, 2026-08-24 cold-verify): the say-send SUCCESS-# path crashed the ah dev shell on ConfRoomROG within ~30s of a real Send-# click - the historical run-prompt kill; the panel's gate fix does not cover-# it. While parked, say_send refuses BEFORE any control-API call; the page's-# existing failure path (reason toast + copy fallback) carries users safely.-# The ah build thread owns certification; flip this flag on their broadcast.-SAY_SEND_PARKED = True+MIN_SHELL_VERSION = (1, 0, 142) # the build that fixed the run-prompt kill+++def _ver_tuple(v):+ """Numeric version tuple. A LEXICAL compare passes 1.0.99 as >= 1.0.142+ (issue #59), so every comparison in this file goes through here."""+ out = []+ for part in str(v or "").strip().lstrip("v").split("."):+ digits = "".join(c for c in part if c.isdigit())+ out.append(int(digits) if digits else 0)+ return tuple(out[:3]) or (0,) def say_send(phrase):- """Send-to-AI per the say-strip contract (John's ruling 2026-08-24): open- a NEW AI thread (staged name + dock tag) and submit the phrase. NEW- threads only - resuming or typing into existing threads stays parked;- this endpoint never calls resume and never targets a census tid."""- if SAY_SEND_PARKED:- return {"ok": False, "parked": True,- "why": "Send to AI is parked pending a panel fix. The phrase was copied instead."}+ """Send-to-AI (un-parked 2026-08-24 on John's ruling, after the ah build+ thread certified the run-prompt crash family dead on the gate-fixed build).++ ONE call, per the corrected contract in issue #58: POST /ai-threads/run-prompt+ with new:true. The earlier two-call flow (/ai-threads/new then run-prompt+ with its sid) was WRONG and shipped fleet-wide: /new has never returned an+ id, because a thread has no sid until its first submitted prompt, so the+ button opened a tab and delivered nothing.++ Still NEW threads only: resume and typing-into-existing stay parked.+ Every failure path returns ok:False with a readable `why`; the page then+ error-toasts AND copies the phrase, so a user is never stranded.""" import urllib.request - def _post(base, path, body):- req = urllib.request.Request(base + path,- data=json.dumps(body).encode("utf-8"),- method="POST",- headers={"Content-Type": "application/json"})- with urllib.request.urlopen(req, timeout=5) as r:- return json.loads(r.read().decode("utf-8") or "{}")- base = _hydrogen_control_url() if not base: return {"ok": False, "why": "no AI panel on this machine (hydrogen-control-url missing)"}++ def _get(path, timeout):+ with urllib.request.urlopen(base + path, timeout=timeout) as r:+ return json.loads(r.read().decode("utf-8") or "{}")++ # SAFETY GATE (issue #59): a shell older than 1.0.142 still carries the+ # crash, so refuse honestly there instead of killing the user's editor.+ # Numeric compare, never lexical.+ try:+ st = _get("/updater/status", 8)+ d = st.get("data") if isinstance(st.get("data"), dict) else st+ cur = (d.get("updater") or d).get("current_version") or d.get("current_version")+ if cur and _ver_tuple(cur) < MIN_SHELL_VERSION:+ return {"ok": False, "shellTooOld": True, "currentVersion": str(cur),+ "why": f"your Hydrogen ({cur}) predates the Send to AI fix; "+ f"update to 1.0.142 or newer to enable it. The phrase was copied instead."}+ except Exception:+ pass # cannot read the updater: fall through rather than block a healthy shell++ body = {"new": True, "prompt": phrase, "submit": True,+ "name": "KiCad - say it", "tags": ["dock:kicad-bridge"]}+ req = urllib.request.Request(base + "/ai-threads/run-prompt",+ data=json.dumps(body).encode("utf-8"), method="POST",+ headers={"Content-Type": "application/json"}) try:- # TAG LITERAL IS RULED, NOT GUESSED (dock thread, adom-ui-design 1.9.3- # literal table): `dock:` + the app's WIKI PAGE SLUG, never the dock- # card id. dock:kicad-bridge, not dock:kicad - the page slug is what- # identifies the package, the issue tracker and the dockbar manifest,- # and card ids carry no uniqueness guarantee. A census query for this- # app's threads matches on exactly this string, so do not "tidy" it.- new = _post(base, "/ai-threads/new",- {"name": "KiCad - say it", "tags": ["dock:kicad-bridge"]})- d = new.get("data") if isinstance(new.get("data"), dict) else new- sid = d.get("sid") or d.get("id")- if not sid:- return {"ok": False, "why": "ai-threads/new returned no sid"}- _post(base, "/ai-threads/run-prompt",- {"sid": sid, "prompt": phrase, "submit": True})- return {"ok": True, "sid": sid}+ # 120s: creating the tab alone can take 60s. NEVER retry blind on a+ # timeout - a retry can open a second tab for one click.+ with urllib.request.urlopen(req, timeout=120) as r:+ res = json.loads(r.read().decode("utf-8") or "{}") except Exception as e:- return {"ok": False, "why": f"{e.__class__.__name__}: {e}"}+ return {"ok": False, "why": f"the AI panel did not answer ({e.__class__.__name__}). "+ f"Not retried, so no duplicate tab. The phrase was copied instead."}++ d = res.get("data") if isinstance(res.get("data"), dict) else res+ # VERIFY BY DELIVERY, never by the thread's name (issue #59: a correctly+ # named tab that delivered nothing, and an unnamed tab that delivered).+ delivered = d.get("delivered_to") or d.get("delivered_in_conversation")+ if not delivered:+ hint = d.get("_hints") or d.get("_hint") or res.get("_hints") or ""+ if isinstance(hint, dict):+ hint = " ".join(str(v) for v in hint.values())+ return {"ok": False, "why": ("the AI panel opened a thread but did not confirm delivery. "+ + (str(hint)[:200] if hint else "")).strip()}+ return {"ok": True, "deliveredTo": str(delivered), "sid": d.get("sid")} def stage_phrase(phrase):
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "kicad-bridge",- "version": "1.0.72",+ "version": "1.0.73", "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": [
page.json+1−1@@ -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.72",+ "version": "1.0.73", "tags": [ "kicad", "pcb",