← Commit history

Publish 1.0.73

John Lauer ·abcf9b69df ·1mo ago ·parent e3359a0
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",