/** * web_session — tmux-style persistent browser sessions. A thin, faithful * pass-through to the daemon's session.create/list/close/act operations, * deliberately kept separate from web_fetch rather than overloading its * contract (web_fetch's contract must never change). See daemon-side * session-service.ts for the actual behavior/safety guarantees this tool * exposes but does not reimplement: * - one owned Playwright browser process per named session, isolated * from the operator's own browser and every other session. * - snapshotVersion is a deliberate safety mechanism, not busywork: the * daemon fails closed (a StaleSnapshotError) if the page navigated or * changed since the caller last observed it. This tool does NOT track * snapshotVersion on the caller's behalf — every act() response * returns the current value; pass it back on the next call. Removing * this friction here would silently undermine the reason it exists. * - every act() call is journaled (content-free — selectors and enum * values only, never typed text, scripts, or page content). */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { Static } from "typebox"; import { Type } from "typebox"; import type { CallMeta, OperationGateway } from "./operation-gateway.js"; import { createSessionActDetails, createSessionLifecycleDetails, createSessionListDetails, renderWebSessionCall, renderWebSessionResult, } from "./session-presentation.js"; const sessionParamsSchema = Type.Object({ operation: Type.Union([Type.Literal("create"), Type.Literal("list"), Type.Literal("close"), Type.Literal("act")], { description: "create/list/close a named session, or act on one", }), name: Type.Optional(Type.String({ description: "Session name (create/close/act)" })), forceChromeChannel: Type.Optional( Type.Boolean({ description: "create: use the full installed Chrome channel instead of Playwright's bundled Chromium" }), ), headed: Type.Optional( Type.Boolean({ description: "create: show a real browser window for human takeover (CAPTCHA/login/consent); defaults false. The agent can resume the same session afterward.", }), ), snapshotVersion: Type.Optional( Type.Number({ description: "act, required: expected snapshot version, from the previous response (create returns 0). " + "A stale value fails closed rather than acting on a page that navigated underneath you.", }), ), action: Type.Optional( Type.Union( [ Type.Literal("navigate"), Type.Literal("click"), Type.Literal("hover"), Type.Literal("pressKey"), Type.Literal("type"), Type.Literal("select"), Type.Literal("waitFor"), Type.Literal("queryText"), Type.Literal("readTable"), Type.Literal("snapshot"), Type.Literal("handleDialog"), Type.Literal("downloads"), Type.Literal("consoleMessages"), Type.Literal("networkRequests"), Type.Literal("tabs"), Type.Literal("eval"), Type.Literal("screenshot"), ], { description: "act, required. navigate/click/hover/pressKey/type/select act on the page (track snapshotVersion). " + "waitFor blocks for a condition instead of guessing a delay. queryText/readTable return structured " + "data. snapshot returns a YAML a11y tree -- prefer it over screenshot for page structure. " + "handleDialog arms accept/dismiss for the next dialog. downloads/consoleMessages/networkRequests read " + "captured session activity. tabs manages multiple tabs. eval runs arbitrary JavaScript -- prefer the " + "structured actions above when they fit. screenshot returns a PNG.", }, ), ), url: Type.Optional(Type.String({ description: "navigate: URL to load. tabs (new): optional URL for the new tab." })), selector: Type.Optional( Type.String({ description: "CSS selector for click/hover/type/select/waitFor/queryText/readTable/snapshot(scope)/screenshot(scope); optional focus target for pressKey.", }), ), text: Type.Optional(Type.String({ description: "type: text to type as real keystrokes. waitFor: text to wait for." })), clear: Type.Optional(Type.Boolean({ description: "type: clear existing content first (default true)" })), value: Type.Optional(Type.String({ description: "select: match an option by its value attribute" })), label: Type.Optional(Type.String({ description: "select: match an option by its visible label" })), loadState: Type.Optional( Type.Union([Type.Literal("load"), Type.Literal("domcontentloaded"), Type.Literal("networkidle")], { description: "waitFor: navigation state to wait for instead of a selector/text condition", }), ), state: Type.Optional( Type.Union([Type.Literal("visible"), Type.Literal("hidden"), Type.Literal("attached"), Type.Literal("detached")], { description: "waitFor: element state to wait for alongside selector/text (default visible)", }), ), script: Type.Optional(Type.String({ description: "eval: JavaScript to run in the page; returns its JSON-serializable result" })), timeoutMs: Type.Optional(Type.Number({ description: "Per-action timeout ms (Playwright's own default applies when omitted)" })), key: Type.Optional(Type.String({ description: 'pressKey: key to press, e.g. "Enter", "Escape", "Tab", "ArrowLeft"' })), fullPage: Type.Optional( Type.Boolean({ description: "screenshot: capture the whole scrollable page instead of the viewport; not valid with selector" }), ), scale: Type.Optional( Type.Union([Type.Literal("css"), Type.Literal("device")], { description: "screenshot: image resolution -- css pixels (default) or real device pixel ratio", }), ), depth: Type.Optional(Type.Number({ description: "snapshot: limit the accessibility tree's depth" })), boxes: Type.Optional(Type.Boolean({ description: "snapshot: include each node's viewport-relative bounding box" })), mode: Type.Optional( Type.Union([Type.Literal("ai"), Type.Literal("default")], { description: 'snapshot: "ai" adds element references, doesn\'t wait for a matching element, and includes