main
John Lauer grok-bridge 1.3.1: browser windows never count as the host app, grok_settings verb, grok_login presses Sign in, sign-in flow documented 7e59f9f 6d ago
"""App-window verbs: close / resize / record / login / logout / settings / plugin_install.

All driving is caller_identity.ad_callback (desktop_*). Never shell_execute /
pull_file / send_files. Prefer hide over process_kill. Never kill Cursor.
Background by default. Never invent grokbot:// routes other than
settings?id=* and plugin/add?id=.
"""

from __future__ import annotations

from urllib.parse import quote

from grok_detect import HOST_APP, detect_host
from handlers.windows import (
    _cb,
    _callback_ok,
    _hwnd_of,
    grok_hwnd,
    launch_host,
    pick_grok_window,
    window_is_maximized,
    window_is_minimized,
    window_rect,
)

SETTINGS_DEEPLINK = "grokbot://app/v1/settings?id=theme"
PLUGIN_ADD_DEEPLINK = "grokbot://app/v1/plugin/add?id="


def _window_missing(verb: str) -> dict:
    info = detect_host()
    return {
        "success": False,
        "errorCode": "window_not_found",
        "error": "No Grok Bot window.",
        "hostApp": HOST_APP,
        "hostAppFallback": info.get("hostAppFallback"),
        "_hint": (
            "No Grok Bot hwnd (titleContains Grok Bot, then Grok). "
            "Call grok_open {} to launch Grok Bot in the background, then retry "
            f"{verb}. Never kill Cursor."
            + (
                " Grok Bot.exe was not found; Cursor is last-resort fallback and will not be closed."
                if info.get("hostAppFallback") else ""
            )
        ),
        "_next": f"grok_open {{}} then grok_{verb}",
    }


def handle_close(_args: dict) -> dict:
    """Hide the Grok Bot window. Prefer hide over process_kill. Never kill Cursor."""
    # Do NOT target a generic Cursor window — that is the user's IDE on fallback.
    hwnd, win = grok_hwnd(allow_cursor=False)
    if not hwnd:
        miss = _window_missing("close")
        miss["closed"] = False
        return miss

    hide = _cb("desktop_set_window_state", {"hwnd": hwnd, "state": "hide"})
    if _callback_ok(hide):
        return {
            "success": True,
            "closed": True,
            "via": "desktop_set_window_state hide",
            "hwnd": hwnd,
            "title": (win or {}).get("title"),
            "killedProcess": False,
            "windowEtiquette": "background",
            "_hint": (
                f"Hid Grok Bot hwnd={hwnd} via desktop_set_window_state state=hide. "
                "Did not kill the process and did not touch Cursor. grok_open to show it again."
            ),
        }

    # Hide failed — restore (if needed) then try a UIA Close without process_kill.
    if window_is_minimized(win) or window_is_maximized(win):
        _cb("desktop_set_window_state", {"hwnd": hwnd, "state": "restore"})
    click = _cb("desktop_ui_click", {"hwnd": hwnd, "name": "Close"})
    if _callback_ok(click):
        return {
            "success": True,
            "closed": True,
            "via": "desktop_ui_click Close",
            "hwnd": hwnd,
            "hideAttempt": hide,
            "killedProcess": False,
            "windowEtiquette": "background",
            "_hint": (
                f"Hide failed; clicked UIA name=Close on Grok Bot hwnd={hwnd} in the background. "
                "Did not process_kill. Never kills Cursor."
            ),
        }

    return {
        "success": False,
        "errorCode": "close_failed",
        "closed": False,
        "hwnd": hwnd,
        "hideAttempt": hide,
        "clickAttempt": click,
        "killedProcess": False,
        "_hint": (
            "Could not hide or UIA-Close the Grok Bot window. Not killing the process "
            "(prefer hide over process_kill; never kill Cursor). "
            "Ask the human to close or hide Grok Bot, or retry grok_close after grok_list_windows."
        ),
        "_next": "grok_list_windows {} then retry grok_close {}",
    }


def _as_int(val, name: str):
    if val is None or val == "":
        return None
    try:
        return int(val)
    except (TypeError, ValueError):
        raise ValueError(name)


def handle_resize(args: dict) -> dict:
    """desktop_set_window_bounds on the Grok Bot hwnd. Physical px (same space as ab)."""
    try:
        w = _as_int(args.get("w") if args.get("w") is not None else args.get("width"), "w")
        h = _as_int(args.get("h") if args.get("h") is not None else args.get("height"), "h")
        x = _as_int(args.get("x") if args.get("x") is not None else args.get("left"), "x")
        y = _as_int(args.get("y") if args.get("y") is not None else args.get("top"), "y")
    except ValueError as e:
        return {
            "success": False,
            "errorCode": "invalid_bounds",
            "error": f"Invalid {e} — need integer physical px.",
            "_hint": "grok_resize {w, h, x?, y?} in ab physical pixels. w and h required.",
            "_next": "grok_resize {\"w\":1200,\"h\":800}",
        }
    if w is None or h is None:
        return {
            "success": False,
            "errorCode": "bounds_required",
            "error": "grok_resize needs {w, h}.",
            "_hint": "Pass w and h in ab physical px. x and y default to the window's current origin.",
            "_next": "grok_resize {\"w\":1200,\"h\":800}",
        }

    hwnd, win = grok_hwnd(allow_cursor=True)
    if not hwnd:
        return _window_missing("resize")

    if window_is_maximized(win) or window_is_minimized(win):
        _cb("desktop_set_window_state", {"hwnd": hwnd, "state": "restore"})
        # Re-read origin after restore so omitted x,y stay physical-correct.
        win = pick_grok_window(allow_cursor=True) or win
        hwnd = _hwnd_of(win) or hwnd

    cur = window_rect(win)
    if x is None:
        x = cur.get("x")
    if y is None:
        y = cur.get("y")

    bounds = {"hwnd": hwnd, "w": w, "h": h}
    if x is not None:
        bounds["x"] = x
    if y is not None:
        bounds["y"] = y
    # restore:false so MoveWindow does not raise (pup lesson: bounds can steal focus).
    bounds["restore"] = False

    res = _cb("desktop_set_window_bounds", bounds)
    ok = _callback_ok(res)
    if ok:
        hint = (
            f"desktop_set_window_bounds hwnd={hwnd} w={w} h={h}"
            + (f" x={x}" if x is not None else " (kept current x)")
            + (f" y={y}" if y is not None else " (kept current y)")
            + " in ab physical px. Restored first if maximized. Background — did not steal focus."
        )
    else:
        hint = (
            "desktop_set_window_bounds failed. grok_list_windows to confirm the hwnd, then retry. "
            "w/h/x/y are physical px (same space as ab)."
        )
    out = {
        "success": ok,
        "hwnd": hwnd,
        "bounds": {k: bounds[k] for k in ("x", "y", "w", "h") if k in bounds},
        "callback": res,
        "windowEtiquette": "background",
        "_hint": hint,
    }
    if not ok:
        out["errorCode"] = "resize_failed"
        out["_next"] = "grok_list_windows {} then grok_resize {\"w\":...,\"h\":...}"
    return out


def handle_record_start(args: dict) -> dict:
    reason = (args.get("reason") or "").strip()
    if not reason:
        return {
            "success": False,
            "errorCode": "reason_required",
            "error": "grok_record_start needs {reason}.",
            "_hint": (
                "Pass reason (forwarded to desktop_record_window_start). "
                "Example: grok_record_start {\"reason\":\"demo of grok-bridge app control\"}."
            ),
            "_next": "grok_record_start {\"reason\":\"...\"}",
        }
    hwnd, win = grok_hwnd(allow_cursor=True)
    if not hwnd:
        return _window_missing("record_start")
    rec_args = {"hwnd": hwnd, "reason": reason}
    res = _cb("desktop_record_window_start", rec_args, timeout=45)
    ok = _callback_ok(res)
    out = dict(res) if isinstance(res, dict) else {"callback": res}
    out["success"] = ok
    out.setdefault("hwnd", hwnd)
    out.setdefault("reason", reason)
    out.setdefault("title", (win or {}).get("title"))
    out.setdefault("windowEtiquette", "background")
    if not ok:
        out.setdefault("errorCode", "record_start_failed")
        out["_next"] = "grok_open {} then grok_record_start {\"reason\":\"...\"}"
    out["_hint"] = (
        f"desktop_record_window_start on Grok Bot hwnd={hwnd} reason={reason!r}. "
        "Background WGC — no picker, no focus steal. Stop with grok_record_stop {}; "
        "poll grok_record_status {}. The mp4 lands on the desktop; pull it with the CLI "
        "(this HTTP callback never calls pull_file)."
        if ok else
        "desktop_record_window_start failed. Is Grok Bot visible to WGC? grok_open, then retry. "
        "reason is required and is passed through."
    )
    return out


def handle_record_stop(args: dict) -> dict:
    rec_args = {}
    for k in ("recordingId", "id"):
        if args.get(k):
            rec_args["recordingId"] = args[k]
            break
    res = _cb("desktop_record_window_stop", rec_args, timeout=60)
    ok = _callback_ok(res)
    out = dict(res) if isinstance(res, dict) else {"callback": res}
    out["success"] = ok
    if not ok:
        out.setdefault("errorCode", "record_stop_failed")
        out["_next"] = "grok_record_status {} then grok_record_stop {}"
    out["_hint"] = (
        "desktop_record_window_stop. Recording file is on the desktop "
        "(%TEMP%\\adom-bridge-recordings or the path in this payload). "
        "Pull it with the adom-bridge CLI — this HTTP callback never calls pull_file."
        if ok else
        "desktop_record_window_stop failed. grok_record_status to see if a recording is active."
    )
    return out


def handle_record_status(_args: dict) -> dict:
    res = _cb("desktop_record_window_status", {})
    ok = _callback_ok(res)
    out = dict(res) if isinstance(res, dict) else {"callback": res}
    out["success"] = ok if res else True
    out["_hint"] = (
        "desktop_record_window_status wrap. Start with grok_record_start {reason}; "
        "stop with grok_record_stop {}."
    )
    return out


def _open_deeplink(url: str) -> dict:
    """desktop_open_url refuses grokbot:// (http/https/mailto/ftp only). Launch the exe with the URL as an arg."""
    return launch_host([url])


SIGN_IN_STEPS = (
    "Sign-in is xAI's page ('Log into your Grok Bot account': Google / X / Apple / GitHub / email) in the OS "
    "default browser, then Cursor's 'Enable data sharing' (leave Share Data off, press Continue), then "
    "'Sign in to Grok Bot' with the account (press Sign in), then the browser's 'This site is trying to open "
    "Grok Bot' prompt (press Open). Account-chooser rows are not UIA-invokable: use a foregrounded "
    "desktop_click on the row only when the PC is idle (desktop_user_idle_ms). Never type passwords. "
    "Afterwards the app may say 'Setting up Grok Bot's computer', then 'having trouble connecting' or "
    "'Couldn't load Bots' for several minutes; wait, restart the app once, and only press Recover computer "
    "if the user says the computer's installed apps can go (it removes them)."
)


def handle_login(_args: dict) -> dict:
    """Start sign-in. Signed-out splash: press its Sign in button (background UIA). Never types passwords."""
    hwnd, _win = grok_hwnd(allow_cursor=False)
    pressed = False
    press_res = None
    if hwnd:
        press_res = _cb("desktop_ui_click", {"hwnd": hwnd, "name": "Sign in"})
        pressed = _callback_ok(press_res) and not (press_res or {}).get("error")
    if pressed:
        return {
            "success": True,
            "pressedSignIn": True,
            "hwnd": hwnd,
            "typedPassword": False,
            "windowEtiquette": "background",
            "_hint": "Pressed Sign in on the Grok Bot splash (background UIA). The app now says 'Continue in your browser'. " + SIGN_IN_STEPS,
            "_next": "desktop_list_windows {} to find the browser tab, then desktop_screenshot_window on it",
        }
    res = _open_deeplink(SETTINGS_DEEPLINK)
    ok = _callback_ok(res)
    return {
        "success": ok or True,  # opening settings is best-effort; the human signs in
        "pressedSignIn": False,
        "opened": ok,
        "url": SETTINGS_DEEPLINK,
        "background": True,
        "typedPassword": False,
        "callback": res,
        "windowEtiquette": "background",
        "_hint": (
            "No signed-out splash found (already signed in, or the window is not up: grok_open first). Opened "
            "Grok Bot settings via grokbot://app/v1/settings?id=theme in the BACKGROUND. " + SIGN_IN_STEPS
        ),
    }


SETTINGS_PAGES = {"general": "general", "computer": "computer", "usage": "usage", "billing": "usage",
                  "updates": "updates", "theme": "theme"}


def handle_settings(args: dict) -> dict:
    """Open a Grok Bot Settings page (General / Computer / Usage & Billing / Updates) in the background."""
    page = str(args.get("page") or args.get("id") or "usage").strip().lower()
    sid = SETTINGS_PAGES.get(page)
    if sid is None:
        return {
            "success": False,
            "errorCode": "bad_args",
            "error": f"unknown settings page {page!r}",
            "_hint": "page is one of general, computer, usage (Usage & Billing: plan, weekly limit, on-demand cap), updates.",
            "_next": "grok_settings {\"page\":\"usage\"}",
        }
    url = f"grokbot://app/v1/settings?id={sid}"
    res = _open_deeplink(url)
    ok = _callback_ok(res)
    return {
        "success": ok,
        "opened": ok,
        "page": page,
        "url": url,
        "windowEtiquette": "background",
        "callback": res,
        "_hint": (
            (f"Opened Grok Bot Settings > {page} in the background. " if ok else
             f"Could not launch Grok Bot with {url} (see callback); is it installed? grok_readiness. ")
            + "Read it with grok_screenshot. "
            "Usage & Billing shows the plan (e.g. 'Current plan: Pro'), the weekly usage % and reset, and the "
            "on-demand monthly cap billed through Cursor. Close the overlay with its X before typing in the chat."
        ),
        "_next": "grok_screenshot {}",
        "related": ["grok_screenshot", "grok_login"],
    }


def handle_logout(_args: dict) -> dict:
    """Open settings; try background UIA Sign Out. Else needsHuman."""
    opened = _open_deeplink(SETTINGS_DEEPLINK)
    hwnd, win = grok_hwnd(allow_cursor=False)
    if not hwnd:
        hwnd, win = grok_hwnd(allow_cursor=True)

    clicked = False
    click_res = None
    found = None
    if hwnd:
        found = _cb("desktop_find_control", {"hwnd": hwnd, "name": "Sign Out"})
        click_res = _cb("desktop_ui_click", {"hwnd": hwnd, "name": "Sign Out"})
        clicked = _callback_ok(click_res) and not click_res.get("error")
        if not clicked:
            # Allowed path: sidebar account button, then Sign Out.
            acct = _cb("desktop_ui_click", {"hwnd": hwnd, "name": "Account"})
            if _callback_ok(acct):
                click_res = _cb("desktop_ui_click", {"hwnd": hwnd, "name": "Sign Out"})
                clicked = _callback_ok(click_res) and not click_res.get("error")

    if clicked:
        return {
            "success": True,
            "loggedOut": True,
            "needsHuman": False,
            "hwnd": hwnd,
            "openedSettings": _callback_ok(opened),
            "windowEtiquette": "background",
            "_hint": (
                "Clicked UIA name containing Sign Out on the Grok Bot window in the background. "
                "Did not type anything. If the session is still signed in, the human should "
                "click Sign Out on the account card."
            ),
        }

    return {
        "success": True,
        "loggedOut": False,
        "needsHuman": True,
        "hwnd": hwnd,
        "openedSettings": _callback_ok(opened),
        "findControl": found,
        "clickAttempt": click_res,
        "windowEtiquette": "background",
        "_hint": (
            "Launched Grok Bot.exe with settings deep link as an arg (desktop_open_url refuses grokbot://). "
            "Sign Out was not background-actionable via UIA. needsHuman:true — please click "
            "**Sign Out** on the account card (sidebar account button → Sign Out). "
            "This verb will not invent other UI paths and will not type."
        ),
        "_next": "Human clicks Sign Out on the Grok Bot account card.",
    }


def handle_plugin_install(args: dict) -> dict:
    plugin_id = str(args.get("id") or args.get("pluginId") or "").strip()
    if not plugin_id:
        return {
            "success": False,
            "errorCode": "id_required",
            "error": "grok_plugin_install needs {id}.",
            "_hint": "Pass the plugin id. This opens grokbot://app/v1/plugin/add?id=<id> and does not confirm install — the app UI does.",
            "_next": "grok_plugin_install {\"id\":\"...\"}",
        }
    url = PLUGIN_ADD_DEEPLINK + quote(plugin_id, safe="")
    res = _open_deeplink(url)
    ok = _callback_ok(res)
    return {
        "success": ok or True,
        "opened": ok,
        "id": plugin_id,
        "url": url,
        "confirmed": False,
        "background": True,
        "callback": res,
        "windowEtiquette": "background",
        "_hint": (
            f"Launched Grok Bot.exe with {url} as an arg (desktop_open_url refuses grokbot://). This does NOT confirm install — the Grok Bot "
            "app UI does. The human may need to accept the plugin add dialog."
        ),
    }