surf-cli 2.13.1 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
+ import { createHash } from "node:crypto";
2
3
  import { Type } from "typebox";
3
4
 
4
5
  const require = createRequire(import.meta.url);
@@ -26,6 +27,9 @@ const ORACLE_FINISHED_CHANNEL = "surf-oracle:finished";
26
27
  const BACKGROUND_WORK_PROTOCOL_VERSION = 1;
27
28
  const BACKGROUND_WORK_REGISTRY_KEY = "pi-subagents.background-work.v1";
28
29
  const BACKGROUND_WORK_MODULE_SPECIFIER = "pi-subagents/background-work";
30
+ const EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION = 1;
31
+ const EXTERNAL_JOB_PROVIDER_REGISTRY_KEY = "pi-subagents.external-job-providers.v1";
32
+ const EXTERNAL_JOB_PROVIDER_MODULE_SPECIFIER = "pi-subagents/external-job-provider";
29
33
 
30
34
  type Pi = {
31
35
  registerTool(tool: Record<string, unknown>): void;
@@ -37,6 +41,20 @@ type SurfEndpoint = { kind?: string };
37
41
 
38
42
  type ToolResult = { content: Array<{ type: "text" | "image"; text?: string; data?: string; mimeType?: string }>; details?: unknown; isError?: boolean };
39
43
 
44
+ type OracleJob = {
45
+ id: string;
46
+ state: string;
47
+ conversationUrl?: string | null;
48
+ model?: string | null;
49
+ modelRequested?: string | null;
50
+ modelVerified?: string | null;
51
+ effortRequested?: string | null;
52
+ effortVerified?: string | null;
53
+ promptDigest?: string | null;
54
+ response?: string;
55
+ error?: { code?: string; message?: string } | null;
56
+ };
57
+
40
58
  type BackgroundWorkProvider = {
41
59
  name: string;
42
60
  wakeChannels: string[];
@@ -54,6 +72,46 @@ type BackgroundWorkRegistry = {
54
72
  providers: Map<string, BackgroundWorkProvider>;
55
73
  };
56
74
 
75
+ type OracleExternalJob = {
76
+ provider: "surf-oracle";
77
+ id: string;
78
+ state: string;
79
+ conversationUrl: string | null;
80
+ requestedModel: string | null;
81
+ verifiedModel: string | null;
82
+ requestedEffort: string | null;
83
+ verifiedEffort: string | null;
84
+ promptDigest: string | null;
85
+ resultText?: string;
86
+ resultArtifact?: { kind: "inline-text"; bytes: number };
87
+ failure?: { code: string; message: string };
88
+ };
89
+
90
+ type OracleExternalJobProvider = {
91
+ name: "surf-oracle";
92
+ kind: "external-job";
93
+ wakeChannels: string[];
94
+ start(input: Record<string, unknown>): Promise<OracleExternalJob>;
95
+ status(id: string): Promise<OracleExternalJob>;
96
+ result(id: string, input?: Record<string, unknown>): Promise<OracleExternalJob>;
97
+ reattach(id: string, input?: Record<string, unknown>): Promise<OracleExternalJob>;
98
+ follow(id: string, message: string, input?: Record<string, unknown>): Promise<OracleExternalJob>;
99
+ };
100
+
101
+ type RegisterExternalJobProvider = (provider: OracleExternalJobProvider) => () => void;
102
+
103
+ type ExternalJobProviderModule = {
104
+ registerExternalJobProvider?: unknown;
105
+ };
106
+
107
+ type ExternalJobProviderRegistry = {
108
+ version: typeof EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION;
109
+ providers: Map<string, OracleExternalJobProvider>;
110
+ };
111
+
112
+ type RememberOracleJob = (jobId: string) => boolean;
113
+ type EmitOracleJob = (job: Pick<OracleExternalJob, "id" | "state">) => boolean;
114
+
57
115
  function textResult(value: unknown, isError = false): ToolResult {
58
116
  const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
59
117
  const bounded = text.length > MAX_OUTPUT_CHARS
@@ -64,7 +122,12 @@ function textResult(value: unknown, isError = false): ToolResult {
64
122
 
65
123
  export function resultFromHost(response: Record<string, unknown>): ToolResult {
66
124
  const error = response.error as { content?: Array<{ text?: string }> } | undefined;
67
- if (error) return textResult(error.content?.map((item) => item.text ?? "").join("\n") || "Surf request failed", true);
125
+ if (error) {
126
+ return {
127
+ ...textResult(error.content?.map((item) => item.text ?? "").join("\n") || "Surf request failed", true),
128
+ details: error,
129
+ };
130
+ }
68
131
  const result = response.result as { content?: ToolResult["content"] } | undefined;
69
132
  if (!result?.content) return textResult(result ?? "OK");
70
133
  const text = result.content.find((item) => item.type === "text")?.text;
@@ -138,6 +201,33 @@ export function registerGlobalBackgroundProvider(provider: BackgroundWorkProvide
138
201
  };
139
202
  }
140
203
 
204
+ export function registerGlobalExternalJobProvider(provider: OracleExternalJobProvider): () => void {
205
+ const key = Symbol.for(EXTERNAL_JOB_PROVIDER_REGISTRY_KEY);
206
+ const globalObject = globalThis as Record<PropertyKey, unknown>;
207
+ const existing = globalObject[key];
208
+ let registry: ExternalJobProviderRegistry;
209
+
210
+ if (existing === undefined) {
211
+ registry = { version: EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION, providers: new Map() };
212
+ globalObject[key] = registry;
213
+ } else if (
214
+ existing &&
215
+ typeof existing === "object" &&
216
+ !Array.isArray(existing) &&
217
+ (existing as Partial<ExternalJobProviderRegistry>).version === EXTERNAL_JOB_PROVIDER_PROTOCOL_VERSION &&
218
+ (existing as Partial<ExternalJobProviderRegistry>).providers instanceof Map
219
+ ) {
220
+ registry = existing as ExternalJobProviderRegistry;
221
+ } else {
222
+ throw new Error(`Unsupported external-job provider registry at Symbol.for("${EXTERNAL_JOB_PROVIDER_REGISTRY_KEY}").`);
223
+ }
224
+
225
+ registry.providers.set(provider.name, provider);
226
+ return () => {
227
+ if (registry.providers.get(provider.name) === provider) registry.providers.delete(provider.name);
228
+ };
229
+ }
230
+
141
231
  export async function resolveBackgroundWorkRegister(
142
232
  loadModule: () => Promise<BackgroundWorkModule> = () => import(BACKGROUND_WORK_MODULE_SPECIFIER) as Promise<BackgroundWorkModule>,
143
233
  ): Promise<RegisterBackgroundWorkProvider> {
@@ -152,6 +242,151 @@ export async function resolveBackgroundWorkRegister(
152
242
  return registerGlobalBackgroundProvider;
153
243
  }
154
244
 
245
+ export async function resolveExternalJobProviderRegister(
246
+ loadModule: () => Promise<ExternalJobProviderModule> = () => import(EXTERNAL_JOB_PROVIDER_MODULE_SPECIFIER) as Promise<ExternalJobProviderModule>,
247
+ ): Promise<RegisterExternalJobProvider> {
248
+ try {
249
+ const module = await loadModule();
250
+ if (typeof module.registerExternalJobProvider === "function") {
251
+ return module.registerExternalJobProvider as RegisterExternalJobProvider;
252
+ }
253
+ } catch {
254
+ // The provider bridge is optional until pi-subagents ships this consumer API.
255
+ }
256
+ return registerGlobalExternalJobProvider;
257
+ }
258
+
259
+ function digestPrompt(prompt: string) {
260
+ return `sha256:${createHash("sha256").update(prompt).digest("hex")}`;
261
+ }
262
+
263
+ function asOracleJob(value: unknown): OracleJob {
264
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Surf oracle response did not include job metadata");
265
+ const job = value as Partial<OracleJob>;
266
+ if (typeof job.id !== "string" || typeof job.state !== "string") throw new Error("Surf oracle response did not include job id and state");
267
+ return job as OracleJob;
268
+ }
269
+
270
+ function oracleExternalJob(job: OracleJob): OracleExternalJob {
271
+ const resultText = typeof job.response === "string" ? job.response : undefined;
272
+ const failure = job.error
273
+ ? { code: job.error.code || "failed", message: job.error.message || "Surf oracle job failed" }
274
+ : undefined;
275
+ const requestedModel = Object.hasOwn(job, "modelRequested") ? job.modelRequested ?? null : job.model ?? null;
276
+ return {
277
+ provider: "surf-oracle",
278
+ id: job.id,
279
+ state: job.state,
280
+ conversationUrl: job.conversationUrl ?? null,
281
+ requestedModel,
282
+ verifiedModel: job.modelVerified ?? null,
283
+ requestedEffort: job.effortRequested ?? null,
284
+ verifiedEffort: job.effortVerified ?? null,
285
+ promptDigest: job.promptDigest ?? null,
286
+ ...(resultText === undefined ? {} : { resultText, resultArtifact: { kind: "inline-text", bytes: Buffer.byteLength(resultText, "utf8") } }),
287
+ ...(failure ? { failure } : {}),
288
+ };
289
+ }
290
+
291
+ async function requestOracleJob(request: typeof requestSurf, tool: string, args: Record<string, unknown>) {
292
+ const result = await request(tool, args);
293
+ if (result.isError) {
294
+ const details = result.details as { code?: unknown; jobId?: unknown; message?: unknown } | undefined;
295
+ const message = typeof details?.message === "string"
296
+ ? details.message
297
+ : result.content.map((item) => item.text ?? "").join("\n") || "Surf oracle request failed";
298
+ const error = new Error(message);
299
+ if (typeof details?.code === "string") Object.assign(error, { code: details.code });
300
+ if (typeof details?.jobId === "string") Object.assign(error, { jobId: details.jobId });
301
+ throw error;
302
+ }
303
+ return oracleExternalJob(asOracleJob(result.details));
304
+ }
305
+
306
+ function emitFailedOracleJob(error: unknown, emitTerminal: EmitOracleJob) {
307
+ if (!error || typeof error !== "object" || !("jobId" in error) || typeof error.jobId !== "string") return;
308
+ emitTerminal({ id: error.jobId, state: "failed" });
309
+ }
310
+
311
+ function oracleOption(input: Record<string, unknown>, key: "model" | "effort"): string | undefined {
312
+ const options = input.options;
313
+ if (options && typeof options === "object" && !Array.isArray(options)) {
314
+ const value = (options as Record<string, unknown>)[key];
315
+ if (typeof value === "string") return value;
316
+ }
317
+ const direct = input[key];
318
+ return typeof direct === "string" ? direct : undefined;
319
+ }
320
+
321
+ export function createOracleExternalJobProvider(
322
+ sessionId: string,
323
+ jobIds: Set<string>,
324
+ request: typeof requestSurf = requestSurf,
325
+ rememberJob: RememberOracleJob = (jobId) => {
326
+ jobIds.add(jobId);
327
+ return true;
328
+ },
329
+ emitTerminal: EmitOracleJob = () => false,
330
+ ): OracleExternalJobProvider {
331
+ return {
332
+ name: "surf-oracle",
333
+ kind: "external-job",
334
+ wakeChannels: [ORACLE_FINISHED_CHANNEL],
335
+ async start(input) {
336
+ const prompt = typeof input.prompt === "string" ? input.prompt : "";
337
+ if (!prompt.trim()) throw new Error("prompt required");
338
+ const model = oracleOption(input, "model");
339
+ const effort = oracleOption(input, "effort");
340
+ const job = await requestOracleJob(request, "oracle.ask", {
341
+ prompt,
342
+ ...(model !== undefined ? { model } : {}),
343
+ ...(effort !== undefined ? { effort } : {}),
344
+ });
345
+ rememberJob(job.id);
346
+ return { ...job, promptDigest: job.promptDigest ?? digestPrompt(prompt) };
347
+ },
348
+ status(id) {
349
+ return requestOracleJob(request, "oracle.status", { id });
350
+ },
351
+ result(id, input = {}) {
352
+ return requestOracleJob(request, "oracle.result", { id, ...(typeof input.timeout === "number" ? { timeout: input.timeout } : {}) })
353
+ .then((job) => {
354
+ emitTerminal({ id: job.id, state: job.state });
355
+ return job;
356
+ })
357
+ .catch((error) => {
358
+ emitFailedOracleJob(error, emitTerminal);
359
+ throw error;
360
+ });
361
+ },
362
+ reattach(id, input = {}) {
363
+ return requestOracleJob(request, "oracle.result", { id, ...(typeof input.timeout === "number" ? { timeout: input.timeout } : {}) })
364
+ .then((job) => {
365
+ rememberJob(job.id);
366
+ emitTerminal({ id: job.id, state: job.state });
367
+ return job;
368
+ })
369
+ .catch((error) => {
370
+ emitFailedOracleJob(error, emitTerminal);
371
+ throw error;
372
+ });
373
+ },
374
+ async follow(id, message, input = {}) {
375
+ if (!message.trim()) throw new Error("message required");
376
+ const model = oracleOption(input, "model");
377
+ const effort = oracleOption(input, "effort");
378
+ const job = await requestOracleJob(request, "oracle.ask", {
379
+ follow: id,
380
+ prompt: message,
381
+ ...(model !== undefined ? { model } : {}),
382
+ ...(effort !== undefined ? { effort } : {}),
383
+ });
384
+ rememberJob(job.id);
385
+ return { ...job, promptDigest: job.promptDigest ?? digestPrompt(message) };
386
+ },
387
+ };
388
+ }
389
+
155
390
  export function registerOptionalBackgroundProvider(sessionId: string, jobIds: Set<string>, listJobs: () => Array<{ id: string; state: string }>, register: RegisterBackgroundWorkProvider) {
156
391
  return register({
157
392
  name: "surf-oracle",
@@ -162,6 +397,17 @@ export function registerOptionalBackgroundProvider(sessionId: string, jobIds: Se
162
397
  });
163
398
  }
164
399
 
400
+ export function registerOptionalExternalJobProvider(
401
+ sessionId: string,
402
+ jobIds: Set<string>,
403
+ register: RegisterExternalJobProvider,
404
+ request: typeof requestSurf = requestSurf,
405
+ rememberJob?: RememberOracleJob,
406
+ emitTerminal?: EmitOracleJob,
407
+ ) {
408
+ return register(createOracleExternalJobProvider(sessionId, jobIds, request, rememberJob, emitTerminal));
409
+ }
410
+
165
411
  export function rememberOracleJobForSession(jobIds: Set<string>, jobId: unknown, requestGeneration: number, currentGeneration: number, sessionActive: boolean): boolean {
166
412
  if (typeof jobId !== "string" || !sessionActive || requestGeneration !== currentGeneration) return false;
167
413
  jobIds.add(jobId);
@@ -219,7 +465,12 @@ export default function surfExtension(pi: Pi) {
219
465
  async execute(_id: string, args: Record<string, unknown>) {
220
466
  try {
221
467
  const result = await requestSurf("oracle.result", args);
222
- emitOracleFinished(pi, result.details ?? { id: args.id, state: result.isError ? "failed" : undefined });
468
+ const errorDetails = result.details as { jobId?: unknown } | undefined;
469
+ if (result.isError) {
470
+ if (typeof errorDetails?.jobId === "string") emitOracleFinished(pi, { id: errorDetails.jobId, state: "failed" });
471
+ } else {
472
+ emitOracleFinished(pi, result.details);
473
+ }
223
474
  return result;
224
475
  } catch (error) {
225
476
  return textResult(error instanceof Error ? error.message : String(error), true);
@@ -249,12 +500,15 @@ export default function surfExtension(pi: Pi) {
249
500
  });
250
501
 
251
502
  let dispose: (() => void) | undefined;
503
+ let disposeExternal: (() => void) | undefined;
252
504
  pi.on("session_start", (_event, ctx) => {
253
505
  sessionGeneration++;
254
506
  const generation = sessionGeneration;
255
507
  sessionActive = false;
256
508
  dispose?.();
509
+ disposeExternal?.();
257
510
  dispose = undefined;
511
+ disposeExternal = undefined;
258
512
  oracleJobIds.clear();
259
513
 
260
514
  const session = ctx as { sessionManager?: { getSessionId?: () => string }; sessionId?: string };
@@ -262,7 +516,10 @@ export default function surfExtension(pi: Pi) {
262
516
  if (!sessionId) return;
263
517
  try {
264
518
  const jobs = require("../native/oracle-jobs.cjs") as { listJobs(): Array<{ id: string; state: string }> };
519
+ const rememberForGeneration = (jobId: string) => rememberOracleJobForSession(oracleJobIds, jobId, generation, sessionGeneration, sessionActive);
520
+ const emitFinished = (job: Pick<OracleExternalJob, "id" | "state">) => emitOracleFinished(pi, job);
265
521
  dispose = registerOptionalBackgroundProvider(sessionId, oracleJobIds, jobs.listJobs, registerGlobalBackgroundProvider);
522
+ disposeExternal = registerOptionalExternalJobProvider(sessionId, oracleJobIds, registerGlobalExternalJobProvider, requestSurf, rememberForGeneration, emitFinished);
266
523
  sessionActive = true;
267
524
  void resolveBackgroundWorkRegister().then((register) => {
268
525
  try {
@@ -278,6 +535,20 @@ export default function surfExtension(pi: Pi) {
278
535
  // Keep the already-registered fallback provider.
279
536
  }
280
537
  });
538
+ void resolveExternalJobProviderRegister().then((register) => {
539
+ try {
540
+ if (register === registerGlobalExternalJobProvider || generation !== sessionGeneration) return;
541
+ const nextDispose = registerOptionalExternalJobProvider(sessionId, oracleJobIds, register, requestSurf, rememberForGeneration, emitFinished);
542
+ if (generation !== sessionGeneration) {
543
+ nextDispose();
544
+ return;
545
+ }
546
+ disposeExternal?.();
547
+ disposeExternal = nextDispose;
548
+ } catch {
549
+ // Keep the already-registered fallback provider.
550
+ }
551
+ });
281
552
  } catch {
282
553
  // The Pi bridge is optional. Browser tools work without pi-subagents.
283
554
  }
@@ -286,7 +557,9 @@ export default function surfExtension(pi: Pi) {
286
557
  sessionGeneration++;
287
558
  sessionActive = false;
288
559
  dispose?.();
560
+ disposeExternal?.();
289
561
  dispose = undefined;
562
+ disposeExternal = undefined;
290
563
  oracleJobIds.clear();
291
564
  });
292
565
  }
@@ -47,6 +47,17 @@ surf --find <term> # Search tools
47
47
  surf --help-topic <topic> # Topic guide (refs, semantic, frames, devices, windows)
48
48
  ```
49
49
 
50
+ ## First Command for Independent Agents
51
+
52
+ Before the first browser command in each independent agent shell, choose a unique valid session name and ensure its target exists:
53
+
54
+ ```bash
55
+ export SURF_SESSION="$(basename "$PWD" | sed 's/[^A-Za-z0-9._-]/-/g')"
56
+ surf session.ensure "$SURF_SESSION" about:blank
57
+ ```
58
+
59
+ `session.ensure` is idempotent. It creates a missing session, reuses a live binding, and reopens a stale or closed tab. Keep `SURF_SESSION` set for every later tab-scoped command in that shell. Use a distinct worktree/directory name per agent; when agents share one directory, append a stable agent identifier. Use `surf session.info "$SURF_SESSION"` to inspect the target and queue state.
60
+
50
61
  ## Core Workflow
51
62
 
52
63
  ```bash
@@ -78,7 +89,7 @@ Query AI models using your browser's logged-in session. Must be logged into the
78
89
  ```bash
79
90
  surf chatgpt "explain this code"
80
91
  surf chatgpt "summarize" --with-page # Include current page context
81
- surf chatgpt "review" --model gpt-4o # Specify model
92
+ surf chatgpt "review" --model gpt-5.5 # Specify model
82
93
  surf chatgpt "analyze" --file document.pdf # With file attachment
83
94
  ```
84
95
 
@@ -91,7 +102,7 @@ For agent workflows, detach after dispatch and keep the returned `.id`:
91
102
  ```bash
92
103
  surf oracle ask "Review this change and identify release risks" \
93
104
  --files "src/**/*.ts" --files "package.json" \
94
- --model pro --effort extended --detach --json
105
+ --model gpt-5.5 --effort pro --detach --json
95
106
 
96
107
  surf oracle status <job-id> --json
97
108
  surf oracle result <job-id> --json
@@ -101,7 +112,11 @@ surf oracle result <job-id> --wait --json
101
112
 
102
113
  `status` reads persisted state without touching Chrome. `result` attempts to harvest the answer and returns the job object with `response` once its state is `captured`. A Ctrl-C during waiting exits with status 130 and prints `Recover with: surf oracle result <id>`. Once the job is `awaiting`, the persisted ChatGPT conversation URL is its durable key, so `surf oracle result <id>` can recover after CLI exit, native-host restart, or Chrome restart by reopening that conversation.
103
114
 
104
- Treat Pro quota as scarce. Oracle never selects Pro implicitly; request it with `--model pro`. Accepted `--effort` values are `light`, `standard`, `extended`, and `heavy`. Requested model and effort selections are read back before submission, and an unverifiable selection fails with `model_verification_failed` instead of silently continuing. Capacity is one non-terminal oracle job. A `capacity` error includes the in-flight job ID; poll that job or wait for it to finish rather than submitting the same consult again.
115
+ Treat Pro quota as scarce. Oracle never selects Pro implicitly; request it with `--model pro` or `--effort pro`. ChatGPT model aliases include `instant`, `thinking`, `pro`, `gpt-5.5`, and `gpt-5.6-sol`. Accepted `--effort` values are `light`, `standard`, `extended`, `heavy`, and `pro`. Requested model and effort selections are read back before submission, and an unverifiable selection fails with `model_verification_failed` instead of silently continuing. Capacity is one non-terminal oracle job. A `capacity` error includes the in-flight job ID; poll that job or wait for it to finish rather than submitting the same consult again.
116
+
117
+ When loaded as a Pi extension, Surf also registers a `surf-oracle` external-job provider when the runtime exposes that bridge. The provider maps `start`, `status`, `result`, `reattach`, and `follow` to durable Surf Oracle jobs. It honors `options.model` and `options.effort` for starts and follows, so `model: pro` selects ChatGPT GPT-5.6 Sol Pro web mode through the browser. It returns the conversation URL, requested and verified model and effort, prompt digest, result text, and failure details. `reattach` only harvests an existing job by ID; it never submits the prompt again.
118
+
119
+ When Surf is installed as a Pi package, it exposes an optional `gpt-pro` package agent for `pi-subagents`. That profile uses `runner.type: external-job`, provider `surf-oracle`, and `options.model: pro`. Surf remains useful without Pi or `pi-subagents`.
105
120
 
106
121
  Context comes from repeatable `--files` globs. Surf fails closed when a glob matches nothing or a matched file is unreadable, binary, or invalid UTF-8. It also blocks gitignored files and basenames matching `.env*`, `*.pem`, `*.key`, `id_rsa*`, `id_ed25519*`, `*.p12`, `*.pfx`, `credentials*`, or `secrets*`. Use `--allow-sensitive` only after intentionally reviewing those files; it overrides the block rather than redacting content. Context up to 60,000 evidence characters is inserted inline, while larger context becomes one private text attachment. The assembly manifest records each path, byte count, SHA-256, inline or bundle disposition, and deny-list outcome.
107
122
 
@@ -245,20 +260,27 @@ surf window.resize --id 123 --width 1920 --height 1080
245
260
  surf window.resize --id 123 --state maximized # States: normal, minimized, maximized, fullscreen
246
261
  ```
247
262
 
248
- **Multi-agent isolation:**
263
+ **Concurrent agent sessions:**
264
+
249
265
  ```bash
250
- # Create a separate window for one agent and keep using its ID
251
- surf window.new "https://example.com"
252
- surf --window-id 123 tab.list
253
- surf --window-id 123 go "https://other.com"
266
+ # Required first command rule for each independent agent shell
267
+ export SURF_SESSION="$(basename "$PWD" | sed 's/[^A-Za-z0-9._-]/-/g')"
268
+ surf session.ensure "$SURF_SESSION" about:blank
269
+
270
+ # Explicit form when an environment variable is inconvenient
271
+ surf --session research go "https://example.com"
272
+ surf --session research read
254
273
 
255
- # Pin work to a specific tab, or name it for easier handoff
256
- surf read --tab-id 456
257
- surf tab.name agent-a --tab-id 456
258
- surf tab.switch agent-a
274
+ # Inspect bindings and scheduler state
275
+ surf session.list --refresh
276
+ surf session.info research --refresh
259
277
  ```
260
278
 
261
- Use `window.new`, `--window-id`, `--tab-id`, and named tabs to keep parallel agents on separate targets. Surf serializes non-streaming browser CLI requests per socket with a file-based lock, so agents sharing one native host wait instead of interleaving commands. Use `--no-lock` only for intentional bypasses. For hard isolation, run separate browser/profile instances with separate native hosts and `SURF_SOCKET` values; each socket gets its own lock. Surf does not yet have `session.new`, session IDs, or independent per-agent CDP sessions.
279
+ Each session owns one explicit tab and defaults to a separate unfocused window. Commands for the same tab are FIFO; different session tabs may run concurrently. Browser-wide writers wait for tab lanes to drain. `--no-wait` returns `tab_busy` or `browser_busy` immediately. On `tab_gone` or `session_epoch_stale`, run the exact command printed after `Recovery:`—normally `surf session.reopen <name>`.
280
+
281
+ Browser-login provider commands (`chatgpt`, `gemini`, `perplexity`, `grok`, `kimi`, `aistudio`, and `oracle ask`) take exclusive browser access and print a warning before dispatch. Do not assume Surf is hung while that warning is visible; inspect `surf session.info <name>` from another shell to see the active writer.
282
+
283
+ Sessions share cookies, authentication, same-origin storage, downloads, history, bookmarks, and other Chrome-profile state. Use separate browser/profile instances and `SURF_SOCKET` values only when hard isolation is required. Explicit `--tab-id`, `--window-id`, and named tabs remain available for one-off targeting.
262
284
 
263
285
  ## Input Methods
264
286
 
@@ -437,14 +459,16 @@ surf upload --ref e5 --files "/path/file1.txt,/path/file2.txt"
437
459
 
438
460
  ```bash
439
461
  surf frame.list # List frames with IDs
440
- surf frame.switch "FRAME_ID" # Switch to iframe context
462
+ surf frame.switch --selector "#payment-iframe"
463
+ surf frame.switch --name "checkout"
464
+ surf frame.switch --index 0 # First iframe
441
465
  surf frame.main # Return to main frame
442
- surf frame.js --id "FRAME_ID" --code "return document.title"
466
+ surf frame.js "return document.title" --id "FRAME_ID"
443
467
 
444
468
  # After frame.switch, subsequent commands target that frame:
445
- surf frame.switch "iframe-1"
469
+ surf frame.switch --selector "#payment-iframe"
446
470
  surf page.read # Reads iframe content
447
- surf click e5 # Clicks in iframe
471
+ surf click --selector "#pay" # Clicks in iframe
448
472
  surf frame.main # Back to main page
449
473
  ```
450
474
 
@@ -548,6 +572,9 @@ surf do 'go "https://example.com" | click e5 | screenshot'
548
572
  # Multi-step login flow
549
573
  surf do 'go "https://example.com/login" | type "user@example.com" --selector "#email" | type "pass" --selector "#password" | click --selector "button[type=submit]"'
550
574
 
575
+ # JSON action batch. Uses SURF_SESSION when it is set.
576
+ surf batch --actions '[{"type":"frame.switch","index":0},{"type":"click","selector":"#pay"}]'
577
+
551
578
  # Validate without executing
552
579
  surf do 'go "url" | click e5' --dry-run
553
580
  ```
@@ -681,9 +708,11 @@ surf wait.element ".missing" --auto-capture --timeout 2000
681
708
  ## Common Options
682
709
 
683
710
  ```bash
684
- --tab-id <id> # Target specific tab
685
- --window-id <id> # Target specific window
686
- --json # Raw JSON output
711
+ --session <name> # Target a durable named session (or set SURF_SESSION)
712
+ --tab-id <id> # Target a specific tab
713
+ --window-id <id> # Target a specific window
714
+ --no-wait # Return tab_busy/browser_busy instead of queueing
715
+ --json # Raw JSON including target metadata
687
716
  --auto-capture # Screenshot + console on error
688
717
  --timeout <ms> # Override default timeout
689
718
  ```
@@ -701,12 +730,12 @@ surf wait.element ".missing" --auto-capture --timeout 2000
701
730
  9. **AI Studio for unrestricted Gemini** - `surf aistudio` gives less filtered responses than `surf gemini` for the same models
702
731
  10. **Use `surf do` for multi-step tasks** - Reduces token overhead and improves reliability
703
732
  11. **Dry-run workflows first** - `surf do '...' --dry-run` validates without executing
704
- 12. **Window isolation** - Use `window.new` + `--window-id` or `--tab-id` to keep agent work separate from your browsing
705
- 13. **Request lock** - Non-streaming browser CLI requests serialize per socket; use `--no-lock` only when you intentionally want to bypass it
733
+ 12. **Session first** - Set a unique `SURF_SESSION` and run `session.ensure` before the first browser command in every independent agent shell
734
+ 13. **Queue diagnostics** - `session.info` distinguishes the session's own tab queue, other active tabs, and browser-wide writers; use `--no-wait` for immediate busy errors
706
735
  14. **Native host diagnostics** - If commands fail with socket/native-host errors, run `surf doctor` or `surf doctor --browser all` before guessing at reinstall steps
707
736
  15. **HTML export** - Use `surf page.html > artifact.html` to save Claude artifacts or any rendered page as static HTML
708
737
  16. **Animation capture** - Use `surf record --duration 2000 --fps 10 --output /tmp/anim.gif` when the agent needs to see motion; use `animate-audit` for numeric timelines and `perf-audit` for jank/layout-shift snapshots
709
- 17. **Hard isolation** - Use separate browser/profile instances plus separate `SURF_SOCKET` values when agents must not share a host or target
738
+ 17. **Hard isolation** - Sessions share a Chrome profile; use separate browser/profile instances plus separate `SURF_SOCKET` values when profile state must not be shared
710
739
  18. **Semantic locators** - `locate.role`, `locate.text`, `locate.label` for more robust element finding
711
740
  19. **Frame context** - Use `frame.switch` before interacting with iframe content
712
741
 
@@ -1,11 +0,0 @@
1
- const J=new Set(["alert","alertdialog","application","article","banner","blockquote","button","caption","cell","checkbox","code","columnheader","combobox","complementary","contentinfo","definition","deletion","dialog","directory","document","emphasis","feed","figure","form","generic","grid","gridcell","group","heading","img","insertion","link","list","listbox","listitem","log","main","mark","marquee","math","menu","menubar","menuitem","menuitemcheckbox","menuitemradio","meter","navigation","none","note","option","paragraph","presentation","progressbar","radio","radiogroup","region","row","rowgroup","rowheader","scrollbar","search","searchbox","separator","slider","spinbutton","status","strong","subscript","superscript","switch","tab","table","tablist","tabpanel","term","textbox","time","timer","toolbar","tooltip","tree","treegrid","treeitem"]);function Q(i){const g=i.tagName.toLowerCase();if(["button","input","select","textarea"].includes(g))return!i.disabled;if(g==="a"&&i.hasAttribute("href"))return!0;if(i.hasAttribute("tabindex")){const r=parseInt(i.getAttribute("tabindex")||"",10);return!isNaN(r)&&r>=0}return i.getAttribute("contenteditable")==="true"}function Z(i){const g=i.getAttribute("role");if(!g)return null;const r=g.split(/\s+/).filter(e=>e);for(const e of r)if(J.has(e))return e;return null}function X(i){const g=i.tagName.toLowerCase(),r=i.getAttribute("type"),e={a:o=>o.hasAttribute("href")?"link":"generic",article:"article",aside:"complementary",button:"button",datalist:"listbox",dd:"definition",details:"group",dialog:"dialog",dt:"term",fieldset:"group",figure:"figure",footer:o=>o.closest("article, aside, main, nav, section")?"generic":"contentinfo",form:o=>o.hasAttribute("aria-label")||o.hasAttribute("aria-labelledby")?"form":"generic",h1:"heading",h2:"heading",h3:"heading",h4:"heading",h5:"heading",h6:"heading",header:o=>o.closest("article, aside, main, nav, section")?"generic":"banner",hr:"separator",img:o=>o.getAttribute("alt")===""?"presentation":"img",li:"listitem",main:"main",math:"math",menu:"list",meter:"meter",nav:"navigation",ol:"list",optgroup:"group",option:"option",output:"status",p:"paragraph",progress:"progressbar",search:"search",section:o=>o.hasAttribute("aria-label")||o.hasAttribute("aria-labelledby")?"region":"generic",select:o=>{const t=o;return t.hasAttribute("multiple")||t.size&&t.size>1?"listbox":"combobox"},table:"table",tbody:"rowgroup",td:"cell",textarea:"textbox",tfoot:"rowgroup",th:"columnheader",thead:"rowgroup",time:"time",tr:"row",ul:"list"};if(g==="input")return{button:"button",checkbox:"checkbox",email:"textbox",file:"button",image:"button",number:"spinbutton",radio:"radio",range:"slider",reset:"button",search:"searchbox",submit:"button",tel:"textbox",text:"textbox",url:"textbox"}[r||""]||"textbox";const c=e[g];return typeof c=="function"?c(i):c||"generic"}function U(i){const g=Z(i);return!g||(g==="none"||g==="presentation")&&Q(i)?X(i):g}window.__piElementMap||(window.__piElementMap={});let ee=0;function q(i,g,r){const e=i._piRef;if(e&&e.role===g&&e.name===r)return e.ref;const c=`e${++ee}`;return i._piRef={role:g,name:r,ref:c},c}function Y(){const i=[];return document.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog[open]').forEach(r=>{const e=window.getComputedStyle(r);if(!(e.display!=="none"&&e.visibility!=="hidden"&&e.opacity!=="0"&&r.offsetWidth>0&&r.offsetHeight>0))return;const o=r.getAttribute("role")||"dialog";let t=r.getAttribute("aria-label")||r.querySelector('[role="heading"], h1, h2, h3')?.textContent?.trim()||"Dialog";t.length>100&&(t=t.substring(0,100)+"..."),i.push({type:o,description:`${o}: ${t}`,clearedBy:"computer(action=key, text=Escape)"})}),i}const j={wait(i){return new Promise(g=>setTimeout(g,i))},async waitForSelector(i,g={}){const{state:r="visible",timeout:e=2e4}=g,c=t=>{if(!t)return!1;const l=window.getComputedStyle(t);return l.display!=="none"&&l.visibility!=="hidden"&&l.opacity!=="0"&&t.offsetWidth>0&&t.offsetHeight>0},o=()=>{const t=document.querySelector(i);switch(r){case"attached":return t;case"detached":return t?null:document.body;case"hidden":return t?c(t)?null:t:document.body;default:return c(t)?t:null}};return new Promise((t,l)=>{const u=o();if(u){t(r==="detached"||r==="hidden"?null:u);return}const d=new MutationObserver(()=>{const a=o();a&&(d.disconnect(),clearTimeout(w),t(r==="detached"||r==="hidden"?null:a))}),w=setTimeout(()=>{d.disconnect(),l(new Error(`Timeout waiting for "${i}" to be ${r}`))},e);d.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["style","class","hidden"]})})},async waitForText(i,g={}){const{selector:r,timeout:e=2e4}=g,c=()=>{const o=r?document.querySelector(r):document.body;if(!o)return null;const t=document.createTreeWalker(o,NodeFilter.SHOW_TEXT);for(;t.nextNode();)if(t.currentNode.textContent?.includes(i))return t.currentNode.parentElement;return null};return new Promise((o,t)=>{const l=c();if(l){o(l);return}const u=new MutationObserver(()=>{const w=c();w&&(u.disconnect(),clearTimeout(d),o(w))}),d=setTimeout(()=>{u.disconnect(),t(new Error(`Timeout waiting for text "${i}"`))},e);u.observe(document.documentElement,{childList:!0,subtree:!0,characterData:!0})})},async waitForHidden(i,g=2e4){await j.waitForSelector(i,{state:"hidden",timeout:g})},getByRole(i,g={}){const{name:r}=g,e={button:["button",'input[type="button"]','input[type="submit"]','input[type="reset"]'],link:["a[href]"],textbox:["input:not([type])",'input[type="text"]','input[type="email"]','input[type="password"]','input[type="search"]','input[type="tel"]','input[type="url"]',"textarea"],checkbox:['input[type="checkbox"]'],radio:['input[type="radio"]'],combobox:["select"],heading:["h1","h2","h3","h4","h5","h6"],list:["ul","ol"],listitem:["li"],navigation:["nav"],main:["main"],banner:["header"],contentinfo:["footer"],form:["form"],img:["img"],table:["table"]},c=[];c.push(...document.querySelectorAll(`[role="${i}"]`));const o=e[i];if(o)for(const l of o)c.push(...document.querySelectorAll(`${l}:not([role])`));if(!r)return c[0]||null;const t=r.toLowerCase().trim();for(const l of c){const u=l.getAttribute("aria-label")?.toLowerCase().trim(),d=l.textContent?.toLowerCase().trim(),w=l.getAttribute("title")?.toLowerCase().trim(),a=l.getAttribute("placeholder")?.toLowerCase().trim();if(u===t||d===t||w===t||a===t||u?.includes(t)||d?.includes(t))return l}return null}};window.__piHelpers||(window.__piHelpers=j,window.piHelpers=j);function I(){return window.__piElementMap}function B(i="interactive",g=15,r,e=!1,c=!1){try{let o=function(n){return U(n)},t=function(n){const h=n.tagName.toLowerCase(),_=n.getAttribute("aria-labelledby");if(_){const y=_.split(/\s+/).map(E=>document.getElementById(E)?.textContent?.trim()||"").filter(Boolean);if(y.length){const E=y.join(" ");return E.length>100?E.substring(0,100)+"...":E}}if(h==="select"){const y=n,E=y.querySelector("option[selected]")||(y.selectedIndex>=0?y.options[y.selectedIndex]:null);if(E?.textContent?.trim())return E.textContent.trim()}const L=n.getAttribute("aria-label");if(L?.trim())return L.trim();const N=n.getAttribute("placeholder");if(N?.trim())return N.trim();const D=n.getAttribute("title");if(D?.trim())return D.trim();const k=n.getAttribute("alt");if(k?.trim())return k.trim();if(n.id){const y=document.querySelector(`label[for="${n.id}"]`);if(y?.textContent?.trim())return y.textContent.trim()}if(h==="input"){const y=n,E=n.getAttribute("type")||"",H=n.getAttribute("value");if(E==="submit"&&H?.trim())return H.trim();if(y.value&&y.value.length<50&&y.value.trim())return y.value.trim()}if(["button","a","summary"].includes(h)){let y="";for(const E of n.childNodes)E.nodeType===Node.TEXT_NODE&&(y+=E.textContent);if(y.trim())return y.trim()}if(/^h[1-6]$/.test(h)){const y=n.textContent;if(y?.trim()){const E=y.trim();return E.length>100?E.substring(0,100)+"...":E}}if(h==="img")return"";let S="";for(const y of n.childNodes)y.nodeType===Node.TEXT_NODE&&(S+=y.textContent);if(S?.trim()&&S.trim().length>=3){const y=S.trim();return y.length>100?y.substring(0,100)+"...":y}return""},l=function(n){const h={},_=n.getAttribute("aria-checked");_==="true"?h.checked=!0:_==="false"?h.checked=!1:_==="mixed"?h.checked="mixed":n instanceof HTMLInputElement&&(n.type==="checkbox"||n.type==="radio")&&(n.type==="checkbox"&&n.indeterminate?h.checked="mixed":h.checked=n.checked);const L=n instanceof HTMLButtonElement||n instanceof HTMLInputElement||n instanceof HTMLSelectElement||n instanceof HTMLTextAreaElement;(n.getAttribute("aria-disabled")==="true"||L&&n.disabled||n.closest("fieldset:disabled"))&&(h.disabled=!0);const N=n.getAttribute("aria-expanded");N==="true"?h.expanded=!0:N==="false"&&(h.expanded=!1);const D=n.getAttribute("aria-pressed");D==="true"?h.pressed=!0:D==="false"?h.pressed=!1:D==="mixed"&&(h.pressed="mixed");const k=n.getAttribute("aria-selected");k==="true"?h.selected=!0:k==="false"&&(h.selected=!1);const S=n.getAttribute("aria-current");S&&S!=="false"&&(h.active=!0);const y=n.tagName.toLowerCase();if(/^h[1-6]$/.test(y))h.level=parseInt(y[1],10);else{const E=n.getAttribute("aria-level");E&&(h.level=parseInt(E,10))}return h},u=function(n){const h=[];return n.checked!==void 0&&h.push(n.checked==="mixed"?"[checked=mixed]":n.checked?"[checked]":"[unchecked]"),n.disabled&&h.push("[disabled]"),n.expanded!==void 0&&h.push(n.expanded?"[expanded]":"[collapsed]"),n.pressed!==void 0&&h.push(n.pressed==="mixed"?"[pressed=mixed]":n.pressed?"[pressed]":"[not-pressed]"),n.selected!==void 0&&h.push(n.selected?"[selected]":"[not-selected]"),n.active&&h.push("[active]"),n.level!==void 0&&h.push(`[level=${n.level}]`),h.join(" ")},d=function(n){const h=window.getComputedStyle(n);return h.display!=="none"&&h.visibility!=="hidden"&&h.opacity!=="0"&&n.offsetWidth>0&&n.offsetHeight>0},w=function(n){const h=n.tagName.toLowerCase();return["a","button","input","select","textarea","details","summary"].includes(h)||n.hasAttribute("onclick")||n.hasAttribute("tabindex")||n.getAttribute("role")==="button"||n.getAttribute("role")==="link"||n.getAttribute("contenteditable")==="true"},a=function(n){const h=n.tagName.toLowerCase();return["h1","h2","h3","h4","h5","h6","nav","main","header","footer","section","article","aside"].includes(h)||n.hasAttribute("role")},f=function(n){return window.getComputedStyle(n).cursor==="pointer"},m=function(n,h){const _=n.tagName.toLowerCase();if(["script","style","meta","link","title","noscript"].includes(_)||h.filter!=="all"&&n.getAttribute("aria-hidden")==="true"||h.filter!=="all"&&!d(n))return!1;if(h.filter!=="all"&&!h.refId){const N=n.getBoundingClientRect();if(!(N.top<window.innerHeight&&N.bottom>0&&N.left<window.innerWidth&&N.right>0))return!1}if(h.filter==="interactive")return w(n);if(w(n)||a(n)||t(n).length>0)return!0;const L=o(n);return h.compact&&new Set(["generic","group","region","article","section","complementary"]).has(L)&&t(n).length===0?!1:L!=="generic"&&L!=="img"},b=function(n,h){const _=[],L={filter:i,refId:r||null,compact:c},N=I(),D=m(n,L)||r&&h===0;if(D){const k=o(n),S=t(n),y=l(n),E=q(n,k,S);window.__piRefs[E]=n,N[E]={element:new WeakRef(n),role:k,name:S};let W=`${" ".repeat(h)}${k}`;if(S){const K=S.replace(/\s+/g," ").replace(/"/g,'\\"');W+=` "${K}"`}W+=` [${E}]`;const R=u(y);R&&(W+=` ${R}`),f(n)&&(W+=" [cursor=pointer]");const P=n.getAttribute("href");P&&(W+=` href="${P}"`);const G=n.getAttribute("type");G&&(W+=` type="${G}"`);const V=n.getAttribute("placeholder");V&&(W+=` placeholder="${V}"`),_.push(W)}if(h<g)for(const k of n.children)_.push(...b(k,D?h+1:h));return _},s=function(n){return n.replace(/\[e\d+\]/g,"[REF]")},p=function(n){const h=new Map;for(const _ of n){if(!_.trim())continue;const L=s(_);h.set(L,(h.get(L)||0)+1)}return h},T=function(n,h){const _=n.split(`
2
- `),L=h.split(`
3
- `),N=p(_),D=p(L),k=[],S=[];for(const H of L){if(!H.trim())continue;const W=s(H),R=N.get(W)||0;(D.get(W)||0)>R&&(k.push(H),N.set(W,R+1))}const y=p(_);for(const H of _){if(!H.trim())continue;const W=s(H),R=y.get(W)||0,P=D.get(W)||0;R>P&&(S.push(H),y.set(W,R-1))}if(k.length===0&&S.length===0)return{diff:"[NO CHANGES]",hasChanges:!1};const E=[];return S.length>0&&E.push(...S.map(H=>`- ${H}`)),k.length>0&&E.push(...k.map(H=>`+ ${H}`)),{diff:E.join(`
4
- `),hasChanges:!0}};window.__piRefs={};const A=I();let C=null;if(r){const n=A[r];if(!n)return{error:`Element with ref_id '${r}' not found. Use read_page without ref_id to get current elements.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};const h=n.element.deref();if(!h)return delete A[r],{error:`Element with ref_id '${r}' no longer exists. Use read_page without ref_id to get current elements.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};C=h}else C=document.body;const O=C?b(C,0):[];for(const n of Object.keys(A))A[n].element.deref()||delete A[n];const $=O.join(`
5
- `);if($.length>5e4)return{error:`Output exceeds 50000 character limit (${$.length} characters). Try using filter="interactive" or specify a ref_id.`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}};const M=Y();let x,v=!1;const F=window.__piLastSnapshot;return!e&&!r&&F&&Date.now()-F.timestamp<5e3&&(x=T(F.content,$).diff,v=!0),window.__piLastSnapshot={content:$,timestamp:Date.now()},{pageContent:$+`
6
-
7
- [Viewport: ${window.innerWidth}x${window.innerHeight}]`,diff:v?x:void 0,viewport:{width:window.innerWidth,height:window.innerHeight},modalStates:M.length>0?M:void 0,modalLimitations:"Only custom modals ([role=dialog]) detected. Native alert/confirm/prompt dialogs and system file choosers cannot be detected from content scripts.",isIncremental:v}}catch(o){return{error:`Error generating accessibility tree: ${o instanceof Error?o.message:"Unknown error"}`,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}}}}function z(i){return i.length?/[\n\r]/.test(i)||/^[\s]/.test(i)||/[\s]$/.test(i)||/[:"{}[\]]/.test(i)?'"'+i.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r")+'"':i:'""'}function te(i="interactive",g=15){try{let r=function(s){return U(s)},e=function(s){const p=s.tagName.toLowerCase(),T=s.getAttribute("aria-labelledby");if(T){const x=T.split(/\s+/).map(v=>document.getElementById(v)?.textContent?.trim()||"").filter(Boolean);if(x.length){const v=x.join(" ");return v.length>100?v.substring(0,100)+"...":v}}if(p==="select"){const x=s,v=x.querySelector("option[selected]")||(x.selectedIndex>=0?x.options[x.selectedIndex]:null);if(v?.textContent?.trim())return v.textContent.trim()}const A=s.getAttribute("aria-label");if(A?.trim())return A.trim();const C=s.getAttribute("placeholder");if(C?.trim())return C.trim();const O=s.getAttribute("title");if(O?.trim())return O.trim();const $=s.getAttribute("alt");if($?.trim())return $.trim();if(s.id){const x=document.querySelector(`label[for="${s.id}"]`);if(x?.textContent?.trim())return x.textContent.trim()}if(p==="input"){const x=s,v=s.getAttribute("type")||"",F=s.getAttribute("value");if(v==="submit"&&F?.trim())return F.trim();if(x.value&&x.value.length<50&&x.value.trim())return x.value.trim()}if(["button","a","summary"].includes(p)){let x="";for(const v of s.childNodes)v.nodeType===Node.TEXT_NODE&&(x+=v.textContent);if(x.trim())return x.trim()}if(/^h[1-6]$/.test(p)){const x=s.textContent;if(x?.trim()){const v=x.trim();return v.length>100?v.substring(0,100)+"...":v}}if(p==="img")return"";let M="";for(const x of s.childNodes)x.nodeType===Node.TEXT_NODE&&(M+=x.textContent);if(M?.trim()&&M.trim().length>=3){const x=M.trim();return x.length>100?x.substring(0,100)+"...":x}return""},c=function(s){const p={},T=s.getAttribute("aria-checked");T==="true"?p.checked=!0:T==="false"?p.checked=!1:T==="mixed"?p.checked="mixed":s instanceof HTMLInputElement&&(s.type==="checkbox"||s.type==="radio")&&(s.type==="checkbox"&&s.indeterminate?p.checked="mixed":p.checked=s.checked);const A=s instanceof HTMLButtonElement||s instanceof HTMLInputElement||s instanceof HTMLSelectElement||s instanceof HTMLTextAreaElement;(s.getAttribute("aria-disabled")==="true"||A&&s.disabled||s.closest("fieldset:disabled"))&&(p.disabled=!0);const C=s.getAttribute("aria-expanded");C==="true"?p.expanded=!0:C==="false"&&(p.expanded=!1);const O=s.getAttribute("aria-pressed");O==="true"?p.pressed=!0:O==="false"?p.pressed=!1:O==="mixed"&&(p.pressed="mixed");const $=s.getAttribute("aria-selected");$==="true"?p.selected=!0:$==="false"&&(p.selected=!1);const M=s.getAttribute("aria-current");M&&M!=="false"&&(p.active=!0);const x=s.tagName.toLowerCase();if(/^h[1-6]$/.test(x))p.level=parseInt(x[1],10);else{const v=s.getAttribute("aria-level");v&&(p.level=parseInt(v,10))}return p},o=function(s){const p=[];return s.checked!==void 0&&p.push(s.checked==="mixed"?"[checked=mixed]":s.checked?"[checked]":"[unchecked]"),s.disabled&&p.push("[disabled]"),s.expanded!==void 0&&p.push(s.expanded?"[expanded]":"[collapsed]"),s.pressed!==void 0&&p.push(s.pressed==="mixed"?"[pressed=mixed]":s.pressed?"[pressed]":"[not-pressed]"),s.selected!==void 0&&p.push(s.selected?"[selected]":"[not-selected]"),s.active&&p.push("[active]"),s.level!==void 0&&p.push(`[level=${s.level}]`),p.join(" ")},t=function(s){const p=window.getComputedStyle(s);return p.display!=="none"&&p.visibility!=="hidden"&&p.opacity!=="0"&&s.offsetWidth>0&&s.offsetHeight>0},l=function(s){const p=s.tagName.toLowerCase();return["a","button","input","select","textarea","details","summary"].includes(p)||s.hasAttribute("onclick")||s.hasAttribute("tabindex")||s.getAttribute("role")==="button"||s.getAttribute("role")==="link"||s.getAttribute("contenteditable")==="true"},u=function(s){const p=s.tagName.toLowerCase();return["h1","h2","h3","h4","h5","h6","nav","main","header","footer","section","article","aside"].includes(p)||s.hasAttribute("role")},d=function(s){return window.getComputedStyle(s).cursor==="pointer"},w=function(s,p,T,A){let C=s;p&&(C+=" "+z(p));const O=q(T,s,p);window.__piRefs[O]=T,C+=` [ref=${O}]`;const $=o(A);return $&&(C+=` ${$}`),d(T)&&(C+=" [cursor=pointer]"),C},a=function(s){const p={},T=s.getAttribute("href");T&&(p.url=T);const A=s.getAttribute("placeholder");return A&&(p.placeholder=A),p},f=function(s,p,T){if(p>g)return;const A=s.tagName.toLowerCase();if(["script","style","meta","link","title","noscript"].includes(A)||i!=="all"&&s.getAttribute("aria-hidden")==="true"||i!=="all"&&!t(s))return;if(i!=="all"){const n=s.getBoundingClientRect();if(!(n.top<window.innerHeight&&n.bottom>0&&n.left<window.innerWidth&&n.right>0))return}const C=r(s),O=e(s),$=c(s),M=l(s),x=u(s),v=O.length>0;let F;if(i==="interactive"?F=M:i==="all"?F=!0:F=M||x||v||C!=="generic"&&C!=="img",F){const n=" ".repeat(p),h=w(C,O,s,$),_=a(s),L=[];for(const k of s.children)L.push(k);const N=L.length>0,D=Object.keys(_).length>0;if(!N&&!D)m.push(`${n}- ${h}`);else{m.push(`${n}- ${h}:`);for(const[k,S]of Object.entries(_))m.push(`${n} - /${k}: ${z(S)}`);for(const k of L)f(k,p+1,!0)}}else for(const n of s.children)f(n,p,T)};window.__piRefs={};const m=[];f(document.body,0,!1);const b=m.join(`
8
- `);return b.length>5e4?{error:`Output exceeds 50000 character limit (${b.length} characters). Try using filter="interactive".`,yaml:"",viewport:{width:window.innerWidth,height:window.innerHeight}}:{yaml:b+`
9
-
10
- [Viewport: ${window.innerWidth}x${window.innerHeight}]`,viewport:{width:window.innerWidth,height:window.innerHeight}}}catch(r){return{error:`Error generating YAML tree: ${r instanceof Error?r.message:"Unknown error"}`,yaml:"",viewport:{width:window.innerWidth,height:window.innerHeight}}}}function re(i){const g=I(),r=g[i];let e;if(r&&(e=r.element.deref(),e||delete g[i]),!e&&window.__piRefs&&(e=window.__piRefs[i]),!e)return{x:0,y:0,error:`Element ${i} not found. Use read_page to get current elements.`};const c=e.getBoundingClientRect(),o=Math.round(c.left+c.width/2),t=Math.round(c.top+c.height/2);return{x:o,y:t}}function ne(i,g){const r=I(),e=r[i];let c;if(e&&(c=e.element.deref(),c||delete r[i]),!c&&window.__piRefs&&(c=window.__piRefs[i]),!c)return{success:!1,error:`Element ${i} not found. Use read_page to get current elements.`};const o=c.tagName.toLowerCase();try{if(o==="input"){const t=c,l=t.type.toLowerCase();l==="checkbox"||l==="radio"?(t.checked=!!g,t.dispatchEvent(new Event("change",{bubbles:!0}))):(t.value=String(g),t.dispatchEvent(new Event("input",{bubbles:!0})),t.dispatchEvent(new Event("change",{bubbles:!0})))}else if(o==="textarea"){const t=c;t.value=String(g),t.dispatchEvent(new Event("input",{bubbles:!0})),t.dispatchEvent(new Event("change",{bubbles:!0}))}else if(o==="select"){const t=c,l=String(g);let u=!1;for(const d of t.options)if(d.value===l||d.textContent?.trim()===l){t.value=d.value,u=!0;break}if(!u)return{success:!1,error:`Option "${g}" not found in select element ${i}`};t.dispatchEvent(new Event("change",{bubbles:!0}))}else if(c.getAttribute("contenteditable")==="true")c.textContent=String(g),c.dispatchEvent(new Event("input",{bubbles:!0}));else return{success:!1,error:`Element ${i} (${o}) is not a form field`};return{success:!0}}catch(t){return{success:!1,error:`Failed to set value: ${t instanceof Error?t.message:"Unknown error"}`}}}function ie(){try{const i=document.querySelector("article"),g=document.querySelector("main");return{text:(i||g||document.body).textContent?.replace(/\s+/g," ").trim().substring(0,5e4)||"",title:document.title,url:window.location.href}}catch(i){return{text:"",title:"",url:"",error:`Failed to extract text: ${i instanceof Error?i.message:"Unknown error"}`}}}function oe(i){const g=I(),r=g[i];let e;return r&&(e=r.element.deref(),e||delete g[i]),!e&&window.__piRefs&&(e=window.__piRefs[i]),e?(e.scrollIntoView({behavior:"smooth",block:"center"}),{success:!0}):{success:!1,error:`Element ${i} not found. Run read_page to get current element refs.`}}function se(i,g,r,e="screenshot.png"){try{const c=atob(i),o=new ArrayBuffer(c.length),t=new Uint8Array(o);for(let f=0;f<c.length;f++)t[f]=c.charCodeAt(f);const l=new Blob([o],{type:"image/png"}),u=new File([l],e,{type:"image/png"});let d=null;if(g){const f=I(),m=f[g];if(m&&(d=m.element.deref(),d||delete f[g]),!d&&window.__piRefs&&(d=window.__piRefs[g]),!d)return{success:!1,error:`Element ${g} not found. Run read_page to get current element refs.`}}else if(r&&(d=document.elementFromPoint(r[0],r[1]),!d))return{success:!1,error:`No element at (${r[0]}, ${r[1]})`};if(!d)return{success:!1,error:"No target element"};if(d.tagName==="INPUT"&&d.type==="file"){const f=d,m=new DataTransfer;return m.items.add(u),f.files=m.files,f.dispatchEvent(new Event("change",{bubbles:!0})),{success:!0}}const w=new DataTransfer;w.items.add(u);const a=new DragEvent("drop",{bubbles:!0,cancelable:!0,dataTransfer:w});return d.dispatchEvent(a),{success:!0}}catch(c){return{success:!1,error:c instanceof Error?c.message:"Upload failed"}}}chrome.runtime.onMessage.addListener((i,g,r)=>{switch(i.type){case"GENERATE_ACCESSIBILITY_TREE":{const e=i.options||{};if(e.format==="yaml"){const c=te(e.filter||"interactive",e.depth??15),o=Y();c.error?r({error:c.error,pageContent:"",viewport:c.viewport}):r({pageContent:c.yaml,viewport:c.viewport,modalStates:o.length>0?o:void 0,modalLimitations:"Only custom modals ([role=dialog]) detected. Native alert/confirm/prompt dialogs and system file choosers cannot be detected from content scripts."})}else{const c=B(e.filter||"interactive",e.depth??15,e.refId,e.forceFullSnapshot??!1,e.compact??!1);r(c)}break}case"GET_ELEMENT_COORDINATES":{const e=re(i.ref);r(e);break}case"CLICK_ELEMENT":{const e=I(),c=e[i.ref];let o;if(c&&(o=c.element.deref(),o||delete e[i.ref]),!o&&window.__piRefs&&(o=window.__piRefs[i.ref]),!o){r({error:`Element ${i.ref} not found. Use read_page to get current elements.`});break}if(i.button==="triple"){const t=new MouseEvent("click",{bubbles:!0,cancelable:!0,view:window,detail:3});o.dispatchEvent(t)}else i.button==="double"?o.dispatchEvent(new MouseEvent("dblclick",{bubbles:!0,cancelable:!0,view:window})):i.button==="right"?o.dispatchEvent(new MouseEvent("contextmenu",{bubbles:!0,cancelable:!0,view:window})):o.click();r({success:!0});break}case"FORM_INPUT":{const e=ne(i.ref,i.value);r(e);break}case"EVAL_IN_PAGE":{try{const e=document.createElement("script");e.textContent=`(function() { ${i.code} })();`,document.documentElement.appendChild(e),e.remove(),r({success:!0})}catch(e){r({success:!1,error:e instanceof Error?e.message:String(e)})}break}case"GET_PAGE_TEXT":{const e=ie();r(e);break}case"GET_FRAME_BY_SELECTOR":{try{const e=document.querySelector(i.selector);if(!e||e.tagName.toLowerCase()!=="iframe"){r({error:`No iframe found with selector "${i.selector}"`});break}r({url:e.src,name:e.name||void 0})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"GET_FRAME_NAME":{try{r({name:window.name||null})}catch{r({name:null})}break}case"LOCATE_ROLE":{try{const{role:e,name:c,all:o}=i,t=I(),u={button:["button",'input[type="button"]','input[type="submit"]','input[type="reset"]','[role="button"]'],link:["a[href]",'[role="link"]'],textbox:["input:not([type])",'input[type="text"]','input[type="email"]','input[type="password"]','input[type="search"]','input[type="tel"]','input[type="url"]',"textarea",'[role="textbox"]'],checkbox:['input[type="checkbox"]','[role="checkbox"]'],radio:['input[type="radio"]','[role="radio"]'],combobox:["select",'[role="combobox"]'],listbox:['[role="listbox"]',"select[multiple]"],option:["option",'[role="option"]'],heading:["h1","h2","h3","h4","h5","h6",'[role="heading"]'],navigation:["nav",'[role="navigation"]'],main:["main",'[role="main"]'],img:["img[alt]",'[role="img"]'],dialog:["dialog",'[role="dialog"]','[role="alertdialog"]'],tab:['[role="tab"]'],tabpanel:['[role="tabpanel"]'],menu:['[role="menu"]'],menuitem:['[role="menuitem"]']}[e]||[`[role="${e}"]`],d=[];for(const m of u)try{d.push(...document.querySelectorAll(m))}catch{}const w=d.filter(m=>{const b=window.getComputedStyle(m);return b.display!=="none"&&b.visibility!=="hidden"&&m.offsetWidth>0&&m.offsetHeight>0});let a=w;if(c){const m=c.toLowerCase();a=w.filter(b=>{const s=b.getAttribute("aria-label")?.toLowerCase(),p=b.textContent?.trim().toLowerCase(),T=b.getAttribute("title")?.toLowerCase(),A=b.placeholder?.toLowerCase(),C=b.value?.toLowerCase();return s?.includes(m)||p?.includes(m)||T?.includes(m)||A?.includes(m)||C?.includes(m)})}if(a.length===0){r({error:`No element found with role "${e}"${c?` and name "${c}"`:""}`});break}const f=a.map(m=>{const b=q(m,e,c||"");return window.__piRefs=window.__piRefs||{},window.__piRefs[b]=m,t[b]={element:new WeakRef(m),role:e,name:c||""},{ref:b,text:m.textContent?.trim().slice(0,50)}});r(o?{matches:f}:{ref:f[0].ref,text:f[0].text})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"LOCATE_TEXT":{try{const{text:e,exact:c}=i,o=I(),t=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),l=[];for(;t.nextNode();){const a=t.currentNode.textContent||"";if(c?a.trim()===e:a.toLowerCase().includes(e.toLowerCase())){const m=t.currentNode.parentElement;if(m&&!l.includes(m)){const b=window.getComputedStyle(m);b.display!=="none"&&b.visibility!=="hidden"&&l.push(m)}}}if(l.length===0){r({error:`No element found with text "${e}"`});break}const u=l.sort((a,f)=>(a.textContent?.length||0)-(f.textContent?.length||0))[0],d=U(u),w=q(u,d,e);window.__piRefs=window.__piRefs||{},window.__piRefs[w]=u,o[w]={element:new WeakRef(u),role:d,name:e},r({ref:w,text:u.textContent?.trim().slice(0,50)})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"LOCATE_LABEL":{try{const{label:e}=i,c=I(),o=document.querySelectorAll("label");let t=null;for(const d of o)if(d.textContent?.trim().toLowerCase()?.includes(e.toLowerCase())){const a=d.getAttribute("for");if(a&&(t=document.getElementById(a)),t||(t=d.querySelector("input, select, textarea")),t)break}if(!t){const d=e.toLowerCase();t=document.querySelector(`input[aria-label*="${e}" i], input[placeholder*="${e}" i], textarea[aria-label*="${e}" i], textarea[placeholder*="${e}" i], select[aria-label*="${e}" i]`)}if(!t){r({error:`No form field found with label "${e}"`});break}const l=U(t),u=q(t,l,e);window.__piRefs=window.__piRefs||{},window.__piRefs[u]=t,c[u]={element:new WeakRef(t),role:l,name:e},r({ref:u,label:e})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"GET_ELEMENT_STYLES":{try{const{selector:e}=i,c=I(),o=t=>{const l=getComputedStyle(t),u=t.getBoundingClientRect();return{tag:t.tagName.toLowerCase(),text:t.innerText?.trim().slice(0,80)||null,box:{x:Math.round(u.x),y:Math.round(u.y),width:Math.round(u.width),height:Math.round(u.height)},styles:{fontSize:l.fontSize,fontWeight:l.fontWeight,fontFamily:l.fontFamily.split(",")[0].trim().replace(/"/g,""),color:l.color,backgroundColor:l.backgroundColor,borderRadius:l.borderRadius,border:l.border!=="none"&&l.borderWidth!=="0px"?l.border:null,boxShadow:l.boxShadow!=="none"?l.boxShadow:null,padding:l.padding}}};if(/^e\d+$/.test(e)){const t=c[e];let l;if(t&&(l=t.element.deref(),l||delete c[e]),!l&&window.__piRefs&&(l=window.__piRefs[e]),!l){r({error:`Element ${e} not found`});break}r({styles:[o(l)]})}else{const t=document.querySelectorAll(e);if(t.length===0){r({error:`No elements found matching "${e}"`});break}const l=Array.from(t).map(o);r({styles:l})}}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"SELECT_OPTION":{try{const{selector:e,values:c,by:o}=i,t=I();let l=null;if(/^e\d+$/.test(e)){const a=t[e];let f;if(a&&(f=a.element.deref(),f||delete t[e]),!f&&window.__piRefs&&(f=window.__piRefs[e]),!f){r({error:`Element ${e} not found`});break}if(f.tagName!=="SELECT"){r({error:`Element ${e} is not a <select>`});break}l=f}else{if(l=document.querySelector(e),!l){r({error:`No element found matching "${e}"`});break}if(l.tagName!=="SELECT"){r({error:`Element "${e}" is not a <select>`});break}}if(l.multiple)for(const a of l.options)a.selected=!1;const u=[],d=[],w=l.multiple?c:[c[0]];for(const a of w){let f=!1;for(const m of l.options){let b=!1;if(o==="index"?b=m.index===parseInt(a,10):o==="label"?b=m.text.toLowerCase().includes(a.toLowerCase()):b=m.value===a,b){m.selected=!0,u.push(m.value),f=!0;break}}f||d.push(a)}l.dispatchEvent(new Event("change",{bubbles:!0})),d.length>0?r({selected:u,warning:`Values not found: ${d.join(", ")}`}):r({selected:u})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"GET_ELEMENT_TEXT":{try{const{ref:e}=i,c=I(),o=c[e];let t;if(o&&(t=o.element.deref(),t||delete c[e]),!t&&window.__piRefs&&(t=window.__piRefs[e]),!t){r({error:`Element ${e} not found`});break}r({text:t.textContent?.trim()||""})}catch(e){r({error:e instanceof Error?e.message:String(e)})}break}case"SCROLL_TO_ELEMENT":{const e=oe(i.ref);r(e);break}case"UPLOAD_IMAGE":{const e=se(i.base64,i.ref,i.coordinate,i.filename);r(e);break}case"WAIT_FOR_ELEMENT":{const{selector:e,state:c="visible",timeout:o=2e4}=i,t=Math.min(o,6e4),l=a=>{if(!a)return!1;const f=window.getComputedStyle(a);return f.display!=="none"&&f.visibility!=="hidden"&&f.opacity!=="0"&&a.offsetWidth>0&&a.offsetHeight>0},u=()=>{const a=document.querySelector(e);switch(c){case"attached":return!!a;case"detached":return!a;case"hidden":return!a||!l(a);default:return l(a)}},d=Date.now();return new Promise(a=>{if(u()){a({success:!0,waited:Date.now()-d});return}const f=new MutationObserver(()=>{u()&&(f.disconnect(),clearTimeout(m),a({success:!0,waited:Date.now()-d}))}),m=setTimeout(()=>{f.disconnect(),a({success:!1,waited:Date.now()-d,error:`Timeout waiting for "${e}" to be ${c}`})},t);f.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["style","class","hidden","disabled"]})}).then(a=>{if(!a.success){r({error:a.error,waited:a.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const f=B("interactive",15,void 0,!0);r({...f,waited:a.waited})}),!0}case"WAIT_FOR_URL":{const{pattern:e,timeout:c=2e4}=i,o=Math.min(c,6e4),t=d=>{if(e.includes("*")){const w=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*\*/g,"<<<GLOBSTAR>>>").replace(/\*/g,"[^/]*").replace(/<<<GLOBSTAR>>>/g,".*");return new RegExp(`^${w}$`).test(d)}return d.includes(e)},l=Date.now();return new Promise(d=>{if(t(window.location.href)){d({success:!0,waited:Date.now()-l});return}let w=!1;const a=()=>{w||t(window.location.href)&&(w=!0,clearInterval(f),clearTimeout(m),window.removeEventListener("popstate",a),window.removeEventListener("hashchange",a),d({success:!0,waited:Date.now()-l}))},f=setInterval(a,100),m=setTimeout(()=>{w||(w=!0,clearInterval(f),window.removeEventListener("popstate",a),window.removeEventListener("hashchange",a),d({success:!1,waited:Date.now()-l,error:`Timeout waiting for URL to match "${e}". Current: ${window.location.href}`}))},o);window.addEventListener("popstate",a),window.addEventListener("hashchange",a)}).then(d=>{if(!d.success){r({error:d.error,waited:d.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const w=B("interactive",15,void 0,!0);r({...w,waited:d.waited})}),!0}case"WAIT_FOR_DOM_STABLE":{const{stable:e=100,timeout:c=5e3}=i,o=Math.min(c,3e4),t=Date.now();return new Promise(u=>{let d=Date.now(),w=!1;const a=()=>{if(w)return;Date.now()-d>=e&&(w=!0,f.disconnect(),clearTimeout(m),clearInterval(b),u({success:!0,waited:Date.now()-t}))},f=new MutationObserver(()=>{d=Date.now()}),m=setTimeout(()=>{w||(w=!0,f.disconnect(),clearInterval(b),u({success:!1,waited:Date.now()-t,error:`Timeout: DOM did not stabilize within ${o}ms`}))},o),b=setInterval(a,Math.max(10,Math.min(50,e/2)));f.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!0,characterData:!0}),a()}).then(u=>{if(!u.success){r({error:u.error,waited:u.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const d=B("interactive",15,void 0,!0);r({...d,waited:u.waited})}),!0}case"FORM_FILL":{const{data:e}=i;if(!Array.isArray(e))return r({error:"data must be an array of {ref, value} pairs"}),!0;const c=I(),o=[];for(const l of e){const{ref:u,value:d}=l;if(!u){o.push({ref:u||"unknown",success:!1,error:"Missing ref"});continue}const w=c[u];if(!w){o.push({ref:u,success:!1,error:"Element not found (run page.read first)"});continue}const a=w.element.deref();if(!a){delete c[u],o.push({ref:u,success:!1,error:"Element no longer exists"});continue}try{if(a instanceof HTMLInputElement){const f=a.type.toLowerCase();if(f==="checkbox"||f==="radio"){const m=d===!0||d==="true"||d==="1"||d==="checked";a.checked=m,a.dispatchEvent(new Event("change",{bubbles:!0}))}else a.focus(),a.value=String(d),a.dispatchEvent(new Event("input",{bubbles:!0})),a.dispatchEvent(new Event("change",{bubbles:!0}));o.push({ref:u,success:!0})}else a instanceof HTMLTextAreaElement?(a.focus(),a.value=String(d),a.dispatchEvent(new Event("input",{bubbles:!0})),a.dispatchEvent(new Event("change",{bubbles:!0})),o.push({ref:u,success:!0})):a instanceof HTMLSelectElement?(a.value=String(d),a.dispatchEvent(new Event("change",{bubbles:!0})),o.push({ref:u,success:!0})):a.isContentEditable?(a.focus(),a.textContent=String(d),a.dispatchEvent(new Event("input",{bubbles:!0})),o.push({ref:u,success:!0})):o.push({ref:u,success:!1,error:"Element is not fillable"})}catch(f){o.push({ref:u,success:!1,error:f instanceof Error?f.message:String(f)})}}const t=o.filter(l=>!l.success);return r({success:t.length===0,filled:o.filter(l=>l.success).length,failed:t.length,results:o}),!0}case"GET_FILE_INPUT_SELECTOR":{const{ref:e}=i;if(!e)return r({error:"No ref provided"}),!0;const c=I(),o=c[e];if(!o)return r({error:"Element not found (run page.read first)"}),!0;const t=o.element.deref();if(!t)return delete c[e],r({error:"Element no longer exists"}),!0;if(!(t instanceof HTMLInputElement)||t.type!=="file")return r({error:"Element is not a file input"}),!0;const l=`__pi_file_${Date.now()}`;return t.setAttribute("data-pi-file-id",l),r({selector:`[data-pi-file-id="${l}"]`}),!0}case"WAIT_FOR_NETWORK_IDLE":{const{timeout:e=1e4}=i,c=Math.min(e,6e4),o=["doubleclick.net","googlesyndication.com","googletagmanager.com","google-analytics.com","facebook.net","connect.facebook.net","analytics","ads","tracking","pixel","hotjar.com","clarity.ms","mixpanel.com","segment.com","newrelic.com","nr-data.net","/tracker/","/collector/","/beacon/","/telemetry/","/log/","/events/","/track.","/metrics/"],t=["img","image","font","icon"],l=f=>o.some(m=>f.includes(m)),u=f=>{const m=f.initiatorType||"unknown";return!!(t.includes(m)||/\.(jpg|jpeg|png|gif|webp|svg|ico|woff|woff2|ttf|eot)(\?|$)/i.test(f.name))},d=()=>{const f=performance.now();return performance.getEntriesByType("resource").filter(b=>{if(b.responseEnd!==0||b.name.startsWith("data:")||b.name.length>500||l(b.name))return!1;const s=f-b.startTime;return!(s>1e4||u(b)&&s>3e3)})},w=Date.now();return new Promise(f=>{const m=()=>{const b=d(),s=Date.now()-w;if(b.length===0){f({success:!0,waited:s});return}if(s>=c){f({success:!1,waited:s,pendingCount:b.length});return}setTimeout(m,100)};m()}).then(f=>{if(!f.success){r({error:`Network not idle after ${f.waited}ms (${f.pendingCount} requests pending)`,waited:f.waited,pageContent:"",viewport:{width:window.innerWidth,height:window.innerHeight}});return}const m=B("interactive",15,void 0,!0);r({...m,waited:f.waited})}),!0}case"SEARCH_PAGE":{const{term:e,caseSensitive:c,limit:o}=i,t=ce(e,c||!1,o||10);r({query:e,count:t.length,matches:t});break}case"GET_ELEMENT_BOUNDS_FOR_ANNOTATION":{const e=I(),c=[];for(const[o,t]of Object.entries(e)){const l=t.element.deref();if(!l)continue;const u=l.getBoundingClientRect();u.width<=0||u.height<=0||u.bottom<0||u.top>window.innerHeight||u.right<0||u.left>window.innerWidth||c.push({ref:o,tag:l.tagName.toLowerCase(),bounds:{x:u.x,y:u.y,width:u.width,height:u.height}})}r({elements:c});break}default:return!1}return!1});function ce(i,g,r){const e=[],c=g?i:i.toLowerCase(),o=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),t=I();let l=0;for(;o.nextNode()&&e.length<r;){const u=o.currentNode,d=u.textContent||"",w=g?d:d.toLowerCase();let a=0;for(;(a=w.indexOf(c,a))!==-1&&e.length<r;){const f=u.parentElement;if(!f){a++;continue}const m=document.createRange();m.setStart(u,a),m.setEnd(u,Math.min(a+i.length,d.length));const b=m.getBoundingClientRect();if(b.width===0||b.height===0){a++;continue}const s=u.textContent||"",p=Math.max(0,a-30),T=Math.min(s.length,a+i.length+30),A=s.slice(p,T).trim();let C=null;for(const[O,$]of Object.entries(t)){const M=$.element.deref();if(M&&(M===f||M.contains(f))){C=O;break}}e.push({ref:`m${++l}`,text:s.slice(a,a+i.length),context:A,bounds:{x:Math.round(b.x),y:Math.round(b.y),width:Math.round(b.width),height:Math.round(b.height)},elementRef:C}),a++}}return e}
11
- //# sourceMappingURL=accessibility-tree.js.map