← Commit history

v1.3.4 (insiders): Grok Bot launches readable: grok_open adds the accessibility and no-occlusion switches so a background window paints and its page shows in UIA, and grok_open restart relaunches a Grok Bot its own auto-update started without them.

John Lauer ·a1090bf6d7 ·5d ago ·parent 98098f8
6 files changed +87−13
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.3.3\ No newline at end of file+1.3.4\ No newline at end of file
CLAUDE.md+5
@@ -64,6 +64,11 @@ Document once (8 s per UIA callback, 15 s budget) and report `computerState` / answer at once with `no_transcript` or `computer_unreachable`; focusedThread is never a caption button. readiness, webhook_fire (warning) and computer_screenshot (refuses) use it. +Launch switches (1.3.4): `HOST_FLAGS` in `handlers/windows.py` go on every launch. Without+them a background Grok Bot window never paints and UIA sees only caption buttons. Grok Bot's+auto-update relaunch drops them; `host_launch_flags_ok()` spots that and `grok_open+{restart:true, reason}` fixes it.+ 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+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "grok",   "displayName": "Grok Bot",-  "version": "1.3.3",+  "version": "1.3.4",   "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.",
handlers/chat.py+12−4
@@ -11,11 +11,13 @@ import time  from grok_detect import HOST_APP, detect_host from handlers.windows import (+    RESTART_HINT,     _cb,     _callback_ok,     _title_of,     flatten_callback,     grok_hwnd,+    host_launch_flags_ok, )  # Proven UIA on Grok Bot.exe hwnd (AdomLapper): Document named "Grok Bot",@@ -353,13 +355,14 @@ UNREACHABLE_HINT = (     "events are accepted (HTTP 200) but no reply can come back. Ask the user to press "     "Recover Grok Bot's Computer / Retry in Grok Bot, then re-check grok_chat_status." )-NO_PAGE_REASON = "no thread list on the page (window blank or still loading)"+NO_PAGE_REASON = "no page exposed to UIA (window blank or still loading)"   def _no_page(hwnd: int, win, probe: dict, verb: str) -> dict:     """Fast answer when the window has no Document, or shows the unreachable page."""     state = probe["state"]     down = state in ("unreachable", "reconnecting")+    flags_ok = None if down else host_launch_flags_ok()     out = {         "success": False,         "errorCode": "computer_unreachable" if down else "no_transcript",@@ -377,13 +380,18 @@ def _no_page(hwnd: int, win, probe: dict, verb: str) -> dict:         "title": _title_of(win),         "windowEtiquette": "background",         "_hint": (UNREACHABLE_HINT if down else-                  "The Grok Bot window has no page yet (only caption buttons in UIA). After a "-                  "background grok_open the first paint can take minutes; retry in 30 s or "-                  "grok_screenshot to see it. Answered fast instead of waiting out the verb timeout."),+                  "The Grok Bot window shows no page to UIA (only caption buttons). It may still be "+                  "loading: retry in 30 s, or grok_screenshot to see it. Answered fast instead of "+                  "waiting out the verb timeout."),         "_next": ("grok_chat_status {} after the user presses Retry" if down                   else f"wait 30 s, then grok_{verb} {{}}"),     }     out.update(computer_fields(state, probe["banner"]))+    out["launchedWithAccessibility"] = flags_ok+    if flags_ok is False:+        out["focusedThreadReason"] = "Grok Bot's page is not exposed to UIA (started without the accessibility switches)"+        out["_hint"] = RESTART_HINT+        out["_next"] = "grok_open {\"restart\":true,\"reason\":\"Grok Bot page invisible to UIA\"}"     return out  
handlers/describe.py+1−1
@@ -41,7 +41,7 @@ VERB_CATALOG = {     "open": {         "summary": "Launch Grok Bot in the BACKGROUND if installed. Polls process/window; Start-Process minimized fallback.",         "long": True,-        "hint": "Returns launched, opened, alreadyOpen, hwnd. Polls process_list/window after launch/desktop_open; Start-Process -WindowStyle Minimized if those do not take. Never steals focus.",+        "hint": "Returns launched, opened, alreadyOpen, hwnd. Polls process_list/window after launch/desktop_open; Start-Process -WindowStyle Minimized if those do not take. Never steals focus. Every launch carries --force-renderer-accessibility and the no-occlusion switches so a background window paints and is readable. {restart:true, reason} relaunches a Grok Bot that was started without them (launchedWithAccessibility:false, e.g. after its own auto-update).",         "related": ["grok_list_windows", "grok_screenshot", "grok_close", "grok_readiness"],         "pitfalls": [             "success:true requires a live process/window after poll — not just callback ok.",
handlers/windows.py+67−6
@@ -21,6 +21,19 @@ from grok_detect import (     _grok_bot_running, ) +# Chromium switches every launch of the host carries. Without them a background+# (never-shown, occluded) window does not paint, and the page is invisible to UIA:+# grok_chat_read / grok_chat_status saw only the caption buttons (measured on+# AdomLapper 2026-10-14; also the "blank for minutes" of wiki #5). Grok Bot's own+# auto-update relaunch ("--updated") drops them, so grok_open {restart:true} exists.+HOST_FLAGS = [+    "--force-renderer-accessibility",+    "--disable-features=CalculateNativeWinOcclusion",+    "--disable-backgrounding-occluded-windows",+    "--disable-renderer-backgrounding",+]+_RESTART_REASON_MIN = 10+  def _cb(command: str, args: dict | None = None, timeout: int = 30) -> dict:     """Best-effort ab callback. Never raises."""@@ -347,9 +360,8 @@ def launch_host(argv: list | None = None) -> dict:     """     info = detect_host()     exe = info.get("exe")-    launch_args = {"background": True, "noActivate": True, "detached": True}-    if argv:-        launch_args["args"] = list(argv)+    launch_args = {"background": True, "noActivate": True, "detached": True,+                   "args": HOST_FLAGS + list(argv or [])}     if exe:         launch_args["path"] = exe     else:@@ -436,6 +448,31 @@ def _start_process_minimized(exe: str, extra_args: list | None = None) -> dict:         return {"success": False, "via": "Start-Process", "error": str(e)}  +def host_launch_flags_ok() -> bool | None:+    """True when the running Grok Bot main process carries HOST_FLAGS, False when it does+    not (it was started by its own updater or by hand), None when unknown."""+    try:+        procs = _as_processes(_cb("process_list", {"name": "Grok Bot"}, timeout=10))+    except Exception:+        return None+    mains = []+    for p in procs:+        cmd = str(p.get("commandLine") or p.get("cmdline") or p.get("CommandLine") or "")+        if "grok bot.exe" in cmd.lower() and "--type=" not in cmd:+            mains.append(cmd)+    if not mains:+        return None+    return any("--force-renderer-accessibility" in c for c in mains)+++RESTART_HINT = (+    "Grok Bot was started without the accessibility / no-occlusion switches (its own auto-update "+    "relaunch, or the user started it), so its page is invisible to UIA. Fix: grok_open "+    "{\"restart\":true,\"reason\":\"<why>\"} relaunches it in the background with them; its bots run "+    "in Grok's cloud, so nothing is lost."+)++ def handle_list_windows(args: dict) -> dict:     info = detect_host()     title = (args.get("titleContains") or "").strip()@@ -577,6 +614,20 @@ def handle_open(args: dict) -> dict:         if fallback else ""     ) +    restarted = None+    if args.get("restart"):+        why = str(args.get("reason") or "").strip()+        if fallback:+            return {"success": False, "errorCode": "restart_refused_cursor",+                    "_hint": "Grok Bot.exe is missing and the host is Cursor: never kill Cursor. Nothing restarted."}+        if len(why) < _RESTART_REASON_MIN:+            return {"success": False, "errorCode": "reason_required",+                    "_hint": f"grok_open {{restart:true}} closes the user's Grok Bot window; pass reason "+                             f"(at least {_RESTART_REASON_MIN} characters) saying why.",+                    "_next": "grok_open {\"restart\":true,\"reason\":\"Grok Bot page invisible to UIA after its auto-update\"}"}+        restarted = _cb("process_kill", {"name": "Grok Bot.exe", "reason": why})+        time.sleep(3)+     hwnd0, win0, proc0 = _host_alive(info)     listed = handle_list_windows(args) if (hwnd0 or proc0) else None @@ -617,7 +668,8 @@ def handle_open(args: dict) -> dict:     if hwnd0 or proc0:         if want_fg and hwnd0 and len(reason) >= _FG_REASON_MIN:             _cb("desktop_set_window_state", {"hwnd": hwnd0, "state": "restore"})-        return _envelope(+        flags_ok = None if fallback else host_launch_flags_ok()+        env = _envelope(             success=True,             launched=False,             already_open=bool(hwnd0),@@ -631,11 +683,17 @@ def handle_open(args: dict) -> dict:                 "Grok Bot is already running — nothing launched, focus not stolen. "                 "hwnd/opened/alreadyOpen populated so callers can tell launched vs already up. "                 "To queue a message: grok_send. App control: grok_close / grok_resize / grok_screenshot."+                + (" " + RESTART_HINT if flags_ok is False else "")             ),         )+        env["launchedWithAccessibility"] = flags_ok+        if restarted is not None:+            env["restartKill"] = restarted+            env["_hint"] = "Restart asked, but Grok Bot was still running after the kill. " + env["_hint"]+        return env      launch_name = fallback if fallback else HOST_APP-    launch_args = {"background": True, "noActivate": True, "detached": True}+    launch_args = {"background": True, "noActivate": True, "detached": True, "args": list(HOST_FLAGS)}     if exe:         launch_args["path"] = exe     else:@@ -656,7 +714,7 @@ def handle_open(args: dict) -> dict:      start_fb = None     if not hwnd and not proc:-        start_fb = _start_process_minimized(exe or launch_name)+        start_fb = _start_process_minimized(exe or launch_name, HOST_FLAGS)         via = start_fb.get("via") or "Start-Process -WindowStyle Minimized"         launched_cb = start_fb         hwnd, win, proc = _poll_host(info, timeout=20)@@ -713,6 +771,9 @@ def handle_open(args: dict) -> dict:         host_running=really_up,         extra_hint=hint,     )+    out["launchedWithAccessibility"] = True if via != "desktop_open" else None+    if restarted is not None:+        out["restarted"] = True     if not really_up:         out["errorCode"] = "launch_did_not_start"         out["_next"] = "Retry grok_open {}; confirm Grok Bot.exe path via grok_readiness."