poke-browser 0.2.8

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.
@@ -0,0 +1,1416 @@
1
+ /**
2
+ * Relays automation commands from the service worker into the page.
3
+ */
4
+
5
+ const CONSOLE_RING_MAX = 500;
6
+ const PAGE_ERROR_RING_MAX = 200;
7
+
8
+ /** @type {Array<{ level: string; message: string; timestamp: number; stack?: string }>} */
9
+ let consoleRing = [];
10
+
11
+ /**
12
+ * Uncaught errors and unhandled rejections (separate from console ring).
13
+ * @type {Array<{ kind: string; message: string; stack?: string; filename?: string; lineno?: number; colno?: number; timestamp: number }>}
14
+ */
15
+ let pageErrorRing = [];
16
+
17
+ /**
18
+ * @param {{ kind: string; message: string; stack?: string; filename?: string; lineno?: number; colno?: number; timestamp: number }} entry
19
+ */
20
+ function pushPageError(entry) {
21
+ pageErrorRing.push(entry);
22
+ while (pageErrorRing.length > PAGE_ERROR_RING_MAX) pageErrorRing.shift();
23
+ }
24
+
25
+ window.addEventListener("error", (ev) => {
26
+ try {
27
+ pushPageError({
28
+ kind: "error",
29
+ message: ev.message || String(ev.error || "error"),
30
+ stack: ev.error instanceof Error ? ev.error.stack : undefined,
31
+ filename: ev.filename,
32
+ lineno: ev.lineno,
33
+ colno: ev.colno,
34
+ timestamp: Date.now(),
35
+ });
36
+ } catch {
37
+ /* ignore */
38
+ }
39
+ });
40
+
41
+ window.addEventListener("unhandledrejection", (ev) => {
42
+ try {
43
+ const reason = ev.reason;
44
+ const message =
45
+ reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
46
+ const stack = reason instanceof Error ? reason.stack : undefined;
47
+ pushPageError({
48
+ kind: "unhandledrejection",
49
+ message,
50
+ stack,
51
+ timestamp: Date.now(),
52
+ });
53
+ } catch {
54
+ /* ignore */
55
+ }
56
+ });
57
+
58
+ /**
59
+ * @param {unknown} a
60
+ */
61
+ function formatConsoleArg(a) {
62
+ if (a instanceof Error) return a.stack || a.message;
63
+ if (typeof a === "object" && a !== null) {
64
+ try {
65
+ return JSON.stringify(a);
66
+ } catch {
67
+ return String(a);
68
+ }
69
+ }
70
+ return String(a);
71
+ }
72
+
73
+ /**
74
+ * @param {string} level
75
+ * @param {unknown[]} args
76
+ */
77
+ function pushConsoleEntry(level, args) {
78
+ const message = args.map(formatConsoleArg).join(" ").slice(0, 20000);
79
+ const errArg = args.find((x) => x instanceof Error);
80
+ consoleRing.push({
81
+ level,
82
+ message,
83
+ timestamp: Date.now(),
84
+ stack: errArg instanceof Error ? errArg.stack : undefined,
85
+ });
86
+ while (consoleRing.length > CONSOLE_RING_MAX) consoleRing.shift();
87
+ }
88
+
89
+ ["log", "info", "warn", "error"].forEach((level) => {
90
+ const orig = console[level].bind(console);
91
+ console[level] = function pokeConsolePatched(...args) {
92
+ try {
93
+ pushConsoleEntry(level, args);
94
+ } catch {
95
+ /* ignore ring failures */
96
+ }
97
+ orig(...args);
98
+ };
99
+ });
100
+
101
+ /**
102
+ * Query selector across the document tree and inside open shadow roots (same-document; does not cross iframes).
103
+ * @param {Document | ShadowRoot | Element} root
104
+ * @param {string} selector
105
+ * @returns {Element[]}
106
+ */
107
+ function deepQueryAll(root, selector) {
108
+ /** @type {Element[]} */
109
+ const results = [];
110
+ try {
111
+ results.push(...root.querySelectorAll(selector));
112
+ } catch {
113
+ return results;
114
+ }
115
+ for (const el of root.querySelectorAll("*")) {
116
+ if (el.shadowRoot) {
117
+ results.push(...deepQueryAll(el.shadowRoot, selector));
118
+ }
119
+ }
120
+ return results;
121
+ }
122
+
123
+ /**
124
+ * XPath across the light tree and each open shadow root (shadow evaluated with that root as context).
125
+ * @param {string} expr
126
+ * @returns {Element[]}
127
+ */
128
+ function deepXPathAll(expr) {
129
+ /** @type {Element[]} */
130
+ const out = [];
131
+ /**
132
+ * @param {Document | ShadowRoot} context
133
+ */
134
+ function collectFrom(context) {
135
+ try {
136
+ const r = document.evaluate(expr, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
137
+ for (let i = 0; i < r.snapshotLength; i++) {
138
+ const n = r.snapshotItem(i);
139
+ if (n instanceof Element) out.push(n);
140
+ }
141
+ } catch {
142
+ /* ignore */
143
+ }
144
+ }
145
+ collectFrom(document);
146
+ /**
147
+ * @param {Element} el
148
+ */
149
+ function walk(el) {
150
+ if (el.shadowRoot) {
151
+ collectFrom(el.shadowRoot);
152
+ for (const c of el.shadowRoot.children) walk(c);
153
+ }
154
+ for (const c of el.children) walk(c);
155
+ }
156
+ if (document.documentElement) walk(document.documentElement);
157
+ return out;
158
+ }
159
+
160
+ /**
161
+ * @param {string} selector
162
+ * @returns {Element | null}
163
+ */
164
+ function querySelectorOrXPath(selector) {
165
+ const s = selector.trim();
166
+ if (s.startsWith("//") || s.toLowerCase().startsWith("xpath:")) {
167
+ const expr = s.toLowerCase().startsWith("xpath:") ? s.slice(6).trim() : s;
168
+ const all = deepXPathAll(expr);
169
+ return all[0] ?? null;
170
+ }
171
+ const all = deepQueryAll(document, s);
172
+ return all[0] ?? null;
173
+ }
174
+
175
+ /**
176
+ * @param {Element} el
177
+ */
178
+ function elementSummary(el) {
179
+ const tag = el.tagName.toLowerCase();
180
+ const id = el.id || undefined;
181
+ const classes = typeof el.className === "string" ? el.className : "";
182
+ let text = "";
183
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
184
+ text = el.value?.slice(0, 200) ?? "";
185
+ } else {
186
+ text = (el.textContent || "").trim().slice(0, 200);
187
+ }
188
+ return { tag, id, classes, text };
189
+ }
190
+
191
+ /**
192
+ * Viewport client coordinates used as the synthetic click anchor (same as syntheticClick).
193
+ * @param {Element} el
194
+ */
195
+ function getSyntheticClickClientPoint(el) {
196
+ const r = el.getBoundingClientRect();
197
+ return {
198
+ x: r.left + Math.min(r.width / 2, 50),
199
+ y: r.top + Math.min(r.height / 2, 50),
200
+ };
201
+ }
202
+
203
+ /**
204
+ * @param {Element} el
205
+ */
206
+ function syntheticClick(el) {
207
+ const { x, y } = getSyntheticClickClientPoint(el);
208
+ const init = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
209
+ el.dispatchEvent(new MouseEvent("mousedown", init));
210
+ el.dispatchEvent(new MouseEvent("mouseup", init));
211
+ if (typeof el.click === "function") el.click();
212
+ else el.dispatchEvent(new MouseEvent("click", init));
213
+ }
214
+
215
+ /**
216
+ * @param {unknown} message
217
+ * @param {(r: unknown) => void} sendResponse
218
+ */
219
+ function handleClickElement(message, sendResponse) {
220
+ const m = /** @type {{ selector?: string }} */ (message);
221
+ const selector = typeof m.selector === "string" ? m.selector : "";
222
+ if (!selector) {
223
+ sendResponse({ success: false, error: "Missing selector" });
224
+ return;
225
+ }
226
+ const el = querySelectorOrXPath(selector);
227
+ if (!el) {
228
+ sendResponse({ success: false, error: "Element not found" });
229
+ return;
230
+ }
231
+ try {
232
+ syntheticClick(el);
233
+ sendResponse({ success: true, element: elementSummary(el) });
234
+ } catch (err) {
235
+ sendResponse({ success: false, error: String(err) });
236
+ }
237
+ }
238
+
239
+ /**
240
+ * @param {unknown} message
241
+ * @param {(r: unknown) => void} sendResponse
242
+ */
243
+ function handleResolveClickPoint(message, sendResponse) {
244
+ const m = /** @type {{ selector?: string }} */ (message);
245
+ const selector = typeof m.selector === "string" ? m.selector : "";
246
+ if (!selector) {
247
+ sendResponse({ success: false, error: "Missing selector" });
248
+ return;
249
+ }
250
+ const el = querySelectorOrXPath(selector);
251
+ if (!el) {
252
+ sendResponse({ success: false, error: "Element not found" });
253
+ return;
254
+ }
255
+ const { x, y } = getSyntheticClickClientPoint(el);
256
+ sendResponse({ success: true, x, y });
257
+ }
258
+
259
+ /**
260
+ * @param {unknown} message
261
+ * @param {(r: unknown) => void} sendResponse
262
+ */
263
+ function handleTypeText(message, sendResponse) {
264
+ const m = /** @type {{ text?: string; selector?: string; clear?: boolean }} */ (message);
265
+ const text = typeof m.text === "string" ? m.text : "";
266
+ const shouldClear = m.clear !== false;
267
+ let el = null;
268
+ if (typeof m.selector === "string" && m.selector.trim()) {
269
+ el = querySelectorOrXPath(m.selector);
270
+ } else {
271
+ const a = document.activeElement;
272
+ el = a instanceof Element ? a : null;
273
+ }
274
+ if (!el || !(el instanceof HTMLElement)) {
275
+ sendResponse({ success: false, charsTyped: 0 });
276
+ return;
277
+ }
278
+
279
+ try {
280
+ if (el.isContentEditable) {
281
+ el.focus();
282
+ if (shouldClear) {
283
+ const sel = window.getSelection();
284
+ if (sel && el.firstChild) {
285
+ const range = document.createRange();
286
+ range.selectNodeContents(el);
287
+ sel.removeAllRanges();
288
+ sel.addRange(range);
289
+ }
290
+ document.execCommand("delete");
291
+ el.textContent = text;
292
+ } else {
293
+ el.textContent = (el.textContent || "") + text;
294
+ }
295
+ el.dispatchEvent(new InputEvent("input", { bubbles: true, data: text, inputType: "insertText" }));
296
+ el.dispatchEvent(new Event("change", { bubbles: true }));
297
+ sendResponse({ success: true, charsTyped: text.length });
298
+ return;
299
+ }
300
+
301
+ const tag = el.tagName.toLowerCase();
302
+ if (tag === "input" || tag === "textarea") {
303
+ const input = /** @type {HTMLInputElement | HTMLTextAreaElement} */ (el);
304
+ input.focus();
305
+ if (shouldClear) {
306
+ input.select();
307
+ document.execCommand("delete");
308
+ input.value = text;
309
+ } else {
310
+ input.value = (input.value || "") + text;
311
+ }
312
+ input.dispatchEvent(new InputEvent("input", { bubbles: true, data: text, inputType: "insertText" }));
313
+ input.dispatchEvent(new Event("change", { bubbles: true }));
314
+ sendResponse({ success: true, charsTyped: text.length });
315
+ return;
316
+ }
317
+
318
+ sendResponse({ success: false, charsTyped: 0 });
319
+ } catch (err) {
320
+ sendResponse({ success: false, charsTyped: 0, error: String(err) });
321
+ }
322
+ }
323
+
324
+ /**
325
+ * @param {unknown} message
326
+ * @param {(r: unknown) => void} sendResponse
327
+ */
328
+ function handleScrollWindow(message, sendResponse) {
329
+ const m = /** @type {{ payload?: Record<string, unknown> }} */ (message);
330
+ const p = m.payload && typeof m.payload === "object" ? m.payload : {};
331
+ const behavior = p.behavior === "smooth" ? "smooth" : "auto";
332
+ const selector = typeof p.selector === "string" ? p.selector.trim() : "";
333
+ const dirRaw = typeof p.direction === "string" ? p.direction.toLowerCase() : "";
334
+ const dir =
335
+ dirRaw === "up" || dirRaw === "down" || dirRaw === "left" || dirRaw === "right" ? dirRaw : "";
336
+
337
+ try {
338
+ if (selector) {
339
+ const el = querySelectorOrXPath(selector);
340
+ if (!el) {
341
+ sendResponse({ success: false, scrollX: window.scrollX, scrollY: window.scrollY, error: "Element not found" });
342
+ return;
343
+ }
344
+ el.scrollIntoView({ behavior, block: "center", inline: "nearest" });
345
+ } else if (typeof p.x === "number" || typeof p.y === "number") {
346
+ const left = typeof p.x === "number" ? p.x : window.scrollX;
347
+ const top = typeof p.y === "number" ? p.y : window.scrollY;
348
+ window.scrollTo({ left, top, behavior });
349
+ } else {
350
+ let dx = typeof p.deltaX === "number" && Number.isFinite(p.deltaX) ? p.deltaX : 0;
351
+ let dy = typeof p.deltaY === "number" && Number.isFinite(p.deltaY) ? p.deltaY : 0;
352
+ if (dir) {
353
+ let amt = typeof p.amount === "number" && Number.isFinite(p.amount) ? Math.abs(p.amount) : NaN;
354
+ if (!Number.isFinite(amt) || amt === 0) {
355
+ if (dir === "up" || dir === "down") {
356
+ const fromDelta = typeof p.deltaY === "number" && Number.isFinite(p.deltaY) && p.deltaY !== 0;
357
+ amt = fromDelta ? Math.abs(p.deltaY) : Math.max(200, Math.floor(window.innerHeight * 0.85));
358
+ } else {
359
+ const fromDelta = typeof p.deltaX === "number" && Number.isFinite(p.deltaX) && p.deltaX !== 0;
360
+ amt = fromDelta ? Math.abs(p.deltaX) : Math.max(200, Math.floor(window.innerWidth * 0.85));
361
+ }
362
+ }
363
+ dx = dir === "left" ? -amt : dir === "right" ? amt : 0;
364
+ dy = dir === "up" ? -amt : dir === "down" ? amt : 0;
365
+ }
366
+ window.scrollBy({ left: dx, top: dy, behavior });
367
+ }
368
+ sendResponse({ success: true, scrollX: window.scrollX, scrollY: window.scrollY });
369
+ } catch (err) {
370
+ sendResponse({ success: false, scrollX: window.scrollX, scrollY: window.scrollY, error: String(err) });
371
+ }
372
+ }
373
+
374
+ /**
375
+ * @param {unknown} message
376
+ * @param {(r: unknown) => void} sendResponse
377
+ */
378
+ function handleEval(message, sendResponse) {
379
+ const m = /** @type {{ requestId?: string; code?: string; timeoutMs?: number }} */ (message);
380
+ const requestId = m.requestId || `poke-${Date.now()}-${Math.random().toString(16).slice(2)}`;
381
+ const code = String(m.code ?? "");
382
+ let finished = false;
383
+ const timeoutMs = typeof m.timeoutMs === "number" ? m.timeoutMs : 30000;
384
+
385
+ const timer = setTimeout(() => {
386
+ if (finished) return;
387
+ finished = true;
388
+ window.removeEventListener("message", onWindowMessage);
389
+ sendResponse({ ok: false, error: "evaluate_js timed out in content script" });
390
+ }, timeoutMs);
391
+
392
+ /**
393
+ * @param {MessageEvent} event
394
+ */
395
+ function onWindowMessage(event) {
396
+ if (event.source !== window) return;
397
+ const data = event.data;
398
+ if (!data || data.type !== "POKE_EVAL_RESULT" || data.requestId !== requestId) return;
399
+ if (finished) return;
400
+ finished = true;
401
+ clearTimeout(timer);
402
+ window.removeEventListener("message", onWindowMessage);
403
+ if (data.ok) {
404
+ sendResponse({ ok: true, result: data.result });
405
+ } else {
406
+ sendResponse({ ok: false, error: data.error || "evaluate failed" });
407
+ }
408
+ }
409
+
410
+ window.addEventListener("message", onWindowMessage);
411
+
412
+ const s = document.createElement("script");
413
+ s.textContent = `
414
+ (function () {
415
+ var requestId = ${JSON.stringify(requestId)};
416
+ try {
417
+ var result = (0, eval)(${JSON.stringify(code)});
418
+ window.postMessage({ type: "POKE_EVAL_RESULT", requestId: requestId, ok: true, result: result }, "*");
419
+ } catch (e) {
420
+ window.postMessage({ type: "POKE_EVAL_RESULT", requestId: requestId, ok: false, error: String(e) }, "*");
421
+ }
422
+ })();
423
+ `;
424
+ (document.documentElement || document.head || document.body).appendChild(s);
425
+ s.remove();
426
+ }
427
+
428
+ // --- Perception: shared helpers -------------------------------------------------
429
+
430
+ /**
431
+ * @param {Record<string, unknown>} obj
432
+ */
433
+ function compactJson(obj) {
434
+ return JSON.parse(JSON.stringify(obj));
435
+ }
436
+
437
+ /**
438
+ * @param {Element} el
439
+ */
440
+ function cssEscapeId(id) {
441
+ if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(id);
442
+ return id.replace(/([^\w-])/g, "\\$1");
443
+ }
444
+
445
+ /**
446
+ * @param {Element} el
447
+ */
448
+ function uniqueSelector(el) {
449
+ if (!(el instanceof Element)) return "";
450
+ if (el.id && deepQueryAll(document, `#${cssEscapeId(el.id)}`).length === 1) {
451
+ return `#${cssEscapeId(el.id)}`;
452
+ }
453
+ const parts = [];
454
+ let cur = el;
455
+ while (cur && cur.nodeType === Node.ELEMENT_NODE && cur !== document.documentElement) {
456
+ let part = cur.tagName.toLowerCase();
457
+ if (cur.id) {
458
+ parts.unshift(`#${cssEscapeId(cur.id)}`);
459
+ break;
460
+ }
461
+ const parent = cur.parentElement;
462
+ if (parent) {
463
+ const siblings = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
464
+ const idx = siblings.indexOf(cur) + 1;
465
+ if (siblings.length > 1) part += `:nth-of-type(${idx})`;
466
+ }
467
+ parts.unshift(part);
468
+ cur = /** @type {Element} */ (parent);
469
+ }
470
+ return parts.join(" > ");
471
+ }
472
+
473
+ /**
474
+ * @param {Element} el
475
+ */
476
+ function elementInteractive(el) {
477
+ if (!(el instanceof Element)) return false;
478
+ const tag = el.tagName.toLowerCase();
479
+ if (["a", "button", "input", "select", "textarea", "summary", "option", "label"].includes(tag)) {
480
+ return true;
481
+ }
482
+ const role = el.getAttribute("role");
483
+ if (
484
+ role &&
485
+ ["button", "link", "menuitem", "tab", "checkbox", "radio", "switch", "textbox", "searchbox", "combobox", "slider", "spinbutton"].includes(
486
+ role
487
+ )
488
+ ) {
489
+ return true;
490
+ }
491
+ if (el.hasAttribute("onclick")) return true;
492
+ if (el instanceof HTMLElement && el.isContentEditable) return true;
493
+ const tab = el.getAttribute("tabindex");
494
+ if (tab !== null && tab !== "-1" && !Number.isNaN(Number.parseInt(tab, 10))) return true;
495
+ return false;
496
+ }
497
+
498
+ /**
499
+ * @param {Element} el
500
+ * @param {boolean} includeHidden
501
+ */
502
+ function isSkippedHidden(el, includeHidden) {
503
+ if (includeHidden) return false;
504
+ if (!(el instanceof HTMLElement)) return true;
505
+ if (el === document.body || el === document.documentElement) return false;
506
+ const st = window.getComputedStyle(el);
507
+ if (st.display === "none" || st.visibility === "hidden") return true;
508
+ if (el.offsetParent === null) {
509
+ const pos = st.position;
510
+ if (pos !== "fixed" && pos !== "sticky") return true;
511
+ }
512
+ return false;
513
+ }
514
+
515
+ /**
516
+ * @param {Element} el
517
+ * @param {number} maxLen
518
+ */
519
+ function trimText(el, maxLen) {
520
+ let t = (el.textContent || "").trim().replace(/\s+/g, " ");
521
+ if (t.length > maxLen) t = t.slice(0, maxLen);
522
+ return t;
523
+ }
524
+
525
+ /**
526
+ * @param {Element} el
527
+ * @param {number} depth
528
+ * @param {number} maxDepth
529
+ * @param {boolean} includeHidden
530
+ * @param {boolean} [inShadow]
531
+ */
532
+ function buildDomSnapshotNode(el, depth, maxDepth, includeHidden, inShadow) {
533
+ if (depth > maxDepth) return null;
534
+ if (isSkippedHidden(el, includeHidden)) return null;
535
+ const r = el.getBoundingClientRect();
536
+ /** @type {Record<string, unknown>} */
537
+ const node = {
538
+ tag: el.tagName.toLowerCase(),
539
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height },
540
+ interactive: elementInteractive(el),
541
+ };
542
+ if (inShadow) node.isShadow = true;
543
+ if (el.id) node.id = el.id;
544
+ const cls =
545
+ typeof el.className === "string" && el.className.trim()
546
+ ? el.className.trim().split(/\s+/).filter(Boolean)
547
+ : [];
548
+ if (cls.length) node.classes = cls;
549
+ const role = el.getAttribute("role");
550
+ if (role) node.role = role;
551
+ const al = el.getAttribute("aria-label");
552
+ if (al) node["aria-label"] = al;
553
+ const tx = trimText(el, 120);
554
+ if (tx) node.text = tx;
555
+ const childEls = Array.from(el.children);
556
+ const children = [];
557
+ for (const c of childEls) {
558
+ const sn = buildDomSnapshotNode(c, depth + 1, maxDepth, includeHidden, inShadow);
559
+ if (sn) children.push(sn);
560
+ }
561
+ if (el.shadowRoot) {
562
+ for (const c of Array.from(el.shadowRoot.children)) {
563
+ const sn = buildDomSnapshotNode(c, depth + 1, maxDepth, includeHidden, true);
564
+ if (sn) children.push(sn);
565
+ }
566
+ }
567
+ if (children.length) node.children = children;
568
+ return node;
569
+ }
570
+
571
+ /**
572
+ * @param {unknown} message
573
+ * @param {(r: unknown) => void} sendResponse
574
+ */
575
+ function handleGetDomSnapshot(message, sendResponse) {
576
+ const m = /** @type {{ includeHidden?: boolean; maxDepth?: number }} */ (message);
577
+ const includeHidden = m.includeHidden === true;
578
+ const maxDepth = typeof m.maxDepth === "number" && Number.isFinite(m.maxDepth) ? Math.max(0, Math.min(50, m.maxDepth)) : 6;
579
+ if (!document.body) {
580
+ sendResponse({ error: "No document.body" });
581
+ return;
582
+ }
583
+ const snapshot = buildDomSnapshotNode(document.body, 0, maxDepth, includeHidden);
584
+ sendResponse(
585
+ compactJson({
586
+ snapshot,
587
+ url: location.href,
588
+ title: document.title || "",
589
+ timestamp: Date.now(),
590
+ })
591
+ );
592
+ }
593
+
594
+ /**
595
+ * @param {Element} el
596
+ */
597
+ function impliedRole(el) {
598
+ const r = el.getAttribute("role");
599
+ if (r) return r;
600
+ const t = el.tagName.toLowerCase();
601
+ if (t === "a") return "link";
602
+ if (t === "button") return "button";
603
+ if (t === "select") return "combobox";
604
+ if (t === "textarea") return "textbox";
605
+ if (t === "img") return "img";
606
+ if (t === "form") return "form";
607
+ if (t === "input") {
608
+ const type = (/** @type {HTMLInputElement} */ (el)).type || "text";
609
+ if (type === "checkbox") return "checkbox";
610
+ if (type === "radio") return "radio";
611
+ if (type === "button" || type === "submit" || type === "reset") return "button";
612
+ return "textbox";
613
+ }
614
+ if (/^h[1-6]$/.test(t)) return "heading";
615
+ if (t === "p") return "paragraph";
616
+ if (t === "li") return "listitem";
617
+ return t;
618
+ }
619
+
620
+ /**
621
+ * @param {Element} el
622
+ */
623
+ function accessibilityName(el) {
624
+ const aria = el.getAttribute("aria-label");
625
+ if (aria && aria.trim()) return aria.trim().slice(0, 80);
626
+ if (el instanceof HTMLImageElement && el.alt) return el.alt.trim().slice(0, 80);
627
+ const title = el.getAttribute("title");
628
+ if (title && title.trim()) return title.trim().slice(0, 80);
629
+ const ph = el.getAttribute("aria-placeholder");
630
+ if (ph && ph.trim()) return ph.trim().slice(0, 80);
631
+ const it = (el.innerText || "").trim().replace(/\s+/g, " ");
632
+ return it.length > 80 ? it.slice(0, 80) : it;
633
+ }
634
+
635
+ /**
636
+ * @param {Element} el
637
+ */
638
+ function isFocusableInteractive(el) {
639
+ if (!(el instanceof HTMLElement)) return false;
640
+ if (el.hasAttribute("disabled")) return false;
641
+ if (elementInteractive(el)) {
642
+ const tab = el.getAttribute("tabindex");
643
+ if (tab === "-1" && !["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA", "SUMMARY"].includes(el.tagName)) {
644
+ return false;
645
+ }
646
+ return true;
647
+ }
648
+ return false;
649
+ }
650
+
651
+ /**
652
+ * @param {unknown} message
653
+ * @param {(r: unknown) => void} sendResponse
654
+ */
655
+ function handleGetAccessibilityTree(message, sendResponse) {
656
+ const m = /** @type {{ interactiveOnly?: boolean }} */ (message);
657
+ const interactiveOnly = m.interactiveOnly === true;
658
+ const sel =
659
+ '[role], a, button, input, select, textarea, h1, h2, h3, h4, h5, h6, p, li, img, form';
660
+ const list = Array.from(document.querySelectorAll(sel));
661
+ /** @type {Array<Record<string, unknown>>} */
662
+ const raw = [];
663
+ for (const el of list) {
664
+ if (!(el instanceof Element)) continue;
665
+ if (isSkippedHidden(el, false)) continue;
666
+ if (interactiveOnly && !isFocusableInteractive(el)) continue;
667
+ const r = el.getBoundingClientRect();
668
+ const tag = el.tagName.toLowerCase();
669
+ /** @type {Record<string, unknown>} */
670
+ const row = {
671
+ role: impliedRole(el),
672
+ name: accessibilityName(el),
673
+ selector: uniqueSelector(el),
674
+ disabled: el instanceof HTMLElement && (el.hasAttribute("disabled") || /** @type {HTMLInputElement} */ (el).disabled === true),
675
+ rect: { x: r.x, y: r.y, w: r.width, h: r.height },
676
+ };
677
+ if (el.id) row.id = el.id;
678
+ if (/^h[1-6]$/.test(tag)) row.level = Number(tag[1]);
679
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
680
+ row.value = el.value;
681
+ if (el instanceof HTMLInputElement && (el.type === "checkbox" || el.type === "radio")) {
682
+ row.checked = el.checked;
683
+ }
684
+ }
685
+ raw.push(row);
686
+ }
687
+ raw.sort((a, b) => {
688
+ const ra = /** @type {{ x: number; y: number }} */ (a.rect);
689
+ const rb = /** @type {{ x: number; y: number }} */ (b.rect);
690
+ if (Math.abs(ra.y - rb.y) > 1) return ra.y - rb.y;
691
+ return ra.x - rb.x;
692
+ });
693
+ const nodes = raw.map((row) => compactJson({ ...row }));
694
+ sendResponse({
695
+ nodes,
696
+ count: nodes.length,
697
+ url: location.href,
698
+ });
699
+ }
700
+
701
+ /**
702
+ * @param {string} expr
703
+ * @returns {Element[]}
704
+ */
705
+ function xpathElements(expr) {
706
+ return deepXPathAll(expr);
707
+ }
708
+
709
+ /**
710
+ * @param {string} q
711
+ * @returns {Element[]}
712
+ */
713
+ function findElementsByText(q) {
714
+ const ql = q.toLowerCase().trim();
715
+ if (!ql) return [];
716
+ const all = deepQueryAll(document, "*");
717
+ /** @type {Element[]} */
718
+ const exact = [];
719
+ /** @type {Element[]} */
720
+ const partial = [];
721
+ for (const el of all) {
722
+ if (!(el instanceof HTMLElement)) continue;
723
+ const tn = el.tagName;
724
+ if (tn === "SCRIPT" || tn === "STYLE" || tn === "NOSCRIPT") continue;
725
+ const t = (el.innerText || "").trim();
726
+ if (!t) continue;
727
+ const tl = t.toLowerCase();
728
+ if (tl === ql) exact.push(el);
729
+ else if (tl.includes(ql)) partial.push(el);
730
+ }
731
+ const pool = exact.length ? exact : partial;
732
+ return filterOutAncestors(pool);
733
+ }
734
+
735
+ /**
736
+ * @param {Element[]} els
737
+ */
738
+ function filterOutAncestors(els) {
739
+ /** @type {Element[]} */
740
+ const out = [];
741
+ for (const el of els) {
742
+ let sub = false;
743
+ for (const o of els) {
744
+ if (o !== el && o.contains(el)) {
745
+ sub = true;
746
+ break;
747
+ }
748
+ }
749
+ if (!sub) out.push(el);
750
+ }
751
+ return out;
752
+ }
753
+
754
+ /**
755
+ * @param {string} q
756
+ * @returns {Element[]}
757
+ */
758
+ function findElementsByAria(q) {
759
+ const ql = q.toLowerCase().trim();
760
+ if (!ql) return [];
761
+ const all = deepQueryAll(document, "*");
762
+ /** @type {Element[]} */
763
+ const hits = [];
764
+ for (const el of all) {
765
+ if (!(el instanceof Element)) continue;
766
+ const tn = el.tagName;
767
+ if (tn === "SCRIPT" || tn === "STYLE" || tn === "NOSCRIPT") continue;
768
+ const chunks = [
769
+ el.getAttribute("aria-label"),
770
+ el.getAttribute("aria-placeholder"),
771
+ el.getAttribute("title"),
772
+ el instanceof HTMLImageElement ? el.alt : null,
773
+ ]
774
+ .filter(Boolean)
775
+ .map((s) => String(s).toLowerCase());
776
+ if (chunks.some((c) => c.includes(ql))) hits.push(el);
777
+ }
778
+ return filterOutAncestors(hits);
779
+ }
780
+
781
+ /**
782
+ * @param {Element} el
783
+ * @param {number} index
784
+ */
785
+ function toFoundElement(el, index) {
786
+ const r = el.getBoundingClientRect();
787
+ const cls =
788
+ typeof el.className === "string" && el.className.trim()
789
+ ? el.className.trim().split(/\s+/).filter(Boolean)
790
+ : [];
791
+ /** @type {Record<string, unknown>} */
792
+ const o = {
793
+ index,
794
+ tag: el.tagName.toLowerCase(),
795
+ text: (el.innerText || "").trim().slice(0, 200),
796
+ selector: uniqueSelector(el),
797
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height },
798
+ interactive: elementInteractive(el),
799
+ };
800
+ if (el.id) o.id = el.id;
801
+ if (cls.length) o.classes = cls;
802
+ return compactJson(o);
803
+ }
804
+
805
+ /**
806
+ * @param {unknown} message
807
+ * @param {(r: unknown) => void} sendResponse
808
+ */
809
+ function handleFindElement(message, sendResponse) {
810
+ const m = /** @type {{ query?: string; strategy?: string }} */ (message);
811
+ const query = typeof m.query === "string" ? m.query : "";
812
+ const strategy = m.strategy === "css" || m.strategy === "text" || m.strategy === "aria" || m.strategy === "xpath" ? m.strategy : "auto";
813
+ if (!query.trim()) {
814
+ sendResponse({ elements: [], query: "", strategy_used: strategy });
815
+ return;
816
+ }
817
+
818
+ /** @type {Element[]} */
819
+ let found = [];
820
+ /** @type {string} */
821
+ let used = strategy;
822
+
823
+ function tryCss() {
824
+ try {
825
+ return deepQueryAll(document, query);
826
+ } catch {
827
+ return [];
828
+ }
829
+ }
830
+
831
+ if (strategy === "auto") {
832
+ found = tryCss();
833
+ used = "css";
834
+ if (found.length === 0) {
835
+ found = findElementsByText(query);
836
+ used = "text";
837
+ }
838
+ if (found.length === 0) {
839
+ found = findElementsByAria(query);
840
+ used = "aria";
841
+ }
842
+ } else if (strategy === "css") {
843
+ found = tryCss();
844
+ } else if (strategy === "text") {
845
+ found = findElementsByText(query);
846
+ } else if (strategy === "aria") {
847
+ found = findElementsByAria(query);
848
+ } else if (strategy === "xpath") {
849
+ found = xpathElements(query);
850
+ used = "xpath";
851
+ }
852
+
853
+ const top = found.slice(0, 5);
854
+ const elements = top.map((el, i) => toFoundElement(el, i));
855
+ sendResponse({ elements, query, strategy_used: used });
856
+ }
857
+
858
+ /**
859
+ * @returns {HTMLElement}
860
+ */
861
+ function getReadPageRoot() {
862
+ const main =
863
+ document.querySelector("main") ||
864
+ document.querySelector("article") ||
865
+ document.querySelector('[role="main"]');
866
+ if (main instanceof HTMLElement) return main;
867
+ return document.body || document.documentElement;
868
+ }
869
+
870
+ /**
871
+ * @param {HTMLElement} el
872
+ */
873
+ function stripNoise(el) {
874
+ el.querySelectorAll("script, style, noscript, nav, header, footer").forEach((n) => n.remove());
875
+ }
876
+
877
+ /**
878
+ * @param {string} text
879
+ */
880
+ function wordCountFrom(text) {
881
+ const w = text.trim().split(/\s+/).filter(Boolean);
882
+ return w.length;
883
+ }
884
+
885
+ /**
886
+ * @param {HTMLElement} root
887
+ */
888
+ function readStructured(root) {
889
+ const clone = /** @type {HTMLElement} */ (root.cloneNode(true));
890
+ stripNoise(clone);
891
+ const descMeta = document.querySelector('meta[name="description"]');
892
+ const description = descMeta?.getAttribute("content")?.trim() || "";
893
+ /** @type {{ level: number; text: string }[]} */
894
+ const headings = [];
895
+ clone.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
896
+ const tag = h.tagName.toLowerCase();
897
+ headings.push({ level: Number(tag[1]), text: (h.textContent || "").trim() });
898
+ });
899
+ /** @type {{ text: string; href: string }[]} */
900
+ const links = [];
901
+ clone.querySelectorAll("a[href]").forEach((a) => {
902
+ const href = a.getAttribute("href") || "";
903
+ links.push({ text: (a.textContent || "").trim(), href });
904
+ });
905
+ /** @type {{ alt: string; src: string }[]} */
906
+ const images = [];
907
+ clone.querySelectorAll("img[src]").forEach((img) => {
908
+ images.push({ alt: img.getAttribute("alt") || "", src: img.getAttribute("src") || "" });
909
+ });
910
+ const mainText = (clone.innerText || "").trim().replace(/\s+/g, " ");
911
+ return {
912
+ title: document.title || "",
913
+ url: location.href,
914
+ description,
915
+ mainText,
916
+ headings,
917
+ links,
918
+ images,
919
+ };
920
+ }
921
+
922
+ /**
923
+ * @param {HTMLElement} el
924
+ * @returns {string}
925
+ */
926
+ function elementToMarkdown(el) {
927
+ const tag = el.tagName.toLowerCase();
928
+ if (["script", "style", "noscript", "nav", "header", "footer"].includes(tag)) return "";
929
+ if (tag === "br") return "\n";
930
+ if (el.childNodes.length === 0) return "";
931
+
932
+ /** @type {string[]} */
933
+ const bits = [];
934
+ for (const node of el.childNodes) {
935
+ if (node.nodeType === Node.TEXT_NODE) {
936
+ const t = node.textContent || "";
937
+ if (t.trim()) bits.push(t);
938
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
939
+ const child = /** @type {HTMLElement} */ (node);
940
+ const ct = child.tagName.toLowerCase();
941
+ if (["script", "style", "noscript", "nav", "header", "footer"].includes(ct)) continue;
942
+ if (/^h[1-6]$/.test(ct)) {
943
+ const level = Number(ct[1]);
944
+ bits.push(`${"#".repeat(level)} ${(child.innerText || "").trim()}\n\n`);
945
+ } else if (ct === "p") {
946
+ bits.push(`${(child.innerText || "").trim()}\n\n`);
947
+ } else if (ct === "a" && child.getAttribute("href")) {
948
+ const href = child.getAttribute("href") || "";
949
+ bits.push(`[${(child.textContent || "").trim()}](${href})`);
950
+ } else if (ct === "ul") {
951
+ for (const li of child.querySelectorAll(":scope > li")) {
952
+ bits.push(`- ${(li.textContent || "").trim()}\n`);
953
+ }
954
+ bits.push("\n");
955
+ } else if (ct === "ol") {
956
+ let i = 1;
957
+ for (const li of child.querySelectorAll(":scope > li")) {
958
+ bits.push(`${i}. ${(li.textContent || "").trim()}\n`);
959
+ i += 1;
960
+ }
961
+ bits.push("\n");
962
+ } else if (ct === "pre") {
963
+ bits.push(`\`\`\`\n${(child.textContent || "").trim()}\n\`\`\`\n\n`);
964
+ } else if (ct === "code" && child.parentElement?.tagName.toLowerCase() !== "pre") {
965
+ bits.push(`\`${(child.textContent || "").trim()}\``);
966
+ } else if (ct === "strong" || ct === "b") {
967
+ bits.push(`**${(child.textContent || "").trim()}**`);
968
+ } else if (ct === "img" && child.getAttribute("src")) {
969
+ const src = child.getAttribute("src") || "";
970
+ const alt = child.getAttribute("alt") || "";
971
+ bits.push(`![${alt}](${src})`);
972
+ } else {
973
+ bits.push(elementToMarkdown(child));
974
+ }
975
+ }
976
+ }
977
+ return bits.join("");
978
+ }
979
+
980
+ /**
981
+ * @param {unknown} message
982
+ * @param {(r: unknown) => void} sendResponse
983
+ */
984
+ /**
985
+ * @param {Element} el
986
+ * @param {boolean} requireVisible
987
+ */
988
+ function elementMatchesVisible(el, requireVisible) {
989
+ if (!requireVisible) return true;
990
+ if (!(el instanceof HTMLElement)) return false;
991
+ if (el.offsetParent === null) {
992
+ const st = getComputedStyle(el);
993
+ const pos = st.position;
994
+ if (pos !== "fixed" && pos !== "sticky") return false;
995
+ }
996
+ const st = getComputedStyle(el);
997
+ if (st.display === "none" || st.visibility === "hidden" || Number.parseFloat(st.opacity) === 0) {
998
+ return false;
999
+ }
1000
+ return true;
1001
+ }
1002
+
1003
+ /**
1004
+ * @param {unknown} message
1005
+ * @param {(r: unknown) => void} sendResponse
1006
+ */
1007
+ function handleWaitForSelector(message, sendResponse) {
1008
+ const m = /** @type {{ selector?: string; timeout?: number; visible?: boolean }} */ (message);
1009
+ const selector = typeof m.selector === "string" ? m.selector : "";
1010
+ const timeout = typeof m.timeout === "number" && m.timeout > 0 ? m.timeout : 10000;
1011
+ const visible = m.visible === true;
1012
+ const start = Date.now();
1013
+
1014
+ /** @type {ReturnType<typeof setInterval> | undefined} */
1015
+ let iv;
1016
+
1017
+ function tick() {
1018
+ const el = querySelectorOrXPath(selector);
1019
+ if (el && elementMatchesVisible(el, visible)) {
1020
+ if (iv !== undefined) clearInterval(iv);
1021
+ const r = el.getBoundingClientRect();
1022
+ sendResponse({
1023
+ found: true,
1024
+ selector,
1025
+ elapsed: Date.now() - start,
1026
+ element: {
1027
+ tag: el.tagName.toLowerCase(),
1028
+ id: el.id || undefined,
1029
+ text: (el.textContent || "").trim().slice(0, 200),
1030
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height },
1031
+ },
1032
+ });
1033
+ return;
1034
+ }
1035
+ if (Date.now() - start >= timeout) {
1036
+ if (iv !== undefined) clearInterval(iv);
1037
+ sendResponse({
1038
+ found: false,
1039
+ selector,
1040
+ elapsed: Date.now() - start,
1041
+ error: "timeout",
1042
+ });
1043
+ }
1044
+ }
1045
+
1046
+ iv = setInterval(tick, 100);
1047
+ tick();
1048
+ }
1049
+
1050
+ /**
1051
+ * @param {unknown} message
1052
+ * @param {(r: unknown) => void} sendResponse
1053
+ */
1054
+ function handleGetConsoleLogs(message, sendResponse) {
1055
+ const m = /** @type {{ level?: string; limit?: number }} */ (message);
1056
+ const level = m.level === "error" || m.level === "warn" || m.level === "info" || m.level === "log" ? m.level : "all";
1057
+ const limit = typeof m.limit === "number" ? Math.min(500, Math.max(1, m.limit)) : 100;
1058
+ let logs = consoleRing;
1059
+ if (level !== "all") {
1060
+ logs = logs.filter((e) => e.level === level);
1061
+ }
1062
+ const sliced = logs.slice(-limit);
1063
+ sendResponse({ logs: sliced, count: sliced.length });
1064
+ }
1065
+
1066
+ /**
1067
+ * @param {unknown} _message
1068
+ * @param {(r: unknown) => void} sendResponse
1069
+ */
1070
+ function handleClearConsoleLogs(_message, sendResponse) {
1071
+ consoleRing = [];
1072
+ sendResponse({ cleared: true });
1073
+ }
1074
+
1075
+ /**
1076
+ * @param {unknown} message
1077
+ * @param {(r: unknown) => void} sendResponse
1078
+ */
1079
+ function handleGetPageErrors(message, sendResponse) {
1080
+ const m = /** @type {{ limit?: number }} */ (message);
1081
+ const limit = typeof m.limit === "number" ? Math.min(200, Math.max(1, m.limit)) : 50;
1082
+ const sliced = pageErrorRing.slice(-limit);
1083
+ sendResponse({ errors: sliced, count: sliced.length });
1084
+ }
1085
+
1086
+ /**
1087
+ * @param {unknown} _message
1088
+ * @param {(r: unknown) => void} sendResponse
1089
+ */
1090
+ function handleGetScrollInfo(_message, sendResponse) {
1091
+ const de = document.documentElement;
1092
+ const body = document.body;
1093
+ sendResponse({
1094
+ scrollHeight: Math.max(de.scrollHeight, body ? body.scrollHeight : 0, de.clientHeight),
1095
+ innerHeight: window.innerHeight,
1096
+ innerWidth: window.innerWidth,
1097
+ scrollY: window.scrollY,
1098
+ devicePixelRatio: window.devicePixelRatio || 1,
1099
+ });
1100
+ }
1101
+
1102
+ /**
1103
+ * @param {unknown} message
1104
+ * @param {(r: unknown) => void} sendResponse
1105
+ */
1106
+ function handleScrollTo(message, sendResponse) {
1107
+ const m = /** @type {{ y?: number }} */ (message);
1108
+ const y = typeof m.y === "number" ? m.y : 0;
1109
+ window.scrollTo({ top: y, left: 0, behavior: "instant" });
1110
+ sendResponse({ scrollY: window.scrollY });
1111
+ }
1112
+
1113
+ /**
1114
+ * @param {unknown} message
1115
+ * @param {(r: unknown) => void} sendResponse
1116
+ */
1117
+ function handleHoverElement(message, sendResponse) {
1118
+ const m = /** @type {{ selector?: string }} */ (message);
1119
+ const selector = typeof m.selector === "string" ? m.selector : "";
1120
+ if (!selector.trim()) {
1121
+ sendResponse({ success: false });
1122
+ return;
1123
+ }
1124
+ const el = querySelectorOrXPath(selector);
1125
+ if (!el) {
1126
+ sendResponse({ success: false });
1127
+ return;
1128
+ }
1129
+ const r = el.getBoundingClientRect();
1130
+ const x = r.left + r.width / 2;
1131
+ const y = r.top + r.height / 2;
1132
+ const init = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
1133
+ el.dispatchEvent(new MouseEvent("mousemove", init));
1134
+ el.dispatchEvent(new MouseEvent("mouseover", init));
1135
+ el.dispatchEvent(new MouseEvent("mouseenter", init));
1136
+ sendResponse({
1137
+ success: true,
1138
+ element: {
1139
+ tag: el.tagName.toLowerCase(),
1140
+ id: el.id || undefined,
1141
+ text: (el.textContent || "").trim().slice(0, 200),
1142
+ rect: { x: r.x, y: r.y, width: r.width, height: r.height },
1143
+ },
1144
+ });
1145
+ }
1146
+
1147
+ /**
1148
+ * @param {unknown} message
1149
+ * @param {(r: unknown) => void} sendResponse
1150
+ */
1151
+ function handleScriptInject(message, sendResponse) {
1152
+ const m = /** @type {{ script?: string }} */ (message);
1153
+ const script = typeof m.script === "string" ? m.script : "";
1154
+ if (!script) {
1155
+ sendResponse({ success: false });
1156
+ return;
1157
+ }
1158
+ try {
1159
+ const s = document.createElement("script");
1160
+ s.textContent = script;
1161
+ const root = document.documentElement || document.head || document.body;
1162
+ if (!root) {
1163
+ sendResponse({ success: false });
1164
+ return;
1165
+ }
1166
+ root.appendChild(s);
1167
+ s.remove();
1168
+ sendResponse({ success: true });
1169
+ } catch (err) {
1170
+ sendResponse({ success: false, error: String(err) });
1171
+ }
1172
+ }
1173
+
1174
+ /**
1175
+ * @param {unknown} message
1176
+ * @param {(r: unknown) => void} sendResponse
1177
+ */
1178
+ function handleFillForm(message, sendResponse) {
1179
+ const m = /** @type {{
1180
+ fields?: Array<{ selector?: string; value?: string; type?: string }>;
1181
+ submitAfter?: boolean;
1182
+ submitSelector?: string;
1183
+ }} */ (message);
1184
+ const fields = Array.isArray(m.fields) ? m.fields : [];
1185
+ /** @type {Array<{ selector: string; error: string }>} */
1186
+ const errors = [];
1187
+ let filled = 0;
1188
+
1189
+ for (const f of fields) {
1190
+ const sel = typeof f.selector === "string" ? f.selector : "";
1191
+ const val = typeof f.value === "string" ? f.value : "";
1192
+ const typ = f.type === "select" || f.type === "checkbox" || f.type === "radio" || f.type === "file" ? f.type : "text";
1193
+ if (!sel) {
1194
+ errors.push({ selector: sel, error: "empty selector" });
1195
+ continue;
1196
+ }
1197
+ const el = querySelectorOrXPath(sel);
1198
+ if (!el) {
1199
+ errors.push({ selector: sel, error: "not found" });
1200
+ continue;
1201
+ }
1202
+ try {
1203
+ if (typ === "file") {
1204
+ errors.push({ selector: sel, error: "file inputs are not supported" });
1205
+ continue;
1206
+ }
1207
+ if (typ === "checkbox") {
1208
+ const input = el instanceof HTMLInputElement ? el : null;
1209
+ if (!input || input.type !== "checkbox") {
1210
+ errors.push({ selector: sel, error: "not a checkbox input" });
1211
+ continue;
1212
+ }
1213
+ const vl = val.toLowerCase();
1214
+ input.checked = !(vl === "false" || val === "0" || vl === "off" || val === "");
1215
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1216
+ input.dispatchEvent(new Event("change", { bubbles: true }));
1217
+ filled += 1;
1218
+ continue;
1219
+ }
1220
+ if (typ === "radio") {
1221
+ const input = el instanceof HTMLInputElement ? el : null;
1222
+ if (!input || input.type !== "radio") {
1223
+ errors.push({ selector: sel, error: "not a radio input" });
1224
+ continue;
1225
+ }
1226
+ const vl = val.toLowerCase();
1227
+ const off = val === "" || vl === "false" || val === "0" || vl === "off";
1228
+ if (off) {
1229
+ input.checked = false;
1230
+ } else {
1231
+ input.checked = true;
1232
+ if (input.form) {
1233
+ const rads = input.form.querySelectorAll('input[type="radio"]');
1234
+ rads.forEach((x) => {
1235
+ if (x instanceof HTMLInputElement && x.name === input.name && x !== input) {
1236
+ x.checked = false;
1237
+ }
1238
+ });
1239
+ }
1240
+ }
1241
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1242
+ input.dispatchEvent(new Event("change", { bubbles: true }));
1243
+ filled += 1;
1244
+ continue;
1245
+ }
1246
+ if (typ === "select" && el instanceof HTMLSelectElement) {
1247
+ let matched = false;
1248
+ for (let i = 0; i < el.options.length; i += 1) {
1249
+ const o = el.options[i];
1250
+ if (o.value === val || o.text === val) {
1251
+ el.selectedIndex = i;
1252
+ matched = true;
1253
+ break;
1254
+ }
1255
+ }
1256
+ if (!matched) el.value = val;
1257
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1258
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1259
+ filled += 1;
1260
+ continue;
1261
+ }
1262
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
1263
+ el.focus();
1264
+ el.value = val;
1265
+ el.dispatchEvent(
1266
+ new InputEvent("input", { bubbles: true, data: val, inputType: "insertReplacementText" }),
1267
+ );
1268
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1269
+ filled += 1;
1270
+ continue;
1271
+ }
1272
+ errors.push({ selector: sel, error: "unsupported element for text fill" });
1273
+ } catch (err) {
1274
+ errors.push({ selector: sel, error: String(err) });
1275
+ }
1276
+ }
1277
+
1278
+ let success = errors.length === 0;
1279
+ if (m.submitAfter === true) {
1280
+ const subSel = typeof m.submitSelector === "string" ? m.submitSelector.trim() : "";
1281
+ let sub = subSel ? querySelectorOrXPath(subSel) : null;
1282
+ if (!sub && fields[0]) {
1283
+ const firstSel = typeof fields[0].selector === "string" ? fields[0].selector : "";
1284
+ const first = firstSel ? querySelectorOrXPath(firstSel) : null;
1285
+ const form = first && first.closest ? first.closest("form") : null;
1286
+ if (form) {
1287
+ sub =
1288
+ form.querySelector('button[type="submit"], input[type="submit"], button:not([type])');
1289
+ }
1290
+ }
1291
+ if (sub) {
1292
+ syntheticClick(sub);
1293
+ } else {
1294
+ errors.push({ selector: "[submit]", error: "no submit control found" });
1295
+ success = false;
1296
+ }
1297
+ }
1298
+
1299
+ sendResponse({ success, filled, errors });
1300
+ }
1301
+
1302
+ /**
1303
+ * @param {unknown} message
1304
+ * @param {(r: unknown) => void} sendResponse
1305
+ */
1306
+ function handleGetStoragePage(message, sendResponse) {
1307
+ const m = /** @type {{ storageType?: string; key?: string }} */ (message);
1308
+ const useSession = m.storageType === "session";
1309
+ const t = useSession ? sessionStorage : localStorage;
1310
+ const key = typeof m.key === "string" ? m.key : undefined;
1311
+ /** @type {Record<string, string>} */
1312
+ const data = {};
1313
+ try {
1314
+ if (key) {
1315
+ const v = t.getItem(key);
1316
+ if (v !== null) data[key] = v;
1317
+ } else {
1318
+ for (let i = 0; i < t.length; i += 1) {
1319
+ const k = t.key(i);
1320
+ if (k) data[k] = t.getItem(k) ?? "";
1321
+ }
1322
+ }
1323
+ sendResponse({ data, count: Object.keys(data).length });
1324
+ } catch (err) {
1325
+ sendResponse({ data: {}, count: 0, error: String(err) });
1326
+ }
1327
+ }
1328
+
1329
+ /**
1330
+ * @param {unknown} message
1331
+ * @param {(r: unknown) => void} sendResponse
1332
+ */
1333
+ function handleSetStoragePage(message, sendResponse) {
1334
+ const m = /** @type {{ storageType?: string; key?: string; value?: string }} */ (message);
1335
+ const useSession = m.storageType === "session";
1336
+ const t = useSession ? sessionStorage : localStorage;
1337
+ const key = typeof m.key === "string" ? m.key : "";
1338
+ const value = typeof m.value === "string" ? m.value : "";
1339
+ if (!key) {
1340
+ sendResponse({ success: false });
1341
+ return;
1342
+ }
1343
+ try {
1344
+ t.setItem(key, value);
1345
+ sendResponse({ success: true });
1346
+ } catch (err) {
1347
+ sendResponse({ success: false, error: String(err) });
1348
+ }
1349
+ }
1350
+
1351
+ function handleReadPage(message, sendResponse) {
1352
+ const m = /** @type {{ format?: string }} */ (message);
1353
+ const format =
1354
+ m.format === "markdown" || m.format === "text" || m.format === "structured" ? m.format : "structured";
1355
+ const root = getReadPageRoot();
1356
+ const title = document.title || "";
1357
+ const url = location.href;
1358
+
1359
+ if (format === "text") {
1360
+ const clone = /** @type {HTMLElement} */ (root.cloneNode(true));
1361
+ stripNoise(clone);
1362
+ const text = (clone.innerText || "").trim().replace(/\s+/g, " ");
1363
+ sendResponse({ text, url, title, wordCount: wordCountFrom(text) });
1364
+ return;
1365
+ }
1366
+
1367
+ if (format === "markdown") {
1368
+ const clone = /** @type {HTMLElement} */ (root.cloneNode(true));
1369
+ stripNoise(clone);
1370
+ const markdown = elementToMarkdown(clone).trim();
1371
+ sendResponse({ markdown, url, title, wordCount: wordCountFrom(markdown) });
1372
+ return;
1373
+ }
1374
+
1375
+ const structured = readStructured(root);
1376
+ const wc = wordCountFrom(structured.mainText);
1377
+ sendResponse({ ...structured, wordCount: wc });
1378
+ }
1379
+
1380
+ /** @type {Record<string, (message: unknown, sendResponse: (r: unknown) => void) => void>} */
1381
+ const MESSAGE_HANDLERS = {
1382
+ POKE_CLICK_ELEMENT: handleClickElement,
1383
+ POKE_RESOLVE_CLICK_POINT: handleResolveClickPoint,
1384
+ POKE_TYPE_TEXT: handleTypeText,
1385
+ POKE_SCROLL_WINDOW: handleScrollWindow,
1386
+ POKE_EVAL: handleEval,
1387
+ POKE_GET_DOM_SNAPSHOT: handleGetDomSnapshot,
1388
+ POKE_GET_A11Y_TREE: handleGetAccessibilityTree,
1389
+ POKE_FIND_ELEMENT: handleFindElement,
1390
+ POKE_READ_PAGE: handleReadPage,
1391
+ POKE_WAIT_FOR_SELECTOR: handleWaitForSelector,
1392
+ POKE_GET_CONSOLE_LOGS: handleGetConsoleLogs,
1393
+ POKE_CLEAR_CONSOLE_LOGS: handleClearConsoleLogs,
1394
+ POKE_GET_PAGE_ERRORS: handleGetPageErrors,
1395
+ POKE_GET_SCROLL_INFO: handleGetScrollInfo,
1396
+ POKE_SCROLL_TO: handleScrollTo,
1397
+ POKE_HOVER_ELEMENT: handleHoverElement,
1398
+ POKE_SCRIPT_INJECT: handleScriptInject,
1399
+ POKE_FILL_FORM: handleFillForm,
1400
+ POKE_GET_STORAGE: handleGetStoragePage,
1401
+ POKE_SET_STORAGE: handleSetStoragePage,
1402
+ };
1403
+
1404
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
1405
+ const t = message && typeof message === "object" && "type" in message ? String(message.type) : "";
1406
+ const fn = MESSAGE_HANDLERS[t];
1407
+ if (!fn) return undefined;
1408
+ queueMicrotask(() => {
1409
+ try {
1410
+ fn(message, sendResponse);
1411
+ } catch (err) {
1412
+ sendResponse({ success: false, error: String(err), ok: false });
1413
+ }
1414
+ });
1415
+ return true;
1416
+ });