aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-28 20:30:49 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commitb67faca60b21b0cf74d5c967f0f8d215ad601e93 (patch)
tree12e8580e1b88c3ce6a2d9157cfa678cb7a55a73c /src
parenta6ec1d0915106b127affbd6b5fcc9efec48ef712 (diff)
refactor(domains): strip comment noise
Removes module banners, step markers, and comments restating the code. Keeps one-line notes for safeJs's trust boundary, the select-match null sentinels, the TUI width clamp, the single-window tab rule, the input synthesis quirks (char events, keyCode, mousemove, focus, compositor), the stale AX value, ignored-wrapper recursion, and isolated-tab routing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/domains/click.ts4
-rw-r--r--src/domains/console.ts2
-rw-r--r--src/domains/drag.ts1
-rw-r--r--src/domains/files.ts3
-rw-r--r--src/domains/form.ts15
-rw-r--r--src/domains/isolated-tab.ts24
-rw-r--r--src/domains/js.ts17
-rw-r--r--src/domains/keyboard.ts10
-rw-r--r--src/domains/navigate.ts17
-rw-r--r--src/domains/network.ts3
-rw-r--r--src/domains/readpage/capture.ts11
-rw-r--r--src/domains/readpage/readability.ts23
-rw-r--r--src/domains/ref-resolve.ts30
-rw-r--r--src/domains/render.ts17
-rw-r--r--src/domains/screenshot.ts6
-rw-r--r--src/domains/scroll.ts7
-rw-r--r--src/domains/search/extract.ts15
-rw-r--r--src/domains/search/google-serp.ts28
-rw-r--r--src/domains/setup.ts10
-rw-r--r--src/domains/snapshot.ts58
-rw-r--r--src/domains/tabs.ts2
21 files changed, 18 insertions, 285 deletions
diff --git a/src/domains/click.ts b/src/domains/click.ts
index 0215d9a..cb4b007 100644
--- a/src/domains/click.ts
+++ b/src/domains/click.ts
@@ -36,9 +36,7 @@ const dispatchClick = async (
button: "left" | "right" | "middle",
count: number,
): Promise<Result<void, ToolErr>> => {
- // Move the pointer to the target first. Some focus/hover handlers (and React
- // synthetic events) only fire when a mousemove precedes the press, so without
- // it a click can land without focusing the element.
+ // Some focus/hover handlers and React synthetic events only fire when a mousemove precedes the press, so without it a click lands without focusing.
const moved = await cdpCall(client, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
if (!moved.success) return moved;
const pressed = await cdpCall(client, "Input.dispatchMouseEvent", {
diff --git a/src/domains/console.ts b/src/domains/console.ts
index c38da2a..7809f77 100644
--- a/src/domains/console.ts
+++ b/src/domains/console.ts
@@ -182,7 +182,7 @@ export const consoleTool = defineBrowserTool({
const text = renderConsoleMarkdown(drained.records, {
total: drained.total,
bufferOverflowed: drained.bufferOverflowed,
- expanded: true, // full text always returned to the LLM; renderResult handles compact view for humans
+ expanded: true,
nextCursor,
});
const trunc = await applyTruncation(text, "console");
diff --git a/src/domains/drag.ts b/src/domains/drag.ts
index 49a5a58..34a0154 100644
--- a/src/domains/drag.ts
+++ b/src/domains/drag.ts
@@ -40,7 +40,6 @@ export const dragAndDropTool = defineBrowserTool({
const calls: ReadonlyArray<InputCall> = [
["Input.dispatchMouseEvent", { type: "mousePressed", x: args.startX, y: args.startY, button: "left", clickCount: 1 }],
["Input.dispatchDragEvent", { type: "dragEnter", x: args.startX, y: args.startY, data, modifiers: 0 }],
- // 5 interpolated dragOver steps from start → end so the page sees a smooth drag.
...Array.from({ length: 5 }, (_, i): InputCall => {
const t = (i + 1) / 5;
return [
diff --git a/src/domains/files.ts b/src/domains/files.ts
index ebc5a83..534a674 100644
--- a/src/domains/files.ts
+++ b/src/domains/files.ts
@@ -17,8 +17,6 @@ const UploadArgs = Type.Object({
filePath: Type.String({ description: "Absolute path to the file to upload" }),
});
-// Ref path: set files directly on the resolved backendNodeId via CDP — simplest
-// and most reliable, no selector needed.
const tryRefUpload = async (
client: BrowserClient,
backendId: number,
@@ -115,7 +113,6 @@ export const uploadFileTool = defineBrowserTool({
const readable = await verifyReadable(args.filePath);
if (!readable.success) return readable;
- // Ref path: resolve to backendNodeId and set files directly via CDP.
if (args.ref !== undefined) {
const backendId = resolveRefToBackendId(client, args.ref);
if (!backendId.success) return backendId;
diff --git a/src/domains/form.ts b/src/domains/form.ts
index 78e3db1..b97b22d 100644
--- a/src/domains/form.ts
+++ b/src/domains/form.ts
@@ -18,16 +18,6 @@ const FillArgs = Type.Object({
value: Type.String({ description: "Value to set in the field" }),
});
-/**
- * Run an element-scoped function either against a CSS selector (querySelector +
- * apply) or a ref (resolve to objectId, callFunctionOn with element as `this`).
- * The shared `fnBody` is a function body where `this` is the target element and
- * the single argument is the value — identical logic on both paths. Returns the
- * function's return value (parsed) or a ToolErr.
- *
- * On the ref path a detached node yields a stale-ref error; on the selector path
- * a missing element returns { status: "not_found" } from the function body.
- */
const runOnElement = async (
client: BrowserClient,
opts: {
@@ -60,9 +50,7 @@ const runOnElement = async (
if (opts.selector === undefined) {
return err({ kind: "invalid_state", message: "Provide either `ref` or `selector`." });
}
- // Selector path: bind the same trusted function body to the matched element
- // via .call(). fnBody is harness-authored (not user input); only the selector
- // and value are interpolated, and those go through safeJs.
+ // fnBody is harness-authored, not user input; only the selector and value are interpolated, and those go through safeJs.
const prelude = safeJs`
(() => {
const el = document.querySelector(${opts.selector});
@@ -232,7 +220,6 @@ const isSelectResult = (v: unknown): v is SelectResult => {
return false;
};
-// `this` is the <select>; `arg` is { wv, wl, wi } (null = not provided).
const SELECT_FN = `
const el = this;
if (el.tagName !== "SELECT") return { status: "not_select", tag: el.tagName };
diff --git a/src/domains/isolated-tab.ts b/src/domains/isolated-tab.ts
index d0f3def..ba00756 100644
--- a/src/domains/isolated-tab.ts
+++ b/src/domains/isolated-tab.ts
@@ -1,15 +1,3 @@
-/**
- * Isolated-attach tab lifecycle (ADR-0002 v1). Opens a harness-owned tab,
- * attaches to it by its own CDP sessionId, and drives it via callOnTarget /
- * evaluateJs(expr, sid) WITHOUT ever touching the global "current" tab — so a
- * search or page-read never disturbs the tab the user is looking at, even when
- * several subagents run concurrently.
- *
- * Mirrors the create/attach sequence in browser_open_urls
- * (src/domains/navigate.ts:84-139). Every page evaluation is bounded by a
- * timeout, so a JS dialog on the (uninstrumented) isolated tab surfaces as a
- * typed timeout rather than hanging — see the dialog-hang caveat in plan.md.
- */
import type { BrowserClient } from "../client";
import { type Result, err, ok } from "../util/result";
import { safeJs } from "../util/js-template";
@@ -17,7 +5,6 @@ import { ensureHarnessWindow, openHarnessTab } from "../cdp/target-factory";
import { evaluateJson } from "../cdp/evaluate";
import type { ToolErr } from "../util/tool";
-/** A tab attached by its own sessionId, ready to drive via callOnTarget. */
export type IsolatedTab = {
readonly targetId: string;
readonly sessionId: string;
@@ -28,14 +15,8 @@ const READY_POLL_INTERVAL_MS = 50;
const toToolErr = (message: string, kind: ToolErr["kind"] = "cdp_error"): ToolErr => ({ kind, message });
-/**
- * Open a harness-owned tab and attach to it, returning its targetId + private
- * sessionId. Does not navigate. The caller MUST `closeIsolatedTab` when done.
- */
export const openIsolatedTab = async (client: BrowserClient): Promise<Result<IsolatedTab, ToolErr>> => {
- // Routed through the target factory so an isolated tab lands in the pinned
- // profile like every other harness tab — otherwise a search or page read
- // would run with a different profile's cookies than the visible tabs.
+ // Routed through the target factory so an isolated tab lands in the pinned profile, not another profile's cookies.
const window = await ensureHarnessWindow(client);
if (!window.success) return err(toToolErr(window.error.message));
let targetId: string;
@@ -62,7 +43,6 @@ export const openIsolatedTab = async (client: BrowserClient): Promise<Result<Iso
return ok({ targetId, sessionId });
};
-/** Navigate an isolated tab to a URL (does not wait for load). */
export const navigateIsolatedTab = async (
client: BrowserClient,
tab: IsolatedTab,
@@ -75,7 +55,6 @@ export const navigateIsolatedTab = async (
return ok(undefined);
};
-/** Poll the isolated tab until document.readyState === 'complete' or timeout. */
export const waitForIsolatedLoad = async (
client: BrowserClient,
tab: IsolatedTab,
@@ -108,7 +87,6 @@ export const evalInIsolatedTab = async <T>(
return ok(r.data);
};
-/** Close an isolated tab and drop its ownership. Best-effort; never throws. */
export const closeIsolatedTab = async (client: BrowserClient, tab: IsolatedTab): Promise<void> => {
await client.closeTab(tab.targetId).catch(() => {});
};
diff --git a/src/domains/js.ts b/src/domains/js.ts
index 6d9ace0..a64b4ac 100644
--- a/src/domains/js.ts
+++ b/src/domains/js.ts
@@ -18,17 +18,10 @@ const formatBytes = (n: number): string => {
return `${n} B`;
};
-// AsyncFunction constructor — the documented Node.js mechanism for compiling
-// arbitrary user-supplied source. Equivalent to `new Function` but produces an
-// async function we can `await`. The full RCE risk is contained at the source
-// boundary: see the path allowlist + timeout below. A plain `unknown` cast
-// would lose the constructor signature; we keep this single typed cast.
+// The AsyncFunction constructor is the documented way to compile user source; a plain `unknown` cast would lose the constructor signature, so this typed cast stays.
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as
new (...args: ReadonlyArray<string>) => (...args: ReadonlyArray<unknown>) => Promise<unknown>;
-// ESNext module — bare `require` is not in scope. createRequire provides the
-// CJS resolver anchored to this file's URL so scripts can require() builtins
-// and installed CJS packages.
const requireFromHere = createRequire(import.meta.url);
const ExecuteJsArgs = Type.Object({
@@ -58,8 +51,6 @@ export const executeJsTool = defineBrowserTool({
if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
const valueStr = r.data === undefined ? "undefined" : JSON.stringify(r.data);
const truncated = await applyTruncation(valueStr, "js");
- // If the value is JSON, pretty-print for the expanded TUI view. Falls back
- // to the raw string if parse fails (e.g. value is "undefined" sentinel).
let pretty: string | undefined;
try {
const parsed: unknown = JSON.parse(valueStr);
@@ -71,8 +62,6 @@ export const executeJsTool = defineBrowserTool({
text: truncated.text,
details: {
valueLength: valueStr.length,
- // Full value for the expanded renderer. Capped at 200 KB so a giant
- // dump doesn't bloat the conversation transcript on disk.
full: valueStr.length > 200_000 ? valueStr.slice(0, 200_000) : valueStr,
...(pretty !== undefined ? { pretty: pretty.length > 200_000 ? pretty.slice(0, 200_000) : pretty } : {}),
...(truncated.fullOutputPath !== undefined ? { fullOutputPath: truncated.fullOutputPath } : {}),
@@ -210,7 +199,7 @@ export const runScriptTool = defineBrowserTool({
try {
const scriptPromise = executeFn(
args.params ?? {},
- client, // bound as `daemon` — name preserved for back-compat
+ client,
requireFromHere,
ac.signal,
(u: unknown) => {
@@ -221,7 +210,7 @@ export const runScriptTool = defineBrowserTool({
if (!isRecord(first)) return;
const txt = first["text"];
if (typeof txt !== "string") return;
- try { onUpdate({ text: txt }); } catch { /* swallow */ }
+ try { onUpdate({ text: txt }); } catch {}
},
extensionCtx ?? { cwd: process.cwd() },
console, fetch, JSON, Buffer, setTimeout, clearTimeout,
diff --git a/src/domains/keyboard.ts b/src/domains/keyboard.ts
index 4ee8243..f0f1a6d 100644
--- a/src/domains/keyboard.ts
+++ b/src/domains/keyboard.ts
@@ -97,9 +97,7 @@ export const pressKeyTool = defineBrowserTool({
};
const down = await cdpCall(client, "Input.dispatchKeyEvent", downParams);
if (!down.success) return down;
- // For a printable character with no command modifier (Ctrl/Meta/Alt = 1|2|4),
- // emit a `char` event so the page receives keypress/textInput and the
- // character is actually inserted. Shift (8) is allowed (capitals/symbols).
+ // A printable character needs a `char` event for the page to receive keypress/textInput and actually insert it; Shift (8) still counts as printable.
const hasCommandModifier = (modifiers & (1 | 2 | 4)) !== 0;
if (isChar && !hasCommandModifier) {
const charEv = await cdpCall(client, "Input.dispatchKeyEvent", { type: "char", text: k, key: k });
@@ -143,12 +141,8 @@ export const dispatchKeyTool = defineBrowserTool({
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
const eventType = args.eventType ?? "keydown";
const target = args.ref ?? args.selector ?? "";
- // Legacy handlers commonly branch on e.keyCode/e.which (e.g. === 13 for
- // Enter) rather than e.key, so populate both. virtualKeyCode maps named
- // keys and single chars; 0 for anything unknown.
+ // Legacy handlers branch on e.keyCode/e.which rather than e.key, so populate both.
const code = virtualKeyCode(args.key);
- // Ref path: dispatch on the single resolved node. Selector path: dispatch on
- // all matches (preserves the original multi-match behavior).
if (args.ref !== undefined) {
const objectId = await resolveRefToObjectId(client, args.ref);
if (!objectId.success) return objectId;
diff --git a/src/domains/navigate.ts b/src/domains/navigate.ts
index 93c6a89..86cc272 100644
--- a/src/domains/navigate.ts
+++ b/src/domains/navigate.ts
@@ -88,28 +88,21 @@ export const openUrlsTool = defineBrowserTool({
renderCall: (a) => new Text(`🌐 Opening ${a.urls.length} URL${a.urls.length !== 1 ? "s" : ""}…`, 0, 0),
async handler(args, { client, onUpdate }): Promise<Result<ToolOk, ToolErr>> {
const total = args.urls.length;
- // Phase 1: create tabs. All must land in ONE window. If the harness window
- // already exists, open every url as its child in parallel. If it doesn't
- // (fresh/closed), create the FIRST url as a new dedicated window (binding
- // its windowId) and open the rest as children of it — a parallel
- // newWindow:true per url would otherwise scatter tabs across N windows.
+ // All tabs must land in ONE window: a parallel newWindow:true per url would scatter them across N windows, so the first url creates the window and the rest open as its children.
const url0 = args.urls[0];
if (url0 === undefined) return err({ kind: "invalid_state", message: "no URLs provided" });
const window = await ensureHarnessWindow(client);
if (!window.success) return err({ kind: "cdp_error", message: window.error.message });
const seededWindow = window.data.targetId;
- // A freshly created window's seed tab serves as the first URL's tab.
const seedResult: TabResult | undefined = window.data.freshlyCreated
? { url: url0, targetId: seededWindow, ok: true }
: undefined;
const created = await Promise.all(args.urls.map(async (url, i): Promise<TabResult> => {
- // Reuse the seed tab (created above) for the first url instead of duplicating it.
if (i === 0 && seedResult) return seedResult;
const r = await openHarnessTab(client, seededWindow);
if (!r.success) return { url, targetId: "", ok: false, error: r.error.message };
return { url, targetId: r.data, ok: true };
}));
- // Phase 2: attach + navigate each created tab in parallel.
let completed = 0;
const settled = await Promise.all(
created.filter((t) => t.ok).map(async (tab): Promise<TabResult> => {
@@ -121,8 +114,6 @@ export const openUrlsTool = defineBrowserTool({
if (!enabled.success) return { ...tab, ok: false, error: enabled.error.message };
const nav = await client.session().callOnTarget("Page.navigate", { url: tab.url }, sid);
if (!nav.success) return { ...tab, ok: false, error: nav.error.message };
- // Mark the tab with a green circle so the user can see which
- // tabs the agent opened. Best-effort; don't fail on CSP blocks.
client.evaluateJs(
safeJs`if(!document.title.startsWith('🟢'))document.title='🟢 '+document.title`,
sid
@@ -130,11 +121,10 @@ export const openUrlsTool = defineBrowserTool({
return tab;
} finally {
completed++;
- // onUpdate must never escape: a renderer crash would otherwise
- // tear down the tool call.
+ // onUpdate must never escape: a renderer crash would otherwise tear down the tool call.
try {
onUpdate({ text: `Opening URLs… ${completed}/${created.filter((t) => t.ok).length} navigated` });
- } catch { /* swallow */ }
+ } catch {}
}
}),
);
@@ -142,7 +132,6 @@ export const openUrlsTool = defineBrowserTool({
const all: ReadonlyArray<TabResult> = [...settled, ...failures];
const okTabs = all.filter((r) => r.ok);
const failTabs = all.filter((r) => !r.ok);
- // Phase 3: activate the last successfully-opened tab so the user sees something.
if (okTabs.length > 0) {
const last = okTabs[okTabs.length - 1];
if (last) {
diff --git a/src/domains/network.ts b/src/domains/network.ts
index fa755b3..fc54eb2 100644
--- a/src/domains/network.ts
+++ b/src/domains/network.ts
@@ -131,7 +131,6 @@ const renderNetworkMarkdown = (
return `${headerLines.join("\n")}\n\n${rows}${more}`;
}
- // Expanded: full table + optional body sections.
const tableHeader = `| # | Method | Status | URL | Type | ms |\n|---|---|---|---|---|---|`;
const tableRows = records
.map((r, i) => {
@@ -269,7 +268,7 @@ export const networkRequestsTool = defineBrowserTool({
includeBodies: args.includeResponseBodies ?? false,
total: drained.total,
bufferOverflowed: drained.bufferOverflowed,
- expanded: true, // full text always returned to the LLM; renderResult handles compact view for humans
+ expanded: true,
});
const trunc = await applyTruncation(text, "network");
diff --git a/src/domains/readpage/capture.ts b/src/domains/readpage/capture.ts
index 1ee4500..cf9d055 100644
--- a/src/domains/readpage/capture.ts
+++ b/src/domains/readpage/capture.ts
@@ -1,17 +1,6 @@
-/**
- * The JavaScript source evaluated in a loaded page to capture candidate content
- * blocks + metadata for the pure readability extractor (readability.ts). Crosses
- * the CDP boundary via Runtime.evaluate (returnByValue) and returns a JSON
- * string shaped like PageCapture.
- *
- * The DOM walk lives here (unavoidable); all scoring/selection is pure and lives
- * in readability.ts, so the decision logic stays unit-testable.
- */
-/** Max bytes of body-text fallback captured for thin/structure-less pages. */
const BODY_TEXT_LIMIT = 20_000;
-/** Build the capture expression. No interpolated values ⇒ no injection surface. */
export const buildPageCaptureExpr = (): string => `
(() => {
const BOILERPLATE = new Set(['NAV', 'HEADER', 'FOOTER', 'ASIDE']);
diff --git a/src/domains/readpage/readability.ts b/src/domains/readpage/readability.ts
index 37f2a38..b20cacd 100644
--- a/src/domains/readpage/readability.ts
+++ b/src/domains/readpage/readability.ts
@@ -1,35 +1,18 @@
-/**
- * Pure reader-mode extraction — no CDP, no DOM. The in-page script
- * (see capture.ts) serializes candidate content blocks; these pure functions
- * score them and assemble clean article text, so the selection logic is
- * unit-testable against saved fixtures (test/deep-research/readability-test.ts).
- *
- * Dependency-free by design (no jsdom / Readability): the harness ships no new
- * runtime deps. The heuristic favors blocks with high text density and low link
- * density, which is what separates article body from nav/ads/boilerplate.
- */
-/** One candidate content block captured in-page. */
export type ContentBlock = {
- /** Semantic tag: paragraph | heading | listitem | blockquote | other. */
readonly kind: "paragraph" | "heading" | "listitem" | "blockquote" | "other";
readonly text: string;
- /** Chars of text that sit inside <a> tags — high ratio ⇒ nav/menu, not prose. */
readonly linkTextLength: number;
- /** True when an ancestor is nav/header/footer/aside — structural boilerplate. */
readonly inBoilerplate: boolean;
};
-/** The JSON payload capture.ts returns from the page. */
export type PageCapture = {
readonly url: string;
readonly title: string;
readonly blocks: ReadonlyArray<ContentBlock>;
- /** Bounded innerText fallback when block extraction finds nothing usable. */
readonly bodyText: string;
};
-/** The tool's public output shape. */
export type ReadablePage = {
readonly title: string;
readonly url: string;
@@ -46,7 +29,6 @@ const linkDensity = (block: ContentBlock): number => {
return block.linkTextLength / block.text.length;
};
-/** A block is article prose if it is substantive, text-dense, and not chrome. */
const isArticleBlock = (block: ContentBlock): boolean => {
if (block.inBoilerplate) return false;
if (linkDensity(block) > MAX_LINK_DENSITY) return false;
@@ -69,11 +51,6 @@ const wordCountOf = (text: string): number => {
return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
};
-/**
- * Assemble readable article text from a page capture. Keeps text-dense,
- * non-boilerplate blocks; falls back to the bounded body text when the article
- * is too thin (e.g. a page with no clear content structure).
- */
export const extractReadable = (capture: PageCapture): ReadablePage => {
const articleText = capture.blocks.filter(isArticleBlock).map(render).join("\n\n").replace(/\n{3,}/g, "\n\n").trim();
diff --git a/src/domains/ref-resolve.ts b/src/domains/ref-resolve.ts
index 822f8e8..ece308e 100644
--- a/src/domains/ref-resolve.ts
+++ b/src/domains/ref-resolve.ts
@@ -10,12 +10,6 @@ const staleErr = (ref: string): ToolErr => ({
details: { ref },
});
-/**
- * Resolve a ref to its CDP objectId (a handle to the live JS node). Use when a
- * tool needs to run JS against the element (fill, focus, select, dispatch_key).
- * Re-resolves through the backendNodeId at call time, so it reflects the current
- * DOM even after re-renders — and fails loudly if the node was detached.
- */
export const resolveRefToObjectId = async (
client: BrowserClient,
ref: string,
@@ -24,8 +18,6 @@ export const resolveRefToObjectId = async (
if (backendId === undefined) return err(staleErr(ref));
const resolved = await client.session().call("DOM.resolveNode", { backendNodeId: backendId });
if (!resolved.success) {
- // A detached node (removed by a re-render) fails to resolve — surface it as
- // a stale ref rather than a raw CDP error so the agent knows to re-snapshot.
return err(staleErr(ref));
}
const objectId = resolved.data.object.objectId;
@@ -33,18 +25,12 @@ export const resolveRefToObjectId = async (
return ok(objectId);
};
-/** Resolve a ref to its current backendNodeId (for CDP calls that take one, e.g. DOM.setFileInputFiles). */
export const resolveRefToBackendId = (client: BrowserClient, ref: string): Result<number, ToolErr> => {
const backendId = client.session().resolveRef(ref);
if (backendId === undefined) return err(staleErr(ref));
return ok(backendId);
};
-/**
- * Resolve a ref to the current center of its bounding box, in viewport CSS
- * pixels. Use for browser_click. Fetched fresh via DOM.getBoxModel, so a
- * re-rendered/moved element is clicked at its new position.
- */
export const resolveRefToBox = async (
client: BrowserClient,
ref: string,
@@ -59,19 +45,6 @@ export const resolveRefToBox = async (
const sigOf = (n: SlimNode): string => `${n.role}|${n.name ?? ""}|${n.value ?? ""}|${n.state ?? ""}`;
const keyOf = (n: SlimNode): string => `${n.role}|${n.name ?? ""}`;
-/**
- * Recompute the interactive elements after a mutation and return a compact,
- * human/LLM-readable diff vs. the prior snapshot's baseline — changed values,
- * newly-appeared elements (marked `*`), and removed ones. Re-publishes the fresh
- * ref map so subsequent ref-based calls target the post-mutation DOM.
- *
- * Deliberately cheap: it re-reads the AX tree (no per-node box fetch — the
- * expensive part of a full snapshot) and emits a few lines, not a full tree, so
- * a long action sequence doesn't blow up the context.
- *
- * Returns "" on any failure or when nothing changed — callers append it only if
- * non-empty, so a diff is best-effort and never blocks the primary action.
- */
export const interactiveDiff = async (client: BrowserClient): Promise<string> => {
const session = client.session();
const prev = session.refSignatures();
@@ -82,7 +55,6 @@ export const interactiveDiff = async (client: BrowserClient): Promise<string> =>
const slim = buildTree(rawNodes, { interestingOnly: true, maxNodes: 1000 });
const targets = collectInteractiveTargets(slim);
- // Re-publish fresh refs so later ref calls resolve against the new DOM.
const priorRefs = session.refMappings();
const refByBackendId = new Map<number, string>();
for (const [ref, backendId] of priorRefs) refByBackendId.set(backendId, ref);
@@ -104,8 +76,6 @@ export const interactiveDiff = async (client: BrowserClient): Promise<string> =>
}
session.setRefMap(refMap, refSig);
- // Compare by stable identity (role|name) rather than ref position, since refs
- // renumber when the element set changes. Build prev/current identity → value.
const prevByKey = new Map<string, string>();
for (const sig of prev.values()) {
const parts = sig.split("|");
diff --git a/src/domains/render.ts b/src/domains/render.ts
index 1e4e30d..3f7f0dc 100644
--- a/src/domains/render.ts
+++ b/src/domains/render.ts
@@ -1,12 +1,3 @@
-/**
- * Shared TUI renderer for text-heavy tool output (browser_web_search,
- * browser_read_page). Gives the native pi expand/collapse (Ctrl+O) behavior:
- * a compact preview by default, the full body when expanded — mirroring the
- * pattern in browser_execute_js (src/domains/js.ts:81-109).
- *
- * The full text always goes to the LLM via ToolOk.text; this only governs the
- * human-facing TUI view.
- */
import { Markdown, Text } from "@mariozechner/pi-tui";
import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
import type { Component } from "@mariozechner/pi-tui";
@@ -15,13 +6,9 @@ import { isRecord } from "../util/guards";
const COMPACT_PREVIEW_LINES = 6;
-/** Structured payload a text-heavy tool stores so renderResult can rebuild views. */
export type ExpandableText = {
- /** One-line summary shown above the body in both states (e.g. "5 results · google"). */
readonly summary: string;
- /** The full rendered body (markdown/plain). */
readonly body: string;
- /** Path to the on-disk full output when the LLM text was truncated, if any. */
readonly fullOutputPath?: string;
};
@@ -31,10 +18,6 @@ const isExpandableText = (value: unknown): value is ExpandableText =>
const extractRender = (details: unknown): unknown =>
isRecord(details) ? details["render"] : undefined;
-/**
- * Build a renderResult over an ExpandableText stored in `details.render`.
- * `label` names the tool for the error fallback (e.g. "web_search").
- */
export const renderExpandableText = (
label: string,
result: { readonly details?: unknown },
diff --git a/src/domains/screenshot.ts b/src/domains/screenshot.ts
index 2e9cfdc..04f0fa9 100644
--- a/src/domains/screenshot.ts
+++ b/src/domains/screenshot.ts
@@ -110,10 +110,7 @@ export const screenshotTool = defineBrowserTool({
maxHeightCells: 24,
filename: filePath,
});
- // Image's text fallback (when terminal lacks inline-image support) does
- // not respect the rendered width, so a long file path can blow past the
- // terminal width and crash the host TUI. Wrap the component to truncate
- // each rendered line to fit.
+ // Image's text fallback does not respect the rendered width, so an unwrapped long path can blow past the terminal width and crash the host TUI.
return {
invalidate: () => image.invalidate(),
render: (width: number) =>
@@ -151,7 +148,6 @@ export const captureWithCrosshair = async (
await rename(tmp, cap.data.path);
return ok({ path: cap.data.path });
} catch {
- // Crosshair overlay is best-effort; on failure return the plain screenshot.
return ok({ path: cap.data.path });
}
};
diff --git a/src/domains/scroll.ts b/src/domains/scroll.ts
index 1819a48..832f584 100644
--- a/src/domains/scroll.ts
+++ b/src/domains/scroll.ts
@@ -38,13 +38,10 @@ export const scrollTool = defineBrowserTool({
const cy = args.y ?? Math.round(info.data.height / 2);
const dx = args.deltaX ?? 0;
const dy = args.deltaY ?? 300;
- // The page target must be focused or Input events are dropped silently
- // (mouseWheel hangs; synthesizeScrollGesture returns ok but no scroll).
- // Page.bringToFront makes the target the active one in the browser.
+ // The page target must be focused or Input events are dropped silently: mouseWheel hangs and synthesizeScrollGesture returns ok without scrolling.
const front = await cdpCall(client, "Page.bringToFront", {});
if (!front.success) return front;
- // Establish compositor mouse position before mouseWheel; without this,
- // CDP sometimes drops the wheel event with a timeout.
+ // Establish compositor mouse position first, or CDP sometimes drops the wheel event with a timeout.
const moved = await cdpCall(client, "Input.dispatchMouseEvent", {
type: "mouseMoved", x: cx, y: cy, button: "none", buttons: 0,
});
diff --git a/src/domains/search/extract.ts b/src/domains/search/extract.ts
index 52f7f86..05ee182 100644
--- a/src/domains/search/extract.ts
+++ b/src/domains/search/extract.ts
@@ -1,21 +1,6 @@
-/**
- * The JavaScript source evaluated inside the Google SERP page to capture raw
- * anchors + page text. This string crosses the CDP boundary via
- * Runtime.evaluate (returnByValue) and returns a JSON string that
- * `parseGoogleSerp` / `classifySerp` consume on the harness side.
- *
- * ADR-0001: anchors are selected structurally (a link carrying an <h3>), not by
- * Google CSS class names, so cosmetic markup churn does not break extraction.
- */
-/** Max bytes of page text captured for CAPTCHA / no-results classification. */
const PAGE_TEXT_LIMIT = 4000;
-/**
- * Build the extraction expression. Takes no interpolated values, so it is a
- * fixed source string (no injection surface). Returns
- * `JSON.stringify({ anchors, pageText })`.
- */
export const buildSerpExtractionExpr = (): string => `
(() => {
const main = document.querySelector('#search, #rso, #main') || document.body;
diff --git a/src/domains/search/google-serp.ts b/src/domains/search/google-serp.ts
index 7e49bea..4b8a319 100644
--- a/src/domains/search/google-serp.ts
+++ b/src/domains/search/google-serp.ts
@@ -1,27 +1,15 @@
-/**
- * Pure Google SERP parsing — no CDP, no browser. Everything here is a pure
- * function over data captured from the page, so it is unit-testable against
- * saved fixtures (see test/deep-research/serp-parser-test.ts).
- *
- * ADR-0001: results are extracted by structure/semantics (anchors carrying an
- * <h3> heading in the main results column), never by brittle Google CSS class
- * names, and Google redirect wrappers are unwound to the real target URL.
- */
-/** One raw anchor captured in-page: a link with a heading and nearby snippet. */
export type SerpAnchor = {
readonly href: string;
readonly heading: string;
readonly snippet: string;
};
-/** The JSON payload the in-page extraction expression returns. */
export type SerpExtraction = {
readonly anchors: ReadonlyArray<SerpAnchor>;
readonly pageText: string;
};
-/** A parsed, ranked search result — the tool's public output shape. */
export type SearchResult = {
readonly title: string;
readonly url: string;
@@ -29,7 +17,6 @@ export type SearchResult = {
readonly rank: number;
};
-/** Why a SERP yielded no usable results, or that it did. */
export type SerpVerdict = "ok" | "captcha" | "no_results";
const CAPTCHA_MARKERS: ReadonlyArray<string> = [
@@ -42,10 +29,6 @@ const CAPTCHA_MARKERS: ReadonlyArray<string> = [
"our systems have detected unusual traffic",
];
-/**
- * Decode a Google redirect wrapper (`/url?q=<real>&sa=...`) to its target.
- * Returns the input unchanged when it is not a wrapper.
- */
const unwrapRedirect = (href: string): string => {
const wrapper = href.match(/^(?:https?:\/\/[^/]*google\.[^/]*)?\/url\?/i);
if (!wrapper) return href;
@@ -66,7 +49,6 @@ const isExternalResult = (url: string): boolean => {
}
};
-/** Normalized key for dedupe: origin + pathname, trailing slash and hash removed. */
const dedupeKey = (url: string): string => {
try {
const u = new URL(url);
@@ -76,11 +58,6 @@ const dedupeKey = (url: string): string => {
}
};
-/**
- * Parse captured anchors into ranked results: unwrap redirects, keep only
- * external http(s) links, dedupe by normalized url (first wins), rank by input
- * order, cap to `limit`.
- */
export const parseGoogleSerp = (anchors: ReadonlyArray<SerpAnchor>, limit: number): ReadonlyArray<SearchResult> => {
const seen = new Set<string>();
const results: SearchResult[] = [];
@@ -101,11 +78,6 @@ export const parseGoogleSerp = (anchors: ReadonlyArray<SerpAnchor>, limit: numbe
return results;
};
-/**
- * Decide whether the SERP is usable, blocked by a CAPTCHA, or genuinely empty.
- * `resultCount` is the number of parsed results; `pageText` is a bounded slice
- * of the page's visible text.
- */
export const classifySerp = (pageText: string, resultCount: number): SerpVerdict => {
if (resultCount > 0) return "ok";
const lower = pageText.toLowerCase();
diff --git a/src/domains/setup.ts b/src/domains/setup.ts
index c2d8a4f..21fa3b3 100644
--- a/src/domains/setup.ts
+++ b/src/domains/setup.ts
@@ -1,10 +1,3 @@
-/**
- * browser_setup tool — agent-callable browser initialization.
- *
- * The agent can call this directly when browser tools fail with
- * "not initialized" instead of asking the user. Idempotent: safe
- * to call even when already connected.
- */
import { Type } from "typebox";
import { Text } from "@mariozechner/pi-tui";
@@ -29,12 +22,9 @@ export const setupTool = defineBrowserTool({
],
parameters: SetupArgs,
concurrency: "parallel",
- // ensureAlive:false — this tool IS the setup; it must bypass the socket guard
ensureAlive: false,
renderCall: () => new Text("🔧 Initializing browser...", 0, 0),
async handler(_args, { client, extensionCtx }): Promise<Result<ToolOk, ToolErr>> {
- // extensionCtx carries the UI, so the agent calling browser_setup gets the
- // same first-run profile picker the user gets from /browser-setup.
const result = await performSetup(client, extensionCtx);
if (result.success) {
return ok({ text: result.data });
diff --git a/src/domains/snapshot.ts b/src/domains/snapshot.ts
index c015203..b33c8f9 100644
--- a/src/domains/snapshot.ts
+++ b/src/domains/snapshot.ts
@@ -40,11 +40,8 @@ export type SlimNode = {
description?: string;
state?: string;
children: SlimNode[];
- /** Internal: kept for the post-build box fetch. Stripped before returning to the LLM. */
_backendId?: number;
- /** Stable handle (e.g. "e12") for interaction tools. Survives re-renders via backendNodeId. */
ref?: string;
- /** Click target — center of the element's bounding box, in viewport CSS pixels. */
box?: Box;
};
@@ -63,9 +60,7 @@ const INTERACTIVE_ROLES = new Set([
"spinbutton",
]);
-// Roles whose live DOM value/checked state should override the (often stale)
-// AX-tree value. The AX `value` is unreliable for freshly-typed or controlled
-// inputs, so for these we read the real DOM property at snapshot time.
+// The AX `value` is stale for freshly-typed or controlled inputs, so for these roles the live DOM property is read instead.
const VALUE_ROLES = new Set([
"textbox",
"searchbox",
@@ -91,7 +86,6 @@ const collectState = (props: RawAxNode["properties"]): string | undefined => {
const name = p.name;
if (!name) continue;
const raw = p.value?.value;
- // Boolean flags worth surfacing in the outline.
if (raw === true && (name === "focused" || name === "required" || name === "disabled" || name === "checked" || name === "expanded" || name === "selected" || name === "pressed" || name === "modal")) {
flags.push(name);
}
@@ -104,12 +98,9 @@ export const buildTree = (
rawNodes: ReadonlyArray<RawAxNode>,
opts: { interestingOnly: boolean; maxNodes: number },
): SlimNode[] => {
- // Index for parent/child lookups.
const byId = new Map<string, RawAxNode>();
for (const n of rawNodes) byId.set(n.nodeId, n);
- // CDP doesn't mark a single root explicitly; the root nodes are the ones
- // whose parentId isn't in the index (typically just one — RootWebArea).
const rootIds: string[] = [];
for (const n of rawNodes) {
if (!n.parentId || !byId.has(n.parentId)) rootIds.push(n.nodeId);
@@ -120,8 +111,7 @@ export const buildTree = (
const slim = (node: RawAxNode): SlimNode | undefined => {
if (budget <= 0) return undefined;
if (opts.interestingOnly && node.ignored) {
- // Skip the ignored node but recurse through its children — Chrome often
- // ignores wrapper divs but the meaningful descendants are still there.
+ // Chrome often ignores wrapper divs while their descendants remain meaningful, so recurse through an ignored node's children.
const out: SlimNode[] = [];
for (const cid of node.childIds ?? []) {
const child = byId.get(cid);
@@ -129,10 +119,6 @@ export const buildTree = (
const slimChild = slim(child);
if (slimChild) out.push(slimChild);
}
- // Hoist children into the parent. We return undefined; the parent's
- // recursion will see them by re-walking — but that double-counts. Instead
- // we handle hoisting at the parent level. So here just signal "skip me"
- // by returning undefined; the parent will hoist by checking ignored flag.
return out.length > 0 ? { role: "_hoist", children: out } : undefined;
}
budget--;
@@ -147,7 +133,6 @@ export const buildTree = (
if (!child) continue;
const slimChild = slim(child);
if (!slimChild) continue;
- // Hoist any synthetic _hoist nodes' children into this level.
if (slimChild.role === "_hoist") children.push(...slimChild.children);
else children.push(slimChild);
}
@@ -187,7 +172,6 @@ const summarize = (nodes: ReadonlyArray<SlimNode>): string => {
}
};
walk(nodes);
- // Group landmarks (roles that are top-level structural).
const landmarkRoles = new Set(["banner", "navigation", "main", "complementary", "contentinfo", "search", "form", "region"]);
let landmarks = 0;
for (const [role, c] of counts) if (landmarkRoles.has(role)) landmarks += c;
@@ -211,8 +195,6 @@ const renderOutline = (nodes: ReadonlyArray<SlimNode>): string => {
if (n.name) line += ` "${n.name}"`;
if (n.value && n.value !== n.name) line += ` = ${JSON.stringify(n.value)}`;
if (n.state) line += ` (${n.state})`;
- // Surface the stable ref + click coordinates for interactive nodes. Agents
- // pass `ref` to interaction tools (survives re-renders); @(x,y) is a fallback.
if (n.ref) line += ` [${n.ref}]`;
if (n.box && INTERACTIVE_ROLES.has(n.role)) {
line += ` @(${n.box.cx},${n.box.cy})`;
@@ -225,18 +207,12 @@ const renderOutline = (nodes: ReadonlyArray<SlimNode>): string => {
return lines.join("\n");
};
-/** Strip internal _backendId from a slim tree before returning to callers. */
const stripInternals = (nodes: ReadonlyArray<SlimNode>): SlimNode[] =>
nodes.map((n) => {
const { _backendId: _bid, ...rest } = n;
return { ...rest, children: stripInternals(n.children) };
});
-/**
- * Walk all slim nodes and collect (slimNode, backendId) pairs for interactive
- * roles. Caller fetches DOM.getBoxModel for each and writes box back into the
- * slim node by reference.
- */
export const collectInteractiveTargets = (
nodes: ReadonlyArray<SlimNode>,
): Array<{ node: SlimNode; backendId: number }> => {
@@ -253,13 +229,6 @@ export const collectInteractiveTargets = (
return out;
};
-/**
- * Read the live DOM value for a form control by backend node id. Resolves the
- * node to a JS object then reads the property that matters for its type:
- * `.value` for inputs/selects/textareas, `.checked` (→ "checked"/"unchecked")
- * for checkboxes/radios. Returns undefined on any failure so the caller falls
- * back to the AX value. Mirrors the box-fetch error handling: never throws.
- */
const liveValue = async (session: CdpSession, backendId: number): Promise<string | undefined> => {
const resolved = await session.call("DOM.resolveNode", { backendNodeId: backendId });
if (!resolved.success) return undefined;
@@ -309,33 +278,22 @@ export const snapshotTool = defineBrowserTool({
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
const session = client.session();
- // 1. AX tree
const axRes = await cdpCall(client, "Accessibility.getFullAXTree", {});
if (!axRes.success) return axRes;
const rawNodes = axRes.data.nodes;
- // 2. Page url/title
const piRes = await client.pageInfo();
if (!piRes.success) return err({ kind: "cdp_error", message: piRes.error.message });
const pageUrl = "dialog" in piRes.data ? "" : piRes.data.url;
const pageTitle = "dialog" in piRes.data ? "" : piRes.data.title;
- // 3. Slim + format
const slim = buildTree(rawNodes, {
interestingOnly: args.interestingOnly ?? true,
maxNodes: args.maxNodes ?? 1000,
});
- // 4. Fetch bounding boxes for interactive nodes so the agent can click
- // without a screenshot round-trip. Capped budget so a slow page can't
- // wedge the call.
const targets = collectInteractiveTargets(slim);
- // Assign stable refs (e1, e2, …) to every interactive target. Interaction
- // tools resolve these refs to backendNodeIds at call time, so they stay
- // valid across re-renders. The ref → backendNodeId map (and the per-ref
- // signature baseline for the post-mutation diff) is published below, after
- // the box/live-value fetch so the signature reflects the live DOM value.
const refMap = new Map<string, number>();
targets.forEach(({ node, backendId }, i) => {
const ref = `e${i + 1}`;
@@ -353,10 +311,6 @@ export const snapshotTool = defineBrowserTool({
if (!box.success) return;
if (box.data.x < 0 || box.data.y < 0) return;
node.box = box.data;
- // Override the AX value with the live DOM property for form controls.
- // The AX `value` is stale for freshly-typed / controlled inputs, so the
- // agent can't trust it as a read-back signal. Reading .value/.checked
- // from the actual element fixes that.
if (ac.signal.aborted || !VALUE_ROLES.has(node.role)) return;
const live = await liveValue(session, backendId);
if (live !== undefined) node.value = live;
@@ -365,9 +319,6 @@ export const snapshotTool = defineBrowserTool({
clearTimeout(timer);
}
- // Publish refs to the active tab. The signature (role|name|value|state) is
- // the baseline the post-mutation auto-diff compares against, so it must use
- // the live values resolved above.
const refSig = new Map<string, string>();
for (const { node } of targets) {
if (node.ref === undefined) continue;
@@ -379,7 +330,6 @@ export const snapshotTool = defineBrowserTool({
const text = (args.format ?? "outline") === "outline" ? renderOutline(slim) : JSON.stringify(stripped, null, 2);
const trunc = await applyTruncation(text, "snapshot");
- // 4. Optional screenshot — JPEG q=80 to match screenshotTool's default.
let shotPath: string | undefined;
if (args.includeScreenshot) {
const shot = await session.call("Page.captureScreenshot", { format: "jpeg", quality: 80 });
@@ -429,9 +379,6 @@ export const snapshotTool = defineBrowserTool({
return new Markdown(md, 0, 0, getMarkdownTheme());
}
- // Expanded view: read truncated text from result.content (the spilled file
- // is too large to inline) — the same `text` we returned from handler is in
- // result.content[0]. Fall back to a placeholder if shape is unexpected.
const content = result.content[0];
const treeText = content && content.type === "text" ? content.text : "";
@@ -451,7 +398,6 @@ export const snapshotTool = defineBrowserTool({
if (!details.screenshotPath) return treeBlock;
- // Mirror screenshotTool's width-clamp wrapper for the inline image.
try {
const buf = readFileSync(details.screenshotPath);
const b64 = buf.toString("base64");
diff --git a/src/domains/tabs.ts b/src/domains/tabs.ts
index 2971a6b..f932fbf 100644
--- a/src/domains/tabs.ts
+++ b/src/domains/tabs.ts
@@ -87,8 +87,6 @@ export const switchTabTool = defineBrowserTool({
parameters: SwitchTabArgs,
concurrency: "serialized",
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
- // Resolve to a concrete targetId (exact, or unique hex prefix). We resolve
- // first so the ownership check sees the canonical id, not the prefix.
let resolved = args.targetId;
if (!client.owns(resolved)) {
const isHexPrefix = /^[0-9A-Fa-f]{8,}$/.test(args.targetId);