tabctl 0.6.0-alpha.9 → 0.6.0-rc.10

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,455 +1,766 @@
1
1
  (() => {
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
2
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __commonJS = (cb, mod) => function __require() {
4
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
8
  };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
6
22
 
7
- // dist/extension/lib/screenshot.js
8
- var require_screenshot = __commonJS({
9
- "dist/extension/lib/screenshot.js"(exports) {
10
- "use strict";
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.SCREENSHOT_PROCESS_TIMEOUT_MS = exports.SCREENSHOT_CAPTURE_DELAY_MS = exports.SCREENSHOT_SCROLL_DELAY_MS = exports.SCREENSHOT_QUALITY = exports.SCREENSHOT_MAX_BYTES = exports.SCREENSHOT_TILE_MAX_DIM = void 0;
13
- exports.estimateDataUrlBytes = estimateDataUrlBytes;
14
- exports.arrayBufferToBase64 = arrayBufferToBase64;
15
- exports.resizeDataUrl = resizeDataUrl;
16
- exports.resizeDataUrlToMaxDim = resizeDataUrlToMaxDim;
17
- exports.cropDataUrl = cropDataUrl;
18
- exports.ensureMaxBytes = ensureMaxBytes;
19
- exports.constrainDataUrl = constrainDataUrl;
20
- exports.captureVisible = captureVisible;
21
- exports.getPageMetrics = getPageMetrics;
22
- exports.scrollToPosition = scrollToPosition;
23
- exports.captureTabTiles = captureTabTiles;
24
- exports.SCREENSHOT_TILE_MAX_DIM = 2e3;
25
- exports.SCREENSHOT_MAX_BYTES = 2e6;
26
- exports.SCREENSHOT_QUALITY = 80;
27
- exports.SCREENSHOT_SCROLL_DELAY_MS = 150;
28
- exports.SCREENSHOT_CAPTURE_DELAY_MS = 350;
29
- exports.SCREENSHOT_PROCESS_TIMEOUT_MS = 8e3;
30
- function estimateDataUrlBytes(dataUrl) {
31
- const commaIndex = dataUrl.indexOf(",");
32
- if (commaIndex < 0) {
33
- return dataUrl.length;
34
- }
35
- const base64 = dataUrl.slice(commaIndex + 1);
36
- const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
37
- return Math.max(0, Math.floor(base64.length * 3 / 4) - padding);
38
- }
39
- function arrayBufferToBase64(buffer) {
40
- const bytes = new Uint8Array(buffer);
41
- let binary = "";
42
- for (let i = 0; i < bytes.length; i += 1) {
43
- binary += String.fromCharCode(bytes[i]);
44
- }
45
- return btoa(binary);
46
- }
47
- async function resizeDataUrl(dataUrl, format, quality, scale) {
48
- if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
49
- return null;
50
- }
51
- const response = await fetch(dataUrl);
52
- const blob = await response.blob();
53
- const bitmap = await createImageBitmap(blob);
54
- const width = Math.max(1, Math.floor(bitmap.width * scale));
55
- const height = Math.max(1, Math.floor(bitmap.height * scale));
56
- const canvas = new OffscreenCanvas(width, height);
57
- const ctx = canvas.getContext("2d");
58
- if (!ctx) {
59
- return null;
60
- }
61
- ctx.drawImage(bitmap, 0, 0, width, height);
62
- const type = format === "jpeg" ? "image/jpeg" : "image/png";
63
- const blobOut = await canvas.convertToBlob({ type, quality: format === "jpeg" ? quality / 100 : void 0 });
64
- const buffer = await blobOut.arrayBuffer();
65
- const base64 = arrayBufferToBase64(buffer);
66
- return {
67
- dataUrl: `data:${type};base64,${base64}`,
68
- bytes: buffer.byteLength
69
- };
70
- }
71
- async function resizeDataUrlToMaxDim(dataUrl, format, quality, maxDim) {
72
- if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
73
- return null;
74
- }
75
- const response = await fetch(dataUrl);
76
- const blob = await response.blob();
77
- const bitmap = await createImageBitmap(blob);
78
- const maxSize = Math.max(bitmap.width, bitmap.height);
79
- if (!Number.isFinite(maxSize) || maxSize <= maxDim) {
80
- return null;
81
- }
82
- const scale = maxDim / maxSize;
83
- return resizeDataUrl(dataUrl, format, quality, scale);
23
+ // src/extension/lib/screenshot.ts
24
+ var screenshot_exports = {};
25
+ __export(screenshot_exports, {
26
+ SCREENSHOT_CAPTURE_DELAY_MS: () => SCREENSHOT_CAPTURE_DELAY_MS,
27
+ SCREENSHOT_MAX_BYTES: () => SCREENSHOT_MAX_BYTES,
28
+ SCREENSHOT_PROCESS_TIMEOUT_MS: () => SCREENSHOT_PROCESS_TIMEOUT_MS,
29
+ SCREENSHOT_QUALITY: () => SCREENSHOT_QUALITY,
30
+ SCREENSHOT_SCROLL_DELAY_MS: () => SCREENSHOT_SCROLL_DELAY_MS,
31
+ SCREENSHOT_TILE_MAX_DIM: () => SCREENSHOT_TILE_MAX_DIM,
32
+ arrayBufferToBase64: () => arrayBufferToBase64,
33
+ captureTabTiles: () => captureTabTiles,
34
+ captureVisible: () => captureVisible,
35
+ constrainDataUrl: () => constrainDataUrl,
36
+ cropDataUrl: () => cropDataUrl,
37
+ ensureMaxBytes: () => ensureMaxBytes,
38
+ estimateDataUrlBytes: () => estimateDataUrlBytes,
39
+ getPageMetrics: () => getPageMetrics,
40
+ resizeDataUrl: () => resizeDataUrl,
41
+ resizeDataUrlToMaxDim: () => resizeDataUrlToMaxDim,
42
+ scrollToPosition: () => scrollToPosition
43
+ });
44
+ function estimateDataUrlBytes(dataUrl) {
45
+ const commaIndex = dataUrl.indexOf(",");
46
+ if (commaIndex < 0) {
47
+ return dataUrl.length;
48
+ }
49
+ const base64 = dataUrl.slice(commaIndex + 1);
50
+ const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
51
+ return Math.max(0, Math.floor(base64.length * 3 / 4) - padding);
52
+ }
53
+ function arrayBufferToBase64(buffer) {
54
+ const bytes = new Uint8Array(buffer);
55
+ let binary = "";
56
+ for (let i = 0; i < bytes.length; i += 1) {
57
+ binary += String.fromCharCode(bytes[i]);
58
+ }
59
+ return btoa(binary);
60
+ }
61
+ async function resizeDataUrl(dataUrl, format, quality, scale) {
62
+ if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
63
+ return null;
64
+ }
65
+ const response = await fetch(dataUrl);
66
+ const blob = await response.blob();
67
+ const bitmap = await createImageBitmap(blob);
68
+ const width = Math.max(1, Math.floor(bitmap.width * scale));
69
+ const height = Math.max(1, Math.floor(bitmap.height * scale));
70
+ const canvas = new OffscreenCanvas(width, height);
71
+ const ctx = canvas.getContext("2d");
72
+ if (!ctx) {
73
+ return null;
74
+ }
75
+ ctx.drawImage(bitmap, 0, 0, width, height);
76
+ const type = format === "jpeg" ? "image/jpeg" : "image/png";
77
+ const blobOut = await canvas.convertToBlob({ type, quality: format === "jpeg" ? quality / 100 : void 0 });
78
+ const buffer = await blobOut.arrayBuffer();
79
+ const base64 = arrayBufferToBase64(buffer);
80
+ return {
81
+ dataUrl: `data:${type};base64,${base64}`,
82
+ bytes: buffer.byteLength
83
+ };
84
+ }
85
+ async function resizeDataUrlToMaxDim(dataUrl, format, quality, maxDim) {
86
+ if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
87
+ return null;
88
+ }
89
+ const response = await fetch(dataUrl);
90
+ const blob = await response.blob();
91
+ const bitmap = await createImageBitmap(blob);
92
+ const maxSize = Math.max(bitmap.width, bitmap.height);
93
+ if (!Number.isFinite(maxSize) || maxSize <= maxDim) {
94
+ return null;
95
+ }
96
+ const scale = maxDim / maxSize;
97
+ return resizeDataUrl(dataUrl, format, quality, scale);
98
+ }
99
+ async function cropDataUrl(dataUrl, format, quality, width, height, devicePixelRatio) {
100
+ if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
101
+ return dataUrl;
102
+ }
103
+ const response = await fetch(dataUrl);
104
+ const blob = await response.blob();
105
+ const bitmap = await createImageBitmap(blob);
106
+ const targetWidth = Math.min(bitmap.width, Math.max(1, Math.round(width * devicePixelRatio)));
107
+ const targetHeight = Math.min(bitmap.height, Math.max(1, Math.round(height * devicePixelRatio)));
108
+ if (targetWidth === bitmap.width && targetHeight === bitmap.height) {
109
+ return dataUrl;
110
+ }
111
+ const canvas = new OffscreenCanvas(targetWidth, targetHeight);
112
+ const ctx = canvas.getContext("2d");
113
+ if (!ctx) {
114
+ return dataUrl;
115
+ }
116
+ ctx.drawImage(bitmap, 0, 0, targetWidth, targetHeight, 0, 0, targetWidth, targetHeight);
117
+ const type = format === "jpeg" ? "image/jpeg" : "image/png";
118
+ const blobOut = await canvas.convertToBlob({ type, quality: format === "jpeg" ? quality / 100 : void 0 });
119
+ const buffer = await blobOut.arrayBuffer();
120
+ const base64 = arrayBufferToBase64(buffer);
121
+ return `data:${type};base64,${base64}`;
122
+ }
123
+ async function ensureMaxBytes(dataUrl, format, quality, maxBytes) {
124
+ let currentUrl = dataUrl;
125
+ let currentBytes = estimateDataUrlBytes(currentUrl);
126
+ if (currentBytes <= maxBytes) {
127
+ return { dataUrl: currentUrl, bytes: currentBytes, scaled: false, oversized: false };
128
+ }
129
+ let scaled = false;
130
+ let attempts = 0;
131
+ while (currentBytes > maxBytes && attempts < 3) {
132
+ const scale = Math.max(0.2, Math.sqrt(maxBytes / currentBytes) * 0.95);
133
+ if (scale >= 0.99) {
134
+ break;
84
135
  }
85
- async function cropDataUrl(dataUrl, format, quality, width, height, devicePixelRatio) {
86
- if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
87
- return dataUrl;
88
- }
89
- const response = await fetch(dataUrl);
90
- const blob = await response.blob();
91
- const bitmap = await createImageBitmap(blob);
92
- const targetWidth = Math.min(bitmap.width, Math.max(1, Math.round(width * devicePixelRatio)));
93
- const targetHeight = Math.min(bitmap.height, Math.max(1, Math.round(height * devicePixelRatio)));
94
- if (targetWidth === bitmap.width && targetHeight === bitmap.height) {
95
- return dataUrl;
96
- }
97
- const canvas = new OffscreenCanvas(targetWidth, targetHeight);
98
- const ctx = canvas.getContext("2d");
99
- if (!ctx) {
100
- return dataUrl;
101
- }
102
- ctx.drawImage(bitmap, 0, 0, targetWidth, targetHeight, 0, 0, targetWidth, targetHeight);
103
- const type = format === "jpeg" ? "image/jpeg" : "image/png";
104
- const blobOut = await canvas.convertToBlob({ type, quality: format === "jpeg" ? quality / 100 : void 0 });
105
- const buffer = await blobOut.arrayBuffer();
106
- const base64 = arrayBufferToBase64(buffer);
107
- return `data:${type};base64,${base64}`;
108
- }
109
- async function ensureMaxBytes(dataUrl, format, quality, maxBytes) {
110
- let currentUrl = dataUrl;
111
- let currentBytes = estimateDataUrlBytes(currentUrl);
112
- if (currentBytes <= maxBytes) {
113
- return { dataUrl: currentUrl, bytes: currentBytes, scaled: false, oversized: false };
114
- }
115
- let scaled = false;
116
- let attempts = 0;
117
- while (currentBytes > maxBytes && attempts < 3) {
118
- const scale = Math.max(0.2, Math.sqrt(maxBytes / currentBytes) * 0.95);
119
- if (scale >= 0.99) {
120
- break;
121
- }
122
- const resized = await resizeDataUrl(currentUrl, format, quality, scale);
123
- if (!resized) {
124
- break;
125
- }
126
- currentUrl = resized.dataUrl;
127
- currentBytes = resized.bytes;
128
- scaled = true;
129
- attempts += 1;
130
- }
131
- return {
132
- dataUrl: currentUrl,
133
- bytes: currentBytes,
134
- scaled,
135
- oversized: currentBytes > maxBytes
136
- };
136
+ const resized = await resizeDataUrl(currentUrl, format, quality, scale);
137
+ if (!resized) {
138
+ break;
137
139
  }
138
- async function constrainDataUrl(dataUrl, format, quality, maxDim, maxBytes) {
139
- let currentUrl = dataUrl;
140
- let currentBytes = estimateDataUrlBytes(currentUrl);
141
- let scaled = false;
142
- if (Number.isFinite(maxDim) && maxDim > 0) {
143
- const resized = await resizeDataUrlToMaxDim(currentUrl, format, quality, maxDim);
144
- if (resized) {
145
- currentUrl = resized.dataUrl;
146
- currentBytes = resized.bytes;
147
- scaled = true;
148
- }
149
- }
150
- if (currentBytes > maxBytes) {
151
- const resized = await ensureMaxBytes(currentUrl, format, quality, maxBytes);
152
- return {
153
- dataUrl: resized.dataUrl,
154
- bytes: resized.bytes,
155
- scaled: scaled || resized.scaled,
156
- oversized: resized.oversized
157
- };
158
- }
159
- return { dataUrl: currentUrl, bytes: currentBytes, scaled, oversized: false };
140
+ currentUrl = resized.dataUrl;
141
+ currentBytes = resized.bytes;
142
+ scaled = true;
143
+ attempts += 1;
144
+ }
145
+ return {
146
+ dataUrl: currentUrl,
147
+ bytes: currentBytes,
148
+ scaled,
149
+ oversized: currentBytes > maxBytes
150
+ };
151
+ }
152
+ async function constrainDataUrl(dataUrl, format, quality, maxDim, maxBytes) {
153
+ let currentUrl = dataUrl;
154
+ let currentBytes = estimateDataUrlBytes(currentUrl);
155
+ let scaled = false;
156
+ if (Number.isFinite(maxDim) && maxDim > 0) {
157
+ const resized = await resizeDataUrlToMaxDim(currentUrl, format, quality, maxDim);
158
+ if (resized) {
159
+ currentUrl = resized.dataUrl;
160
+ currentBytes = resized.bytes;
161
+ scaled = true;
160
162
  }
161
- async function captureVisible(windowId, format, quality) {
162
- const options = { format };
163
- if (format === "jpeg") {
164
- options.quality = quality;
165
- }
166
- return chrome.tabs.captureVisibleTab(windowId, options);
167
- }
168
- async function getPageMetrics(tabId, timeoutMs, deps) {
169
- const result = await deps.executeWithTimeout(tabId, timeoutMs, () => {
170
- const doc = document.documentElement;
171
- const body = document.body;
172
- const pageWidth = Math.max(doc.scrollWidth, doc.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);
173
- const pageHeight = Math.max(doc.scrollHeight, doc.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);
174
- return {
175
- pageWidth,
176
- pageHeight,
177
- viewportWidth: window.innerWidth,
178
- viewportHeight: window.innerHeight,
179
- devicePixelRatio: window.devicePixelRatio || 1,
180
- scrollX: window.scrollX || window.pageXOffset || 0,
181
- scrollY: window.scrollY || window.pageYOffset || 0
182
- };
183
- });
184
- if (!result || typeof result !== "object") {
185
- return null;
186
- }
187
- return result;
163
+ }
164
+ if (currentBytes > maxBytes) {
165
+ const resized = await ensureMaxBytes(currentUrl, format, quality, maxBytes);
166
+ return {
167
+ dataUrl: resized.dataUrl,
168
+ bytes: resized.bytes,
169
+ scaled: scaled || resized.scaled,
170
+ oversized: resized.oversized
171
+ };
172
+ }
173
+ return { dataUrl: currentUrl, bytes: currentBytes, scaled, oversized: false };
174
+ }
175
+ async function captureVisible(windowId, format, quality) {
176
+ const options = { format };
177
+ if (format === "jpeg") {
178
+ options.quality = quality;
179
+ }
180
+ try {
181
+ return await chrome.tabs.captureVisibleTab(windowId, options);
182
+ } catch {
183
+ await new Promise((r) => setTimeout(r, 500));
184
+ return chrome.tabs.captureVisibleTab(windowId, options);
185
+ }
186
+ }
187
+ async function getPageMetrics(tabId, timeoutMs, deps) {
188
+ const result = await deps.executeWithTimeout(tabId, timeoutMs, () => {
189
+ const doc = document.documentElement;
190
+ const body = document.body;
191
+ const pageWidth = Math.max(
192
+ doc.scrollWidth,
193
+ doc.clientWidth,
194
+ body ? body.scrollWidth : 0,
195
+ body ? body.clientWidth : 0
196
+ );
197
+ const pageHeight = Math.max(
198
+ doc.scrollHeight,
199
+ doc.clientHeight,
200
+ body ? body.scrollHeight : 0,
201
+ body ? body.clientHeight : 0
202
+ );
203
+ return {
204
+ pageWidth,
205
+ pageHeight,
206
+ viewportWidth: window.innerWidth,
207
+ viewportHeight: window.innerHeight,
208
+ devicePixelRatio: window.devicePixelRatio || 1,
209
+ scrollX: window.scrollX || window.pageXOffset || 0,
210
+ scrollY: window.scrollY || window.pageYOffset || 0
211
+ };
212
+ });
213
+ if (!result || typeof result !== "object") {
214
+ return null;
215
+ }
216
+ return result;
217
+ }
218
+ async function scrollToPosition(tabId, timeoutMs, x, y, deps) {
219
+ const result = await deps.executeWithTimeout(tabId, timeoutMs, (scrollX, scrollY) => {
220
+ window.scrollTo(scrollX, scrollY);
221
+ return {
222
+ scrollX: window.scrollX || window.pageXOffset || 0,
223
+ scrollY: window.scrollY || window.pageYOffset || 0
224
+ };
225
+ }, [x, y]);
226
+ if (!result || typeof result !== "object") {
227
+ return null;
228
+ }
229
+ return result;
230
+ }
231
+ async function captureTabTiles(tab, options, deps) {
232
+ const tabId = tab.tabId;
233
+ const windowId = tab.windowId;
234
+ if (!Number.isFinite(tabId) || !Number.isFinite(windowId)) {
235
+ throw new Error("Missing tab/window id");
236
+ }
237
+ const metrics = await getPageMetrics(tabId, SCREENSHOT_PROCESS_TIMEOUT_MS, deps);
238
+ if (!metrics) {
239
+ throw new Error("Failed to read page metrics");
240
+ }
241
+ const pageWidth = Number(metrics.pageWidth);
242
+ const pageHeight = Number(metrics.pageHeight);
243
+ const viewportWidth = Number(metrics.viewportWidth);
244
+ const viewportHeight = Number(metrics.viewportHeight);
245
+ const devicePixelRatio = Number(metrics.devicePixelRatio) || 1;
246
+ const startScrollX = Number(metrics.scrollX) || 0;
247
+ const startScrollY = Number(metrics.scrollY) || 0;
248
+ if (!Number.isFinite(viewportWidth) || !Number.isFinite(viewportHeight)) {
249
+ throw new Error("Viewport size unavailable");
250
+ }
251
+ const tiles = [];
252
+ let tileIndex = 0;
253
+ const captureTile = async (x, y, width, height, total) => {
254
+ if (tileIndex > 0) {
255
+ await deps.delay(SCREENSHOT_CAPTURE_DELAY_MS);
188
256
  }
189
- async function scrollToPosition(tabId, timeoutMs, x, y, deps) {
190
- const result = await deps.executeWithTimeout(tabId, timeoutMs, (scrollX, scrollY) => {
191
- window.scrollTo(scrollX, scrollY);
192
- return {
193
- scrollX: window.scrollX || window.pageXOffset || 0,
194
- scrollY: window.scrollY || window.pageYOffset || 0
195
- };
196
- }, [x, y]);
197
- if (!result || typeof result !== "object") {
198
- return null;
199
- }
200
- return result;
257
+ const rawDataUrl = await captureVisible(windowId, options.format, options.quality);
258
+ if (!rawDataUrl) {
259
+ throw new Error("Capture failed");
201
260
  }
202
- async function captureTabTiles(tab, options, deps) {
203
- const tabId = tab.tabId;
204
- const windowId = tab.windowId;
205
- if (!Number.isFinite(tabId) || !Number.isFinite(windowId)) {
206
- throw new Error("Missing tab/window id");
207
- }
208
- const metrics = await getPageMetrics(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, deps);
209
- if (!metrics) {
210
- throw new Error("Failed to read page metrics");
211
- }
212
- const pageWidth = Number(metrics.pageWidth);
213
- const pageHeight = Number(metrics.pageHeight);
214
- const viewportWidth = Number(metrics.viewportWidth);
215
- const viewportHeight = Number(metrics.viewportHeight);
216
- const devicePixelRatio = Number(metrics.devicePixelRatio) || 1;
217
- const startScrollX = Number(metrics.scrollX) || 0;
218
- const startScrollY = Number(metrics.scrollY) || 0;
219
- if (!Number.isFinite(viewportWidth) || !Number.isFinite(viewportHeight)) {
220
- throw new Error("Viewport size unavailable");
221
- }
222
- const tiles = [];
223
- let tileIndex = 0;
224
- const captureTile = async (x, y, width, height, total) => {
225
- if (tileIndex > 0) {
226
- await deps.delay(exports.SCREENSHOT_CAPTURE_DELAY_MS);
227
- }
228
- const rawDataUrl = await captureVisible(windowId, options.format, options.quality);
229
- if (!rawDataUrl) {
230
- throw new Error("Capture failed");
231
- }
232
- const croppedUrl = await cropDataUrl(rawDataUrl, options.format, options.quality, width, height, devicePixelRatio);
233
- const sizeResult = await constrainDataUrl(croppedUrl, options.format, options.quality, options.tileMaxDim, options.maxBytes);
234
- tiles.push({
235
- index: tileIndex,
236
- total,
237
- x,
238
- y,
239
- width,
240
- height,
241
- scale: devicePixelRatio,
242
- format: options.format,
243
- bytes: sizeResult.bytes,
244
- scaled: sizeResult.scaled,
245
- oversized: sizeResult.oversized,
246
- dataUrl: sizeResult.dataUrl
247
- });
248
- tileIndex += 1;
249
- };
250
- if (options.mode === "viewport") {
251
- await captureTile(startScrollX, startScrollY, viewportWidth, viewportHeight, 1);
252
- return tiles;
253
- }
254
- const stepX = viewportWidth;
255
- const stepY = Math.min(viewportHeight, options.tileMaxDim);
256
- const maxX = viewportWidth;
257
- const maxY = Math.max(viewportHeight, pageHeight);
258
- const tileCount = Math.ceil(maxX / stepX) * Math.ceil(maxY / stepY);
259
- for (let y = 0; y < maxY; y += stepY) {
260
- for (let x = 0; x < maxX; x += stepX) {
261
- await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, x, y, deps);
262
- await deps.delay(exports.SCREENSHOT_SCROLL_DELAY_MS);
263
- const width = Math.min(stepX, maxX - x);
264
- const height = Math.min(stepY, maxY - y);
265
- try {
266
- await captureTile(x, y, width, height, tileCount);
267
- } catch (err) {
268
- const message = err instanceof Error ? err.message : "capture_failed";
269
- if (message.includes("MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND")) {
270
- await deps.delay(1e3);
271
- await captureTile(x, y, width, height, tileCount);
272
- } else {
273
- throw err;
274
- }
275
- }
261
+ const croppedUrl = await cropDataUrl(
262
+ rawDataUrl,
263
+ options.format,
264
+ options.quality,
265
+ width,
266
+ height,
267
+ devicePixelRatio
268
+ );
269
+ const sizeResult = await constrainDataUrl(
270
+ croppedUrl,
271
+ options.format,
272
+ options.quality,
273
+ options.tileMaxDim,
274
+ options.maxBytes
275
+ );
276
+ tiles.push({
277
+ index: tileIndex,
278
+ total,
279
+ x,
280
+ y,
281
+ width,
282
+ height,
283
+ scale: devicePixelRatio,
284
+ format: options.format,
285
+ bytes: sizeResult.bytes,
286
+ scaled: sizeResult.scaled,
287
+ oversized: sizeResult.oversized,
288
+ dataUrl: sizeResult.dataUrl
289
+ });
290
+ tileIndex += 1;
291
+ };
292
+ if (options.mode === "viewport") {
293
+ await captureTile(startScrollX, startScrollY, viewportWidth, viewportHeight, 1);
294
+ return tiles;
295
+ }
296
+ const stepX = viewportWidth;
297
+ const stepY = Math.min(viewportHeight, options.tileMaxDim);
298
+ const maxX = viewportWidth;
299
+ const maxY = Math.max(viewportHeight, pageHeight);
300
+ const tileCount = Math.ceil(maxX / stepX) * Math.ceil(maxY / stepY);
301
+ for (let y = 0; y < maxY; y += stepY) {
302
+ for (let x = 0; x < maxX; x += stepX) {
303
+ await scrollToPosition(tabId, SCREENSHOT_PROCESS_TIMEOUT_MS, x, y, deps);
304
+ await deps.delay(SCREENSHOT_SCROLL_DELAY_MS);
305
+ const width = Math.min(stepX, maxX - x);
306
+ const height = Math.min(stepY, maxY - y);
307
+ try {
308
+ await captureTile(x, y, width, height, tileCount);
309
+ } catch (err) {
310
+ const message = err instanceof Error ? err.message : "capture_failed";
311
+ if (message.includes("MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND")) {
312
+ await deps.delay(1e3);
313
+ await captureTile(x, y, width, height, tileCount);
314
+ } else {
315
+ throw err;
276
316
  }
277
317
  }
278
- await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, startScrollX, startScrollY, deps);
279
- return tiles;
280
318
  }
281
319
  }
320
+ await scrollToPosition(tabId, SCREENSHOT_PROCESS_TIMEOUT_MS, startScrollX, startScrollY, deps);
321
+ return tiles;
322
+ }
323
+ var SCREENSHOT_TILE_MAX_DIM, SCREENSHOT_MAX_BYTES, SCREENSHOT_QUALITY, SCREENSHOT_SCROLL_DELAY_MS, SCREENSHOT_CAPTURE_DELAY_MS, SCREENSHOT_PROCESS_TIMEOUT_MS;
324
+ var init_screenshot = __esm({
325
+ "src/extension/lib/screenshot.ts"() {
326
+ SCREENSHOT_TILE_MAX_DIM = 2e3;
327
+ SCREENSHOT_MAX_BYTES = 2e6;
328
+ SCREENSHOT_QUALITY = 80;
329
+ SCREENSHOT_SCROLL_DELAY_MS = 150;
330
+ SCREENSHOT_CAPTURE_DELAY_MS = 350;
331
+ SCREENSHOT_PROCESS_TIMEOUT_MS = 8e3;
332
+ }
282
333
  });
283
334
 
284
- // dist/extension/lib/content.js
285
- var require_content = __commonJS({
286
- "dist/extension/lib/content.js"(exports) {
287
- "use strict";
288
- Object.defineProperty(exports, "__esModule", { value: true });
289
- exports.delay = delay2;
290
- exports.executeWithTimeout = executeWithTimeout2;
291
- exports.extractPageMeta = extractPageMeta;
292
- exports.extractSelectorSignal = extractSelectorSignal;
293
- function delay2(ms) {
294
- return new Promise((resolve) => setTimeout(resolve, ms));
295
- }
296
- async function executeWithTimeout2(tabId, timeoutMs, func, args = []) {
297
- const execPromise = chrome.scripting.executeScript({
298
- target: { tabId },
299
- func,
300
- args
301
- });
302
- const timeoutPromise = new Promise((resolve) => {
303
- const handle = setTimeout(() => {
304
- clearTimeout(handle);
305
- resolve(null);
306
- }, timeoutMs);
307
- });
308
- try {
309
- const result = await Promise.race([execPromise, timeoutPromise]);
310
- if (!result || !Array.isArray(result)) {
311
- return null;
312
- }
313
- const [{ result: value }] = result;
314
- return value ?? null;
315
- } catch {
316
- return null;
317
- }
335
+ // src/extension/lib/content.ts
336
+ var content_exports = {};
337
+ __export(content_exports, {
338
+ delay: () => delay,
339
+ executeWithTimeout: () => executeWithTimeout,
340
+ executeWithTimeoutDetailed: () => executeWithTimeoutDetailed,
341
+ extractPageHtml: () => extractPageHtml,
342
+ extractPageMarkdown: () => extractPageMarkdown,
343
+ extractPageMeta: () => extractPageMeta,
344
+ extractSelectorSignal: () => extractSelectorSignal,
345
+ probePageQuiescence: () => probePageQuiescence
346
+ });
347
+ function delay(ms) {
348
+ return new Promise((resolve) => setTimeout(resolve, ms));
349
+ }
350
+ async function executeWithTimeout(tabId, timeoutMs, func, args = []) {
351
+ const execPromise = chrome.scripting.executeScript({
352
+ target: { tabId },
353
+ func,
354
+ args
355
+ });
356
+ const timeoutPromise = new Promise((resolve) => {
357
+ const handle = setTimeout(() => {
358
+ clearTimeout(handle);
359
+ resolve(null);
360
+ }, timeoutMs);
361
+ });
362
+ try {
363
+ const result = await Promise.race([execPromise, timeoutPromise]);
364
+ if (!result || !Array.isArray(result)) {
365
+ return null;
318
366
  }
319
- async function extractPageMeta(tabId, timeoutMs, descriptionMaxLength) {
320
- const result = await executeWithTimeout2(tabId, timeoutMs, () => {
321
- const pickContent = (selector) => {
322
- const el = document.querySelector(selector);
323
- if (!el) {
324
- return "";
367
+ const [{ result: value }] = result;
368
+ return value ?? null;
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+ async function executeWithTimeoutDetailed(tabId, timeoutMs, func, args = []) {
374
+ const execPromise = chrome.scripting.executeScript({
375
+ target: { tabId },
376
+ func,
377
+ args
378
+ }).then((result) => {
379
+ if (!result || !Array.isArray(result)) {
380
+ return { kind: "ok", value: null };
381
+ }
382
+ const [{ result: value }] = result;
383
+ return { kind: "ok", value: value ?? null };
384
+ }).catch((err) => ({
385
+ kind: "error",
386
+ message: err instanceof Error ? err.message : String(err)
387
+ }));
388
+ const timeoutPromise = new Promise((resolve) => {
389
+ const handle = setTimeout(() => {
390
+ clearTimeout(handle);
391
+ resolve({ kind: "timeout" });
392
+ }, timeoutMs);
393
+ });
394
+ return Promise.race([execPromise, timeoutPromise]);
395
+ }
396
+ async function extractPageMeta(tabId, timeoutMs, descriptionMaxLength) {
397
+ const result = await executeWithTimeout(tabId, timeoutMs, () => {
398
+ const pickContent = (selector) => {
399
+ const el = document.querySelector(selector);
400
+ if (!el) {
401
+ return "";
402
+ }
403
+ const content2 = el.getAttribute("content") || el.textContent || "";
404
+ return content2.trim();
405
+ };
406
+ const description = pickContent("meta[name='description']") || pickContent("meta[property='og:description']") || pickContent("meta[name='twitter:description']");
407
+ const h1 = document.querySelector("h1");
408
+ const h1Text = h1 ? h1.textContent?.trim() : "";
409
+ return {
410
+ description: description.replace(/\s+/g, " ").trim(),
411
+ h1: (h1Text || "").replace(/\s+/g, " ").trim()
412
+ };
413
+ });
414
+ if (!result || typeof result !== "object") {
415
+ return null;
416
+ }
417
+ const meta = result;
418
+ return {
419
+ description: (meta.description || "").slice(0, descriptionMaxLength),
420
+ h1: (meta.h1 || "").slice(0, descriptionMaxLength)
421
+ };
422
+ }
423
+ async function extractPageMarkdown(tabId, timeoutMs, maxHtmlChars) {
424
+ const result = await executeWithTimeout(tabId, timeoutMs, (cap) => {
425
+ const raw = document.documentElement?.outerHTML || "";
426
+ return raw.length > cap ? raw.slice(0, cap) : raw;
427
+ }, [maxHtmlChars]);
428
+ return typeof result === "string" ? result : "";
429
+ }
430
+ function injectionStatus(message) {
431
+ const lower = message.toLowerCase();
432
+ if (lower.includes("cannot access") || lower.includes("permission") || lower.includes("extensions gallery")) {
433
+ return "PROTECTED";
434
+ }
435
+ return "INJECTION_FAILED";
436
+ }
437
+ async function extractPageHtml(tabId, timeoutMs, maxHtmlChars) {
438
+ const result = await executeWithTimeoutDetailed(tabId, timeoutMs, (cap) => {
439
+ const raw = document.documentElement?.outerHTML || "";
440
+ const text = document.body?.innerText || document.documentElement?.textContent || "";
441
+ return {
442
+ html: raw.length > cap ? raw.slice(0, cap) : raw,
443
+ sourceHtmlChars: raw.length,
444
+ sourceTextChars: text.length,
445
+ documentReadyState: document.readyState,
446
+ truncatedHtml: raw.length > cap
447
+ };
448
+ }, [maxHtmlChars]);
449
+ if (result.kind === "timeout") {
450
+ return {
451
+ status: "TIMED_OUT",
452
+ html: "",
453
+ sourceHtmlChars: 0,
454
+ sourceTextChars: 0,
455
+ documentReadyState: null,
456
+ truncatedHtml: false,
457
+ error: `Timed out after ${timeoutMs}ms`
458
+ };
459
+ }
460
+ if (result.kind === "error") {
461
+ return {
462
+ status: injectionStatus(result.message),
463
+ html: "",
464
+ sourceHtmlChars: 0,
465
+ sourceTextChars: 0,
466
+ documentReadyState: null,
467
+ truncatedHtml: false,
468
+ error: result.message
469
+ };
470
+ }
471
+ if (!result.value) {
472
+ return {
473
+ status: "EXTRACTION_FAILED",
474
+ html: "",
475
+ sourceHtmlChars: 0,
476
+ sourceTextChars: 0,
477
+ documentReadyState: null,
478
+ truncatedHtml: false,
479
+ error: "Content script returned no page HTML payload"
480
+ };
481
+ }
482
+ const status = result.value.documentReadyState === "loading" ? "NOT_LOADED" : "READ";
483
+ return {
484
+ status,
485
+ ...result.value,
486
+ error: null
487
+ };
488
+ }
489
+ async function probePageQuiescence(tabId, timeoutMs, sampleWindowMs) {
490
+ const result = await executeWithTimeoutDetailed(tabId, timeoutMs, async (rawWindowMs) => {
491
+ const startedAt = Date.now();
492
+ const windowMs = Math.max(50, Math.min(Number(rawWindowMs) || 350, 1500));
493
+ const htmlCap = 25e4;
494
+ const sample = () => {
495
+ const root = document.documentElement;
496
+ const bodyText = document.body?.innerText || root?.textContent || "";
497
+ const html = root?.outerHTML || "";
498
+ const resources = typeof performance !== "undefined" && typeof performance.getEntriesByType === "function" ? performance.getEntriesByType("resource").length : null;
499
+ return {
500
+ textChars: bodyText.length,
501
+ htmlChars: Math.min(html.length, htmlCap),
502
+ domElements: document.getElementsByTagName("*").length,
503
+ resourceCount: resources
504
+ };
505
+ };
506
+ const waitForIdleWindow = () => new Promise((resolve) => {
507
+ const win = window;
508
+ const done = () => setTimeout(resolve, windowMs);
509
+ if (typeof win.requestIdleCallback === "function") {
510
+ let resolved = false;
511
+ const finish = () => {
512
+ if (resolved) {
513
+ return;
325
514
  }
326
- const content2 = el.getAttribute("content") || el.textContent || "";
327
- return content2.trim();
515
+ resolved = true;
516
+ done();
328
517
  };
329
- const description = pickContent("meta[name='description']") || pickContent("meta[property='og:description']") || pickContent("meta[name='twitter:description']");
330
- const h1 = document.querySelector("h1");
331
- const h1Text = h1 ? h1.textContent?.trim() : "";
518
+ const fallback = setTimeout(finish, windowMs + 100);
519
+ win.requestIdleCallback(() => {
520
+ clearTimeout(fallback);
521
+ finish();
522
+ }, { timeout: windowMs });
523
+ return;
524
+ }
525
+ setTimeout(resolve, windowMs);
526
+ });
527
+ try {
528
+ const readyState = document.readyState;
529
+ if (readyState !== "interactive" && readyState !== "complete") {
332
530
  return {
333
- description: description.replace(/\s+/g, " ").trim(),
334
- h1: (h1Text || "").replace(/\s+/g, " ").trim()
531
+ quiet: false,
532
+ reason: "not-ready",
533
+ documentReadyState: readyState,
534
+ before: null,
535
+ after: null,
536
+ elapsedMs: Date.now() - startedAt,
537
+ error: null
335
538
  };
336
- });
337
- if (!result || typeof result !== "object") {
338
- return null;
339
539
  }
340
- const meta = result;
540
+ const before = sample();
541
+ await waitForIdleWindow();
542
+ const after = sample();
543
+ const stable = before.textChars === after.textChars && before.htmlChars === after.htmlChars && before.domElements === after.domElements && before.resourceCount === after.resourceCount;
544
+ return {
545
+ quiet: stable,
546
+ reason: stable ? "stable" : "changed",
547
+ documentReadyState: document.readyState,
548
+ before,
549
+ after,
550
+ elapsedMs: Date.now() - startedAt,
551
+ error: null
552
+ };
553
+ } catch (err) {
341
554
  return {
342
- description: (meta.description || "").slice(0, descriptionMaxLength),
343
- h1: (meta.h1 || "").slice(0, descriptionMaxLength)
555
+ quiet: false,
556
+ reason: "probe-error",
557
+ documentReadyState: typeof document !== "undefined" ? document.readyState : null,
558
+ before: null,
559
+ after: null,
560
+ elapsedMs: Date.now() - startedAt,
561
+ error: err instanceof Error ? err.message : String(err)
344
562
  };
345
563
  }
346
- async function extractSelectorSignal(tabId, specs, timeoutMs, selectorValueMaxLength) {
347
- if (!specs.length) {
348
- return null;
564
+ }, [sampleWindowMs]);
565
+ if (result.kind === "timeout") {
566
+ return {
567
+ quiet: false,
568
+ reason: "timed-out",
569
+ documentReadyState: null,
570
+ before: null,
571
+ after: null,
572
+ elapsedMs: timeoutMs,
573
+ error: `Timed out after ${timeoutMs}ms`
574
+ };
575
+ }
576
+ if (result.kind === "error") {
577
+ return {
578
+ quiet: false,
579
+ reason: "injection-error",
580
+ documentReadyState: null,
581
+ before: null,
582
+ after: null,
583
+ elapsedMs: 0,
584
+ error: result.message
585
+ };
586
+ }
587
+ return result.value ?? {
588
+ quiet: false,
589
+ reason: "no-result",
590
+ documentReadyState: null,
591
+ before: null,
592
+ after: null,
593
+ elapsedMs: 0,
594
+ error: "Content script returned no quiescence probe payload"
595
+ };
596
+ }
597
+ async function extractSelectorSignal(tabId, specs, timeoutMs, selectorValueMaxLength) {
598
+ if (!specs.length) {
599
+ return null;
600
+ }
601
+ const result = await executeWithTimeout(tabId, timeoutMs, (rawSpecs, maxLen) => {
602
+ const values = {};
603
+ const missing = [];
604
+ const errors = {};
605
+ const hints = {};
606
+ const stringCap = typeof maxLen === "number" && maxLen > 0 ? maxLen : 500;
607
+ const htmlCap = typeof maxLen === "number" && maxLen > 0 ? maxLen : 4096;
608
+ const normalizeStringValue = (value, cap) => value.replace(/\s+/g, " ").trim().slice(0, cap);
609
+ for (const raw of rawSpecs) {
610
+ const selector = typeof raw.selector === "string" ? raw.selector : "";
611
+ if (!selector) {
612
+ continue;
613
+ }
614
+ const name = typeof raw.name === "string" && raw.name ? raw.name : selector;
615
+ const attr = typeof raw.attr === "string" ? raw.attr : "text";
616
+ const all = Boolean(raw.all);
617
+ const text = typeof raw.text === "string" ? raw.text.trim() : "";
618
+ const textMode = typeof raw.textMode === "string" ? raw.textMode.trim().toLowerCase() : "";
619
+ const normalizedTextMode = textMode === "includes" ? "contains" : textMode;
620
+ const textModes = /* @__PURE__ */ new Set(["", "contains", "exact", "starts-with"]);
621
+ const styleProps = Array.isArray(raw.styleProps) ? raw.styleProps.filter((prop) => typeof prop === "string").map((prop) => prop.trim()).filter(Boolean) : [];
622
+ if (!textModes.has(normalizedTextMode)) {
623
+ errors[name] = `Unsupported textMode: ${textMode || "unknown"}`;
624
+ hints[name] = "Use textMode: contains | exact | starts-with";
625
+ continue;
349
626
  }
350
- const result = await executeWithTimeout2(tabId, timeoutMs, (rawSpecs, maxLen) => {
351
- const values = {};
352
- const missing = [];
353
- const errors = {};
354
- const hints = {};
355
- for (const raw of rawSpecs) {
356
- const selector = typeof raw.selector === "string" ? raw.selector : "";
357
- if (!selector) {
358
- continue;
627
+ try {
628
+ const elements = Array.from(document.querySelectorAll(selector));
629
+ const matchesText = (el) => {
630
+ if (!text) {
631
+ return true;
359
632
  }
360
- const name = typeof raw.name === "string" && raw.name ? raw.name : selector;
361
- const attr = typeof raw.attr === "string" ? raw.attr : "text";
362
- const all = Boolean(raw.all);
363
- const text = typeof raw.text === "string" ? raw.text.trim() : "";
364
- const textMode = typeof raw.textMode === "string" ? raw.textMode.trim().toLowerCase() : "";
365
- const normalizedTextMode = textMode === "includes" ? "contains" : textMode;
366
- const textModes = /* @__PURE__ */ new Set(["", "contains", "exact", "starts-with"]);
367
- if (!textModes.has(normalizedTextMode)) {
368
- errors[name] = `Unsupported textMode: ${textMode || "unknown"}`;
369
- hints[name] = "Use textMode: contains | exact | starts-with";
370
- continue;
633
+ const content2 = (el.textContent || "").replace(/\s+/g, " ").trim();
634
+ if (normalizedTextMode === "exact") {
635
+ return content2 === text;
371
636
  }
372
- try {
373
- const elements = Array.from(document.querySelectorAll(selector));
374
- if (!elements.length) {
375
- missing.push(name);
376
- if (selector.includes(":contains(")) {
377
- hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector.";
378
- } else {
379
- hints[name] = "No matches found; capture a screenshot for context or adjust the selector.";
380
- }
381
- continue;
637
+ if (normalizedTextMode === "starts-with") {
638
+ return content2.startsWith(text);
639
+ }
640
+ return content2.includes(text);
641
+ };
642
+ const filtered = text ? elements.filter(matchesText) : elements;
643
+ if (attr === "count") {
644
+ values[name] = filtered.length;
645
+ continue;
646
+ }
647
+ if (!elements.length) {
648
+ missing.push(name);
649
+ if (selector.includes(":contains(")) {
650
+ hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector.";
651
+ } else {
652
+ hints[name] = "No matches found; capture a screenshot for context or adjust the selector.";
653
+ }
654
+ continue;
655
+ }
656
+ if (!filtered.length) {
657
+ missing.push(name);
658
+ hints[name] = "Selector matched elements, but none matched the text filter; capture a screenshot for context or adjust text/textMode.";
659
+ continue;
660
+ }
661
+ const getValue = (el) => {
662
+ if (attr === "text") {
663
+ return normalizeStringValue(el.textContent || "", stringCap);
664
+ }
665
+ if (attr === "html") {
666
+ return normalizeStringValue(el.outerHTML || "", htmlCap);
667
+ }
668
+ if (attr === "href-url" || attr === "src-url") {
669
+ const rawValue = el.getAttribute(attr === "href-url" ? "href" : "src") || "";
670
+ if (!rawValue) {
671
+ return "";
382
672
  }
383
- const matchesText = (el) => {
384
- if (!text) {
385
- return true;
673
+ try {
674
+ const resolved = new URL(rawValue, document.baseURI);
675
+ if (resolved.protocol === "http:" || resolved.protocol === "https:") {
676
+ return normalizeStringValue(resolved.toString(), stringCap);
386
677
  }
387
- const content2 = (el.textContent || "").replace(/\s+/g, " ").trim();
388
- if (normalizedTextMode === "exact") {
389
- return content2 === text;
390
- }
391
- if (normalizedTextMode === "starts-with") {
392
- return content2.startsWith(text);
393
- }
394
- return content2.includes(text);
395
- };
396
- const filtered = text ? elements.filter(matchesText) : elements;
397
- if (!filtered.length) {
398
- missing.push(name);
399
- hints[name] = "Selector matched elements, but none matched the text filter; capture a screenshot for context or adjust text/textMode.";
400
- continue;
678
+ return "";
679
+ } catch {
680
+ return "";
401
681
  }
402
- const getValue = (el) => {
403
- let value = "";
404
- if (attr === "text") {
405
- value = el.textContent || "";
406
- } else if (attr === "href-url" || attr === "src-url") {
407
- const rawValue = el.getAttribute(attr === "href-url" ? "href" : "src") || "";
408
- if (!rawValue) {
409
- value = "";
410
- } else {
411
- try {
412
- const resolved = new URL(rawValue, document.baseURI);
413
- if (resolved.protocol === "http:" || resolved.protocol === "https:") {
414
- value = resolved.toString();
415
- } else {
416
- value = "";
417
- }
418
- } catch {
419
- value = "";
420
- }
421
- }
422
- } else {
423
- value = el.getAttribute(attr) || "";
424
- }
425
- return value.replace(/\s+/g, " ").trim().slice(0, maxLen);
682
+ }
683
+ if (attr === "value") {
684
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
685
+ return el.value;
686
+ }
687
+ return null;
688
+ }
689
+ if (attr === "box") {
690
+ const rect = el.getBoundingClientRect();
691
+ return {
692
+ x: rect.x,
693
+ y: rect.y,
694
+ width: rect.width,
695
+ height: rect.height,
696
+ top: rect.top,
697
+ right: rect.right,
698
+ bottom: rect.bottom,
699
+ left: rect.left
426
700
  };
427
- if (all) {
428
- values[name] = filtered.map(getValue).filter((val) => val.length > 0);
429
- } else {
430
- values[name] = getValue(filtered[0]);
701
+ }
702
+ if (attr === "styles") {
703
+ if (!styleProps.length) {
704
+ return {};
431
705
  }
432
- } catch (err) {
433
- const message = err instanceof Error ? err.message : "selector_error";
434
- errors[name] = message;
435
- if (selector.includes(":contains(")) {
436
- hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector.";
437
- } else {
438
- hints[name] = "Selector failed to evaluate; capture a screenshot for context or adjust the selector.";
706
+ const computed = window.getComputedStyle(el);
707
+ const selected = {};
708
+ for (const prop of styleProps) {
709
+ selected[prop] = computed.getPropertyValue(prop);
439
710
  }
711
+ return selected;
712
+ }
713
+ if (attr === "visible") {
714
+ const computed = window.getComputedStyle(el);
715
+ const rect = el.getBoundingClientRect();
716
+ const styleVisible = computed.display !== "none" && computed.visibility !== "hidden" && computed.opacity !== "0";
717
+ const hasRenderedBox = rect.width > 0 || rect.height > 0;
718
+ return styleVisible && hasRenderedBox;
719
+ }
720
+ if (attr === "enabled") {
721
+ const candidate = el;
722
+ return candidate.disabled !== true && el.getAttribute("aria-disabled") !== "true";
440
723
  }
724
+ if (attr === "checked") {
725
+ if (el instanceof HTMLInputElement) {
726
+ return el.checked;
727
+ }
728
+ return el.getAttribute("aria-checked") === "true";
729
+ }
730
+ return normalizeStringValue(el.getAttribute(attr) || "", stringCap);
731
+ };
732
+ if (attr === "box") {
733
+ values[name] = getValue(filtered[0]);
734
+ continue;
735
+ }
736
+ if (all) {
737
+ values[name] = filtered.map(getValue).filter((val) => typeof val !== "string" || val.length > 0);
738
+ } else {
739
+ values[name] = getValue(filtered[0]);
740
+ }
741
+ } catch (err) {
742
+ const message = err instanceof Error ? err.message : "selector_error";
743
+ errors[name] = message;
744
+ if (selector.includes(":contains(")) {
745
+ hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector.";
746
+ } else {
747
+ hints[name] = "Selector failed to evaluate; capture a screenshot for context or adjust the selector.";
441
748
  }
442
- return { values, missing, errors, hints };
443
- }, [specs, selectorValueMaxLength]);
444
- if (!result || typeof result !== "object") {
445
- return null;
446
749
  }
447
- return result;
448
750
  }
751
+ return { values, missing, errors, hints };
752
+ }, [specs, selectorValueMaxLength]);
753
+ if (!result || typeof result !== "object") {
754
+ return null;
755
+ }
756
+ return result;
757
+ }
758
+ var init_content = __esm({
759
+ "src/extension/lib/content.ts"() {
449
760
  }
450
761
  });
451
762
 
452
- // dist/extension/background.js
763
+ // src/extension/background.ts
453
764
  var HOST_NAME = "com.erwinkroon.tabctl";
454
765
  var manifest = chrome.runtime.getManifest();
455
766
  var MANIFEST_VERSION = manifest.version || "0.0.0";
@@ -469,36 +780,128 @@
469
780
  dirty: parsed.dirty
470
781
  };
471
782
  var KEEPALIVE_ALARM = "tabctl-keepalive";
783
+ var RECONNECT_ALARM = "tabctl-reconnect";
472
784
  var KEEPALIVE_INTERVAL_MINUTES = 1;
473
- var screenshot = require_screenshot();
474
- var content = require_content();
475
- var { delay, executeWithTimeout } = content;
785
+ var BROWSER_STATE_SYNC_DEBOUNCE_MS = 750;
786
+ var ACTIVE_PAGE_CACHE_DEBOUNCE_MS = 1e3;
787
+ var ACTIVE_PAGE_CACHE_TIMEOUT_MS = 5e3;
788
+ var MAX_PAGE_HTML_CHARS = 10 * 1024 * 1024;
789
+ var ACTIVE_PAGE_CACHE_MAX_HTML_CHARS = MAX_PAGE_HTML_CHARS;
790
+ var ACTIVE_PAGE_CACHE_QUIESCENT_DELAY_MS = 6e3;
791
+ var ACTIVE_PAGE_CACHE_QUIESCENT_RETRY_MS = 1e3;
792
+ var ACTIVE_PAGE_CACHE_QUIESCENT_TIMEOUT_MS = 2500;
793
+ var ACTIVE_PAGE_CACHE_QUIESCENT_SAMPLE_MS = 350;
794
+ var ACTIVE_PAGE_CACHE_QUIESCENT_COOLDOWN_MS = 3e4;
795
+ var ACTIVE_PAGE_CACHE_STATUS_TIMEOUT_MS = 3e4;
796
+ var CACHE_AVAILABLE_BADGE_TEXT = "C";
797
+ var CACHE_AVAILABLE_BADGE_COLOR = "#2da44e";
798
+ var RECONNECT_INITIAL_DELAY_MS = 250;
799
+ var RECONNECT_MAX_DELAY_MS = 3e4;
800
+ var RECONNECT_ALARM_MIN_DELAY_MS = 3e4;
801
+ var RECONNECT_STABLE_RESET_MS = 5e3;
802
+ var screenshot = (init_screenshot(), __toCommonJS(screenshot_exports));
803
+ var content = (init_content(), __toCommonJS(content_exports));
804
+ var { delay: delay2, executeWithTimeout: executeWithTimeout2 } = content;
476
805
  var DESCRIPTION_MAX_LENGTH = 250;
477
806
  function requireFiniteId(value, name) {
478
807
  const n = Number(value);
479
- if (!Number.isFinite(n))
480
- throw new Error(`${name} must be a finite number, got: ${String(value)}`);
808
+ if (!Number.isFinite(n)) throw new Error(`${name} must be a finite number, got: ${String(value)}`);
481
809
  return n;
482
810
  }
483
811
  var state = {
484
812
  port: null,
485
- lastFocused: {},
486
- lastFocusedLoaded: false
813
+ reconnectTimer: null,
814
+ reconnectStableTimer: null,
815
+ reconnectAttempt: 0
816
+ };
817
+ var browserState = {
818
+ nextId: 1,
819
+ syncTimer: null,
820
+ pendingEvents: [],
821
+ incognitoWindowIds: /* @__PURE__ */ new Set(),
822
+ incognitoTabIds: /* @__PURE__ */ new Set(),
823
+ incognitoGroupIds: /* @__PURE__ */ new Set()
824
+ };
825
+ var activePageCache = {
826
+ nextId: 1,
827
+ timer: null,
828
+ pending: null,
829
+ quiescentTimer: null,
830
+ quiescentPending: null,
831
+ inFlightKeys: /* @__PURE__ */ new Set(),
832
+ lastCapturedKey: null,
833
+ lastQuiescentCapturedKey: null,
834
+ lastQuiescentCapturedAt: 0,
835
+ statusRequests: /* @__PURE__ */ new Map()
487
836
  };
488
837
  function log(...args) {
489
838
  console.log("[tabctl]", ...args);
490
839
  }
491
- function sendResponse(id, ok, payload) {
492
- if (!state.port) {
840
+ function reconnectDelayMs(attempt) {
841
+ return Math.min(RECONNECT_INITIAL_DELAY_MS * 2 ** attempt, RECONNECT_MAX_DELAY_MS);
842
+ }
843
+ function clearReconnectTimer() {
844
+ if (!state.reconnectTimer) {
845
+ return;
846
+ }
847
+ clearTimeout(state.reconnectTimer);
848
+ state.reconnectTimer = null;
849
+ }
850
+ function clearReconnectAlarm() {
851
+ chrome.alarms.clear(RECONNECT_ALARM);
852
+ }
853
+ function clearReconnectStableTimer() {
854
+ if (!state.reconnectStableTimer) {
493
855
  return;
494
856
  }
495
- if (ok) {
496
- const data = typeof payload === "object" && payload !== null ? payload : { payload };
497
- state.port.postMessage({ id, ok: true, data });
857
+ clearTimeout(state.reconnectStableTimer);
858
+ state.reconnectStableTimer = null;
859
+ }
860
+ function scheduleReconnect(reason) {
861
+ if (state.port || state.reconnectTimer) {
862
+ return;
863
+ }
864
+ const attempt = state.reconnectAttempt;
865
+ const delayMs = reconnectDelayMs(attempt);
866
+ state.reconnectAttempt += 1;
867
+ log("Scheduling native host reconnect", { reason, delayMs, attempt });
868
+ chrome.alarms.create(RECONNECT_ALARM, {
869
+ delayInMinutes: Math.max(delayMs, RECONNECT_ALARM_MIN_DELAY_MS) / 6e4
870
+ });
871
+ state.reconnectTimer = setTimeout(() => {
872
+ state.reconnectTimer = null;
873
+ clearReconnectAlarm();
874
+ connectNative();
875
+ }, delayMs);
876
+ }
877
+ function resetReconnectBackoffAfterStablePort(port) {
878
+ clearReconnectStableTimer();
879
+ state.reconnectStableTimer = setTimeout(() => {
880
+ state.reconnectStableTimer = null;
881
+ if (state.port === port) {
882
+ state.reconnectAttempt = 0;
883
+ }
884
+ }, RECONNECT_STABLE_RESET_MS);
885
+ }
886
+ function ensureKeepaliveAlarm() {
887
+ chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
888
+ }
889
+ function sendResponse(port, id, ok, payload) {
890
+ if (!port) {
891
+ log("dropping response because native port is unavailable", { id, ok });
498
892
  return;
499
893
  }
500
- const error = payload instanceof Error ? { message: payload.message, stack: payload.stack } : payload;
501
- state.port.postMessage({ id, ok: false, error });
894
+ try {
895
+ if (ok) {
896
+ const data = typeof payload === "object" && payload !== null ? payload : { payload };
897
+ port.postMessage({ id, ok: true, data });
898
+ return;
899
+ }
900
+ const error = payload instanceof Error ? { message: payload.message, stack: payload.stack } : payload;
901
+ port.postMessage({ id, ok: false, error });
902
+ } catch (error) {
903
+ log("failed to send native response", { id, ok, error });
904
+ }
502
905
  }
503
906
  function connectNative() {
504
907
  if (state.port) {
@@ -506,8 +909,16 @@
506
909
  }
507
910
  try {
508
911
  const port = chrome.runtime.connectNative(HOST_NAME);
912
+ clearReconnectTimer();
913
+ clearReconnectAlarm();
509
914
  state.port = port;
510
- port.onMessage.addListener(handleNativeMessage);
915
+ resetReconnectBackoffAfterStablePort(port);
916
+ port.onMessage.addListener((message) => {
917
+ if (handlePageCacheStatusMessage(message)) {
918
+ return;
919
+ }
920
+ void handleNativeMessage(port, message);
921
+ });
511
922
  port.onDisconnect.addListener(() => {
512
923
  const lastError = chrome.runtime.lastError;
513
924
  if (lastError) {
@@ -515,62 +926,617 @@
515
926
  } else {
516
927
  log("Native host disconnected");
517
928
  }
518
- state.port = null;
929
+ if (state.port === port) {
930
+ state.port = null;
931
+ }
932
+ activePageCache.statusRequests.clear();
933
+ clearReconnectStableTimer();
934
+ scheduleReconnect("disconnect");
519
935
  });
520
936
  log("Native host connected");
937
+ queueBrowserStateSync("startup");
938
+ void refreshActivePageCacheIndicator("connectNative");
521
939
  } catch (error) {
522
940
  log("Native host connection failed", error);
941
+ scheduleReconnect("connect-failed");
523
942
  }
524
943
  }
525
- chrome.runtime.onInstalled.addListener(() => {
526
- connectNative();
527
- chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
528
- });
529
- chrome.runtime.onStartup.addListener(() => {
530
- connectNative();
531
- chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
532
- });
533
- chrome.alarms.onAlarm.addListener((alarm) => {
534
- if (alarm.name === KEEPALIVE_ALARM) {
944
+ function nextBrowserStateId() {
945
+ const id = browserState.nextId;
946
+ browserState.nextId += 1;
947
+ return `browser-state-${Date.now()}-${id}`;
948
+ }
949
+ function nextActivePageCacheId() {
950
+ const id = activePageCache.nextId;
951
+ activePageCache.nextId += 1;
952
+ return `page-cache-capture-${Date.now()}-${id}`;
953
+ }
954
+ function nextActivePageCacheStatusId() {
955
+ const id = activePageCache.nextId;
956
+ activePageCache.nextId += 1;
957
+ return `page-cache-status-${Date.now()}-${id}`;
958
+ }
959
+ function trackPageCacheStatusRequest(id, tabId, url) {
960
+ activePageCache.statusRequests.set(id, { tabId, url });
961
+ setTimeout(() => {
962
+ activePageCache.statusRequests.delete(id);
963
+ }, ACTIVE_PAGE_CACHE_STATUS_TIMEOUT_MS);
964
+ }
965
+ function isScriptableUrl(url) {
966
+ const lower = url.toLowerCase();
967
+ return lower.startsWith("http://") || lower.startsWith("https://");
968
+ }
969
+ function activePageCacheKey(tab, url) {
970
+ return `${tab.id}:${url}`;
971
+ }
972
+ function activePageCacheUrl(tab) {
973
+ return tab.url || tab.pendingUrl || "";
974
+ }
975
+ async function tabUrlMatches(tabId, expectedUrl) {
976
+ try {
977
+ return activePageCacheUrl(await chrome.tabs.get(tabId)) === expectedUrl;
978
+ } catch {
979
+ return false;
980
+ }
981
+ }
982
+ async function clearCacheAvailableIndicator(tabId) {
983
+ try {
984
+ await chrome.action?.setBadgeText?.({ tabId, text: "" });
985
+ await chrome.action?.setTitle?.({ tabId, title: "Tab Control" });
986
+ } catch (error) {
987
+ log("clear cache indicator failed", { tabId, error });
988
+ }
989
+ }
990
+ async function setCacheAvailableIndicator(tabId, expectedUrl) {
991
+ try {
992
+ const tab = await chrome.tabs.get(tabId);
993
+ if (!isEligibleActivePageCacheTab(tab) || activePageCacheUrl(tab) !== expectedUrl) {
994
+ await clearCacheAvailableIndicator(tabId);
995
+ return;
996
+ }
997
+ await chrome.action?.setBadgeBackgroundColor?.({ tabId, color: CACHE_AVAILABLE_BADGE_COLOR });
998
+ await chrome.action?.setBadgeText?.({ tabId, text: CACHE_AVAILABLE_BADGE_TEXT });
999
+ await chrome.action?.setTitle?.({ tabId, title: "Tab Control - page cache available" });
1000
+ } catch (error) {
1001
+ log("set cache indicator failed", { tabId, error });
1002
+ }
1003
+ }
1004
+ async function applyPageCacheStatus(tabId, expectedUrl, available) {
1005
+ try {
1006
+ const tab = await chrome.tabs.get(tabId);
1007
+ if (activePageCacheUrl(tab) !== expectedUrl) {
1008
+ return;
1009
+ }
1010
+ if (!isEligibleActivePageCacheTab(tab) || !available) {
1011
+ await clearCacheAvailableIndicator(tabId);
1012
+ return;
1013
+ }
1014
+ await setCacheAvailableIndicator(tabId, expectedUrl);
1015
+ } catch {
1016
+ await clearCacheAvailableIndicator(tabId);
1017
+ }
1018
+ }
1019
+ function handlePageCacheStatusMessage(message) {
1020
+ if (!message || typeof message !== "object" || message.action) {
1021
+ return false;
1022
+ }
1023
+ const requestId = typeof message.id === "string" ? message.id : "";
1024
+ const pending = activePageCache.statusRequests.get(requestId);
1025
+ if (!pending) {
1026
+ return false;
1027
+ }
1028
+ activePageCache.statusRequests.delete(requestId);
1029
+ const data = message.data && typeof message.data === "object" ? message.data : {};
1030
+ const tabId = typeof data.tabId === "number" ? data.tabId : pending.tabId;
1031
+ const url = typeof data.url === "string" ? data.url : pending.url;
1032
+ const available = message.ok === true && data.available === true && tabId === pending.tabId && url === pending.url;
1033
+ void applyPageCacheStatus(pending.tabId, pending.url, available);
1034
+ return true;
1035
+ }
1036
+ function requestPageCacheStatus(tab, reason) {
1037
+ const url = activePageCacheUrl(tab);
1038
+ if (typeof tab.id !== "number" || !isEligibleActivePageCacheTab(tab)) {
1039
+ if (typeof tab.id === "number") {
1040
+ void clearCacheAvailableIndicator(tab.id);
1041
+ }
1042
+ return;
1043
+ }
1044
+ const port = state.port;
1045
+ if (!port) {
535
1046
  connectNative();
1047
+ void clearCacheAvailableIndicator(tab.id);
1048
+ return;
536
1049
  }
537
- });
538
- async function ensureLastFocusedLoaded() {
539
- if (state.lastFocusedLoaded) {
1050
+ const id = nextActivePageCacheStatusId();
1051
+ trackPageCacheStatusRequest(id, tab.id, url);
1052
+ port.postMessage({
1053
+ id,
1054
+ action: "page-cache-status",
1055
+ ok: true,
1056
+ data: {
1057
+ reason,
1058
+ requestedAt: Date.now(),
1059
+ tab: {
1060
+ tabId: tab.id,
1061
+ url,
1062
+ incognito: tab.incognito || false,
1063
+ discarded: tab.discarded || false,
1064
+ status: tab.status || null
1065
+ }
1066
+ }
1067
+ });
1068
+ }
1069
+ async function requestPageCacheStatusForTab(tabId, reason) {
1070
+ try {
1071
+ requestPageCacheStatus(await chrome.tabs.get(tabId), reason);
1072
+ } catch {
1073
+ await clearCacheAvailableIndicator(tabId);
1074
+ }
1075
+ }
1076
+ async function refreshActivePageCacheIndicator(reason) {
1077
+ try {
1078
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
1079
+ if (tab) {
1080
+ requestPageCacheStatus(tab, reason);
1081
+ }
1082
+ } catch (error) {
1083
+ log("active page cache indicator refresh failed", { reason, error });
1084
+ }
1085
+ }
1086
+ function urlMismatchPageHtmlResponse(expectedUrl) {
1087
+ return {
1088
+ status: "URL_MISMATCH",
1089
+ html: "",
1090
+ sourceHtmlChars: 0,
1091
+ sourceTextChars: 0,
1092
+ documentReadyState: null,
1093
+ truncatedHtml: false,
1094
+ error: `Tab URL changed before page HTML extraction completed: expected ${expectedUrl}`
1095
+ };
1096
+ }
1097
+ function isEligibleActivePageCacheTab(tab) {
1098
+ const url = activePageCacheUrl(tab);
1099
+ return typeof tab.id === "number" && Boolean(url) && tab.incognito !== true && !browserState.incognitoTabIds.has(tab.id) && (typeof tab.windowId !== "number" || !browserState.incognitoWindowIds.has(tab.windowId)) && tab.discarded !== true && tab.status !== "loading" && isScriptableUrl(url);
1100
+ }
1101
+ function clearPendingActivePageCacheCapture() {
1102
+ if (activePageCache.timer) {
1103
+ clearTimeout(activePageCache.timer);
1104
+ activePageCache.timer = null;
1105
+ }
1106
+ activePageCache.pending = null;
1107
+ }
1108
+ function clearPendingQuiescentActivePageCacheCapture() {
1109
+ if (activePageCache.quiescentTimer) {
1110
+ clearTimeout(activePageCache.quiescentTimer);
1111
+ activePageCache.quiescentTimer = null;
1112
+ }
1113
+ activePageCache.quiescentPending = null;
1114
+ }
1115
+ function isQuiescentCaptureCoolingDown(key) {
1116
+ return activePageCache.lastQuiescentCapturedKey === key && Date.now() - activePageCache.lastQuiescentCapturedAt < ACTIVE_PAGE_CACHE_QUIESCENT_COOLDOWN_MS;
1117
+ }
1118
+ function scheduleQuiescentActivePageCacheCapture(tab, reason, key) {
1119
+ if (isQuiescentCaptureCoolingDown(key)) {
1120
+ return;
1121
+ }
1122
+ clearPendingQuiescentActivePageCacheCapture();
1123
+ activePageCache.quiescentPending = {
1124
+ tabId: tab.id,
1125
+ key,
1126
+ reason: `${reason}:quiescent`,
1127
+ attempts: 0
1128
+ };
1129
+ activePageCache.quiescentTimer = setTimeout(() => {
1130
+ activePageCache.quiescentTimer = null;
1131
+ const pending = activePageCache.quiescentPending;
1132
+ activePageCache.quiescentPending = null;
1133
+ if (pending) {
1134
+ void captureQuiescentActivePageCache(pending);
1135
+ }
1136
+ }, ACTIVE_PAGE_CACHE_QUIESCENT_DELAY_MS);
1137
+ }
1138
+ function scheduleActivePageCacheCapture(tab, reason) {
1139
+ if (!isEligibleActivePageCacheTab(tab)) {
1140
+ if (typeof tab.id === "number") {
1141
+ void clearCacheAvailableIndicator(tab.id);
1142
+ }
1143
+ clearPendingActivePageCacheCapture();
1144
+ clearPendingQuiescentActivePageCacheCapture();
540
1145
  return;
541
1146
  }
542
- const stored = await chrome.storage.local.get("lastFocused");
543
- state.lastFocused = stored.lastFocused || {};
544
- state.lastFocusedLoaded = true;
1147
+ requestPageCacheStatus(tab, reason);
1148
+ const url = activePageCacheUrl(tab);
1149
+ const key = activePageCacheKey(tab, url);
1150
+ scheduleQuiescentActivePageCacheCapture(tab, reason, key);
1151
+ if (activePageCache.inFlightKeys.has(key) || activePageCache.lastCapturedKey === key) {
1152
+ return;
1153
+ }
1154
+ clearPendingActivePageCacheCapture();
1155
+ activePageCache.pending = { tab, reason, key };
1156
+ activePageCache.timer = setTimeout(() => {
1157
+ activePageCache.timer = null;
1158
+ const pending = activePageCache.pending;
1159
+ activePageCache.pending = null;
1160
+ if (pending) {
1161
+ void captureActivePageCache(pending.tab, pending.reason, pending.key);
1162
+ }
1163
+ }, ACTIVE_PAGE_CACHE_DEBOUNCE_MS);
545
1164
  }
546
- async function setLastFocused(tabId) {
547
- await ensureLastFocusedLoaded();
548
- state.lastFocused[String(tabId)] = Date.now();
549
- await chrome.storage.local.set({ lastFocused: state.lastFocused });
1165
+ async function scheduleActivePageCacheCaptureForTab(tabId, reason) {
1166
+ try {
1167
+ scheduleActivePageCacheCapture(await chrome.tabs.get(tabId), reason);
1168
+ } catch (error) {
1169
+ log("active page cache tab lookup failed", { tabId, reason, error });
1170
+ await clearCacheAvailableIndicator(tabId);
1171
+ }
550
1172
  }
551
- chrome.tabs.onActivated.addListener((info) => {
552
- setLastFocused(info.tabId).catch((error) => log("Failed to set last focused", error));
553
- });
554
- chrome.tabs.onRemoved.addListener((tabId) => {
555
- ensureLastFocusedLoaded().then(() => {
556
- const key = String(tabId);
557
- if (state.lastFocused[key]) {
558
- delete state.lastFocused[key];
559
- chrome.storage.local.set({ lastFocused: state.lastFocused });
560
- }
561
- }).catch((error) => log("Failed to prune last focused", error));
562
- });
563
- chrome.windows.onFocusChanged.addListener((windowId) => {
1173
+ async function scheduleActivePageCacheCaptureForWindow(windowId, reason) {
564
1174
  if (windowId === chrome.windows.WINDOW_ID_NONE) {
565
1175
  return;
566
1176
  }
567
- chrome.tabs.query({ windowId, active: true }).then((tabs) => {
568
- if (tabs[0] && tabs[0].id != null) {
569
- setLastFocused(tabs[0].id).catch((error) => log("Failed to set last focused", error));
1177
+ try {
1178
+ const [tab] = await chrome.tabs.query({ active: true, windowId });
1179
+ if (tab) {
1180
+ scheduleActivePageCacheCapture(tab, reason);
1181
+ }
1182
+ } catch (error) {
1183
+ log("active page cache window lookup failed", { windowId, reason, error });
1184
+ }
1185
+ }
1186
+ async function currentActivePageCacheTab(tabId, key) {
1187
+ try {
1188
+ const tab = await chrome.tabs.get(tabId);
1189
+ const url = activePageCacheUrl(tab);
1190
+ if (!tab.active || !isEligibleActivePageCacheTab(tab) || activePageCacheKey(tab, url) !== key) {
1191
+ return null;
1192
+ }
1193
+ return tab;
1194
+ } catch {
1195
+ return null;
1196
+ }
1197
+ }
1198
+ async function rescheduleQuiescentActivePageCacheCapture(pending) {
1199
+ if (pending.attempts >= 2 || isQuiescentCaptureCoolingDown(pending.key)) {
1200
+ return;
1201
+ }
1202
+ activePageCache.quiescentPending = { ...pending, attempts: pending.attempts + 1 };
1203
+ activePageCache.quiescentTimer = setTimeout(() => {
1204
+ activePageCache.quiescentTimer = null;
1205
+ const retry = activePageCache.quiescentPending;
1206
+ activePageCache.quiescentPending = null;
1207
+ if (retry) {
1208
+ void captureQuiescentActivePageCache(retry);
1209
+ }
1210
+ }, ACTIVE_PAGE_CACHE_QUIESCENT_RETRY_MS);
1211
+ }
1212
+ async function captureQuiescentActivePageCache(pending) {
1213
+ if (isQuiescentCaptureCoolingDown(pending.key)) {
1214
+ return;
1215
+ }
1216
+ if (activePageCache.inFlightKeys.has(pending.key)) {
1217
+ await rescheduleQuiescentActivePageCacheCapture(pending);
1218
+ return;
1219
+ }
1220
+ const tab = await currentActivePageCacheTab(pending.tabId, pending.key);
1221
+ if (!tab) {
1222
+ return;
1223
+ }
1224
+ const probe = await content.probePageQuiescence(
1225
+ tab.id,
1226
+ ACTIVE_PAGE_CACHE_QUIESCENT_TIMEOUT_MS,
1227
+ ACTIVE_PAGE_CACHE_QUIESCENT_SAMPLE_MS
1228
+ );
1229
+ if (!probe.quiet) {
1230
+ await rescheduleQuiescentActivePageCacheCapture(pending);
1231
+ return;
1232
+ }
1233
+ const captured = await captureActivePageCache(tab, pending.reason, pending.key);
1234
+ if (captured) {
1235
+ activePageCache.lastQuiescentCapturedKey = pending.key;
1236
+ activePageCache.lastQuiescentCapturedAt = Date.now();
1237
+ }
1238
+ }
1239
+ async function captureActivePageCache(tab, reason, key) {
1240
+ if (!state.port) {
1241
+ connectNative();
1242
+ return false;
1243
+ }
1244
+ if (!isEligibleActivePageCacheTab(tab) || activePageCache.inFlightKeys.has(key)) {
1245
+ return false;
1246
+ }
1247
+ activePageCache.inFlightKeys.add(key);
1248
+ try {
1249
+ const captureTab = await currentActivePageCacheTab(tab.id, key);
1250
+ if (!captureTab) {
1251
+ return false;
1252
+ }
1253
+ const extraction = await content.extractPageHtml(captureTab.id, ACTIVE_PAGE_CACHE_TIMEOUT_MS, ACTIVE_PAGE_CACHE_MAX_HTML_CHARS);
1254
+ if (extraction.status !== "READ" || typeof extraction.html !== "string" || extraction.html.length === 0) {
1255
+ void requestPageCacheStatusForTab(captureTab.id, `${reason}:capture-failed`);
1256
+ return false;
1257
+ }
1258
+ const verifiedTab = await currentActivePageCacheTab(captureTab.id, key);
1259
+ if (!verifiedTab) {
1260
+ return false;
1261
+ }
1262
+ const port = state.port;
1263
+ if (!port) {
1264
+ return false;
1265
+ }
1266
+ const id = nextActivePageCacheId();
1267
+ trackPageCacheStatusRequest(id, verifiedTab.id, activePageCacheUrl(verifiedTab));
1268
+ port.postMessage({
1269
+ id,
1270
+ action: "page-cache-capture",
1271
+ ok: true,
1272
+ data: {
1273
+ reason,
1274
+ capturedAt: Date.now(),
1275
+ tab: {
1276
+ tabId: verifiedTab.id,
1277
+ windowId: verifiedTab.windowId,
1278
+ index: verifiedTab.index,
1279
+ url: activePageCacheUrl(verifiedTab),
1280
+ title: verifiedTab.title,
1281
+ groupId: verifiedTab.groupId,
1282
+ favIconUrl: verifiedTab.favIconUrl || null,
1283
+ status: verifiedTab.status || null,
1284
+ pinned: verifiedTab.pinned || false,
1285
+ active: verifiedTab.active || false,
1286
+ incognito: verifiedTab.incognito || false,
1287
+ discarded: verifiedTab.discarded || false,
1288
+ lastAccessedAt: verifiedTab.lastAccessed || null
1289
+ },
1290
+ extraction
1291
+ }
1292
+ });
1293
+ activePageCache.lastCapturedKey = key;
1294
+ return true;
1295
+ } catch (error) {
1296
+ log("active page cache capture failed", { tabId: tab.id, reason, error });
1297
+ void requestPageCacheStatusForTab(tab.id, `${reason}:capture-error`);
1298
+ return false;
1299
+ } finally {
1300
+ activePageCache.inFlightKeys.delete(key);
1301
+ }
1302
+ }
1303
+ function normalizeEventPayload(kind, payload) {
1304
+ const event = {
1305
+ kind,
1306
+ occurredAt: Date.now()
1307
+ };
1308
+ for (const [key, value] of Object.entries(payload)) {
1309
+ if (value === void 0) {
1310
+ continue;
1311
+ }
1312
+ event[key] = value;
1313
+ }
1314
+ if (event.incognito !== true && inferIncognitoEvent(payload)) {
1315
+ event.incognito = true;
1316
+ }
1317
+ return event;
1318
+ }
1319
+ function inferIncognitoEvent(payload) {
1320
+ const tabId = typeof payload.tabId === "number" ? payload.tabId : null;
1321
+ if (tabId !== null && browserState.incognitoTabIds.has(tabId)) {
1322
+ return true;
1323
+ }
1324
+ const groupId = typeof payload.groupId === "number" ? payload.groupId : null;
1325
+ if (groupId !== null && browserState.incognitoGroupIds.has(groupId)) {
1326
+ return true;
1327
+ }
1328
+ const windowId = typeof payload.windowId === "number" ? payload.windowId : null;
1329
+ return windowId !== null && browserState.incognitoWindowIds.has(windowId);
1330
+ }
1331
+ function updateIncognitoState(snapshot) {
1332
+ browserState.incognitoWindowIds.clear();
1333
+ browserState.incognitoTabIds.clear();
1334
+ browserState.incognitoGroupIds.clear();
1335
+ for (const window2 of snapshot.windows || []) {
1336
+ if (window2.incognito !== true || typeof window2.windowId !== "number") {
1337
+ continue;
1338
+ }
1339
+ browserState.incognitoWindowIds.add(window2.windowId);
1340
+ for (const tab of window2.tabs || []) {
1341
+ if (typeof tab.tabId === "number") {
1342
+ browserState.incognitoTabIds.add(tab.tabId);
1343
+ }
1344
+ }
1345
+ for (const group of window2.groups || []) {
1346
+ if (typeof group.groupId === "number") {
1347
+ browserState.incognitoGroupIds.add(group.groupId);
1348
+ }
1349
+ }
1350
+ }
1351
+ }
1352
+ async function postBrowserStateSync(reason) {
1353
+ if (!state.port) {
1354
+ return;
1355
+ }
1356
+ const eventCount = browserState.pendingEvents.length;
1357
+ const events = browserState.pendingEvents.slice(0, eventCount);
1358
+ try {
1359
+ const snapshot = await getTabSnapshot();
1360
+ updateIncognitoState(snapshot);
1361
+ state.port.postMessage({
1362
+ id: nextBrowserStateId(),
1363
+ action: "browser-state-sync",
1364
+ ok: true,
1365
+ data: {
1366
+ reason,
1367
+ recordedAt: Date.now(),
1368
+ events,
1369
+ snapshot
1370
+ }
1371
+ });
1372
+ browserState.pendingEvents.splice(0, eventCount);
1373
+ } catch (error) {
1374
+ log("Browser state sync failed", error);
1375
+ }
1376
+ }
1377
+ function queueBrowserStateSync(reason) {
1378
+ if (!state.port) {
1379
+ connectNative();
1380
+ return;
1381
+ }
1382
+ if (browserState.syncTimer) {
1383
+ clearTimeout(browserState.syncTimer);
1384
+ }
1385
+ const delayMs = reason === "startup" ? 0 : BROWSER_STATE_SYNC_DEBOUNCE_MS;
1386
+ browserState.syncTimer = setTimeout(() => {
1387
+ browserState.syncTimer = null;
1388
+ void postBrowserStateSync(reason);
1389
+ }, delayMs);
1390
+ }
1391
+ function enqueueBrowserStateEvent(kind, payload, reason = "event") {
1392
+ browserState.pendingEvents.push(normalizeEventPayload(kind, payload));
1393
+ queueBrowserStateSync(reason);
1394
+ }
1395
+ function registerBrowserStateListeners() {
1396
+ chrome.tabs?.onCreated?.addListener((tab) => {
1397
+ enqueueBrowserStateEvent("tabs.onCreated", {
1398
+ tabId: tab.id,
1399
+ windowId: tab.windowId,
1400
+ groupId: tab.groupId,
1401
+ incognito: tab.incognito,
1402
+ url: tab.url || tab.pendingUrl,
1403
+ title: tab.title,
1404
+ index: tab.index
1405
+ });
1406
+ });
1407
+ chrome.tabs?.onUpdated?.addListener((tabId, changeInfo, tab) => {
1408
+ const interesting = ["url", "title", "status", "pinned", "audible", "discarded", "favIconUrl"];
1409
+ if (!interesting.some((key) => key in changeInfo)) {
1410
+ return;
570
1411
  }
571
- }).catch((error) => log("Failed to query active tab", error));
1412
+ enqueueBrowserStateEvent("tabs.onUpdated", {
1413
+ tabId,
1414
+ windowId: tab.windowId,
1415
+ groupId: tab.groupId,
1416
+ incognito: tab.incognito,
1417
+ changeInfo
1418
+ });
1419
+ if ("url" in changeInfo || "status" in changeInfo || "discarded" in changeInfo) {
1420
+ void clearCacheAvailableIndicator(tabId);
1421
+ }
1422
+ if (tab.active && ("url" in changeInfo || "status" in changeInfo || "discarded" in changeInfo)) {
1423
+ scheduleActivePageCacheCapture(tab, "tabs.onUpdated");
1424
+ }
1425
+ });
1426
+ chrome.tabs?.onMoved?.addListener((tabId, moveInfo) => {
1427
+ enqueueBrowserStateEvent("tabs.onMoved", {
1428
+ tabId,
1429
+ windowId: moveInfo.windowId,
1430
+ fromIndex: moveInfo.fromIndex,
1431
+ toIndex: moveInfo.toIndex
1432
+ });
1433
+ });
1434
+ chrome.tabs?.onAttached?.addListener((tabId, attachInfo) => {
1435
+ enqueueBrowserStateEvent("tabs.onAttached", {
1436
+ tabId,
1437
+ windowId: attachInfo.newWindowId,
1438
+ newPosition: attachInfo.newPosition
1439
+ });
1440
+ });
1441
+ chrome.tabs?.onDetached?.addListener((tabId, detachInfo) => {
1442
+ enqueueBrowserStateEvent("tabs.onDetached", {
1443
+ tabId,
1444
+ windowId: detachInfo.oldWindowId,
1445
+ oldPosition: detachInfo.oldPosition
1446
+ });
1447
+ });
1448
+ chrome.tabs?.onRemoved?.addListener((tabId, removeInfo) => {
1449
+ enqueueBrowserStateEvent("tabs.onRemoved", {
1450
+ tabId,
1451
+ windowId: removeInfo.windowId,
1452
+ isWindowClosing: removeInfo.isWindowClosing
1453
+ });
1454
+ activePageCache.statusRequests.forEach((pending, requestId) => {
1455
+ if (pending.tabId === tabId) {
1456
+ activePageCache.statusRequests.delete(requestId);
1457
+ }
1458
+ });
1459
+ void clearCacheAvailableIndicator(tabId);
1460
+ });
1461
+ chrome.tabs?.onActivated?.addListener((activeInfo) => {
1462
+ enqueueBrowserStateEvent("tabs.onActivated", {
1463
+ tabId: activeInfo.tabId,
1464
+ windowId: activeInfo.windowId
1465
+ });
1466
+ void scheduleActivePageCacheCaptureForTab(activeInfo.tabId, "tabs.onActivated");
1467
+ });
1468
+ chrome.tabGroups?.onCreated?.addListener((group) => {
1469
+ enqueueBrowserStateEvent("tabGroups.onCreated", {
1470
+ groupId: group.id,
1471
+ windowId: group.windowId,
1472
+ title: group.title,
1473
+ color: group.color,
1474
+ collapsed: group.collapsed
1475
+ });
1476
+ });
1477
+ chrome.tabGroups?.onUpdated?.addListener((group) => {
1478
+ enqueueBrowserStateEvent("tabGroups.onUpdated", {
1479
+ groupId: group.id,
1480
+ windowId: group.windowId,
1481
+ title: group.title,
1482
+ color: group.color,
1483
+ collapsed: group.collapsed
1484
+ });
1485
+ });
1486
+ chrome.tabGroups?.onMoved?.addListener((group) => {
1487
+ enqueueBrowserStateEvent("tabGroups.onMoved", {
1488
+ groupId: group.id,
1489
+ windowId: group.windowId,
1490
+ title: group.title,
1491
+ color: group.color,
1492
+ collapsed: group.collapsed
1493
+ });
1494
+ });
1495
+ chrome.tabGroups?.onRemoved?.addListener((group) => {
1496
+ enqueueBrowserStateEvent("tabGroups.onRemoved", {
1497
+ groupId: group.id,
1498
+ windowId: group.windowId,
1499
+ title: group.title,
1500
+ color: group.color,
1501
+ collapsed: group.collapsed
1502
+ });
1503
+ });
1504
+ chrome.windows?.onCreated?.addListener((window2) => {
1505
+ enqueueBrowserStateEvent("windows.onCreated", {
1506
+ windowId: window2.id,
1507
+ incognito: window2.incognito,
1508
+ focused: window2.focused,
1509
+ state: window2.state
1510
+ });
1511
+ });
1512
+ chrome.windows?.onRemoved?.addListener((windowId) => {
1513
+ enqueueBrowserStateEvent("windows.onRemoved", { windowId });
1514
+ });
1515
+ chrome.windows?.onFocusChanged?.addListener((windowId) => {
1516
+ enqueueBrowserStateEvent("windows.onFocusChanged", { windowId });
1517
+ void scheduleActivePageCacheCaptureForWindow(windowId, "windows.onFocusChanged");
1518
+ });
1519
+ }
1520
+ connectNative();
1521
+ registerBrowserStateListeners();
1522
+ ensureKeepaliveAlarm();
1523
+ chrome.runtime.onInstalled.addListener(() => {
1524
+ connectNative();
1525
+ ensureKeepaliveAlarm();
1526
+ });
1527
+ chrome.runtime.onStartup.addListener(() => {
1528
+ connectNative();
1529
+ ensureKeepaliveAlarm();
1530
+ });
1531
+ chrome.alarms.onAlarm.addListener((alarm) => {
1532
+ if (alarm.name === KEEPALIVE_ALARM) {
1533
+ connectNative();
1534
+ } else if (alarm.name === RECONNECT_ALARM) {
1535
+ clearReconnectTimer();
1536
+ connectNative();
1537
+ }
572
1538
  });
573
- async function handleNativeMessage(message) {
1539
+ async function handleNativeMessage(requestPort, message) {
574
1540
  if (!message || typeof message !== "object") {
575
1541
  return;
576
1542
  }
@@ -580,9 +1546,9 @@
580
1546
  }
581
1547
  try {
582
1548
  const data = await handleAction(action, params || {}, id);
583
- sendResponse(id, true, data);
1549
+ sendResponse(requestPort, id, true, data);
584
1550
  } catch (error) {
585
- sendResponse(id, false, error);
1551
+ sendResponse(requestPort, id, false, error);
586
1552
  }
587
1553
  }
588
1554
  async function handleAction(action, params, requestId) {
@@ -602,6 +1568,9 @@
602
1568
  component: "extension"
603
1569
  };
604
1570
  case "reload":
1571
+ chrome.alarms.create(RECONNECT_ALARM, {
1572
+ delayInMinutes: RECONNECT_ALARM_MIN_DELAY_MS / 6e4
1573
+ });
605
1574
  setTimeout(() => chrome.runtime.reload(), 100);
606
1575
  return { reloading: true };
607
1576
  // --- Primitives: thin Chrome API wrappers (p: prefix) ---
@@ -655,8 +1624,7 @@
655
1624
  return await content.extractPageMeta(targetTabId, timeoutMs, funcArgs[0] || DESCRIPTION_MAX_LENGTH);
656
1625
  case "extractSelectorSignal": {
657
1626
  const specs = funcArgs[0];
658
- if (!Array.isArray(specs))
659
- throw new Error("extractSelectorSignal requires specs array as first arg");
1627
+ if (!Array.isArray(specs)) throw new Error("extractSelectorSignal requires specs array as first arg");
660
1628
  const selectorMaxLen = funcArgs[1] || 500;
661
1629
  return await content.extractSelectorSignal(targetTabId, specs, timeoutMs, selectorMaxLen);
662
1630
  }
@@ -671,14 +1639,31 @@
671
1639
  const quality = typeof opts.quality === "number" ? Math.max(1, Math.min(100, opts.quality)) : 80;
672
1640
  const tileMaxDim = typeof opts.tileMaxDim === "number" ? Math.max(50, opts.tileMaxDim) : 50;
673
1641
  const maxBytes = typeof opts.maxBytes === "number" ? Math.max(5e4, opts.maxBytes) : 5e4;
674
- return await screenshot.captureTabTiles(params.tab, { mode, format: fmt, quality, tileMaxDim, maxBytes }, { delay, executeWithTimeout });
1642
+ return await screenshot.captureTabTiles(
1643
+ params.tab,
1644
+ { mode, format: fmt, quality, tileMaxDim, maxBytes },
1645
+ { delay: delay2, executeWithTimeout: executeWithTimeout2 }
1646
+ );
1647
+ }
1648
+ case "p:page-html": {
1649
+ const targetTabId = requireFiniteId(params.tabId, "tabId");
1650
+ const expectedUrl = typeof params.expectedUrl === "string" ? params.expectedUrl : "";
1651
+ const maxHtmlChars = typeof params.maxHtmlChars === "number" ? Math.max(1, Math.min(params.maxHtmlChars, MAX_PAGE_HTML_CHARS)) : MAX_PAGE_HTML_CHARS;
1652
+ const timeoutMs = typeof params.timeoutMs === "number" ? Math.max(1, params.timeoutMs) : 15e3;
1653
+ if (expectedUrl && !await tabUrlMatches(targetTabId, expectedUrl)) {
1654
+ return urlMismatchPageHtmlResponse(expectedUrl);
1655
+ }
1656
+ const result = await content.extractPageHtml(targetTabId, timeoutMs, maxHtmlChars);
1657
+ if (expectedUrl && !await tabUrlMatches(targetTabId, expectedUrl)) {
1658
+ return urlMismatchPageHtmlResponse(expectedUrl);
1659
+ }
1660
+ return result;
675
1661
  }
676
1662
  default:
677
1663
  throw new Error(`Unknown action: ${action}`);
678
1664
  }
679
1665
  }
680
1666
  async function getTabSnapshot() {
681
- await ensureLastFocusedLoaded();
682
1667
  const windows = await chrome.windows.getAll({ populate: true, windowTypes: ["normal"] });
683
1668
  const groups = await chrome.tabGroups.query({});
684
1669
  const groupById = new Map(groups.map((group) => [group.id, group]));
@@ -689,7 +1674,8 @@
689
1674
  tabId: tab.id,
690
1675
  windowId: win.id,
691
1676
  index: tab.index,
692
- url: tab.url,
1677
+ incognito: win.incognito || false,
1678
+ url: tab.url || tab.pendingUrl,
693
1679
  title: tab.title,
694
1680
  active: tab.active,
695
1681
  pinned: tab.pinned,
@@ -697,7 +1683,11 @@
697
1683
  groupTitle: group ? group.title : null,
698
1684
  groupColor: group ? group.color : null,
699
1685
  groupCollapsed: group ? group.collapsed : null,
700
- lastFocusedAt: state.lastFocused[String(tab.id)] || null
1686
+ lastAccessedAt: tab.lastAccessed || null,
1687
+ favIconUrl: tab.favIconUrl || null,
1688
+ status: tab.status || null,
1689
+ discarded: tab.discarded || false,
1690
+ audible: tab.audible || false
701
1691
  };
702
1692
  });
703
1693
  const windowGroups = groups.filter((group) => group.windowId === win.id).map((group) => ({
@@ -709,6 +1699,7 @@
709
1699
  return {
710
1700
  windowId: win.id,
711
1701
  focused: win.focused,
1702
+ incognito: win.incognito || false,
712
1703
  state: win.state,
713
1704
  tabs,
714
1705
  groups: windowGroups