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
v1.3.5 (insiders): Never a stray window: picks the largest Grok Bot window, reports main_window_hidden and grok_open re-shows it behind your work; grok_webhook_fire requireApproval asks the user with an Allow/Deny prompt and stamps the approval; per-verb timeouts so slow verbs are not cut at 60 s.
7 files changed
+211−9
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.3.4+1.3.5
CLAUDE.md+6@@ -69,6 +69,12 @@ them a background Grok Bot window never paints and UIA sees only caption buttons auto-update relaunch drops them; `host_launch_flags_ok()` spots that and `grok_open {restart:true, reason}` fixes it. +Windows + approval (1.3.5, wiki #5 items 5-6): `pick_grok_window` skips strays (< 400x300),+takes the largest, remembers the main hwnd in `~/.adom/grok-main-window.json`; `main_window_hidden`+when only strays are visible, `grok_open` re-shows it (show + bottom). `grok_webhook_fire+{requireApproval, approvalSummary}` waits on a notify_user Allow/Deny toast and stamps+`payload.approval`; caller-supplied approval fields are always stripped.+ Webhook bind file: `~/.adom/grok-webhook.json` `{url, key, header, boundAt}` (never log the key). `grok_webhook_ensure` reads UIA Edits Webhook URL / Webhook key (no OCR). ## The publish loop: insiders first, public on a human's words (wiki #2)
bridge.json+8−2@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "grok", "displayName": "Grok Bot",- "version": "1.3.4",+ "version": "1.3.5", "description": "Drive Grok Bot (standalone Electron app, Grok Bot.exe) from Adom Bridge: open/close/resize/screenshot/record the app, read thinking vs idle and the visible transcript of the focused thread, queue messages, bind the Grok Bridge webhook and create bots/channels. No public Grok HTTP API \u2014 the runtime talks back to ab over desktop_* callbacks. Cursor is last-resort fallback only if Grok Bot.exe is missing.", "homepage": "https://wiki.adom.inc/adom/grok-bridge", "author": "Adom Inc.",@@ -55,7 +55,13 @@ "statusVerb": "grok_readiness", "timeouts": { "default": 60,- "long": 300+ "verbs": {+ "webhook_fire": 300,+ "computer_screenshot": 330,+ "open": 120,+ "webhook_ensure": 120,+ "install": 120+ } }, "dependencies": { "python": ">=3.10"
handlers/chat.py+11@@ -18,6 +18,7 @@ from handlers.windows import ( flatten_callback, grok_hwnd, host_launch_flags_ok,+ main_window_state, ) # Proven UIA on Grok Bot.exe hwnd (AdomLapper): Document named "Grok Bot",@@ -175,6 +176,16 @@ def _redact_secrets(text: str) -> str: def _window_missing(verb: str) -> dict: info = detect_host() thinking = False+ ms = main_window_state()+ if ms["mainWindowHidden"]:+ out = {"success": False, "errorCode": "main_window_hidden", "thinking": False, "idle": False,+ "agentName": None, "hwnd": None, "title": None, "hostApp": HOST_APP,+ "windowEtiquette": "background",+ "_hint": "Grok Bot's main window is hidden (only small secondary windows are visible). "+ f"grok_open {{}} shows it behind the user's work, then grok_{verb} {{}}.",+ "_next": f"grok_open {{}} then grok_{verb} {{}}"}+ out.update(ms)+ return out return { "success": False, "errorCode": "window_not_found",
handlers/describe.py+2−2@@ -53,7 +53,7 @@ VERB_CATALOG = { "screenshot": { "summary": "Screenshot a Grok Bot window via ab desktop_screenshot_window.", "long": False,- "hint": "titleContains Grok Bot, then Grok. Does not raise the window.",+ "hint": "Captures the picked MAIN window by hwnd (largest visible Grok Bot window, never one under 400x300); explicit titleContains only when you pass it. main_window_hidden when only strays are visible: grok_open {} re-shows it behind the user's work. Does not raise the window.", "related": ["grok_list_windows", "grok_open", "grok_record_start"], "pitfalls": [ "Needs a live window — grok_open first if hostAppRunning is false.",@@ -227,7 +227,7 @@ VERB_CATALOG = { "webhook_fire": { "summary": "POST {event, payload, source:grok-bridge, agent?} to the bound url. Auth is Authorization: Bearer, never a body key.", "long": False,- "hint": "Optional agent (name or id) as args.agent or payload.agent — grok-bridge relays into that teammate and pastes the reply into its own thread. Header Authorization: Bearer <key>. errorCode webhook_not_bound if unbound — grok_webhook_ensure first.",+ "hint": "Optional agent (name or id) as args.agent or payload.agent — grok-bridge relays into that teammate and pastes the reply into its own thread. Header Authorization: Bearer <key>. errorCode webhook_not_bound if unbound — grok_webhook_ensure first. requireApproval:true + approvalSummary raises an Allow/Deny toast on the user's PC, fires only on Allow and stamps payload.approval (caller-supplied approval fields are stripped). The verb budget is 300 s (bridge.json timeouts).", "related": ["grok_webhook_ensure", "grok_bot_create", "grok_channel_create", "grok_bot_list"], "pitfalls": [ "A JSON field named key is WRONG — auth is a header.",
handlers/webhook.py+73@@ -317,6 +317,66 @@ def handle_run_status(args: dict) -> dict: return out +APPROVAL_WAIT_DEFAULT = 180+APPROVAL_WAIT_MIN = 30+APPROVAL_WAIT_MAX = 270+APPROVE_LABEL = "Allow"+DENY_LABEL = "Deny"+++def _ask_user_approval(event: str, args: dict, agent, hostname) -> dict:+ """Raise a buttoned Adom Bridge toast on the user's machine and wait for the click.++ Returns {approved:True, stamp} or {approved:False, refusal}. The click is the human's;+ the caller's own claims of approval are never trusted (stripped in handle_webhook_fire).+ """+ import caller_identity+ summary = str(args.get("approvalSummary") or "").strip()+ thread = caller_identity.thread_name() or "an unnamed AI thread"+ if len(summary) < 10:+ return {"approved": False, "refusal": {+ "success": False, "fired": False, "errorCode": "approval_summary_required",+ "_hint": "requireApproval needs approvalSummary: one plain sentence (>= 10 chars) saying "+ "what Grok Bot's agent will do, shown to the user on the Allow / Deny prompt.",+ }}+ try:+ wait = int(args.get("approvalTimeoutSeconds") or APPROVAL_WAIT_DEFAULT)+ except (TypeError, ValueError):+ wait = APPROVAL_WAIT_DEFAULT+ wait = max(APPROVAL_WAIT_MIN, min(APPROVAL_WAIT_MAX, wait))+ body = (f'AI thread "{thread}" wants Grok Bot\'s agent'+ + (f" ({agent})" if agent else "") + f" to: {summary}")+ res = _cb("notify_user", {+ "title": "Allow this Grok Bot request?",+ "body": body[:400],+ "scenario": "reminder",+ "buttons": [{"label": APPROVE_LABEL}, {"label": DENY_LABEL}],+ "wait": True,+ "timeoutSec": wait,+ }, timeout=wait + 20)+ from handlers.windows import flatten_callback+ merged = flatten_callback(res)+ merged = merged if isinstance(merged, dict) else {}+ action = str(merged.get("action") or (res or {}).get("action") or "").strip()+ if action != APPROVE_LABEL:+ why = ("the user pressed Deny" if action == DENY_LABEL else+ "no answer before the prompt timed out" if not action else f"the user chose {action!r}")+ return {"approved": False, "refusal": {+ "success": False, "fired": False, "approved": False,+ "errorCode": "approval_denied" if action == DENY_LABEL else "approval_not_given",+ "approvalAction": action or None,+ "_hint": f"Not fired: {why}. Do not retry the same request without asking the user in chat first.",+ }}+ now = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")+ return {"approved": True, "stamp": {+ "approvedBy": "user",+ "approvedVia": "Adom Bridge Allow/Deny prompt on " + (hostname or "the user's PC"),+ "approvedAt": now,+ "approvalSummary": summary,+ "requestedByThread": thread,+ }}++ def handle_webhook_fire(args: dict) -> dict: event = str(args.get("event") or "").strip()@@ -356,6 +416,17 @@ def handle_webhook_fire(args: dict) -> dict: hostname = socket.gethostname() except Exception: hostname = None++ # Wiki #5 item 6: a caller can never stamp approval itself; only a human click below can.+ for k in ("approval", "approvedBy", "approvedAt"):+ payload.pop(k, None)+ approval = None+ if args.get("requireApproval"):+ approval = _ask_user_approval(event, args, agent, hostname)+ if not approval.get("approved"):+ return approval["refusal"]+ payload["approval"] = approval["stamp"]+ body = { "event": event, "payload": payload,@@ -459,6 +530,8 @@ def handle_webhook_fire(args: dict) -> dict: } if agent: out["agent"] = agent+ if approval is not None:+ out["approval"] = approval["stamp"] from handlers.chat import UNREACHABLE_HINT, probe_computer comp = probe_computer() out.update(comp)
handlers/windows.py+110−4@@ -7,6 +7,7 @@ HTTP callback. All driving goes through caller_identity.ad_callback. from __future__ import annotations import json+import os import subprocess import sys import time@@ -34,6 +35,13 @@ HOST_FLAGS = [ ] _RESTART_REASON_MIN = 10 +# Grok Bot owns small secondary top-level windows titled "Grok Bot" too (a 240x30 strip+# was captured instead of the main window once its main window was hidden: wiki #5).+# Anything smaller than this is never "the" window.+STRAY_MIN_W = 400+STRAY_MIN_H = 300+_MAIN_FILE = os.path.join(os.path.expanduser("~"), ".adom", "grok-main-window.json")+ def _cb(command: str, args: dict | None = None, timeout: int = 30) -> dict: """Best-effort ab callback. Never raises."""@@ -287,21 +295,72 @@ def collect_grok_windows(title_contains: str | None = None) -> tuple[list[dict], return _dedupe_windows(filtered), ab_up +def _wh(w: dict | None) -> tuple[int, int]:+ r = (w or {}).get("rect") or {}+ try:+ return int(r.get("width") or 0), int(r.get("height") or 0)+ except (TypeError, ValueError):+ return 0, 0+++def _is_stray(w: dict | None) -> bool:+ """True for a window too small to be the main Grok Bot window. Unknown size counts+ as not stray (some ab shapes carry no rect)."""+ wd, ht = _wh(w)+ if not wd and not ht:+ return False+ return wd < STRAY_MIN_W or ht < STRAY_MIN_H+++def _remember_main(hwnd) -> None:+ try:+ os.makedirs(os.path.dirname(_MAIN_FILE), exist_ok=True)+ with open(_MAIN_FILE, "w", encoding="utf-8") as f:+ json.dump({"hwnd": int(hwnd), "at": time.time()}, f)+ except Exception:+ pass+++def remembered_main_hwnd() -> int | None:+ try:+ with open(_MAIN_FILE, encoding="utf-8") as f:+ return int(json.load(f).get("hwnd")) or None+ except Exception:+ return None+++# What the last pick saw, for verbs that want to say "main window hidden".+LAST_PICK: dict = {}++ def pick_grok_window(windows: list[dict] | None = None, *, allow_cursor: bool = True) -> dict | None:+ """Best title rank, never a stray, then the LARGEST visible window.""" if windows is None: windows, _ = collect_grok_windows() ranked = []+ strays = [] for w in windows or []: rank = _title_rank(_title_of(w)) if rank == 2 and not allow_cursor: continue if rank >= 9: continue- ranked.append((rank, w))+ if _is_stray(w):+ strays.append(w)+ continue+ wd, ht = _wh(w)+ ranked.append((rank, -(wd * ht), w))+ LAST_PICK.clear()+ LAST_PICK["strays"] = [{"hwnd": _hwnd_of(w), "size": "%dx%d" % _wh(w)} for w in strays] if not ranked:+ LAST_PICK["mainWindowHidden"] = bool(strays) return None- ranked.sort(key=lambda x: x[0])- return ranked[0][1]+ ranked.sort(key=lambda x: (x[0], x[1]))+ best = ranked[0][2]+ LAST_PICK["mainWindowHidden"] = False+ if _hwnd_of(best):+ _remember_main(_hwnd_of(best))+ return best def grok_hwnd(*, allow_cursor: bool = True) -> tuple[int | None, dict | None]:@@ -309,6 +368,27 @@ def grok_hwnd(*, allow_cursor: bool = True) -> tuple[int | None, dict | None]: return _hwnd_of(win), win +def main_window_state() -> dict:+ """Fields for callers when no main window is visible."""+ return {"mainWindowHidden": bool(LAST_PICK.get("mainWindowHidden")),+ "strayWindows": LAST_PICK.get("strays") or [],+ "rememberedMainHwnd": remembered_main_hwnd()}+++def show_hidden_main() -> tuple[int | None, dict | None, dict | None]:+ """Re-show the remembered main window BEHIND the user's work (show, then bottom)."""+ h = remembered_main_hwnd()+ if not h:+ return None, None, None+ res = _cb("desktop_set_window_state", {"hwnd": h, "state": "show"})+ if not _callback_ok(res) or (isinstance(res, dict) and res.get("error")):+ return None, None, res+ _cb("desktop_set_window_state", {"hwnd": h, "state": "bottom"})+ time.sleep(0.5)+ hwnd, win = grok_hwnd(allow_cursor=False)+ return hwnd, win, res++ def window_rect(win: dict | None) -> dict: """Physical px {x,y,w,h} from a desktop_list_windows entry (ab physical px).""" if not isinstance(win, dict):@@ -526,7 +606,9 @@ def handle_list_windows(args: dict) -> dict: def handle_screenshot(args: dict) -> dict: title = (args.get("titleContains") or "").strip() info = detect_host()- needles = [title] if title else window_title_needles(info)+ # Explicit titleContains only: ab's titleContains picks the TOPMOST match, which can be a+ # 240x30 secondary strip (wiki #5). By default capture the picked main window by hwnd.+ needles = [title] if title else [] last = None for needle in needles: last = _cb("desktop_screenshot_window", {"titleContains": needle})@@ -557,6 +639,15 @@ def handle_screenshot(args: dict) -> dict: "Window was NOT raised. PNG is on the desktop; pull it with the CLI." ) return shot+ ms = main_window_state()+ if ms["mainWindowHidden"]:+ out = {"success": False, "errorCode": "main_window_hidden",+ "error": "Grok Bot's main window is hidden; only small secondary windows are visible.",+ "_hint": ("Not capturing a stray strip. grok_open {} shows the main window again, behind "+ "the user's other windows (no focus steal), then retry grok_screenshot."),+ "_next": "grok_open {} then grok_screenshot {}"}+ out.update(ms)+ return out return { "success": False, "errorCode": "window_not_found",@@ -629,6 +720,13 @@ def handle_open(args: dict) -> dict: time.sleep(3) hwnd0, win0, proc0 = _host_alive(info)+ hidden_fix = None+ if proc0 and not hwnd0 and not fallback and restarted is None:+ hidden_state = main_window_state()+ h2, w2, res = show_hidden_main()+ hidden_fix = dict(hidden_state, shown=bool(h2), showCallback=res)+ if h2:+ hwnd0, win0 = h2, w2 listed = handle_list_windows(args) if (hwnd0 or proc0) else None def _envelope(*, success, launched, already_open, opened, hwnd, win, via, callback, host_running, extra_hint=""):@@ -687,6 +785,14 @@ def handle_open(args: dict) -> dict: ), ) env["launchedWithAccessibility"] = flags_ok+ if hidden_fix is not None:+ env.update({k: hidden_fix[k] for k in ("mainWindowHidden", "strayWindows", "rememberedMainHwnd")})+ env["shownHiddenMain"] = hidden_fix["shown"]+ env["_hint"] = (("Grok Bot's main window was hidden; showed it again BEHIND the user's other "+ "windows (show + bottom, no focus steal). ") if hidden_fix["shown"] else+ ("Grok Bot is running but its main window is hidden and its hwnd is not "+ "remembered on this PC. grok_open {\"restart\":true,\"reason\":\"...\"} "+ "relaunches it in the background. ")) + env["_hint"] if restarted is not None: env["restartKill"] = restarted env["_hint"] = "Restart asked, but Grok Bot was still running after the kill. " + env["_hint"]