After the locate click, type now refuses unless the located editable is document.activeElement, so characters are not delivered as page input when focus stayed on body. The keystroke loop checks an abort signal between characters; preview.act cancel (timeout or interrupt) and a local Stop both set it, so queued keystrokes stop. A printable press on body/html is refused unless allow_shortcut is set.
232 lines
8.0 KiB
Python
232 lines
8.0 KiB
Python
"""Server→client requests: the backend asks the renderer a question (``server_requests.send``).
|
|
|
|
Every entry is one request method: the ``params`` the frame carries (``session_id`` is added by
|
|
the transport and declared on the shared base) and the ``result`` the client answers with. The
|
|
``request.cancel`` event that withdraws an open request lives here too.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pydantic import Field
|
|
|
|
from .base import JsonValue, Params, Payload, Result, WireEnum
|
|
from .registry import event, server_request
|
|
|
|
|
|
class ServerRequestParams(Params):
|
|
"""Every request frame's params start with the session the question belongs to."""
|
|
|
|
session_id: str
|
|
|
|
|
|
class ValueResult(Result):
|
|
"""The answer to any one-string prompt (sudo, secret, vault prompts, desktop bridges):
|
|
``''`` means skipped / declined."""
|
|
|
|
value: str
|
|
|
|
|
|
# ── clarify ───────────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class ClarifyQuestion(Params):
|
|
qid: str
|
|
question: str
|
|
choices: list[str] | None = None
|
|
multi_select: bool = False
|
|
|
|
|
|
class ClarifyRequestParams(ServerRequestParams):
|
|
"""Single question: ``question`` / ``choices`` (/ ``multi_select``); batch: ``questions``.
|
|
``answers`` rides only on a reconnect replay (locks the server already accepted)."""
|
|
|
|
question: str | None = None
|
|
choices: list[str] | None = None
|
|
multi_select: bool | None = None
|
|
questions: list[ClarifyQuestion] | None = None
|
|
answers: dict[str, str] | None = None
|
|
|
|
|
|
class ClarifyResult(Result):
|
|
"""Single: ``{answer}`` ('' = skip). Batch: ``{answers}`` for the whole set (early locks go through
|
|
the ``clarify.lock`` RPC); a response with neither is cancel-all."""
|
|
|
|
answer: str | None = None
|
|
answers: dict[str, str] | None = None
|
|
|
|
|
|
server_request("clarify", params=ClarifyRequestParams, result=ClarifyResult,
|
|
doc="The clarify tool: ask the user one question or a batch.")
|
|
|
|
|
|
# ── approval ──────────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class ApprovalChoice(WireEnum):
|
|
once = "once"
|
|
session = "session"
|
|
always = "always"
|
|
deny = "deny"
|
|
|
|
|
|
class ApprovalRequestParams(ServerRequestParams):
|
|
"""``tui_gateway/server.py::_approval_request_payload`` — the command is redacted server-side."""
|
|
|
|
request_id: str
|
|
command: str = ""
|
|
description: str = ""
|
|
choices: list[ApprovalChoice] = Field(default_factory=list)
|
|
allow_permanent: bool | None = None
|
|
allow_session: bool | None = None
|
|
smart_denied: bool | None = None
|
|
tool_name: str | None = None
|
|
session_id_hint: str | None = Field(default=None, alias="gateway_session_id")
|
|
# The approval queue entry carries tool-specific context the card may render; the closed set
|
|
# of keys is owned by tools/approval.py, so it stays open here.
|
|
model_config = Params.model_config | {"extra": "allow"}
|
|
|
|
|
|
class ApprovalResult(Result):
|
|
choice: ApprovalChoice
|
|
all: bool | None = None
|
|
|
|
|
|
server_request("approval", params=ApprovalRequestParams, result=ApprovalResult,
|
|
doc="A dangerous command awaits the user's decision.")
|
|
|
|
|
|
# ── one-string prompts ────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class EmptyRequestParams(ServerRequestParams):
|
|
pass
|
|
|
|
|
|
class SudoRequestParams(ServerRequestParams):
|
|
"""Original command, redacted server-side before any password-injection rewrite."""
|
|
|
|
command: str = ""
|
|
|
|
|
|
server_request("sudo", params=SudoRequestParams, result=ValueResult,
|
|
doc="Masked sudo password for the terminal tool.")
|
|
|
|
|
|
class SecretRequestParams(ServerRequestParams):
|
|
env_var: str
|
|
prompt: str
|
|
metadata: dict[str, JsonValue] | None = None
|
|
|
|
|
|
server_request("secret", params=SecretRequestParams, result=ValueResult,
|
|
doc="Masked value for a named env var (skills / setup flows).")
|
|
|
|
|
|
class VaultUnlockRequestParams(ServerRequestParams):
|
|
backend: str
|
|
display_name: str
|
|
|
|
|
|
server_request("vault.unlock_prompt", params=VaultUnlockRequestParams, result=ValueResult,
|
|
doc="Master password to unlock an external password manager for this session.")
|
|
|
|
|
|
class VaultSaveLoginRequestParams(ServerRequestParams):
|
|
origin: str
|
|
site: str
|
|
|
|
|
|
server_request("vault.save_login", params=VaultSaveLoginRequestParams, result=ValueResult,
|
|
doc="Save a login for a site the agent is about to fill; the answer is JSON {identifier, password}.")
|
|
|
|
|
|
class VaultCodeRequestParams(ServerRequestParams):
|
|
site: str | None = None
|
|
hint: str | None = None
|
|
|
|
|
|
server_request("vault.code", params=VaultCodeRequestParams, result=ValueResult,
|
|
doc="A one-time / 2FA code the user reads from their device.")
|
|
|
|
|
|
# ── desktop GUI bridges ───────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class ReadRangeRequestParams(ServerRequestParams):
|
|
start: int | None = None
|
|
count: int | None = None
|
|
|
|
|
|
server_request("terminal.read", params=ReadRangeRequestParams, result=ValueResult,
|
|
doc="Read the visible in-app terminal buffer (JSON text answer).")
|
|
server_request("preview.read", params=ReadRangeRequestParams, result=ValueResult,
|
|
doc="Read the in-app browser preview's text (JSON text answer).")
|
|
server_request("window.read", params=EmptyRequestParams, result=ValueResult,
|
|
doc="Enumerate the native window below the app (JSON text answer).")
|
|
|
|
|
|
class PreviewActRequestParams(ServerRequestParams):
|
|
"""``tools/drive_preview_tool.py`` and ``tools/annotate_preview_tool.py`` field sets."""
|
|
|
|
action: str
|
|
ref: str | None = None
|
|
selector: str | None = None
|
|
text: str | None = None
|
|
key: str | None = None
|
|
submit: bool | None = None
|
|
full: bool | None = None
|
|
to: str | None = None
|
|
amount: int | None = None
|
|
max: int | None = None
|
|
allow_shortcut: bool | None = None
|
|
|
|
|
|
server_request("preview.act", params=PreviewActRequestParams, result=ValueResult,
|
|
doc="Click / type / scroll / annotate inside the in-app browser preview.")
|
|
|
|
|
|
class TourStep(Params):
|
|
selector: str | None = None
|
|
title: str | None = None
|
|
text: str | None = None
|
|
side: str | None = None
|
|
model_config = Params.model_config | {"extra": "allow"}
|
|
|
|
|
|
class TourRequestParams(ServerRequestParams):
|
|
"""``tools/tour_tool.py`` field set."""
|
|
|
|
action: str
|
|
surface: str | None = None
|
|
selector: str | None = None
|
|
title: str | None = None
|
|
text: str | None = None
|
|
side: str | None = None
|
|
steps: list[TourStep] | None = None
|
|
step_index: int | None = None
|
|
|
|
|
|
server_request("tour", params=TourRequestParams, result=ValueResult,
|
|
doc="Drive a guided tour highlight in the desktop renderer.")
|
|
|
|
|
|
# ── withdrawal ────────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class RequestCancelReason(WireEnum):
|
|
timeout = "timeout"
|
|
interrupted = "interrupted"
|
|
shutdown = "shutdown"
|
|
resolved = "resolved"
|
|
session_closed = "session_closed"
|
|
|
|
|
|
class RequestCancelPayload(Payload):
|
|
id: str
|
|
method: str
|
|
reason: str # a RequestCancelReason value; callers in tools/approval may pass their own wording
|
|
|
|
|
|
event("request.cancel", RequestCancelPayload,
|
|
doc="The backend withdrew an open server→client request; clear the matching card only.")
|