main
John Lauer v1.3.8 (insiders): Hidden Grok Bot window: chat verbs read it in place, grok_open no longer re-shows it on its own (sho… 0270b3a 5d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921
"""Window verbs: grok_open / grok_screenshot / grok_list_windows via ab desktop_* callbacks.

cliRequired verbs (shell_execute, pull_file, send_files) are NEVER used from this
HTTP callback. All driving goes through caller_identity.ad_callback.
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
import time

import caller_identity
from grok_detect import (
    HOST_APP,
    detect_host,
    process_name_filter,
    window_title_needles,
    _cursor_running,
    _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

# 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."""
    return caller_identity.ad_callback(command, args or {}, on_behalf=True, timeout=timeout)


def _callback_ok(res: dict | None) -> bool:
    if not isinstance(res, dict):
        return False
    if res.get("success") is False:
        return False
    err = (res.get("error") or "")
    if "not in env" in err or "callback failed" in err.lower():
        return False
    return True


def _parse_output_field(blob: dict):
    """ad_callback may wrap the real payload in blob['output'] (JSON object/array)."""
    raw = blob.get("output")
    if isinstance(raw, (dict, list)):
        return raw
    if not isinstance(raw, str):
        return None
    s = raw.strip()
    if not s or s[0] not in "{[":
        return None
    try:
        parsed = json.loads(s)
    except json.JSONDecodeError:
        return None
    if isinstance(parsed, (dict, list)):
        return parsed
    return None


def flatten_callback(blob):
    """Unwrap ad_callback wrappers: output JSON string, then data dict.

    1. json.loads(blob['output']) when it is a JSON object/array and merge
    2. unwrap blob['data'] if dict
    """
    if isinstance(blob, list):
        return blob
    if not isinstance(blob, dict):
        return blob
    merged = dict(blob)
    parsed = _parse_output_field(blob)
    if isinstance(parsed, list):
        return parsed
    if isinstance(parsed, dict):
        merged.update(parsed)
    data = merged.get("data")
    if isinstance(data, dict):
        inner = dict(merged)
        inner.update(data)
        merged = inner
    elif isinstance(data, list):
        return data
    return merged


def _add_unique_dicts(out: list[dict], seen: set, items) -> None:
    for item in items or []:
        if not isinstance(item, dict):
            continue
        h = item.get("hwnd")
        if h is None:
            h = item.get("handle")
        pid = item.get("pid") if item.get("pid") is not None else item.get("id")
        try:
            if h is not None:
                key = ("hwnd", int(h))
            elif pid is not None:
                key = ("pid", int(pid) if str(pid).lstrip("-").isdigit() else pid)
            else:
                key = ("obj", id(item), str(item.get("title") or item.get("name") or ""))
        except (TypeError, ValueError):
            key = ("obj", id(item), str(item.get("title") or item.get("name") or ""))
        if key in seen:
            continue
        seen.add(key)
        out.append(item)


def _as_windows(blob) -> list[dict]:
    """Collect window dicts from a desktop_list_windows / desktop_find_window blob.

    Unwraps output JSON + data dict. Collects windows/items/result lists AND
    blob['best'] when it has hwnd/title (find_window shape).
    """
    if isinstance(blob, list):
        return [w for w in blob if isinstance(w, dict)]
    if not isinstance(blob, dict):
        return []
    parsed = _parse_output_field(blob)
    if isinstance(parsed, list):
        windows = [w for w in parsed if isinstance(w, dict)]
        best = blob.get("best")
        if isinstance(best, dict) and (best.get("hwnd") or best.get("title") or best.get("handle")):
            windows.append(best)
        return windows

    merged = flatten_callback(blob)
    if isinstance(merged, list):
        return [w for w in merged if isinstance(w, dict)]
    if not isinstance(merged, dict):
        return []

    out: list[dict] = []
    seen: set = set()
    for key in ("windows", "items", "result"):
        val = merged.get(key)
        if isinstance(val, list):
            _add_unique_dicts(out, seen, val)
    data = merged.get("data")
    if isinstance(data, list):
        _add_unique_dicts(out, seen, data)
    best = merged.get("best")
    if isinstance(best, dict) and (best.get("hwnd") or best.get("title") or best.get("handle")):
        _add_unique_dicts(out, seen, [best])
    if not out and (merged.get("hwnd") or merged.get("title") or merged.get("handle")):
        _add_unique_dicts(out, seen, [merged])
    return out


def _as_processes(blob) -> list[dict]:
    """Same unwrap as _as_windows: output JSON, data dict, processes/items/result, best."""
    if isinstance(blob, list):
        return [p for p in blob if isinstance(p, dict)]
    if not isinstance(blob, dict):
        return []
    parsed = _parse_output_field(blob)
    if isinstance(parsed, list):
        return [p for p in parsed if isinstance(p, dict)]

    merged = flatten_callback(blob)
    if isinstance(merged, list):
        return [p for p in merged if isinstance(p, dict)]
    if not isinstance(merged, dict):
        return []

    out: list[dict] = []
    seen: set = set()
    for key in ("processes", "items", "result"):
        val = merged.get(key)
        if isinstance(val, list):
            _add_unique_dicts(out, seen, val)
    data = merged.get("data")
    if isinstance(data, list):
        _add_unique_dicts(out, seen, data)
    best = merged.get("best")
    if isinstance(best, dict) and (best.get("pid") or best.get("name") or best.get("imageName")):
        _add_unique_dicts(out, seen, [best])
    return out


def _hwnd_of(win: dict | None):
    if not isinstance(win, dict):
        return None
    h = win.get("hwnd") or win.get("handle")
    if h is None:
        return None
    try:
        return int(h)
    except (TypeError, ValueError):
        return None


def _title_of(win: dict | None) -> str:
    if not isinstance(win, dict):
        return ""
    return str(win.get("title") or win.get("name") or "")


# Browser windows whose tab title mentions Grok Bot (the sign-in pages "Sign in to
# Grok Bot", "Return to Grok Bot", the wiki page) are NOT the host app.
_BROWSER_MARKERS = (
    "google chrome", "microsoft edge", "microsoft\u200b edge", "mozilla firefox",
    "brave", "opera", "vivaldi", "chrome for testing", "ai-grok-bot",
)


def _is_host_impostor(title: str) -> bool:
    """Any browser window (Chrome / Edge / Firefox / ...) or ai-grok-bot is NOT the Grok Bot host."""
    tl = (title or "").lower()
    return any(m in tl for m in _BROWSER_MARKERS)


def _title_rank(title: str) -> int:
    tl = (title or "").strip().lower()
    if _is_host_impostor(title):
        return 9
    if tl == "grok bot":
        return 0
    if "grok bot" in tl:
        return 1
    if "cursor" in tl:
        return 2
    return 9


def _dedupe_windows(windows: list[dict]) -> list[dict]:
    seen = set()
    out = []
    for w in windows:
        h = _hwnd_of(w)
        key = h if h is not None else (id(w), _title_of(w))
        if key in seen:
            continue
        seen.add(key)
        out.append(w)
    return out


def collect_grok_windows(title_contains: str | None = None) -> tuple[list[dict], bool]:
    """List Grok Bot windows. Prefer desktop_find_window (best.hwnd) over list_windows.

    titleContains: exact "Grok Bot" first. Bare "Grok" is too greedy (Chrome "ai-grok-bot").
    """
    info = detect_host()
    needles = [title_contains] if (title_contains and str(title_contains).strip()) else window_title_needles(info)
    windows: list[dict] = []
    ab_up = False

    def _ingest(blob) -> None:
        nonlocal ab_up
        ab_up = ab_up or _callback_ok(blob)
        windows.extend(_as_windows(blob))

    def _hosts() -> list[dict]:
        # Title must actually name the host: ab's desktop_list_windows can ignore
        # titleContains and return every window on the desktop.
        return [w for w in windows
                if not _is_host_impostor(_title_of(w)) and _title_rank(_title_of(w)) < 9]

    for needle in needles:
        needle = (needle or "").strip()
        if not needle:
            continue
        _ingest(_cb("desktop_find_window", {"titleContains": needle}))
        if _hosts():
            break
    if not _hosts():
        for needle in needles:
            needle = (needle or "").strip()
            if not needle:
                continue
            _ingest(_cb("desktop_list_windows", {"titleContains": needle}))
            if _hosts():
                break
    filtered = _hosts()
    filtered.sort(key=lambda w: _title_rank(_title_of(w)))
    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
        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], 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]:
    win = pick_grok_window(allow_cursor=allow_cursor)
    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)."""
    hidden = bool(LAST_PICK.get("mainWindowHidden"))
    if not hidden and remembered_main_hwnd():
        try:
            hidden = bool(_grok_bot_running())
        except Exception:
            hidden = False
    return {"mainWindowHidden": hidden,
            "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. 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
    _cb("desktop_set_window_state", {"hwnd": h, "state": "restore"})
    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):
        return {}
    rect = win.get("rect") or win.get("windowRect") or win.get("bounds") or {}
    if not isinstance(rect, dict):
        rect = {}
    x = rect.get("x", rect.get("left", win.get("x")))
    y = rect.get("y", rect.get("top", win.get("y")))
    w = rect.get("w", rect.get("width", win.get("width")))
    h = rect.get("h", rect.get("height", win.get("height")))
    out = {}
    for k, v in (("x", x), ("y", y), ("w", w), ("h", h)):
        if v is None:
            continue
        try:
            out[k] = int(v)
        except (TypeError, ValueError):
            continue
    return out


def window_is_maximized(win: dict | None) -> bool:
    if not isinstance(win, dict):
        return False
    if win.get("maximized") or win.get("isMaximized"):
        return True
    state = str(win.get("state") or win.get("showState") or win.get("showCmd") or "").lower()
    return state in ("maximized", "maximize", "zoomed")


def window_is_minimized(win: dict | None) -> bool:
    if not isinstance(win, dict):
        return False
    if win.get("minimized") or win.get("isMinimized"):
        return True
    state = str(win.get("state") or win.get("showState") or win.get("showCmd") or "").lower()
    if state in ("minimized", "minimize", "hidden", "hide"):
        return True
    rect = window_rect(win)
    return rect.get("x", 0) <= -30000 or rect.get("y", 0) <= -30000


def launch_host(argv: list | None = None) -> dict:
    """Launch Grok Bot.exe (background). Optional argv (e.g. a grokbot:// deep link).

    desktop_open_url refuses grokbot:// (http/https/mailto/ftp only), so deep links
    are passed as arguments to the host exe instead.
    """
    info = detect_host()
    exe = info.get("exe")
    launch_args = {"background": True, "noActivate": True, "detached": True,
                   "args": HOST_FLAGS + list(argv or [])}
    if exe:
        launch_args["path"] = exe
    else:
        launch_args["name"] = HOST_APP
    return _cb("launch", launch_args)


CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
_POLL_SECONDS = 12
_POLL_INTERVAL = 1.2
_FG_REASON_MIN = 10


def _ps_quote(s: str) -> str:
    return "'" + str(s).replace("'", "''") + "'"


def _host_alive(info: dict | None = None) -> tuple:
    """Return (hwnd, win, process_up) without stealing focus."""
    info = info or detect_host()
    allow = bool(info.get("hostAppFallback"))
    hwnd, win = grok_hwnd(allow_cursor=allow)
    if hwnd:
        return hwnd, win, True
    filt = process_name_filter(info)
    procs = _as_processes(_cb("process_list", {"nameFilter": filt}))
    if procs:
        return None, None, True
    if filt != "Grok Bot":
        extra = _as_processes(_cb("process_list", {"nameFilter": "Grok Bot"}))
        if extra:
            return None, None, True
    if info.get("hostAppFallback"):
        running = _cursor_running()
    else:
        running = _grok_bot_running()
    return None, None, bool(running)


def _poll_host(info: dict, timeout: float = _POLL_SECONDS) -> tuple:
    deadline = time.time() + timeout
    last = (None, None, False)
    while time.time() < deadline:
        last = _host_alive(info)
        if last[0] or last[2]:
            return last
        time.sleep(_POLL_INTERVAL)
    return last


def _start_process_minimized(exe: str, extra_args: list | None = None) -> dict:
    """Windows Start-Process -WindowStyle Minimized. Never shell_execute. Never Cursor unless that is the last-resort exe."""
    if not exe:
        return {"success": False, "error": "no exe path"}
    if sys.platform != "win32":
        return {"success": False, "via": "Start-Process", "error": "Start-Process fallback is Windows-only"}
    cmd = (
        "$p = Start-Process -FilePath " + _ps_quote(exe)
        + " -WindowStyle Minimized -PassThru"
    )
    if extra_args:
        quoted = ",".join(_ps_quote(a) for a in extra_args)
        cmd = (
            "$p = Start-Process -FilePath " + _ps_quote(exe)
            + " -ArgumentList " + quoted
            + " -WindowStyle Minimized -PassThru"
        )
    cmd += "; if ($p) { Write-Output ('pid=' + $p.Id) } else { Write-Output 'no-process' }"
    try:
        r = subprocess.run(
            ["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd],
            capture_output=True, text=True, timeout=20,
            creationflags=CREATE_NO_WINDOW,
        )
        out = ((r.stdout or "") + (r.stderr or "")).strip()
        ok = r.returncode == 0 and "pid=" in out.lower()
        return {
            "success": ok or r.returncode == 0,
            "via": "Start-Process -WindowStyle Minimized",
            "returncode": r.returncode,
            "outputPreview": out[:300],
        }
    except Exception as e:
        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()
    windows, listed_ok = collect_grok_windows(title or None)
    filt = process_name_filter(info)
    procs = _cb("process_list", {"nameFilter": filt})
    grok_procs = _cb("process_list", {"nameFilter": "Grok Bot"}) if filt != "Grok Bot" else None
    processes = _as_processes(procs)
    if grok_procs:
        extra = _as_processes(grok_procs)
        seen = {p.get("pid") or p.get("id") for p in processes}
        for p in extra:
            key = p.get("pid") or p.get("id")
            if key not in seen:
                processes.append(p)
                seen.add(key)

    ab_up = listed_ok or _callback_ok(procs)
    fallback = info.get("hostAppFallback")
    empty_hint = (
        "Empty? Grok Bot likely isn't running — grok_open launches it in the background."
        if not windows else
        "To capture one, grok_screenshot {}. To queue a message, grok_send. "
        "App control: grok_close / grok_resize / grok_record_start."
    )
    fallback_hint = ""
    if fallback:
        fallback_hint = (
            " Grok Bot.exe was not found; Cursor is a last-resort fallback host. Never kill Cursor."
        )
    return {
        "success": True,
        "hostApp": HOST_APP,
        "hostAppFallback": fallback,
        "windows": windows,
        "processes": processes,
        "count": len(windows),
        "abCallbackReached": ab_up,
        "windowEtiquette": "background",
        "_hint": (
            "Listed via ab desktop_find_window (preferred; best.hwnd) then desktop_list_windows, "
            "titleContains exact Grok Bot (not bare Grok / Chrome ai-grok-bot), and "
            f"process_list nameFilter {filt!r}. Read-only — no focus steal. "
            + empty_hint
            + fallback_hint
            + ("" if ab_up else " ab loopback (ADOM_DIRECT_API_URL) was not reachable — this process can still detect Grok Bot on disk.")
        ),
    }


def handle_screenshot(args: dict) -> dict:
    title = (args.get("titleContains") or "").strip()
    info = detect_host()
    # 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})
        if _callback_ok(last) and not last.get("error"):
            shot = dict(last)
            shot.setdefault("success", True)
            shot.setdefault("titleContains", needle)
            shot.setdefault("windowEtiquette", "background")
            shot.setdefault("openedInBackground", True)
            shot["_hint"] = (
                f"Screenshot via ab desktop_screenshot_window titleContains={needle!r}. "
                "Window was NOT raised. PNG is on the desktop; use the adom-bridge CLI "
                "to pull it — this HTTP callback never calls pull_file / send_files / shell_execute. "
                + (shot.get("_hint") or "")
            )
            return shot
    hwnd, win = grok_hwnd(allow_cursor=bool(info.get("hostAppFallback")))
    if hwnd:
        last = _cb("desktop_screenshot_window", {"hwnd": hwnd})
        if _callback_ok(last) and not last.get("error"):
            shot = dict(last)
            shot.setdefault("success", True)
            shot.setdefault("hwnd", hwnd)
            shot.setdefault("windowEtiquette", "background")
            shot.setdefault("openedInBackground", True)
            shot["_hint"] = (
                f"Screenshot via ab desktop_screenshot_window hwnd={hwnd}. "
                "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": ("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 {
        "success": False,
        "errorCode": "window_not_found",
        "error": "No Grok Bot window to screenshot.",
        "lastCallback": last,
        "hostAppFallback": info.get("hostAppFallback"),
        "_hint": (
            "desktop_screenshot_window found no titleContains Grok Bot or Grok. "
            "Call grok_open to launch Grok Bot in the background, then grok_list_windows, then retry. "
            "Do not steal focus to take the shot."
            + (
                " Cursor is only a last-resort fallback if Grok Bot.exe is missing."
                if info.get("hostAppFallback") else ""
            )
        ),
        "_next": "grok_open {} then grok_list_windows {} then grok_screenshot {}",
    }


def handle_open(args: dict) -> dict:
    """Launch Grok Bot if installed; if running, list windows. Background by default.

    Poll process_list / hwnd after launch. Fall back to Start-Process -WindowStyle
    Minimized when ab launch / desktop_open report success but no process appears.
    Never shell_execute. Never kill Cursor.
    """
    info = detect_host()
    installed = bool(info.get("hostAppInstalled"))
    exe = info.get("exe")
    fallback = info.get("hostAppFallback")
    want_fg = bool(args.get("foreground") or args.get("showToUser"))
    reason = (args.get("foregroundReason") or args.get("reason") or "").strip()

    if want_fg and len(reason) < _FG_REASON_MIN:
        return {
            "success": False,
            "errorCode": "foreground_reason_required",
            "hwnd": None,
            "opened": False,
            "alreadyOpen": False,
            "launched": False,
            "windowEtiquette": "background",
            "openedInBackground": True,
            "_hint": (
                "foreground requested without a foregroundReason of at least "
                f"{_FG_REASON_MIN} characters — pass foreground:true AND "
                "foregroundReason:'<why the user needs to see this>'. "
                "Default is BACKGROUND so Grok Bot never steals the keyboard."
            ),
            "_next": "Re-call grok_open {\"foreground\":true,\"foregroundReason\":\"...\"} or omit foreground.",
        }

    fallback_note = (
        " Grok Bot.exe was not found; Cursor is a LAST-RESORT fallback host. Prefer Grok Bot."
        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)
    hidden_fix = None
    if proc0 and not hwnd0 and not fallback and restarted is None:
        hidden_state = main_window_state()
        # 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
    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=""):
        title = _title_of(win) if win else None
        etiq = "foreground" if (want_fg and len(reason) >= _FG_REASON_MIN) else "background"
        out = {
            "success": success,
            "alreadyRunning": bool(already_open or (proc0 and not launched)),
            "alreadyOpen": bool(already_open),
            "opened": bool(opened),
            "launched": bool(launched),
            "hwnd": hwnd,
            "title": title,
            "via": via,
            "hostApp": HOST_APP,
            "hostAppInstalled": installed,
            "hostAppRunning": bool(host_running),
            "hostAppFallback": fallback,
            "exe": exe,
            "windows": (listed or handle_list_windows(args)).get("windows") or [] if (hwnd or host_running) else [],
            "openedInBackground": etiq == "background",
            "windowEtiquette": etiq,
        }
        if callback is not None:
            out["callback"] = callback
        if not success:
            out["errorCode"] = out.get("errorCode") or "launch_did_not_start"
        out["_hint"] = extra_hint + fallback_note
        out["_foregroundHint"] = (
            "Window is in the BACKGROUND. To show it: grok_open "
            "{\"foreground\":true,\"foregroundReason\":\"<why the user needs to see this>\"}."
            if etiq == "background" else
            "Raised because foregroundReason was provided."
        )
        return out

    if hwnd0 or proc0:
        if want_fg and hwnd0 and len(reason) >= _FG_REASON_MIN:
            _cb("desktop_set_window_state", {"hwnd": hwnd0, "state": "restore"})
        flags_ok = None if fallback else host_launch_flags_ok()
        env = _envelope(
            success=True,
            launched=False,
            already_open=bool(hwnd0),
            opened=bool(hwnd0),
            hwnd=hwnd0,
            win=win0,
            via="already-open",
            callback=None,
            host_running=True,
            extra_hint=(
                "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 hidden_fix is not None:
            env.update({k: hidden_fix[k] for k in ("mainWindowHidden", "strayWindows", "rememberedMainHwnd")})
            env["shownHiddenMain"] = hidden_fix["shown"]
            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"]
        return env

    launch_name = fallback if fallback else HOST_APP
    launch_args = {"background": True, "noActivate": True, "detached": True, "args": list(HOST_FLAGS)}
    if exe:
        launch_args["path"] = exe
    else:
        launch_args["name"] = launch_name

    launched_cb = _cb("launch", launch_args)
    via = "launch"
    hwnd, win, proc = _poll_host(info, timeout=_POLL_SECONDS)
    if not hwnd and not proc:
        open_args = {"background": True}
        if exe:
            open_args["path"] = exe
        else:
            open_args["target"] = launch_name
        launched_cb = _cb("desktop_open", open_args)
        via = "desktop_open"
        hwnd, win, proc = _poll_host(info, timeout=_POLL_SECONDS)

    start_fb = None
    if not hwnd and not proc:
        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)

    if want_fg and hwnd and len(reason) >= _FG_REASON_MIN:
        _cb("desktop_set_window_state", {"hwnd": hwnd, "state": "restore"})

    really_up = bool(hwnd or proc)
    if not installed and not really_up:
        return {
            "success": False,
            "errorCode": "host_app_not_installed",
            "hwnd": None,
            "opened": False,
            "alreadyOpen": False,
            "launched": False,
            "hostApp": HOST_APP,
            "hostAppInstalled": False,
            "hostAppRunning": False,
            "hostAppFallback": fallback,
            "via": via,
            "callback": launched_cb,
            "openedInBackground": True,
            "windowEtiquette": "background",
            "_hint": (
                "Grok Bot isn't installed — want me to open it if present? "
                "Tried ab launch / desktop_open then Start-Process minimized "
                "(typical path C:\\\\Users\\\\john\\\\AppData\\\\Local\\\\Programs\\\\Grok Bot\\\\Grok Bot.exe). "
                "Cursor is a last-resort fallback only if Grok Bot.exe is missing. "
                "Do not send the user to download the host app themselves."
            ),
            "_next": "Retry grok_open after Grok Bot exists. Cursor fallback only if Grok Bot.exe is missing.",
        }

    hint = (
        f"Asked ab then polled process_list/window (via={via}). "
        + ("Process/window appeared. " if really_up else "NO process after launch/desktop_open poll + Start-Process minimized fallback. ")
        + ("Cursor last-resort fallback. " if fallback else "Grok Bot ")
        + (f"at {exe} " if exe else "(by name) ")
        + "in the BACKGROUND. Poll grok_list_windows / grok_chat_read; then grok_screenshot or grok_send."
    )
    if start_fb is not None:
        hint += " Start-Process -WindowStyle Minimized used because launch/desktop_open did not take."

    out = _envelope(
        success=really_up,
        launched=really_up,
        already_open=False,
        opened=bool(hwnd),
        hwnd=hwnd,
        win=win,
        via=via,
        callback=launched_cb,
        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."
    return out