← Commit history

v1.3.8 (insiders): Hidden Grok Bot window: chat verbs read it in place, grok_open no longer re-shows it on its own (showing takes focus), only with foreground and a reason.

John Lauer ·0270b3ac82 ·5d ago ·parent 1077a78
6 files changed +59−29
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.3.7\ No newline at end of file+1.3.8\ No newline at end of file
CLAUDE.md+1−1
@@ -71,7 +71,7 @@ auto-update relaunch drops them; `host_launch_flags_ok()` spots that and `grok_o  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+when hidden; chat verbs read it in place by hwnd (`read_target_hwnd`), and `grok_open` re-shows it only with foreground+foregroundReason (ab's `show` activates; there is no show-without-activate). `grok_webhook_fire {requireApproval, approvalSummary}` waits on a notify_user Allow/Deny toast and stamps `payload.approval`; caller-supplied approval fields are always stripped. 
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "grok",   "displayName": "Grok Bot",-  "version": "1.3.7",+  "version": "1.3.8",   "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+18−10
@@ -19,6 +19,7 @@ from handlers.windows import (     grok_hwnd,     host_launch_flags_ok,     main_window_state,+    read_target_hwnd, )  # Proven UIA on Grok Bot.exe hwnd (AdomLapper): Document named "Grok Bot",@@ -182,8 +183,9 @@ def _window_missing(verb: str) -> dict:                "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} {{}}"}+                        "Its hwnd is not remembered on this PC, so it cannot be read in place. "+                        "grok_open {\"restart\":true,\"reason\":\"...\"} relaunches it in the background.",+               "_next": "grok_open {\"restart\":true,\"reason\":\"...\"}"}         out.update(ms)         return out     return {@@ -424,9 +426,9 @@ def _select_sidebar_thread(hwnd: int, name: str) -> dict: def handle_chat_status(_args: dict) -> dict:      """thinking=true when UIA exposes '<agent> is working' / working chip. Never steals focus."""-    hwnd, win = grok_hwnd(allow_cursor=False)+    hwnd, win, hidden = read_target_hwnd(allow_cursor=False)     if not hwnd:-        hwnd, win = grok_hwnd(allow_cursor=True)+        hwnd, win, hidden = read_target_hwnd(allow_cursor=True)     if not hwnd:         return _window_missing("chat_status") @@ -458,7 +460,11 @@ def handle_chat_status(_args: dict) -> dict:         ),     }     if agent is None:-        out["focusedThreadReason"] = "no conversation heading on the page"+        out["focusedThreadReason"] = ("main window is hidden: the heading and the working chip are not "+                                      "readable until it is shown" if hidden else "no conversation heading on the page")+    out["mainWindowHidden"] = hidden+    if hidden:+        out["thinking"] = None; out["idle"] = None     out.update(computer_fields(probe["state"], probe["banner"]))     return out @@ -473,9 +479,9 @@ def handle_chat_read(args: dict) -> dict:     Always returns focusedThread (heading / agent name). After cold launch the focused     thread may not be grok-bridge — callers should pass thread/agent or wait after grok_open.     """-    hwnd, win = grok_hwnd(allow_cursor=False)+    hwnd, win, hidden = read_target_hwnd(allow_cursor=False)     if not hwnd:-        hwnd, win = grok_hwnd(allow_cursor=True)+        hwnd, win, hidden = read_target_hwnd(allow_cursor=True)     if not hwnd:         miss = _window_missing("chat_read")         miss["text"] = ""@@ -490,7 +496,7 @@ def handle_chat_read(args: dict) -> dict:      wanted = str(args.get("thread") or args.get("agent") or "").strip()     select_attempt = None-    if wanted:+    if wanted and not hidden:         select_attempt = _select_sidebar_thread(hwnd, wanted)      thinking, working_name = _find_working(hwnd)@@ -504,7 +510,7 @@ def handle_chat_read(args: dict) -> dict:     # transcript is empty/tiny, try the grok-bridge sidebar row once.     tiny = len((raw or "").strip()) < 40     agent_l = (agent or "").strip().lower()-    if not wanted and tiny and "grok-bridge" not in agent_l and "grok bridge" not in agent_l:+    if not hidden and not wanted and tiny and "grok-bridge" not in agent_l and "grok bridge" not in agent_l:         select_attempt = _select_sidebar_thread(hwnd, "grok-bridge")         thinking, working_name = _find_working(hwnd)         agent = _read_agent_name(hwnd, working_name)@@ -602,8 +608,10 @@ def handle_chat_read(args: dict) -> dict:     }     out.update(limit_meta)     out.update(computer_fields(*computer_state_from_text(raw)))+    out["mainWindowHidden"] = hidden     if focused is None:-        out["focusedThreadReason"] = "no conversation heading on the page"+        out["focusedThreadReason"] = ("main window is hidden; read in place (thread switching needs it shown)"+                                      if hidden else "no conversation heading on the page")     if err and not text.strip():         out["success"] = False         out["errorCode"] = err
handlers/describe.py+1−1
@@ -53,7 +53,7 @@ VERB_CATALOG = {     "screenshot": {         "summary": "Screenshot a Grok Bot window via ab desktop_screenshot_window.",         "long": False,-        "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.",+        "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 the main window is hidden (a hidden window cannot be captured; showing it takes focus, so only grok_open {foreground:true, foregroundReason}). grok_chat_read still reads it hidden. 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.",
handlers/windows.py+37−15
@@ -371,6 +371,19 @@ def grok_hwnd(*, allow_cursor: bool = True) -> tuple[int | None, dict | None]:     return _hwnd_of(win), win  +def read_target_hwnd(*, allow_cursor: bool = True) -> tuple[int | None, dict | None, bool]:+    """(hwnd, win, hidden) for READ verbs: the visible main window, else the remembered hidden+    main window (its page text stays readable through the hwnd while hidden)."""+    hwnd, win = grok_hwnd(allow_cursor=allow_cursor)+    if hwnd:+        return hwnd, win, False+    if main_window_state()["mainWindowHidden"]:+        h = remembered_main_hwnd()+        if h:+            return h, {"hwnd": h, "title": HOST_APP}, True+    return None, None, False++ def main_window_state() -> dict:     """Fields for callers when no main window is visible. Hidden = only strays visible, or     no window at all while Grok Bot runs and a main hwnd is remembered (grok_close hides it)."""@@ -386,18 +399,15 @@ def main_window_state() -> dict:   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)."""+    """Re-show the remembered main window. This ACTIVATES it: callers gate on foregroundReason."""     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-    # show is asynchronous: a bottom sent at once lands before the window is up and it-    # surfaces just under the foreground app. Wait, send it to the back, then once more.-    for _ in range(2):-        time.sleep(0.5)-        _cb("desktop_set_window_state", {"hwnd": h, "state": "bottom"})+    _cb("desktop_set_window_state", {"hwnd": h, "state": "restore"})+    time.sleep(0.5)     hwnd, win = grok_hwnd(allow_cursor=False)     return hwnd, win, res @@ -656,9 +666,10 @@ def handle_screenshot(args: dict) -> dict:     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 {}"}+               "_hint": ("A hidden window cannot be captured, and a stray strip is not Grok Bot. Showing "+                         "it takes focus, so only if the user needs it: grok_open {\"foreground\":true,"+                         "\"foregroundReason\":\"...\"}, then grok_screenshot. grok_chat_read still reads it hidden."),+               "_next": "grok_chat_read {} (works hidden), or grok_open {\"foreground\":true,\"foregroundReason\":\"...\"}"}         out.update(ms)         return out     return {@@ -736,7 +747,12 @@ def handle_open(args: dict) -> dict:     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()+        # Showing a hidden window activates it (ab has no show-without-activate), so it+        # is a foreground action: only with foreground:true + foregroundReason.+        if want_fg and len(reason) >= _FG_REASON_MIN:+            h2, w2, res = show_hidden_main()+        else:+            h2, w2, res = None, None, None         hidden_fix = dict(hidden_state, shown=bool(h2), showCallback=res)         if h2:             hwnd0, win0 = h2, w2@@ -801,11 +817,17 @@ def handle_open(args: dict) -> dict:         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 hidden_fix["shown"]:+                env["_hint"] = "Grok Bot's main window was hidden; shown again because foregroundReason was given. " + env["_hint"]+            elif hidden_fix.get("rememberedMainHwnd"):+                env["_hint"] = ("Grok Bot is running with its main window HIDDEN (not shown: showing it takes "+                                "focus). grok_chat_read / grok_chat_status still read it in place. To show it: "+                                "grok_open {\"foreground\":true,\"foregroundReason\":\"<why the user needs to see it>\"}. " + env["_hint"])+                env["_next"] = "grok_open {\"foreground\":true,\"foregroundReason\":\"...\"} only if the user needs to see it"+            else:+                env["_hint"] = ("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"]