app
Grok Bot
Public Made by Adomby adom
Grok Bridge lets Adom talk to Grok Bot. Adom's AI environment is for electrical engineers: any provider, any desktop power tool, one place.
← Commit history
Publish 1.3.3
6 files changed
+185−17
dashboard/grok_dashboard.py+79−5@@ -111,7 +111,8 @@ _EXACT_ROUTES = ("/health", "/console", "/events", "/api/targets", _SUFFIX_ROUTES = ("/newbie-panel.js", "/favicon.svg", "/favicon.png", "/favicon.ico", "/icon.svg", "/demo.mp4", "/updater/status", "/api/viewport",- "/ai-threads/run-prompt", "/api/say-send",+ "/ai-threads/run-prompt", "/ai-threads/run-prompt/status",+ "/api/say-send", "/demo/steps.json") _REGEX_ROUTES = (r"/assets/(.+)$", r"^/demo/audio/(.+)$",@@ -182,8 +183,12 @@ def say_send(phrase): except Exception: pass - body = {"new": True, "prompt": phrase, "submit": True,- "name": "Grok Bot - say it", "tags": ["dock:grok-bridge"]}+ # Wiki #4 (Hydrogen 1.0.213 AI picker): NO `type`, so the human picks the AI in+ # the shell's picker; the app: tag keys its "remember for this app" checkbox.+ # background:true (1.0.219): the answer is an immediate {job, state:"accepted"}+ # and the picker's 5 to 45 s happen detached; the status route narrates them.+ body = {"new": True, "prompt": phrase, "submit": True, "background": True,+ "name": "Grok Bot - say it", "tags": ["dock:grok-bridge", "app:adom/grok-bridge"]} req = urllib.request.Request(base + "/ai-threads/run-prompt", data=json.dumps(body).encode("utf-8"), method="POST", headers={"Content-Type": "application/json"})@@ -195,6 +200,12 @@ def say_send(phrase): f"Not retried, so no duplicate tab. The phrase was copied instead."} d = res.get("data") if isinstance(res.get("data"), dict) else res+ if isinstance(d, dict) and d.get("cancelled"):+ return {"ok": False, "cancelled": True,+ "why": "you dismissed the AI picker; nothing was opened or sent. The phrase is still on your clipboard."}+ job = d.get("job") if isinstance(d, dict) else None+ if job:+ return _poll_run_prompt(_get, job, d) inject = d.get("inject") if isinstance(d.get("inject"), dict) else {} delivered = (d.get("delivered_to") or inject.get("delivered_in_conversation")@@ -214,6 +225,47 @@ def say_send(phrase): "tid": d.get("tid"), "sid": d.get("sid")} ++# Hydrogen 1.0.219 run-prompt job states: accepted, picking, picked, opening,+# prefilled | tab_found, submitted, working, needs_you | done; Codex and Kimi end+# at delivered. `terminal:true` ends the job; `human` is the ready 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")+++def _poll_run_prompt(_get, job, d):+ import urllib.parse+ last = str(d.get("state") or "accepted"); human = str(d.get("human") or "")+ picked = None+ deadline = time.monotonic() + 75.0 # the picker alone may hold 45 s+ while time.monotonic() < deadline:+ terminal = False+ 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); 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+ if last == "cancelled":+ # the user pressed Escape in the picker: never retry, never name an AI instead+ 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, "ai": picked, "deliveredTo": picked,+ "why": human or f"sent to {picked or 'your AI'}"}+ if last in _FAIL_STATES or terminal:+ why = ("the prompt is sitting in the new tab and Send was never clicked; press Enter there"+ 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."}+ time.sleep(1.0)+ return {"ok": False, "unconfirmed": True, "job": job, "state": last,+ "why": (human or f"the new thread is still {last}") + " after 75 s; check the new AI tab. The phrase was also copied."}+ def ab(verb, args=None, timeout=25, target=None): cmd = ["adom-bridge", "--ai-thread", AI_THREAD] if target and target != "-":@@ -828,7 +880,15 @@ def build_status(): # Grok's computer is the agent box (this machine when the dashboard is # serving from Grok Bot's container). A connected box is extra evidence. host_name = os.uname().nodename if hasattr(os, "uname") else "this machine"- if _looks_like_agent_box() or targets:+ if ready.get("computerReachable") is False:+ # Wiki #5: Grok Bot shows "Couldn't Reach Grok Bot's Computer"; a connected+ # laptop is not evidence the box is up.+ leds.append(_led(+ "computer", "Grok's computer", "err", "unreachable",+ "Grok Bot says it cannot reach its computer. Webhook events are accepted but "+ "nothing replies. Press Recover Grok Bot's Computer / Retry in Grok Bot.",+ str(ready.get("computerBanner") or "")[:200]))+ elif _looks_like_agent_box() or targets: leds.append(_led( "computer", "Grok's computer", "ok", host_name[:24] or "this box", "This box is Grok Bot's computer (the agent box), not the Grok Bot.exe window. "@@ -984,6 +1044,11 @@ class Handler(BaseHTTPRequestHandler): "webp": "image/webp", "mp4": "video/mp4"}.get( f.suffix.lstrip(".").lower(), "application/octet-stream") return self._file(f, mime)+ if path.endswith("/ai-threads/run-prompt/status"):+ # the module polls this once a second; the ?job= query must travel intact+ q = urlparse(self.path).query or ""+ return _proxy_control(self, "/ai-threads/run-prompt/status" + (("?" + q) if q else ""),+ timeout=8) if path.endswith("/updater/status"): return _proxy_control(self, "/updater/status", timeout=8) if path.endswith("/api/viewport"):@@ -1047,7 +1112,16 @@ class Handler(BaseHTTPRequestHandler): del VIEWPORT_LOG[:-16] return self._json({"ok": True}) if path.endswith("/ai-threads/run-prompt"):- return _proxy_control(self, "/ai-threads/run-prompt", body=body or {}, timeout=125)+ # Wiki #4: strip any `type` (absent = Hydrogen's AI picker asks the user),+ # carry the app: tag so a remembered pick applies here, and go background+ # so the picker never sits inside this request. Reply goes back verbatim.+ b = dict(body or {}); b.pop("type", None)+ b.setdefault("background", True)+ tags = list(b.get("tags") or [])+ if "app:adom/grok-bridge" not in tags:+ tags.append("app:adom/grok-bridge")+ b["tags"] = tags+ return _proxy_control(self, "/ai-threads/run-prompt", body=b, timeout=125) if path.endswith("/api/say-send"): phrase = str((body or {}).get("phrase") or "").strip() if not phrase:
dashboard/ui/newbie-panel.js+84−7@@ -112,6 +112,23 @@ var TWO_COL_MIN = 700, PHRASE_BUDGET = 336; var MIN_API = [1, 0, 142]; + /* Hydrogen 1.0.187+ answers a send with a JOB and reports progress on+ * GET /ai-threads/run-prompt/status?job=<uuid>. Measured on the VM: tab visible ~0.1s,+ * session id ~2.5s, sent by 3-5s, a trivial answer done ~8s. One poll a second is the+ * right cadence for that shape, and it is an in-memory read on the other end. */+ var PHASE = {+ opening: 'Opening a new AI chat…',+ prefilled: 'Prompt ready…',+ submitted: 'Sent, waiting for the AI…',+ working: 'The AI is working…',+ needs_you: 'The AI needs you. Open the tab.',+ done: 'Done.',+ submit_failed: 'The prompt is in the tab. Press Enter there to send it.',+ submitted_unidentified:'Sent, but the AI never checked in.',+ submit_unconfirmed: 'Sent, but delivery was not confirmed.',+ still_working: 'The AI is still working. Open the tab.'+ };+ function css() { if (document.getElementById('anp-css')) return; var s = document.createElement('style');@@ -174,6 +191,9 @@ if (!toastHost) { toastHost = el('div', 'anp-toasts'); document.body.appendChild(toastHost); } var t = el('div', 'anp-toast ' + (kind || 'info'), esc(msg)); toastHost.appendChild(t);+ /* retextable, so a long send can narrate its phases in ONE toast instead of+ * stacking six of them on top of each other. */+ t.setText = function (next) { t.innerHTML = esc(next); return t; }; setTimeout(function () { t.remove(); }, ms || (kind === 'err' ? 7000 : 4000)); return t; }@@ -224,7 +244,7 @@ }) .catch(function () { return { ok: true, version: null, open: true }; }); }- function send(base, phrase, appName, slug) {+ function send(base, phrase, appName, slug, onProgress) { return gate(base).then(function (g) { if (!g.ok) throw new Error('Adom needs updating to send prompts (found ' + g.version + ', needs 1.0.142). Run: adom update'); var ctl = new AbortController(), timer = setTimeout(function () { ctl.abort(); }, 120000);@@ -241,12 +261,55 @@ .then(function (res) { clearTimeout(timer); var j = res.j || {};- /* THREE states, never two. delivered_to is an ARRAY; a thread that opened but did- * not confirm delivery is NOT a failure, and retrying it double-sends. */+ if (!(res.status >= 200 && res.status < 300))+ throw new Error((j._hints && j._hints.note) || j.error || ('AI threads returned ' + res.status));+ /* PRE-1.0.187 SHAPE, kept because older Hydrogen builds still answer this way.+ * delivered_to is an ARRAY; a thread that opened but did not confirm delivery is+ * NOT a failure, and retrying it double-sends. */ var dt = j.delivered_to, arr = Array.isArray(dt) ? dt : (dt ? [dt] : []); if (arr.length || (j.inject && j.inject.delivered_in_conversation)) return { state: 'delivered' };- if (res.status >= 200 && res.status < 300) return { state: 'opened', hint: j._hints && j._hints.note };- throw new Error((j._hints && j._hints.note) || j.error || ('AI threads returned ' + res.status));+ /* 1.0.187+ returns in well under a second with a JOB, not a delivery. ok:true means+ * the job STARTED. delivered_to is never filled on this path any more, so without+ * the poll below every send would report "delivery not confirmed" forever. */+ if (!j.job) return { state: 'opened', hint: j._hints && j._hints.note };+ var started = Date.now(), missing = false;+ return new Promise(function (resolve) {+ (function tick() {+ /* the deadline is checked in BOTH paths. Checking it only after a successful+ * poll leaves a dashboard whose server stops answering polling forever. */+ function again() {+ if (Date.now() - started > 120000)+ return resolve({ state: 'opened', hint: 'no confirmation within 120s' });+ setTimeout(tick, 1000);+ }+ fetch(base + '/ai-threads/run-prompt/status?job=' + encodeURIComponent(j.job))+ .then(function (r) {+ /* THE STATUS ROUTE IS A THIRD PROXY ROUTE. Dashboards proxy /updater/status+ * and POST /ai-threads/run-prompt only. If this one was never added, every+ * poll 404s and the user watches "Opening…" for the full 120s before a+ * hedge, which is worse than the behaviour this replaced. Fail FAST and say+ * exactly which route is missing instead of hanging. */+ if (r.status === 404) { missing = true; return null; }+ return r.json();+ })+ .then(function (st) {+ if (missing) return resolve({ state: 'opened', phase: 'no_status_route',+ hint: 'Your AI chat opened. This app does not proxy GET ' ++ '/ai-threads/run-prompt/status yet, so progress cannot be shown.' });+ var state = (st.job && st.job.state) || '';+ if (typeof onProgress === 'function') { try { onProgress(state, st.job); } catch (e) {} }+ /* working or later IS sent, for the user-facing toast. */+ if (state === 'working' || state === 'done' || state === 'needs_you' ||+ state === 'still_working') return resolve({ state: 'delivered', phase: state });+ if (state === 'submit_failed' || state === 'submitted_unidentified' ||+ state === 'submit_unconfirmed')+ return resolve({ state: 'opened', phase: state,+ hint: (st.job && (st.job.error || st.job.note)) || state });+ again();+ })+ .catch(again);+ })();+ }); }) .catch(function (e) { clearTimeout(timer);@@ -316,7 +379,10 @@ if (sd.disabled) return; sd.disabled = true; var pending = toast('Opening a new AI thread…', 'info', 120000);- send(base, phrase, appName, slug).then(function (r) {+ send(base, phrase, appName, slug, function (state, job) {+ if (PHASE[state]) pending.setText(PHASE[state]);+ if (typeof cfg.onProgress === 'function') { try { cfg.onProgress(state, job); } catch (e) {} }+ }).then(function (r) { pending.remove(); if (r.state === 'delivered') toast('Sent. Your AI thread has the prompt.', 'ok'); else toast('Thread opened, delivery not confirmed. Check your AI threads before sending again.', 'info', 8000);@@ -605,10 +671,21 @@ real !== 'ERR' && real !== 404 && real !== fake); }); })+ .then(function () {+ /* THE STATUS ROUTE, added for Hydrogen 1.0.187+. It is a THIRD proxy route and no+ * dashboard had it before 2.22.0, so it is the row most likely to be red. A GET with+ * a job id that cannot exist must still be ANSWERED (any non-404), because a 404 here+ * means the route was never proxied and progress can never be reported. */+ return fetch(base + '/ai-threads/run-prompt/status?job=__anp_probe__')+ .then(function (r) { return r.status; }, function () { return 'ERR'; })+ .then(function (st) {+ add('progress route wired', 'answers (not 404)', String(st), st !== 'ERR' && st !== 404);+ });+ }) .then(function () { return {ok: ok, rows: rows}; }); } root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, copyText: copyText,- checkSend: checkSend, version: '2.21.0',+ checkSend: checkSend, version: '2.22.0', TWO_COL_MIN: TWO_COL_MIN, PHRASE_BUDGET: PHRASE_BUDGET }; })(typeof window !== 'undefined' ? window : this);
package.json+4−3@@ -1,8 +1,8 @@ { "slug": "grok-bridge",- "version": "1.3.2",+ "version": "1.3.3", "type": "app",- "description": "Grok Bridge lets Adom talk to Grok Bot. Adom's AI environment is for electrical engineers: any provider (Claude, Codex, Grok, Gemini, …), any desktop power tool, through Adom Bridge. grok_* open/close/resize/screenshot/record the app, read the thread, and create bots/channels via webhook.",+ "description": "Grok Bridge lets Adom talk to Grok Bot. Adom's AI environment is for electrical engineers: any provider (Claude, Codex, Grok, Gemini, \u2026), any desktop power tool, through Adom Bridge. grok_* open/close/resize/screenshot/record the app, read the thread, and create bots/channels via webhook.", "keywords": [ "grok", "grok-bot",@@ -143,5 +143,6 @@ ], "hero": { "path": "docs/hero.png"- }+ },+ "changelog": "Grok Bot's computer state: computerReachable on readiness, chat and webhook verbs, fast no_transcript on a blank window, no caption-button thread names; Send to AI uses Hydrogen's AI picker; insiders-first publishing." }
page.json+3−2@@ -4,7 +4,7 @@ "type": "app", "title": "Grok Bot", "brief": "Grok Bridge lets Adom talk to Grok Bot. Adom's AI environment is for electrical engineers: any provider, any desktop power tool, one place.",- "version": "1.3.2",+ "version": "1.3.3", "hero": { "type": "image", "path": "screenshots/hero.png"@@ -162,5 +162,6 @@ }, "dependencies": { "adom/adom-bridge": "^2.0.0"- }+ },+ "changelog": "Grok Bot's computer state: computerReachable on readiness, chat and webhook verbs, fast no_transcript on a blank window, no caption-button thread names; Send to AI uses Hydrogen's AI picker; insiders-first publishing." }
skills/grok-chat/SKILL.md+3@@ -30,6 +30,9 @@ Never steal focus. Surface `_hint` verbatim. - `grok_send` is not a public Grok HTTP API. The Anywhere character (`grok-anywhere`) can poll the inbox. - If list_windows is empty, fix hwnd unwrap first (**grok-app-control**) — chat verbs need that hwnd. - Empty/tiny transcript after cold start: `errorCode` `focused_thread_empty` / `transcript_unavailable` plus `focusedThread`. Call `grok_open` then wait, or `grok_chat_read {"thread":"grok-bridge"}`. Sidebar click is best-effort UIA.+- Window up but no page yet (blank after a background `grok_open`): `errorCode` `no_transcript`, answered at once with `visibleText`, `focusedThread:null` and `focusedThreadReason`. Retry in 30 s.+- Grok Bot showing "Couldn't Reach Grok Bot's Computer": `errorCode` `computer_unreachable`, `computerReachable:false`. Every status/read also carries `computerState`; see **grok-webhook** "Working ON Grok Bot's computer".+- `focusedThread` is never a title-bar button name; when the page has no conversation heading it is `null` with a reason. - `ok:true` is not enough: after send, the toast exists; the composer may be unchanged. Say that honestly. Window open/screenshot: **grok-app-control**. Webhook bots/channels: **grok-webhook**.
skills/grok-webhook/SKILL.md+12@@ -73,6 +73,16 @@ Fallback **only if the verb is unknown** (old runtime): fire `screenshot_compute Do **not** click the monitor icon in Grok Bot and treat that as the computer shot — that is the same window `grok_screenshot` already covers. +## Working ON Grok Bot's computer (the agent box)++grok-bridge's computer is a Linux box in Grok's cloud, not the user's PC. If you need something run or checked there, this is the only way in today:++- **The channel is webhook out, transcript back.** `grok_webhook_fire {"event":"ping","payload":{"nonce":"<fresh>","message":"run X and paste the output with the nonce"}}`, then read it back with `grok_chat_read {"thread":"grok-bridge","limit":8000}` until the nonce appears. No grok_* verb gives you a shell, ssh, or file transfer to the box. The agent there runs the commands and pastes their output into its thread.+- **A round trip takes about 60 to 120 s** per turn, and longer while grok-bridge is busy. The queue is FIFO and drops nothing. `thinking:true` means busy, not stuck.+- **Telling the box is down.** `grok_readiness`, `grok_chat_status` and `grok_chat_read` carry `computerState` (`unreachable` | `reconnecting` | `no_banner` | `unknown`) and `computerReachable` (`false` when Grok Bot shows "Couldn't Reach Grok Bot's Computer", otherwise `null`, because a missing banner does not prove the box is up). While it is down, `grok_webhook_fire` still gets HTTP 200 but adds `warning:"computer_unreachable"`, and `grok_computer_screenshot` refuses at once with `errorCode:"computer_unreachable"` instead of waiting out its timeout. The fix is the user pressing **Recover Grok Bot's Computer / Retry** in Grok Bot.+- **The disk persists between sessions and is shared by every bot on the account.** Earlier work (for example a checked-out design under `/workspace`) is still there for a new bot. If you brief a bot to design something from scratch, name a fresh folder and tell it not to read earlier designs.+- **A faster channel, when it exists.** If an Adom Bridge has been installed on the box and joined the user's mesh, it shows up in `adom-bridge targets`, and `--target <box>` then gives you real verbs (shell, files) there. Check `targets` first; until it is listed, use the webhook loop above.+ ## Named agent (relay, not a second mailbox) The **Grok Bridge webhook** routine belongs to grok-bridge. `grok_webhook_fire` has no other destination — every POST still lands in the grok-bridge thread.@@ -99,4 +109,6 @@ Smoke: `grok_webhook_fire {"event":"ping"}`. Verify with `grok_webhook_status` ( ## Errors worth knowing - `webhook_not_bound`: this PC has no `~/.adom/grok-webhook.json`. Bindings are per PC; a freshly installed PC needs `grok_webhook_ensure`.+- `computer_unreachable`: Grok Bot shows "Couldn't Reach Grok Bot's Computer". Events are accepted but no reply can come back. Ask the user to press Recover / Retry in Grok Bot, then re-check `grok_chat_status`.+- `no_transcript` (from `grok_chat_read` / `grok_chat_status`): the Grok Bot window has no page yet, usually right after a background `grok_open`. The verb answers at once instead of hitting the 60 s timeout; retry in 30 s. `focusedThread` is `null` with a `focusedThreadReason`. - `bot_unavailable` (HTTP 400 "This Bot is unavailable"): the key works but the routine's bot is unavailable, usually because the account's cloud computer is still coming up after a sign-in. Wait, check the sidebar with `grok_screenshot`, then `grok_webhook_ensure`. Retrying at once does not help.