← Commit history

Dashboard 1.0.126: Send to AI runs as a background job (Hydrogen 1.0.219), narrates picker and delivery states, 75 s budget, cancelled and failure terminals reported

John Lauer ·a2aab28af4 ·1mo ago ·parent d8a8502
2 files changed +31−14
dashboard/kicad_dashboard.py+30−13
@@ -266,8 +266,11 @@ def say_send(phrase):     # Wiki #77 (Hydrogen 1.0.213 AI picker): NO `type` here, so the human picks     # the AI (Claude, Codex, Kimi) in the shell's picker; the app: tag keys the     # "remember for this app" checkbox. dock: stays for the dock's own bookkeeping.-    body = {"new": True, "prompt": phrase, "submit": True,+    body = {"new": True, "prompt": phrase, "submit": True, "background": True,             "name": "KiCad - say it", "tags": ["dock:kicad-bridge", "app:adom/kicad-bridge"]}+    # background:true (Hydrogen 1.0.219, wiki #77): the answer comes back at once+    # with {job, state:"accepted"}; the picker's 5 to 45 s block and the delivery+    # happen detached, and the status route narrates them (state + human).     req = urllib.request.Request(base + "/ai-threads/run-prompt",                                  data=json.dumps(body).encode("utf-8"), method="POST",                                  headers={"Content-Type": "application/json"})@@ -296,28 +299,41 @@ def say_send(phrase):     job = d.get("job") if isinstance(d, dict) else None     if job:         import time as _t-        sent_states = ("working", "needs_you", "done")-        fail_states = ("submit_failed", "submitted_unidentified", "submit_unconfirmed", "still_working")-        last = str(d.get("state") or "opening")-        deadline = _t.monotonic() + 12.0+        # Hydrogen 1.0.219 job states (wiki #77): accepted, picking, picked, opening,+        # prefilled | tab_found, submitted, working, needs_you | done; Codex and Kimi+        # end at delivered. Failure terminals: cancelled, rejected, open_failed,+        # open_unconfirmed, tab_not_found, inject_failed, submit_failed, failed,+        # timeout. `terminal:true` says the job is over; `human` is the toast line.+        sent_states = ("working", "needs_you", "done", "delivered")+        fail_states = ("cancelled", "rejected", "open_failed", "open_unconfirmed", "tab_not_found",+                       "inject_failed", "submit_failed", "failed", "timeout",+                       "submitted_unidentified", "submit_unconfirmed", "still_working")+        last = str(d.get("state") or "accepted"); human = str(d.get("human") or "")+        picked = None+        deadline = _t.monotonic() + 75.0          # the picker alone may hold 45 s         while _t.monotonic() < deadline:             try:                 st = _get("/ai-threads/run-prompt/status?job=" + urllib.parse.quote(str(job)), 4)                 sd = st.get("data") if isinstance(st.get("data"), dict) else st-                last = str(sd.get("state") or last)+                last = str(sd.get("state") or last); human = str(sd.get("human") or human)+                jb = sd.get("job") if isinstance(sd.get("job"), dict) else {}+                picked = picked or jb.get("type") or sd.get("type")+                terminal = bool(sd.get("terminal"))             except Exception:-                pass+                terminal = False+            if last == "cancelled":+                return {"ok": False, "cancelled": True, "job": job, "state": last,+                        "why": human or "you dismissed the AI picker; nothing was opened or sent. The phrase is still on your clipboard."}             if last in sent_states:-                return {"ok": True, "job": job, "state": last,-                        "why": "sent: a new AI thread has the phrase and is working on it"}-            if last in fail_states:+                return {"ok": True, "job": job, "state": last, "ai": picked,+                        "why": human or f"sent to {picked or 'your AI'}: the new thread has the phrase"}+            if last in fail_states or (terminal and last not in sent_states):                 why = ("the prompt is sitting in the new tab and Send was never clicked; press Enter there"-                       if last == "submit_failed" else f"the AI panel reported {last}")+                       if last == "submit_failed" else (human or f"the AI panel reported {last}"))                 return {"ok": False, "job": job, "state": last, "why": why + ". The phrase was also copied."}             _t.sleep(1.0)         return {"ok": True, "job": job, "state": last, "unconfirmed": True,-                "why": f"the new thread is still {last} after 12s; it usually lands within 5s, "-                       f"check the new AI tab. The phrase was also copied."}+                "why": (human or f"the new thread is still {last}") + " after 75 s; check the new AI tab. The phrase was also copied."}     # VERIFY BY DELIVERY, never by the thread's name (issue #59: a correctly     # named tab that delivered nothing, and an unnamed tab that delivered -     # measured live on AdomLapper 2026-08-25, where a successful send came back@@ -1425,6 +1441,7 @@ class Handler(BaseHTTPRequestHandler):             # verbatim: cancelled:true, delivered:false and the picker's 45 s             # block are the module's to render.             b = dict(body or {}); b.pop("type", None)+            b.setdefault("background", True)      # Hydrogen 1.0.219: immediate {job, state}; the module polls status             tags = list(b.get("tags") or [])             if "app:adom/kicad-bridge" not in tags:                 tags.append("app:adom/kicad-bridge")
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "kicad-bridge",-  "version": "1.0.125",+  "version": "1.0.126",   "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": [