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.
- package/README.md +97 -76
- package/dist/extension/background.js +1465 -474
- package/dist/extension/lib/content.js +293 -30
- package/dist/extension/lib/screenshot.js +9 -1
- package/dist/extension/manifest.json +4 -1
- package/package.json +3 -2
|
@@ -1,455 +1,766 @@
|
|
|
1
1
|
(() => {
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
2
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var
|
|
4
|
-
|
|
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
|
-
//
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
//
|
|
285
|
-
var
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
515
|
+
resolved = true;
|
|
516
|
+
done();
|
|
328
517
|
};
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
701
|
+
}
|
|
702
|
+
if (attr === "styles") {
|
|
703
|
+
if (!styleProps.length) {
|
|
704
|
+
return {};
|
|
431
705
|
}
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
//
|
|
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
|
|
474
|
-
var
|
|
475
|
-
var
|
|
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
|
-
|
|
486
|
-
|
|
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
|
|
492
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
501
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
539
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|