aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-06-07 22:52:24 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-06-07 22:52:24 -0400
commit4701544c02852a254f9cd49ade83a2fcaa0b1699 (patch)
tree9649ec5f41fc580f3617a7becf256f008e57ccc9
parent20585373694acb50c26144c3ce00b6040acd3327 (diff)
0.15.37: harden Chrome input targeting
-rw-r--r--CHANGELOG.md7
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js234
-rw-r--r--extensions/chrome-profile-bridge/index.ts1
3 files changed, 190 insertions, 52 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0f8d50..50a9bfd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
All notable user-facing changes to `pi-chrome`.
+## 0.15.37 — 2026-06-07
+
+- **Hardened Chrome input targeting.** `chrome_click`/`chrome_fill`/related input paths now fail fast with resolved tab/CDP target metadata when debugger attach hits a stale or protected target, instead of surfacing bare `chrome-extension://` errors or hanging until the bridge timeout.
+- **Internal timeouts and cleanup.** Companion extension commands, debugger attach, CDP commands, and script injection now have shorter internal timeouts with debugger cleanup, so stuck input dispatch returns actionable errors before the 30s bridge timeout.
+- **Clear stale uid errors.** Snapshot uids that no longer map to live elements now report `snapshot uid ... is stale; refresh chrome_snapshot`.
+- **`chrome_fill` fallback.** If real CDP input is blocked by another extension overlay (for example password-manager/autofill UI), `chrome_fill` falls back to setting the field value through the page DOM and dispatching `input`/`change` events, unless `domFallback:false` is passed.
+
## 0.15.36 — 2026-06-03
- **Richer page observation.** `chrome_snapshot` now returns a concise, agent-friendly observation — structural layout/context, page hints, visible actions, form fields, a page map, query matches, and a diff of changes since the previous snapshot — instead of a raw JSON dump. New `mode` (`auto`/`interactive`/`forms`/`pageMap`/`text`/`changes`/`full`), `query`, and `maxTextChars` parameters let the agent zoom in instead of dumping the whole page.
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index c66e23a..fc4ea92 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -4,8 +4,25 @@ const POLL_ERROR_BACKOFF_MS = 2000;
const DEFAULT_GROUP_COLOR = "blue";
const PI_GROUP_RE = /^Pi(\b|\s*-)/i;
const VALID_GROUP_COLORS = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]);
+const COMMAND_TIMEOUT_MS = 25_000;
+const CDP_COMMAND_TIMEOUT_MS = 5_000;
+const SCRIPTING_TIMEOUT_MS = 8_000;
+const ATTACH_TIMEOUT_MS = 8_000;
let polling = false;
+function withTimeout(promise, ms, label, onTimeout) {
+ let timer;
+ return Promise.race([
+ Promise.resolve(promise).finally(() => clearTimeout(timer)),
+ new Promise((_, reject) => {
+ timer = setTimeout(async () => {
+ try { await onTimeout?.(); } catch {}
+ reject(new Error(`${label} timed out after ${ms}ms`));
+ }, ms);
+ }),
+ ]);
+}
+
// =================== Chrome input (CDP) layer ===================
// Tracks which tabs we have attached chrome.debugger to.
const attachedTabs = new Map(); // tabId -> { detachAt: number, pointer: {x,y} }
@@ -29,6 +46,31 @@ function recordAttachEvent(entry) {
if (attachDebugLog.length > 20) attachDebugLog.shift();
}
+function normalPageTarget(target, tabId) {
+ const url = String(target?.url || "");
+ return target?.tabId === tabId && target?.type === "page" && !url.startsWith("chrome://") && !url.startsWith("chrome-extension://") && !url.startsWith("devtools://");
+}
+
+async function pageDebuggeeForTab(tabId) {
+ const targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || []))).catch(() => []);
+ const target = targets.find((t) => normalPageTarget(t, tabId));
+ return target?.id ? { targetId: target.id } : { tabId };
+}
+
+async function debuggerAttachRaw(tabId, preferredDebuggee) {
+ const debuggee = preferredDebuggee || { tabId };
+ await withTimeout(
+ chrome.debugger.attach(debuggee, CDP_VERSION),
+ ATTACH_TIMEOUT_MS,
+ `Chrome debugger attach to tab ${tabId}`,
+ async () => {
+ attachedTabs.delete(tabId);
+ try { await chrome.debugger.detach(debuggee); } catch {}
+ },
+ );
+ return debuggee;
+}
+
async function attachDebugger(tabId) {
if (!chrome.debugger) throw new Error("chrome.debugger API unavailable; reload the extension to grant the new permission");
if (attachedTabs.has(tabId)) {
@@ -50,15 +92,23 @@ async function attachDebugger(tabId) {
}
}
} catch {}
- const attemptAttach = async () => {
+ let attachedDebuggee = null;
+ const attemptAttach = async (debuggee) => {
try {
- await chrome.debugger.attach({ tabId }, CDP_VERSION);
+ attachedDebuggee = await debuggerAttachRaw(tabId, debuggee);
return null;
} catch (error) {
return error;
}
};
+ const retryPageTargetIfExtensionBlocked = async (err, kind) => {
+ if (!/Cannot access a chrome-extension:\/\/ URL of different extension/i.test(String(err?.message || err))) return err;
+ const pageDebuggee = await pageDebuggeeForTab(tabId);
+ recordAttachEvent({ kind, tabId, debuggee: pageDebuggee });
+ return attemptAttach(pageDebuggee);
+ };
let err = await attemptAttach();
+ if (err) err = await retryPageTargetIfExtensionBlocked(err, "attach-page-target-retry");
if (err) {
const msg = String(err?.message || err);
const transient = /Cannot access a chrome-extension|Cannot access contents of|No tab with id|Debugger is not attached|Another debugger|Target closed/i.test(msg);
@@ -70,6 +120,7 @@ async function attachDebugger(tabId) {
}
await sleep(180);
err = await attemptAttach();
+ if (err) err = await retryPageTargetIfExtensionBlocked(err, "attach-page-target-retry2");
if (err) {
recordAttachEvent({ kind: "attach-retry-failed", tabId, message: String(err.message || err), tabUrl: tabSnapshot?.url });
// One more try after a longer settle. Some Chrome builds need ~500ms after a navigation
@@ -77,37 +128,53 @@ async function attachDebugger(tabId) {
// will accept the target.
await sleep(500);
err = await attemptAttach();
+ if (err) err = await retryPageTargetIfExtensionBlocked(err, "attach-page-target-retry3");
if (err) {
recordAttachEvent({ kind: "attach-retry2-failed", tabId, message: String(err.message || err), tabUrl: tabSnapshot?.url });
- throw err;
+ const meta = await describeInputTarget(tabId);
+ throw new Error(`Chrome debugger attach failed for tab ${tabId}: ${String(err.message || err)}${targetMetaSuffix(meta)}`);
}
}
}
- recordAttachEvent({ kind: "attached", tabId });
+ recordAttachEvent({ kind: "attached", tabId, debuggee: attachedDebuggee });
// Seed pointer in a plausible "just left the address bar" location.
- const entry = { detachAt: Date.now() + INPUT_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 } };
+ const entry = { detachAt: Date.now() + INPUT_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 }, debuggee: attachedDebuggee || { tabId } };
attachedTabs.set(tabId, entry);
return entry;
}
-async function inputDebug(params) {
- const tab = params?.targetId ? await chrome.tabs.get(Number(params.targetId)).catch(() => null) : null;
+async function describeInputTarget(tabId) {
+ const tab = await chrome.tabs.get(Number(tabId)).catch(() => null);
+ const active = (await chrome.tabs.query({ active: true, lastFocusedWindow: true }).catch(() => []))[0] || null;
let targets = [];
try { targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || []))); } catch {}
return {
+ resolvedTab: tab ? { id: tab.id, windowId: tab.windowId, url: tab.url, status: tab.status, title: tab.title, active: tab.active } : null,
+ activeTab: active ? { id: active.id, windowId: active.windowId, url: active.url, status: active.status, title: active.title, active: active.active } : null,
+ attachedTabs: Array.from(attachedTabs.keys()),
+ cdpTargets: targets.map((t) => ({ id: t.id, tabId: t.tabId, type: t.type, url: t.url, attached: t.attached, extensionId: t.extensionId })),
+ };
+}
+
+function targetMetaSuffix(meta) {
+ return `\nTarget metadata: ${JSON.stringify(meta).slice(0, 4000)}`;
+}
+
+async function inputDebug(params) {
+ const requested = params?.targetId ? await describeInputTarget(Number(params.targetId)) : await describeInputTarget(-1);
+ return {
extensionVersion: chrome.runtime.getManifest().version,
extensionId: chrome.runtime.id,
- attachedTabs: Array.from(attachedTabs.keys()),
- requestedTab: tab ? { id: tab.id, url: tab.url, status: tab.status, title: tab.title } : null,
- cdpTargets: targets,
+ ...requested,
recentAttachEvents: attachDebugLog.slice(),
};
}
async function detachDebugger(tabId) {
- if (!attachedTabs.has(tabId)) return;
+ const entry = attachedTabs.get(tabId);
+ if (!entry) return;
attachedTabs.delete(tabId);
- try { await chrome.debugger.detach({ tabId }); } catch {}
+ try { await chrome.debugger.detach(entry.debuggee || { tabId }); } catch {}
}
async function detachAll() {
@@ -134,14 +201,22 @@ setInterval(() => {
}, 5000);
function cdpRaw(tabId, method, params) {
- return new Promise((resolve, reject) => {
- chrome.debugger.sendCommand({ tabId }, method, params || {}, (result) => {
+ const debuggee = attachedTabs.get(tabId)?.debuggee || { tabId };
+ return withTimeout(new Promise((resolve, reject) => {
+ chrome.debugger.sendCommand(debuggee, method, params || {}, (result) => {
if (chrome.runtime.lastError) reject(new Error(`${method}: ${chrome.runtime.lastError.message}`));
else resolve(result);
});
+ }), CDP_COMMAND_TIMEOUT_MS, `CDP ${method}`, async () => {
+ attachedTabs.delete(tabId);
+ try { await chrome.debugger.detach(debuggee); } catch {}
});
}
+function executeScriptTimed(options, label) {
+ return withTimeout(chrome.scripting.executeScript(options), SCRIPTING_TIMEOUT_MS, label || "chrome.scripting.executeScript");
+}
+
// Wraps cdpRaw with one auto-recover on detached/closed sessions:
// chrome.debugger.attach can stay cached in attachedTabs even after Chrome killed
// the session (tab nav, devtools opened/closed, etc). Recover by detaching the
@@ -214,8 +289,7 @@ async function cdp(tabId, method, params) {
}
if (!isStale) throw error;
attachedTabs.delete(tabId);
- await chrome.debugger.attach({ tabId }, CDP_VERSION).catch(() => undefined);
- attachedTabs.set(tabId, { detachAt: Date.now() + INPUT_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 } });
+ await attachDebugger(tabId).catch(() => undefined);
return cdpRaw(tabId, method, params);
}
}
@@ -254,14 +328,18 @@ function cdpIsSyntaxError(details) {
// Resolve target -> {x, y, rect} in viewport coords by running tiny script in tab.
async function resolveTargetInTab(tabId, params) {
- const results = await chrome.scripting.executeScript({
+ const results = await executeScriptTimed({
target: { tabId, frameIds: [0] },
world: "MAIN",
func: (selector, uid, x, y) => {
const state = window.__PI_CHROME_STATE__;
let el = null;
- if (uid && state && state.elements && state.elements[uid]) el = state.elements[uid];
- else if (selector) el = document.querySelector(selector);
+ if (uid) {
+ el = state && state.elements ? state.elements[uid] : null;
+ if (!el || !el.isConnected) return { found: false, staleUid: true, reason: `snapshot uid ${uid} is stale; refresh chrome_snapshot`, url: location.href };
+ } else if (selector) {
+ el = document.querySelector(selector);
+ }
if (el) {
el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
const r = el.getBoundingClientRect();
@@ -271,8 +349,9 @@ async function resolveTargetInTab(tabId, params) {
return { found: false };
},
args: [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null],
- });
+ }, `resolve input target in tab ${tabId}`);
const v = results?.[0]?.result;
+ if (v?.staleUid) throw new Error(v.reason || "snapshot uid is stale; refresh chrome_snapshot");
if (!v || !v.found) throw new Error("Could not resolve target element for Chrome input");
return v;
}
@@ -504,29 +583,74 @@ async function chromeInputType(params) {
return { input: "chrome", length: text.length };
}
+async function domFillFallback(tabId, params, cause) {
+ if (!(params.selector || params.uid)) throw cause;
+ const results = await executeScriptTimed({
+ target: { tabId, frameIds: [0] },
+ world: "MAIN",
+ func: async (selector, uid, text, submit) => {
+ const state = window.__PI_CHROME_STATE__;
+ let el = uid && state && state.elements ? state.elements[uid] : null;
+ if (uid && (!el || !el.isConnected)) return { staleUid: true, reason: `snapshot uid ${uid} is stale; refresh chrome_snapshot`, url: location.href };
+ if (!el && selector) el = document.querySelector(selector);
+ if (!el) throw new Error(`DOM fallback target not found: ${uid || selector}`);
+ el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
+ if (typeof el.focus === "function") el.focus({ preventScroll: true });
+ const value = String(text ?? "");
+ if ("value" in el) {
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
+ if (setter) setter.call(el, value);
+ else el.value = value;
+ } else if (el.isContentEditable) {
+ el.textContent = value;
+ } else {
+ throw new Error(`DOM fallback target is not fillable: <${el.tagName.toLowerCase()}>`);
+ }
+ el.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
+ el.dispatchEvent(new Event("change", { bubbles: true }));
+ if (submit) {
+ const form = el.closest("form");
+ if (form) form.requestSubmit ? form.requestSubmit() : form.submit();
+ else document.querySelector("button,[type=submit]")?.click();
+ }
+ return { valueMatches: "value" in el ? el.value === value : el.textContent === value, tag: el.tagName, url: location.href };
+ },
+ args: [params.selector ?? null, params.uid ?? null, params.text ?? "", params.submit === true],
+ }, `DOM fill fallback in tab ${tabId}`);
+ const v = results?.[0]?.result;
+ if (v?.staleUid) throw new Error(v.reason || "snapshot uid is stale; refresh chrome_snapshot");
+ return { input: "dom-fallback", length: String(params.text || "").length, valueMatches: v?.valueMatches, reason: String(cause?.message || cause).slice(0, 500), tag: v?.tag };
+}
+
async function chromeInputFill(params) {
const tab = await getTabByParams(params);
if (params.foreground) await bringToFront(tab);
- await attachDebugger(tab.id);
- if (!(params.selector || params.uid)) throw new Error("chrome.fill: selector or uid required");
- const resolved = await resolveTargetInTab(tab.id, params);
- const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
- await cdpMoveTo(tab.id, point.x, point.y);
- // Triple-click selects all in input fields.
- for (let i = 1; i <= 3; i++) {
- await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse", force: 0.5 });
- await sleep(rng(20, 60));
- await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: i, pointerType: "mouse" });
+ try {
+ await attachDebugger(tab.id);
+ if (!(params.selector || params.uid)) throw new Error("chrome.fill: selector or uid required");
+ const resolved = await resolveTargetInTab(tab.id, params);
+ const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
+ await cdpMoveTo(tab.id, point.x, point.y);
+ // Triple-click selects all in input fields.
+ for (let i = 1; i <= 3; i++) {
+ await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse", force: 0.5 });
+ await sleep(rng(20, 60));
+ await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: i, pointerType: "mouse" });
+ await sleep(rng(20, 60));
+ }
+ // Delete selection.
+ await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyDown", key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 });
+ await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyUp", key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 });
await sleep(rng(20, 60));
+ const text = String(params.text || "");
+ for (const ch of Array.from(text)) await cdpTypeChar(tab.id, ch);
+ if (params.submit) await chromeInputKey({ ...params, key: "Enter" });
+ return { input: "chrome", length: text.length };
+ } catch (error) {
+ if (params.domFallback === false) throw error;
+ return domFillFallback(tab.id, params, error);
}
- // Delete selection.
- await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyDown", key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 });
- await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyUp", key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 });
- await sleep(rng(20, 60));
- const text = String(params.text || "");
- for (const ch of Array.from(text)) await cdpTypeChar(tab.id, ch);
- if (params.submit) await chromeInputKey({ ...params, key: "Enter" });
- return { input: "chrome", length: text.length };
}
async function chromeInputScroll(params) {
@@ -714,7 +838,12 @@ async function pollLoop() {
async function handleCommand(command) {
try {
- const result = await dispatch(command.action, command.params ?? {});
+ const result = await withTimeout(
+ dispatch(command.action, command.params ?? {}),
+ COMMAND_TIMEOUT_MS,
+ command.action || "Chrome command",
+ () => detachAll(),
+ );
await postResult({ id: command.id, ok: true, result });
} catch (error) {
await postResult({ id: command.id, ok: false, error: error?.message ?? String(error) });
@@ -936,7 +1065,7 @@ async function getTabByParams(params) {
let tab;
if (params.targetId !== undefined) {
const id = Number(params.targetId);
- tab = tabs.find((candidate) => candidate.id === id);
+ tab = await chrome.tabs.get(id).catch(() => null);
if (!tab?.id) {
// Chrome tab ids are not stable across reloads/navigations; a long session can hold a
// stale id. Surface the current tabs so the caller can re-target instead of guessing.
@@ -960,8 +1089,9 @@ async function getTabByParams(params) {
tab = active[0] || tabs.find((candidate) => candidate.active) || tabs[0];
}
if (!tab?.id) throw new Error("No matching Chrome tab found");
- if ((tab.url || "").startsWith("chrome://") || (tab.url || "").startsWith("chrome-extension://")) {
- throw new Error(`Chrome blocks extension automation on protected URL: ${tab.url}`);
+ const url = tab.url || "";
+ if (url.startsWith("chrome://") || url.startsWith("chrome-extension://") || url.startsWith("devtools://")) {
+ throw new Error(`Chrome blocks extension automation on protected URL: tab=${tab.id} url=${url}`);
}
// Tabs Pi interacts with (page.* actions) join this session's group so the user can see exactly
// which tabs Pi is driving. We only adopt *ungrouped* tabs — never hijack a tab the user (or
@@ -1032,7 +1162,7 @@ async function executeInTab(params, func, args) {
// Phase 2: run the action via chrome.scripting.executeScript. The `func:` form is
// injected by Chrome itself (not `new Function`), so it is CSP-safe, and it lets Chrome
// serialize the invocation args. The wrapper references window.__piAction defined above.
- const results = await chrome.scripting.executeScript({
+ const results = await executeScriptTimed({
target: { tabId: tab.id },
world: "MAIN",
func: async (invocationArgs) => {
@@ -1043,7 +1173,7 @@ async function executeInTab(params, func, args) {
}
},
args: [args || []],
- });
+ }, `execute page action in tab ${tab.id}`);
const first = results?.[0];
if (first?.error) {
const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error));
@@ -1139,12 +1269,12 @@ async function snapshotInTab(params) {
params.query ?? null,
params.maxTextChars ?? null,
];
- await chrome.scripting.executeScript({
+ await executeScriptTimed({
target: { tabId: tab.id, frameIds: [0] },
world: "MAIN",
files: ["snapshot_injected.js"],
- });
- const results = await chrome.scripting.executeScript({
+ }, `inject snapshot script in tab ${tab.id}`);
+ const results = await executeScriptTimed({
target: { tabId: tab.id, frameIds: [0] },
world: "MAIN",
func: async (invocationArgs) => {
@@ -1157,7 +1287,7 @@ async function snapshotInTab(params) {
}
},
args: [args],
- });
+ }, `run snapshot script in tab ${tab.id}`);
const first = results?.[0];
if (first?.error) {
const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error));
@@ -1175,12 +1305,12 @@ async function inspectInTab(params) {
const tab = await getTabByParams(params);
if (params.foreground) await bringToFront(tab);
const args = [params.uid ?? null, params.selector ?? null, params.scrollIntoView === true];
- await chrome.scripting.executeScript({
+ await executeScriptTimed({
target: { tabId: tab.id, frameIds: [0] },
world: "MAIN",
files: ["snapshot_injected.js"],
- });
- const results = await chrome.scripting.executeScript({
+ }, `inject inspect script in tab ${tab.id}`);
+ const results = await executeScriptTimed({
target: { tabId: tab.id, frameIds: [0] },
world: "MAIN",
func: async (invocationArgs) => {
@@ -1193,7 +1323,7 @@ async function inspectInTab(params) {
}
},
args: [args],
- });
+ }, `run inspect script in tab ${tab.id}`);
const first = results?.[0];
if (first?.error) {
const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error));
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index ef89e23..7542708 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -1478,6 +1478,7 @@ Usage rules:
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot." })),
selector: Type.Optional(Type.String({ description: "CSS selector to fill if uid is omitted." })),
submit: Type.Optional(Type.Boolean({ description: "If true, press Enter after filling." })),
+ domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM value-setting if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, include a fresh chrome_snapshot result after filling." })),
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
targetId: Type.Optional(Type.String()),