tabctl 0.1.4 → 0.2.1

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.
Files changed (48) hide show
  1. package/{cli → dist/cli}/lib/commands/index.js +4 -2
  2. package/dist/cli/lib/commands/meta.js +226 -0
  3. package/dist/cli/lib/commands/params-groups.js +40 -0
  4. package/dist/cli/lib/commands/params-move.js +44 -0
  5. package/{cli → dist/cli}/lib/commands/params.js +61 -125
  6. package/{cli/lib/commands/meta.js → dist/cli/lib/commands/setup.js} +26 -222
  7. package/{cli/lib/options.js → dist/cli/lib/options-commands.js} +3 -155
  8. package/dist/cli/lib/options-groups.js +41 -0
  9. package/dist/cli/lib/options.js +125 -0
  10. package/{cli → dist/cli}/lib/output.js +5 -4
  11. package/dist/cli/lib/policy-filter.js +202 -0
  12. package/dist/cli/lib/response.js +235 -0
  13. package/{cli → dist/cli}/lib/scope.js +3 -31
  14. package/dist/cli/tabctl.js +463 -0
  15. package/dist/extension/background.js +3398 -0
  16. package/dist/extension/lib/archive.js +444 -0
  17. package/dist/extension/lib/content.js +320 -0
  18. package/dist/extension/lib/deps.js +4 -0
  19. package/dist/extension/lib/groups.js +443 -0
  20. package/dist/extension/lib/inspect.js +316 -0
  21. package/dist/extension/lib/move.js +342 -0
  22. package/dist/extension/lib/screenshot.js +367 -0
  23. package/dist/extension/lib/tabs.js +395 -0
  24. package/dist/extension/lib/undo-handlers.js +439 -0
  25. package/{extension → dist/extension}/manifest.json +2 -2
  26. package/dist/host/host.js +124 -0
  27. package/{host/host.js → dist/host/lib/handlers.js} +84 -187
  28. package/{shared → dist/shared}/version.js +2 -2
  29. package/package.json +12 -10
  30. package/cli/tabctl.js +0 -841
  31. package/extension/background.js +0 -3372
  32. package/extension/manifest.template.json +0 -22
  33. /package/{cli → dist/cli}/lib/args.js +0 -0
  34. /package/{cli → dist/cli}/lib/client.js +0 -0
  35. /package/{cli → dist/cli}/lib/commands/list.js +0 -0
  36. /package/{cli → dist/cli}/lib/commands/profile.js +0 -0
  37. /package/{cli → dist/cli}/lib/constants.js +0 -0
  38. /package/{cli → dist/cli}/lib/help.js +0 -0
  39. /package/{cli → dist/cli}/lib/pagination.js +0 -0
  40. /package/{cli → dist/cli}/lib/policy.js +0 -0
  41. /package/{cli → dist/cli}/lib/report.js +0 -0
  42. /package/{cli → dist/cli}/lib/snapshot.js +0 -0
  43. /package/{cli → dist/cli}/lib/types.js +0 -0
  44. /package/{host → dist/host}/host.sh +0 -0
  45. /package/{host → dist/host}/lib/undo.js +0 -0
  46. /package/{shared → dist/shared}/config.js +0 -0
  47. /package/{shared → dist/shared}/extension-sync.js +0 -0
  48. /package/{shared → dist/shared}/profiles.js +0 -0
@@ -0,0 +1,3398 @@
1
+ (() => {
2
+ 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
+ };
6
+
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.screenshotTabs = screenshotTabs;
25
+ exports.SCREENSHOT_TILE_MAX_DIM = 2e3;
26
+ exports.SCREENSHOT_MAX_BYTES = 2e6;
27
+ exports.SCREENSHOT_QUALITY = 80;
28
+ exports.SCREENSHOT_SCROLL_DELAY_MS = 150;
29
+ exports.SCREENSHOT_CAPTURE_DELAY_MS = 350;
30
+ exports.SCREENSHOT_PROCESS_TIMEOUT_MS = 8e3;
31
+ function estimateDataUrlBytes(dataUrl) {
32
+ const commaIndex = dataUrl.indexOf(",");
33
+ if (commaIndex < 0) {
34
+ return dataUrl.length;
35
+ }
36
+ const base64 = dataUrl.slice(commaIndex + 1);
37
+ const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
38
+ return Math.max(0, Math.floor(base64.length * 3 / 4) - padding);
39
+ }
40
+ function arrayBufferToBase64(buffer) {
41
+ const bytes = new Uint8Array(buffer);
42
+ let binary = "";
43
+ for (let i = 0; i < bytes.length; i += 1) {
44
+ binary += String.fromCharCode(bytes[i]);
45
+ }
46
+ return btoa(binary);
47
+ }
48
+ async function resizeDataUrl(dataUrl, format, quality, scale) {
49
+ if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
50
+ return null;
51
+ }
52
+ const response = await fetch(dataUrl);
53
+ const blob = await response.blob();
54
+ const bitmap = await createImageBitmap(blob);
55
+ const width = Math.max(1, Math.floor(bitmap.width * scale));
56
+ const height = Math.max(1, Math.floor(bitmap.height * scale));
57
+ const canvas = new OffscreenCanvas(width, height);
58
+ const ctx = canvas.getContext("2d");
59
+ if (!ctx) {
60
+ return null;
61
+ }
62
+ ctx.drawImage(bitmap, 0, 0, width, height);
63
+ const type = format === "jpeg" ? "image/jpeg" : "image/png";
64
+ const blobOut = await canvas.convertToBlob({ type, quality: format === "jpeg" ? quality / 100 : void 0 });
65
+ const buffer = await blobOut.arrayBuffer();
66
+ const base64 = arrayBufferToBase64(buffer);
67
+ return {
68
+ dataUrl: `data:${type};base64,${base64}`,
69
+ bytes: buffer.byteLength
70
+ };
71
+ }
72
+ async function resizeDataUrlToMaxDim(dataUrl, format, quality, maxDim) {
73
+ if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
74
+ return null;
75
+ }
76
+ const response = await fetch(dataUrl);
77
+ const blob = await response.blob();
78
+ const bitmap = await createImageBitmap(blob);
79
+ const maxSize = Math.max(bitmap.width, bitmap.height);
80
+ if (!Number.isFinite(maxSize) || maxSize <= maxDim) {
81
+ return null;
82
+ }
83
+ const scale = maxDim / maxSize;
84
+ return resizeDataUrl(dataUrl, format, quality, scale);
85
+ }
86
+ async function cropDataUrl(dataUrl, format, quality, width, height, devicePixelRatio) {
87
+ if (!globalThis.OffscreenCanvas || !globalThis.createImageBitmap) {
88
+ return dataUrl;
89
+ }
90
+ const response = await fetch(dataUrl);
91
+ const blob = await response.blob();
92
+ const bitmap = await createImageBitmap(blob);
93
+ const targetWidth = Math.min(bitmap.width, Math.max(1, Math.round(width * devicePixelRatio)));
94
+ const targetHeight = Math.min(bitmap.height, Math.max(1, Math.round(height * devicePixelRatio)));
95
+ if (targetWidth === bitmap.width && targetHeight === bitmap.height) {
96
+ return dataUrl;
97
+ }
98
+ const canvas = new OffscreenCanvas(targetWidth, targetHeight);
99
+ const ctx = canvas.getContext("2d");
100
+ if (!ctx) {
101
+ return dataUrl;
102
+ }
103
+ ctx.drawImage(bitmap, 0, 0, targetWidth, targetHeight, 0, 0, targetWidth, targetHeight);
104
+ const type = format === "jpeg" ? "image/jpeg" : "image/png";
105
+ const blobOut = await canvas.convertToBlob({ type, quality: format === "jpeg" ? quality / 100 : void 0 });
106
+ const buffer = await blobOut.arrayBuffer();
107
+ const base64 = arrayBufferToBase64(buffer);
108
+ return `data:${type};base64,${base64}`;
109
+ }
110
+ async function ensureMaxBytes(dataUrl, format, quality, maxBytes) {
111
+ let currentUrl = dataUrl;
112
+ let currentBytes = estimateDataUrlBytes(currentUrl);
113
+ if (currentBytes <= maxBytes) {
114
+ return { dataUrl: currentUrl, bytes: currentBytes, scaled: false, oversized: false };
115
+ }
116
+ let scaled = false;
117
+ let attempts = 0;
118
+ while (currentBytes > maxBytes && attempts < 3) {
119
+ const scale = Math.max(0.2, Math.sqrt(maxBytes / currentBytes) * 0.95);
120
+ if (scale >= 0.99) {
121
+ break;
122
+ }
123
+ const resized = await resizeDataUrl(currentUrl, format, quality, scale);
124
+ if (!resized) {
125
+ break;
126
+ }
127
+ currentUrl = resized.dataUrl;
128
+ currentBytes = resized.bytes;
129
+ scaled = true;
130
+ attempts += 1;
131
+ }
132
+ return {
133
+ dataUrl: currentUrl,
134
+ bytes: currentBytes,
135
+ scaled,
136
+ oversized: currentBytes > maxBytes
137
+ };
138
+ }
139
+ async function constrainDataUrl(dataUrl, format, quality, maxDim, maxBytes) {
140
+ let currentUrl = dataUrl;
141
+ let currentBytes = estimateDataUrlBytes(currentUrl);
142
+ let scaled = false;
143
+ if (Number.isFinite(maxDim) && maxDim > 0) {
144
+ const resized = await resizeDataUrlToMaxDim(currentUrl, format, quality, maxDim);
145
+ if (resized) {
146
+ currentUrl = resized.dataUrl;
147
+ currentBytes = resized.bytes;
148
+ scaled = true;
149
+ }
150
+ }
151
+ if (currentBytes > maxBytes) {
152
+ const resized = await ensureMaxBytes(currentUrl, format, quality, maxBytes);
153
+ return {
154
+ dataUrl: resized.dataUrl,
155
+ bytes: resized.bytes,
156
+ scaled: scaled || resized.scaled,
157
+ oversized: resized.oversized
158
+ };
159
+ }
160
+ return { dataUrl: currentUrl, bytes: currentBytes, scaled, oversized: false };
161
+ }
162
+ async function captureVisible(windowId, format, quality) {
163
+ const options = { format };
164
+ if (format === "jpeg") {
165
+ options.quality = quality;
166
+ }
167
+ return chrome.tabs.captureVisibleTab(windowId, options);
168
+ }
169
+ async function getPageMetrics(tabId, timeoutMs, deps2) {
170
+ const result = await deps2.executeWithTimeout(tabId, timeoutMs, () => {
171
+ const doc = document.documentElement;
172
+ const body = document.body;
173
+ const pageWidth = Math.max(doc.scrollWidth, doc.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);
174
+ const pageHeight = Math.max(doc.scrollHeight, doc.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);
175
+ return {
176
+ pageWidth,
177
+ pageHeight,
178
+ viewportWidth: window.innerWidth,
179
+ viewportHeight: window.innerHeight,
180
+ devicePixelRatio: window.devicePixelRatio || 1,
181
+ scrollX: window.scrollX || window.pageXOffset || 0,
182
+ scrollY: window.scrollY || window.pageYOffset || 0
183
+ };
184
+ });
185
+ if (!result || typeof result !== "object") {
186
+ return null;
187
+ }
188
+ return result;
189
+ }
190
+ async function scrollToPosition(tabId, timeoutMs, x, y, deps2) {
191
+ const result = await deps2.executeWithTimeout(tabId, timeoutMs, (scrollX, scrollY) => {
192
+ window.scrollTo(scrollX, scrollY);
193
+ return {
194
+ scrollX: window.scrollX || window.pageXOffset || 0,
195
+ scrollY: window.scrollY || window.pageYOffset || 0
196
+ };
197
+ }, [x, y]);
198
+ if (!result || typeof result !== "object") {
199
+ return null;
200
+ }
201
+ return result;
202
+ }
203
+ async function captureTabTiles(tab, options, deps2) {
204
+ const tabId = tab.tabId;
205
+ const windowId = tab.windowId;
206
+ if (!Number.isFinite(tabId) || !Number.isFinite(windowId)) {
207
+ throw new Error("Missing tab/window id");
208
+ }
209
+ const metrics = await getPageMetrics(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, deps2);
210
+ if (!metrics) {
211
+ throw new Error("Failed to read page metrics");
212
+ }
213
+ const pageWidth = Number(metrics.pageWidth);
214
+ const pageHeight = Number(metrics.pageHeight);
215
+ const viewportWidth = Number(metrics.viewportWidth);
216
+ const viewportHeight = Number(metrics.viewportHeight);
217
+ const devicePixelRatio = Number(metrics.devicePixelRatio) || 1;
218
+ const startScrollX = Number(metrics.scrollX) || 0;
219
+ const startScrollY = Number(metrics.scrollY) || 0;
220
+ if (!Number.isFinite(viewportWidth) || !Number.isFinite(viewportHeight)) {
221
+ throw new Error("Viewport size unavailable");
222
+ }
223
+ const tiles = [];
224
+ let tileIndex = 0;
225
+ const captureTile = async (x, y, width, height, total) => {
226
+ if (tileIndex > 0) {
227
+ await deps2.delay(exports.SCREENSHOT_CAPTURE_DELAY_MS);
228
+ }
229
+ const rawDataUrl = await captureVisible(windowId, options.format, options.quality);
230
+ if (!rawDataUrl) {
231
+ throw new Error("Capture failed");
232
+ }
233
+ const croppedUrl = await cropDataUrl(rawDataUrl, options.format, options.quality, width, height, devicePixelRatio);
234
+ const sizeResult = await constrainDataUrl(croppedUrl, options.format, options.quality, options.tileMaxDim, options.maxBytes);
235
+ tiles.push({
236
+ index: tileIndex,
237
+ total,
238
+ x,
239
+ y,
240
+ width,
241
+ height,
242
+ scale: devicePixelRatio,
243
+ format: options.format,
244
+ bytes: sizeResult.bytes,
245
+ scaled: sizeResult.scaled,
246
+ oversized: sizeResult.oversized,
247
+ dataUrl: sizeResult.dataUrl
248
+ });
249
+ tileIndex += 1;
250
+ };
251
+ if (options.mode === "viewport") {
252
+ await captureTile(startScrollX, startScrollY, viewportWidth, viewportHeight, 1);
253
+ return tiles;
254
+ }
255
+ const stepX = viewportWidth;
256
+ const stepY = Math.min(viewportHeight, options.tileMaxDim);
257
+ const maxX = viewportWidth;
258
+ const maxY = Math.max(viewportHeight, pageHeight);
259
+ const tileCount = Math.ceil(maxX / stepX) * Math.ceil(maxY / stepY);
260
+ for (let y = 0; y < maxY; y += stepY) {
261
+ for (let x = 0; x < maxX; x += stepX) {
262
+ await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, x, y, deps2);
263
+ await deps2.delay(exports.SCREENSHOT_SCROLL_DELAY_MS);
264
+ const width = Math.min(stepX, maxX - x);
265
+ const height = Math.min(stepY, maxY - y);
266
+ try {
267
+ await captureTile(x, y, width, height, tileCount);
268
+ } catch (err) {
269
+ const message = err instanceof Error ? err.message : "capture_failed";
270
+ if (message.includes("MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND")) {
271
+ await deps2.delay(1e3);
272
+ await captureTile(x, y, width, height, tileCount);
273
+ } else {
274
+ throw err;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, startScrollX, startScrollY, deps2);
280
+ return tiles;
281
+ }
282
+ async function screenshotTabs(params, requestId, deps2) {
283
+ const snapshot = await deps2.getTabSnapshot();
284
+ const selection = deps2.selectTabsByScope(snapshot, params);
285
+ if (selection.error) {
286
+ throw selection.error;
287
+ }
288
+ const mode = params.mode === "full" ? "full" : "viewport";
289
+ const format = params.format === "jpeg" ? "jpeg" : "png";
290
+ const qualityRaw = Number(params.quality);
291
+ const quality = Number.isFinite(qualityRaw) ? Math.min(100, Math.max(0, Math.floor(qualityRaw))) : exports.SCREENSHOT_QUALITY;
292
+ const tileMaxDimRaw = Number(params.tileMaxDim);
293
+ const tileMaxDim = Number.isFinite(tileMaxDimRaw) && tileMaxDimRaw > 0 ? Math.floor(tileMaxDimRaw) : exports.SCREENSHOT_TILE_MAX_DIM;
294
+ const adjustedTileMaxDim = tileMaxDim < 50 ? 50 : tileMaxDim;
295
+ const maxBytesRaw = Number(params.maxBytes);
296
+ const maxBytes = Number.isFinite(maxBytesRaw) && maxBytesRaw > 0 ? Math.floor(maxBytesRaw) : exports.SCREENSHOT_MAX_BYTES;
297
+ const adjustedMaxBytes = maxBytes < 5e4 ? 5e4 : maxBytes;
298
+ const progressEnabled = params.progress === true;
299
+ const tabs2 = selection.tabs;
300
+ const entries = [];
301
+ let totalTiles = 0;
302
+ const startedAt = Date.now();
303
+ for (let index = 0; index < tabs2.length; index += 1) {
304
+ const tab = tabs2[index];
305
+ const tabId = tab.tabId;
306
+ const url = tab.url;
307
+ if (!deps2.isScriptableUrl(url)) {
308
+ entries.push({
309
+ tabId,
310
+ windowId: tab.windowId,
311
+ groupId: tab.groupId,
312
+ url: tab.url,
313
+ title: tab.title,
314
+ error: { message: "unsupported_url" },
315
+ tiles: []
316
+ });
317
+ if (progressEnabled) {
318
+ deps2.sendProgress(requestId, { phase: "screenshot", processed: index + 1, total: tabs2.length, tabId });
319
+ }
320
+ continue;
321
+ }
322
+ let tiles = [];
323
+ let error = null;
324
+ try {
325
+ const windowId = tab.windowId;
326
+ const activeTabs = await chrome.tabs.query({ windowId, active: true });
327
+ const activeTabId = activeTabs[0]?.id ?? null;
328
+ if (activeTabId && activeTabId !== tabId) {
329
+ await chrome.tabs.update(tabId, { active: true });
330
+ await deps2.delay(exports.SCREENSHOT_SCROLL_DELAY_MS);
331
+ }
332
+ try {
333
+ await deps2.waitForTabReady(tabId, params, exports.SCREENSHOT_PROCESS_TIMEOUT_MS);
334
+ tiles = await captureTabTiles(tab, { mode, format, quality, tileMaxDim: adjustedTileMaxDim, maxBytes: adjustedMaxBytes }, deps2);
335
+ } finally {
336
+ if (activeTabId && activeTabId !== tabId) {
337
+ await chrome.tabs.update(activeTabId, { active: true });
338
+ }
339
+ }
340
+ } catch (err) {
341
+ const message = err instanceof Error ? err.message : "capture_failed";
342
+ error = { message };
343
+ }
344
+ totalTiles += tiles.length;
345
+ entries.push({
346
+ tabId: tab.tabId,
347
+ windowId: tab.windowId,
348
+ groupId: tab.groupId,
349
+ url: tab.url,
350
+ title: tab.title,
351
+ tiles,
352
+ ...error ? { error } : {}
353
+ });
354
+ if (progressEnabled) {
355
+ deps2.sendProgress(requestId, { phase: "screenshot", processed: index + 1, total: tabs2.length, tabId });
356
+ }
357
+ }
358
+ return {
359
+ generatedAt: Date.now(),
360
+ totals: { tabs: tabs2.length, tiles: totalTiles },
361
+ meta: {
362
+ durationMs: Date.now() - startedAt,
363
+ mode,
364
+ format,
365
+ quality: format === "jpeg" ? quality : null,
366
+ tileMaxDim: adjustedTileMaxDim,
367
+ maxBytes: adjustedMaxBytes
368
+ },
369
+ entries
370
+ };
371
+ }
372
+ }
373
+ });
374
+
375
+ // dist/extension/lib/content.js
376
+ var require_content = __commonJS({
377
+ "dist/extension/lib/content.js"(exports) {
378
+ "use strict";
379
+ Object.defineProperty(exports, "__esModule", { value: true });
380
+ exports.SETTLE_POLL_INTERVAL_MS = exports.SETTLE_STABILITY_MS = void 0;
381
+ exports.isScriptableUrl = isScriptableUrl2;
382
+ exports.delay = delay2;
383
+ exports.executeWithTimeout = executeWithTimeout2;
384
+ exports.isGitHubIssueOrPr = isGitHubIssueOrPr2;
385
+ exports.detectGitHubState = detectGitHubState2;
386
+ exports.extractPageMeta = extractPageMeta2;
387
+ exports.extractSelectorSignal = extractSelectorSignal2;
388
+ exports.waitForTabLoad = waitForTabLoad2;
389
+ exports.waitForDomReady = waitForDomReady2;
390
+ exports.waitForSettle = waitForSettle2;
391
+ exports.waitForTabReady = waitForTabReady2;
392
+ exports.SETTLE_STABILITY_MS = 500;
393
+ exports.SETTLE_POLL_INTERVAL_MS = 50;
394
+ function isScriptableUrl2(url) {
395
+ return typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"));
396
+ }
397
+ function delay2(ms) {
398
+ return new Promise((resolve) => setTimeout(resolve, ms));
399
+ }
400
+ async function executeWithTimeout2(tabId, timeoutMs, func, args = []) {
401
+ const execPromise = chrome.scripting.executeScript({
402
+ target: { tabId },
403
+ func,
404
+ args
405
+ });
406
+ const timeoutPromise = new Promise((resolve) => {
407
+ const handle = setTimeout(() => {
408
+ clearTimeout(handle);
409
+ resolve(null);
410
+ }, timeoutMs);
411
+ });
412
+ try {
413
+ const result = await Promise.race([execPromise, timeoutPromise]);
414
+ if (!result || !Array.isArray(result)) {
415
+ return null;
416
+ }
417
+ const [{ result: value }] = result;
418
+ return value ?? null;
419
+ } catch {
420
+ return null;
421
+ }
422
+ }
423
+ function isGitHubIssueOrPr2(url) {
424
+ if (!url) {
425
+ return false;
426
+ }
427
+ return /^https:\/\/github\.com\/[^/]+\/[^/]+\/(issues|pull)\/\d+/.test(url);
428
+ }
429
+ async function detectGitHubState2(tabId, timeoutMs) {
430
+ const result = await executeWithTimeout2(tabId, timeoutMs, () => {
431
+ const stateEl = document.querySelector(".gh-header-meta .State") || document.querySelector(".State") || document.querySelector(".js-issue-state");
432
+ if (!stateEl) {
433
+ return null;
434
+ }
435
+ const text = (stateEl.textContent || "").trim().toLowerCase();
436
+ if (text.includes("merged")) {
437
+ return "merged";
438
+ }
439
+ if (text.includes("closed")) {
440
+ return "closed";
441
+ }
442
+ if (text.includes("open")) {
443
+ return "open";
444
+ }
445
+ return null;
446
+ });
447
+ return typeof result === "string" ? result : null;
448
+ }
449
+ async function extractPageMeta2(tabId, timeoutMs, descriptionMaxLength) {
450
+ const result = await executeWithTimeout2(tabId, timeoutMs, () => {
451
+ const pickContent = (selector) => {
452
+ const el = document.querySelector(selector);
453
+ if (!el) {
454
+ return "";
455
+ }
456
+ const content2 = el.getAttribute("content") || el.textContent || "";
457
+ return content2.trim();
458
+ };
459
+ const description = pickContent("meta[name='description']") || pickContent("meta[property='og:description']") || pickContent("meta[name='twitter:description']");
460
+ const h1 = document.querySelector("h1");
461
+ const h1Text = h1 ? h1.textContent?.trim() : "";
462
+ return {
463
+ description: description.replace(/\s+/g, " ").trim(),
464
+ h1: (h1Text || "").replace(/\s+/g, " ").trim()
465
+ };
466
+ });
467
+ if (!result || typeof result !== "object") {
468
+ return null;
469
+ }
470
+ const meta = result;
471
+ return {
472
+ description: (meta.description || "").slice(0, descriptionMaxLength),
473
+ h1: (meta.h1 || "").slice(0, descriptionMaxLength)
474
+ };
475
+ }
476
+ async function extractSelectorSignal2(tabId, specs, timeoutMs, selectorValueMaxLength) {
477
+ if (!specs.length) {
478
+ return null;
479
+ }
480
+ const result = await executeWithTimeout2(tabId, timeoutMs, (rawSpecs, maxLen) => {
481
+ const values = {};
482
+ const missing = [];
483
+ const errors = {};
484
+ const hints = {};
485
+ for (const raw of rawSpecs) {
486
+ const selector = typeof raw.selector === "string" ? raw.selector : "";
487
+ if (!selector) {
488
+ continue;
489
+ }
490
+ const name = typeof raw.name === "string" && raw.name ? raw.name : selector;
491
+ const attr = typeof raw.attr === "string" ? raw.attr : "text";
492
+ const all = Boolean(raw.all);
493
+ const text = typeof raw.text === "string" ? raw.text.trim() : "";
494
+ const textMode = typeof raw.textMode === "string" ? raw.textMode.trim().toLowerCase() : "";
495
+ const normalizedTextMode = textMode === "includes" ? "contains" : textMode;
496
+ const textModes = /* @__PURE__ */ new Set(["", "contains", "exact", "starts-with"]);
497
+ if (!textModes.has(normalizedTextMode)) {
498
+ errors[name] = `Unsupported textMode: ${textMode || "unknown"}`;
499
+ hints[name] = "Use textMode: contains | exact | starts-with";
500
+ continue;
501
+ }
502
+ try {
503
+ const elements = Array.from(document.querySelectorAll(selector));
504
+ if (!elements.length) {
505
+ missing.push(name);
506
+ if (selector.includes(":contains(")) {
507
+ hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector.";
508
+ } else {
509
+ hints[name] = "No matches found; capture a screenshot for context or adjust the selector.";
510
+ }
511
+ continue;
512
+ }
513
+ const matchesText = (el) => {
514
+ if (!text) {
515
+ return true;
516
+ }
517
+ const content2 = (el.textContent || "").replace(/\s+/g, " ").trim();
518
+ if (normalizedTextMode === "exact") {
519
+ return content2 === text;
520
+ }
521
+ if (normalizedTextMode === "starts-with") {
522
+ return content2.startsWith(text);
523
+ }
524
+ return content2.includes(text);
525
+ };
526
+ const filtered = text ? elements.filter(matchesText) : elements;
527
+ if (!filtered.length) {
528
+ missing.push(name);
529
+ hints[name] = "Selector matched elements, but none matched the text filter; capture a screenshot for context or adjust text/textMode.";
530
+ continue;
531
+ }
532
+ const getValue = (el) => {
533
+ let value = "";
534
+ if (attr === "text") {
535
+ value = el.textContent || "";
536
+ } else if (attr === "href-url" || attr === "src-url") {
537
+ const rawValue = el.getAttribute(attr === "href-url" ? "href" : "src") || "";
538
+ if (!rawValue) {
539
+ value = "";
540
+ } else {
541
+ try {
542
+ const resolved = new URL(rawValue, document.baseURI);
543
+ if (resolved.protocol === "http:" || resolved.protocol === "https:") {
544
+ value = resolved.toString();
545
+ } else {
546
+ value = "";
547
+ }
548
+ } catch {
549
+ value = "";
550
+ }
551
+ }
552
+ } else {
553
+ value = el.getAttribute(attr) || "";
554
+ }
555
+ return value.replace(/\s+/g, " ").trim().slice(0, maxLen);
556
+ };
557
+ if (all) {
558
+ values[name] = filtered.map(getValue).filter((val) => val.length > 0);
559
+ } else {
560
+ values[name] = getValue(filtered[0]);
561
+ }
562
+ } catch (err) {
563
+ const message = err instanceof Error ? err.message : "selector_error";
564
+ errors[name] = message;
565
+ if (selector.includes(":contains(")) {
566
+ hints[name] = "CSS :contains() is not supported; use selector text filters or a different selector.";
567
+ } else {
568
+ hints[name] = "Selector failed to evaluate; capture a screenshot for context or adjust the selector.";
569
+ }
570
+ }
571
+ }
572
+ return { values, missing, errors, hints };
573
+ }, [specs, selectorValueMaxLength]);
574
+ if (!result || typeof result !== "object") {
575
+ return null;
576
+ }
577
+ return result;
578
+ }
579
+ function waitForTabLoad2(tabId, timeoutMs) {
580
+ return new Promise((resolve) => {
581
+ let settled = false;
582
+ const done = () => {
583
+ if (settled) {
584
+ return;
585
+ }
586
+ settled = true;
587
+ chrome.tabs.onUpdated.removeListener(onUpdated);
588
+ resolve();
589
+ };
590
+ const onUpdated = (updatedTabId, info) => {
591
+ if (updatedTabId === tabId && info.status === "complete") {
592
+ done();
593
+ }
594
+ };
595
+ chrome.tabs.onUpdated.addListener(onUpdated);
596
+ chrome.tabs.get(tabId).then((tab) => {
597
+ if (tab.status === "complete") {
598
+ done();
599
+ }
600
+ }).catch(() => {
601
+ done();
602
+ });
603
+ setTimeout(done, timeoutMs);
604
+ });
605
+ }
606
+ async function waitForDomReady2(tabId, timeoutMs) {
607
+ const result = await executeWithTimeout2(tabId, timeoutMs, () => {
608
+ if (document.readyState === "interactive" || document.readyState === "complete") {
609
+ return true;
610
+ }
611
+ return new Promise((resolve) => {
612
+ const onReady = () => {
613
+ document.removeEventListener("DOMContentLoaded", onReady);
614
+ resolve(true);
615
+ };
616
+ document.addEventListener("DOMContentLoaded", onReady, { once: true });
617
+ setTimeout(() => {
618
+ document.removeEventListener("DOMContentLoaded", onReady);
619
+ resolve(false);
620
+ }, Math.max(0, timeoutMs - 50));
621
+ });
622
+ });
623
+ if (result === null) {
624
+ await delay2(Math.min(200, Math.max(50, Math.floor(timeoutMs / 10))));
625
+ }
626
+ }
627
+ async function waitForSettle2(tabId, timeoutMs) {
628
+ const startTime = Date.now();
629
+ let lastUrl = "";
630
+ let lastTitle = "";
631
+ let stableStart = Date.now();
632
+ while (Date.now() - startTime < timeoutMs) {
633
+ const tab = await chrome.tabs.get(tabId).catch(() => null);
634
+ if (!tab)
635
+ return;
636
+ const currentUrl = tab.url || "";
637
+ const currentTitle = tab.title || "";
638
+ if (currentUrl !== lastUrl || currentTitle !== lastTitle) {
639
+ lastUrl = currentUrl;
640
+ lastTitle = currentTitle;
641
+ stableStart = Date.now();
642
+ } else if (isScriptableUrl2(currentUrl) && tab.status === "complete" && Date.now() - stableStart >= exports.SETTLE_STABILITY_MS) {
643
+ return;
644
+ }
645
+ await delay2(exports.SETTLE_POLL_INTERVAL_MS);
646
+ }
647
+ }
648
+ async function waitForTabReady2(tabId, params, fallbackTimeoutMs) {
649
+ const waitFor = typeof params.waitFor === "string" ? params.waitFor.trim().toLowerCase() : "";
650
+ if (!waitFor || waitFor === "none") {
651
+ return;
652
+ }
653
+ const timeoutRaw = Number(params.waitTimeoutMs);
654
+ const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.floor(timeoutRaw) : fallbackTimeoutMs;
655
+ if (waitFor === "settle") {
656
+ await waitForSettle2(tabId, timeoutMs);
657
+ return;
658
+ }
659
+ try {
660
+ const tab = await chrome.tabs.get(tabId);
661
+ if (!isScriptableUrl2(tab.url)) {
662
+ return;
663
+ }
664
+ } catch {
665
+ return;
666
+ }
667
+ if (waitFor === "load") {
668
+ await waitForTabLoad2(tabId, timeoutMs);
669
+ return;
670
+ }
671
+ if (waitFor === "dom") {
672
+ await waitForDomReady2(tabId, timeoutMs);
673
+ }
674
+ }
675
+ }
676
+ });
677
+
678
+ // dist/extension/lib/groups.js
679
+ var require_groups = __commonJS({
680
+ "dist/extension/lib/groups.js"(exports) {
681
+ "use strict";
682
+ Object.defineProperty(exports, "__esModule", { value: true });
683
+ exports.getGroupTabs = getGroupTabs;
684
+ exports.listGroupSummaries = listGroupSummaries;
685
+ exports.summarizeGroupMatch = summarizeGroupMatch;
686
+ exports.findGroupMatches = findGroupMatches;
687
+ exports.resolveGroupByTitle = resolveGroupByTitle2;
688
+ exports.resolveGroupById = resolveGroupById2;
689
+ exports.listGroups = listGroups2;
690
+ exports.groupUpdate = groupUpdate2;
691
+ exports.groupUngroup = groupUngroup2;
692
+ exports.groupAssign = groupAssign2;
693
+ function getGroupTabs(windowSnapshot, groupId) {
694
+ return windowSnapshot.tabs.filter((tab) => tab.groupId === groupId).sort((a, b) => (Number(a.index) || 0) - (Number(b.index) || 0));
695
+ }
696
+ function listGroupSummaries(snapshot, buildWindowLabels2, windowId) {
697
+ const windowLabels = buildWindowLabels2(snapshot);
698
+ const summaries = [];
699
+ const windows = snapshot.windows;
700
+ for (const win of windows) {
701
+ if (windowId && win.windowId !== windowId) {
702
+ continue;
703
+ }
704
+ for (const group of win.groups) {
705
+ summaries.push({
706
+ windowId: win.windowId,
707
+ windowLabel: windowLabels.get(win.windowId) ?? null,
708
+ groupId: group.groupId,
709
+ title: typeof group.title === "string" ? group.title : null
710
+ });
711
+ }
712
+ }
713
+ return summaries;
714
+ }
715
+ function summarizeGroupMatch(match, windowLabels) {
716
+ return {
717
+ windowId: match.windowId,
718
+ windowLabel: windowLabels.get(match.windowId) ?? null,
719
+ groupId: match.group.groupId,
720
+ title: typeof match.group.title === "string" ? match.group.title : null
721
+ };
722
+ }
723
+ function findGroupMatches(snapshot, groupTitle, windowId) {
724
+ const matches = [];
725
+ const windows = snapshot.windows;
726
+ for (const win of windows) {
727
+ if (windowId && win.windowId !== windowId) {
728
+ continue;
729
+ }
730
+ for (const group of win.groups) {
731
+ if (group.title === groupTitle) {
732
+ matches.push({
733
+ windowId: win.windowId,
734
+ group,
735
+ tabs: getGroupTabs(win, group.groupId)
736
+ });
737
+ }
738
+ }
739
+ }
740
+ return matches;
741
+ }
742
+ function resolveGroupByTitle2(snapshot, buildWindowLabels2, groupTitle, windowId) {
743
+ const windowLabels = buildWindowLabels2(snapshot);
744
+ const allMatches = findGroupMatches(snapshot, groupTitle);
745
+ const matches = windowId ? allMatches.filter((match) => match.windowId === windowId) : allMatches;
746
+ const availableGroups = listGroupSummaries(snapshot, buildWindowLabels2);
747
+ if (matches.length === 0) {
748
+ const message = windowId && allMatches.length > 0 ? "Group title not found in specified window" : "No matching group title found";
749
+ return {
750
+ error: {
751
+ message,
752
+ hint: "Use tabctl group-list to see existing groups.",
753
+ matches: allMatches.map((match) => summarizeGroupMatch(match, windowLabels)),
754
+ availableGroups
755
+ }
756
+ };
757
+ }
758
+ if (matches.length > 1) {
759
+ return {
760
+ error: {
761
+ message: "Group title is ambiguous. Provide a windowId.",
762
+ hint: "Use --window to disambiguate group titles.",
763
+ matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
764
+ availableGroups
765
+ }
766
+ };
767
+ }
768
+ return { match: matches[0] };
769
+ }
770
+ function resolveGroupById2(snapshot, buildWindowLabels2, groupId) {
771
+ const windows = snapshot.windows;
772
+ const matches = [];
773
+ for (const win of windows) {
774
+ const group = win.groups.find((entry) => entry.groupId === groupId);
775
+ if (group) {
776
+ matches.push({
777
+ windowId: win.windowId,
778
+ group,
779
+ tabs: getGroupTabs(win, groupId)
780
+ });
781
+ }
782
+ }
783
+ if (matches.length === 0) {
784
+ return {
785
+ error: {
786
+ message: "Group not found",
787
+ hint: "Use tabctl group-list to see existing groups.",
788
+ availableGroups: listGroupSummaries(snapshot, buildWindowLabels2)
789
+ }
790
+ };
791
+ }
792
+ if (matches.length > 1) {
793
+ const windowLabels = buildWindowLabels2(snapshot);
794
+ return {
795
+ error: {
796
+ message: "Group id is ambiguous. Provide a windowId.",
797
+ hint: "Use --window to disambiguate group ids.",
798
+ matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
799
+ availableGroups: listGroupSummaries(snapshot, buildWindowLabels2)
800
+ }
801
+ };
802
+ }
803
+ return { match: matches[0] };
804
+ }
805
+ async function listGroups2(params, deps2) {
806
+ const snapshot = await deps2.getTabSnapshot();
807
+ const windows = snapshot.windows;
808
+ const windowLabels = deps2.buildWindowLabels(snapshot);
809
+ const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
810
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
811
+ throw new Error("Window not found");
812
+ }
813
+ const groups2 = [];
814
+ for (const win of windows) {
815
+ if (windowIdParam && win.windowId !== windowIdParam) {
816
+ continue;
817
+ }
818
+ const counts = /* @__PURE__ */ new Map();
819
+ for (const tab of win.tabs) {
820
+ const groupId = tab.groupId;
821
+ if (typeof groupId === "number" && groupId !== -1) {
822
+ counts.set(groupId, (counts.get(groupId) || 0) + 1);
823
+ }
824
+ }
825
+ for (const group of win.groups) {
826
+ const groupId = group.groupId;
827
+ groups2.push({
828
+ windowId: win.windowId,
829
+ windowLabel: windowLabels.get(win.windowId) ?? null,
830
+ groupId,
831
+ title: group.title ?? null,
832
+ color: group.color ?? null,
833
+ collapsed: group.collapsed ?? null,
834
+ tabCount: counts.get(groupId) || 0
835
+ });
836
+ }
837
+ }
838
+ return { groups: groups2 };
839
+ }
840
+ async function groupUpdate2(params, deps2) {
841
+ const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
842
+ const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
843
+ if (!groupId && !groupTitle) {
844
+ throw new Error("Missing group identifier");
845
+ }
846
+ const snapshot = await deps2.getTabSnapshot();
847
+ const windows = snapshot.windows;
848
+ const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
849
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
850
+ throw new Error("Window not found");
851
+ }
852
+ let match;
853
+ if (groupId != null) {
854
+ const resolved = resolveGroupById2(snapshot, deps2.buildWindowLabels, groupId);
855
+ if (resolved.error) {
856
+ throw resolved.error;
857
+ }
858
+ match = resolved.match;
859
+ if (windowIdParam && windowIdParam !== match.windowId) {
860
+ throw new Error("Group is not in the specified window");
861
+ }
862
+ } else {
863
+ const resolved = resolveGroupByTitle2(snapshot, deps2.buildWindowLabels, groupTitle, windowIdParam || void 0);
864
+ if (resolved.error) {
865
+ throw resolved.error;
866
+ }
867
+ match = resolved.match;
868
+ }
869
+ const update = {};
870
+ if (typeof params.title === "string") {
871
+ update.title = params.title;
872
+ }
873
+ if (typeof params.color === "string" && params.color.trim()) {
874
+ update.color = params.color.trim();
875
+ }
876
+ if (typeof params.collapsed === "boolean") {
877
+ update.collapsed = params.collapsed;
878
+ }
879
+ if (!Object.keys(update).length) {
880
+ throw new Error("Missing group update fields");
881
+ }
882
+ const updated = await chrome.tabGroups.update(match.group.groupId, update);
883
+ return {
884
+ groupId: updated.id,
885
+ windowId: updated.windowId,
886
+ title: updated.title,
887
+ color: updated.color,
888
+ collapsed: updated.collapsed,
889
+ undo: {
890
+ action: "group-update",
891
+ groupId: updated.id,
892
+ windowId: match.windowId,
893
+ previous: {
894
+ title: match.group.title ?? null,
895
+ color: match.group.color ?? null,
896
+ collapsed: match.group.collapsed ?? null
897
+ }
898
+ },
899
+ txid: params.txid || null
900
+ };
901
+ }
902
+ async function groupUngroup2(params, deps2) {
903
+ const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
904
+ const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
905
+ if (!groupId && !groupTitle) {
906
+ throw new Error("Missing group identifier");
907
+ }
908
+ const snapshot = await deps2.getTabSnapshot();
909
+ const windows = snapshot.windows;
910
+ const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
911
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
912
+ throw new Error("Window not found");
913
+ }
914
+ let match;
915
+ if (groupId != null) {
916
+ const resolved = resolveGroupById2(snapshot, deps2.buildWindowLabels, groupId);
917
+ if (resolved.error) {
918
+ throw resolved.error;
919
+ }
920
+ match = resolved.match;
921
+ if (windowIdParam && windowIdParam !== match.windowId) {
922
+ throw new Error("Group is not in the specified window");
923
+ }
924
+ } else {
925
+ const resolved = resolveGroupByTitle2(snapshot, deps2.buildWindowLabels, groupTitle, windowIdParam || void 0);
926
+ if (resolved.error) {
927
+ throw resolved.error;
928
+ }
929
+ match = resolved.match;
930
+ }
931
+ const undoTabs = match.tabs.map((tab) => ({
932
+ tabId: tab.tabId,
933
+ windowId: tab.windowId,
934
+ index: tab.index,
935
+ groupId: tab.groupId,
936
+ groupTitle: tab.groupTitle,
937
+ groupColor: tab.groupColor,
938
+ groupCollapsed: match.group.collapsed ?? null
939
+ })).filter((tab) => typeof tab.tabId === "number");
940
+ const tabIds = match.tabs.map((tab) => tab.tabId).filter((tabId) => typeof tabId === "number");
941
+ if (tabIds.length) {
942
+ await chrome.tabs.ungroup(tabIds);
943
+ }
944
+ return {
945
+ groupId: match.group.groupId,
946
+ groupTitle: match.group.title || null,
947
+ windowId: match.windowId,
948
+ summary: {
949
+ ungroupedTabs: tabIds.length
950
+ },
951
+ undo: {
952
+ action: "group-ungroup",
953
+ groupId: match.group.groupId,
954
+ windowId: match.windowId,
955
+ groupTitle: match.group.title || null,
956
+ groupColor: match.group.color || null,
957
+ groupCollapsed: match.group.collapsed ?? null,
958
+ tabs: undoTabs
959
+ },
960
+ txid: params.txid || null
961
+ };
962
+ }
963
+ async function groupAssign2(params, deps2) {
964
+ const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
965
+ const tabIds = rawTabIds.filter((id) => Number.isFinite(id));
966
+ if (!tabIds.length) {
967
+ throw new Error("Missing tabIds");
968
+ }
969
+ const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
970
+ const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
971
+ if (!groupId && !groupTitle) {
972
+ throw new Error("Missing group identifier");
973
+ }
974
+ const snapshot = await deps2.getTabSnapshot();
975
+ const windows = snapshot.windows;
976
+ const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
977
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
978
+ throw new Error("Window not found");
979
+ }
980
+ const tabIndex = /* @__PURE__ */ new Map();
981
+ for (const win of windows) {
982
+ for (const tab of win.tabs) {
983
+ if (typeof tab.tabId === "number") {
984
+ tabIndex.set(tab.tabId, { tab, windowId: win.windowId });
985
+ }
986
+ }
987
+ }
988
+ const skipped = [];
989
+ const resolvedTabIds = [];
990
+ const sourceWindows = /* @__PURE__ */ new Set();
991
+ const undoTabs = [];
992
+ for (const tabId of tabIds) {
993
+ const entry = tabIndex.get(tabId);
994
+ if (!entry) {
995
+ skipped.push({ tabId, reason: "not_found" });
996
+ continue;
997
+ }
998
+ resolvedTabIds.push(tabId);
999
+ sourceWindows.add(entry.windowId);
1000
+ const tab = entry.tab;
1001
+ undoTabs.push({
1002
+ tabId,
1003
+ windowId: entry.windowId,
1004
+ index: tab.index,
1005
+ groupId: tab.groupId,
1006
+ groupTitle: tab.groupTitle,
1007
+ groupColor: tab.groupColor,
1008
+ groupCollapsed: tab.groupCollapsed ?? null
1009
+ });
1010
+ }
1011
+ if (!resolvedTabIds.length) {
1012
+ throw new Error("No matching tabs found");
1013
+ }
1014
+ let targetGroupId = null;
1015
+ let targetWindowId = null;
1016
+ let targetTitle = null;
1017
+ let created = false;
1018
+ if (groupId != null) {
1019
+ const resolved = resolveGroupById2(snapshot, deps2.buildWindowLabels, groupId);
1020
+ if (resolved.error) {
1021
+ throw resolved.error;
1022
+ }
1023
+ const match = resolved.match;
1024
+ targetGroupId = match.group.groupId;
1025
+ targetWindowId = match.windowId;
1026
+ targetTitle = typeof match.group.title === "string" ? match.group.title : null;
1027
+ if (windowIdParam && windowIdParam !== targetWindowId) {
1028
+ throw new Error("Group is not in the specified window");
1029
+ }
1030
+ } else {
1031
+ const resolved = resolveGroupByTitle2(snapshot, deps2.buildWindowLabels, groupTitle, windowIdParam || void 0);
1032
+ if (resolved.error) {
1033
+ const error = resolved.error;
1034
+ if (error.message === "No matching group title found" && params.create === true) {
1035
+ targetWindowId = windowIdParam || (sourceWindows.size === 1 ? Array.from(sourceWindows)[0] : null);
1036
+ if (!targetWindowId) {
1037
+ throw new Error("Multiple source windows. Provide --window to create a new group.");
1038
+ }
1039
+ targetTitle = groupTitle;
1040
+ created = true;
1041
+ } else {
1042
+ throw error;
1043
+ }
1044
+ } else {
1045
+ const match = resolved.match;
1046
+ targetGroupId = match.group.groupId;
1047
+ targetWindowId = match.windowId;
1048
+ targetTitle = typeof match.group.title === "string" && match.group.title ? match.group.title : groupTitle;
1049
+ }
1050
+ }
1051
+ if (!targetWindowId) {
1052
+ throw new Error("Target window not found");
1053
+ }
1054
+ const moveIds = resolvedTabIds.filter((tabId) => {
1055
+ const entry = tabIndex.get(tabId);
1056
+ return entry && entry.windowId !== targetWindowId;
1057
+ });
1058
+ if (moveIds.length > 0) {
1059
+ await chrome.tabs.move(moveIds, { windowId: targetWindowId, index: -1 });
1060
+ }
1061
+ let assignedGroupId = targetGroupId;
1062
+ if (targetGroupId != null) {
1063
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: resolvedTabIds });
1064
+ } else {
1065
+ assignedGroupId = await chrome.tabs.group({ tabIds: resolvedTabIds, createProperties: { windowId: targetWindowId } });
1066
+ const update = {};
1067
+ if (targetTitle) {
1068
+ update.title = targetTitle;
1069
+ }
1070
+ if (typeof params.color === "string" && params.color.trim()) {
1071
+ update.color = params.color.trim();
1072
+ }
1073
+ if (typeof params.collapsed === "boolean") {
1074
+ update.collapsed = params.collapsed;
1075
+ }
1076
+ if (Object.keys(update).length > 0) {
1077
+ try {
1078
+ await chrome.tabGroups.update(assignedGroupId, update);
1079
+ } catch (error) {
1080
+ deps2.log("Failed to update group", error);
1081
+ }
1082
+ }
1083
+ created = true;
1084
+ }
1085
+ return {
1086
+ groupId: assignedGroupId,
1087
+ groupTitle: targetTitle || groupTitle || null,
1088
+ windowId: targetWindowId,
1089
+ created,
1090
+ summary: {
1091
+ movedTabs: moveIds.length,
1092
+ groupedTabs: resolvedTabIds.length,
1093
+ skippedTabs: skipped.length
1094
+ },
1095
+ skipped,
1096
+ undo: {
1097
+ action: "group-assign",
1098
+ groupId: assignedGroupId,
1099
+ groupTitle: targetTitle || groupTitle || null,
1100
+ groupColor: typeof params.color === "string" && params.color.trim() ? params.color.trim() : null,
1101
+ groupCollapsed: typeof params.collapsed === "boolean" ? params.collapsed : null,
1102
+ created,
1103
+ tabs: undoTabs
1104
+ },
1105
+ txid: params.txid || null
1106
+ };
1107
+ }
1108
+ }
1109
+ });
1110
+
1111
+ // dist/extension/lib/tabs.js
1112
+ var require_tabs = __commonJS({
1113
+ "dist/extension/lib/tabs.js"(exports) {
1114
+ "use strict";
1115
+ Object.defineProperty(exports, "__esModule", { value: true });
1116
+ exports.getMostRecentFocusedWindowId = getMostRecentFocusedWindowId2;
1117
+ exports.normalizeUrl = normalizeUrl;
1118
+ exports.normalizeTabIndex = normalizeTabIndex2;
1119
+ exports.resolveOpenWindow = resolveOpenWindow;
1120
+ exports.focusTab = focusTab;
1121
+ exports.refreshTabs = refreshTabs;
1122
+ exports.openTabs = openTabs;
1123
+ function getMostRecentFocusedWindowId2(windows) {
1124
+ let bestWindowId = null;
1125
+ let bestFocusedAt = -Infinity;
1126
+ for (const win of windows) {
1127
+ for (const tab of win.tabs) {
1128
+ const focusedAt = Number(tab.lastFocusedAt);
1129
+ if (!Number.isFinite(focusedAt)) {
1130
+ continue;
1131
+ }
1132
+ if (focusedAt > bestFocusedAt) {
1133
+ bestFocusedAt = focusedAt;
1134
+ bestWindowId = win.windowId;
1135
+ }
1136
+ }
1137
+ }
1138
+ return bestWindowId;
1139
+ }
1140
+ function normalizeUrl(rawUrl) {
1141
+ if (!rawUrl || typeof rawUrl !== "string") {
1142
+ return null;
1143
+ }
1144
+ let url;
1145
+ try {
1146
+ url = new URL(rawUrl);
1147
+ } catch {
1148
+ return null;
1149
+ }
1150
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1151
+ return null;
1152
+ }
1153
+ url.hash = "";
1154
+ const dropKeys = /* @__PURE__ */ new Set([
1155
+ "fbclid",
1156
+ "gclid",
1157
+ "igshid",
1158
+ "mc_cid",
1159
+ "mc_eid",
1160
+ "ref",
1161
+ "ref_src",
1162
+ "ref_url",
1163
+ "utm_campaign",
1164
+ "utm_content",
1165
+ "utm_medium",
1166
+ "utm_source",
1167
+ "utm_term",
1168
+ "utm_name",
1169
+ "si"
1170
+ ]);
1171
+ for (const key of Array.from(url.searchParams.keys())) {
1172
+ if (key.startsWith("utm_") || dropKeys.has(key)) {
1173
+ url.searchParams.delete(key);
1174
+ }
1175
+ }
1176
+ const search = url.searchParams.toString();
1177
+ url.search = search ? `?${search}` : "";
1178
+ return url.toString();
1179
+ }
1180
+ function normalizeTabIndex2(value) {
1181
+ const index = Number(value);
1182
+ return Number.isFinite(index) ? index : null;
1183
+ }
1184
+ function matchIncludes(value, needle) {
1185
+ if (!needle) {
1186
+ return false;
1187
+ }
1188
+ return typeof value === "string" && value.toLowerCase().includes(needle);
1189
+ }
1190
+ function resolveOpenWindow(snapshot, params) {
1191
+ const windows = snapshot.windows;
1192
+ if (!windows.length) {
1193
+ return { error: { message: "No windows available" } };
1194
+ }
1195
+ if (params.windowId != null) {
1196
+ if (typeof params.windowId === "string") {
1197
+ const normalized = params.windowId.trim().toLowerCase();
1198
+ if (normalized === "active") {
1199
+ const focused2 = windows.find((win) => win.focused);
1200
+ if (focused2) {
1201
+ return { windowId: focused2.windowId };
1202
+ }
1203
+ return { error: { message: "Active window not found" } };
1204
+ }
1205
+ if (normalized === "last-focused") {
1206
+ const lastFocused2 = getMostRecentFocusedWindowId2(windows);
1207
+ if (lastFocused2 != null) {
1208
+ return { windowId: lastFocused2 };
1209
+ }
1210
+ return { error: { message: "Last focused window not found" } };
1211
+ }
1212
+ if (normalized === "new") {
1213
+ return { error: { message: "--window new is only supported by open" } };
1214
+ }
1215
+ }
1216
+ const windowId = Number(params.windowId);
1217
+ const found = windows.find((win) => win.windowId === windowId);
1218
+ if (!found) {
1219
+ return { error: { message: "Window not found" } };
1220
+ }
1221
+ return { windowId };
1222
+ }
1223
+ if (params.windowTabId != null) {
1224
+ const tabId = Number(params.windowTabId);
1225
+ const found = windows.find((win) => win.tabs.some((tab) => tab.tabId === tabId));
1226
+ if (!found) {
1227
+ return { error: { message: "Window not found for tab" } };
1228
+ }
1229
+ return { windowId: found.windowId };
1230
+ }
1231
+ let candidates = [...windows];
1232
+ let filtered = false;
1233
+ if (typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim()) {
1234
+ const groupTitle = params.afterGroupTitle.trim();
1235
+ candidates = candidates.filter((win) => win.groups.some((group) => group.title === groupTitle));
1236
+ filtered = true;
1237
+ }
1238
+ if (typeof params.windowGroupTitle === "string" && params.windowGroupTitle.trim()) {
1239
+ const groupTitle = params.windowGroupTitle.trim();
1240
+ candidates = candidates.filter((win) => win.groups.some((group) => group.title === groupTitle));
1241
+ filtered = true;
1242
+ }
1243
+ if (typeof params.windowUrl === "string" && params.windowUrl.trim()) {
1244
+ const needle = params.windowUrl.trim().toLowerCase();
1245
+ candidates = candidates.filter((win) => win.tabs.some((tab) => matchIncludes(tab.url, needle)));
1246
+ filtered = true;
1247
+ }
1248
+ if (filtered) {
1249
+ if (candidates.length === 1) {
1250
+ return { windowId: candidates[0].windowId };
1251
+ }
1252
+ if (candidates.length === 0) {
1253
+ return { error: { message: "No matching window found" } };
1254
+ }
1255
+ return { error: { message: "Multiple windows match selection. Provide --window to disambiguate." } };
1256
+ }
1257
+ const focused = windows.find((win) => win.focused);
1258
+ if (focused) {
1259
+ return { windowId: focused.windowId };
1260
+ }
1261
+ if (windows.length === 1) {
1262
+ return { windowId: windows[0].windowId };
1263
+ }
1264
+ const lastFocused = getMostRecentFocusedWindowId2(windows);
1265
+ if (lastFocused != null) {
1266
+ return { windowId: lastFocused };
1267
+ }
1268
+ return { error: { message: "Multiple windows available. Provide --window to target one." } };
1269
+ }
1270
+ async function focusTab(params) {
1271
+ const tabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
1272
+ const tabId = Number.isFinite(params.tabId) ? Number(params.tabId) : tabIds.length ? Number(tabIds[0]) : null;
1273
+ if (!tabId) {
1274
+ throw new Error("Missing tabId");
1275
+ }
1276
+ const tab = await chrome.tabs.get(tabId);
1277
+ await chrome.windows.update(tab.windowId, { focused: true });
1278
+ await chrome.tabs.update(tabId, { active: true });
1279
+ return {
1280
+ tabId,
1281
+ windowId: tab.windowId
1282
+ };
1283
+ }
1284
+ async function refreshTabs(params) {
1285
+ const tabId = Number.isFinite(params.tabId) ? Number(params.tabId) : null;
1286
+ if (!tabId) {
1287
+ throw new Error("Missing tabId");
1288
+ }
1289
+ await chrome.tabs.reload(tabId);
1290
+ return {
1291
+ tabId,
1292
+ summary: { refreshedTabs: 1 }
1293
+ };
1294
+ }
1295
+ async function openTabs(params, deps2) {
1296
+ const urls = Array.isArray(params.urls) ? params.urls.map((url) => typeof url === "string" ? url.trim() : "").filter(Boolean) : [];
1297
+ const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
1298
+ const groupColor = typeof params.color === "string" ? params.color.trim() : "";
1299
+ const afterGroupTitle = typeof params.afterGroupTitle === "string" ? params.afterGroupTitle.trim() : "";
1300
+ const beforeTabId = Number.isFinite(params.beforeTabId) ? Number(params.beforeTabId) : null;
1301
+ const afterTabId = Number.isFinite(params.afterTabId) ? Number(params.afterTabId) : null;
1302
+ if (beforeTabId != null && afterTabId != null) {
1303
+ throw new Error("Only one target position is allowed");
1304
+ }
1305
+ const newWindow = params.newWindow === true;
1306
+ if (!urls.length && !newWindow) {
1307
+ throw new Error("No URLs provided");
1308
+ }
1309
+ if (newWindow) {
1310
+ if (afterGroupTitle || beforeTabId || afterTabId) {
1311
+ throw new Error("Cannot use --before/--after with --new-window");
1312
+ }
1313
+ if (params.windowId != null || params.windowGroupTitle || params.windowTabId != null || params.windowUrl) {
1314
+ throw new Error("Cannot combine --new-window with window selectors");
1315
+ }
1316
+ const created2 = [];
1317
+ const skipped2 = [];
1318
+ const createdWindow = await chrome.windows.create({ focused: false });
1319
+ const windowId2 = createdWindow.id;
1320
+ let seedTabs = createdWindow.tabs;
1321
+ if (!seedTabs) {
1322
+ seedTabs = await chrome.tabs.query({ windowId: windowId2 });
1323
+ }
1324
+ const seedTabId = seedTabs.find((tab) => typeof tab.id === "number")?.id ?? null;
1325
+ for (const url of urls) {
1326
+ try {
1327
+ const tab = await chrome.tabs.create({ windowId: windowId2, url, active: false });
1328
+ created2.push({
1329
+ tabId: tab.id,
1330
+ windowId: tab.windowId,
1331
+ index: tab.index,
1332
+ url: tab.url,
1333
+ title: tab.title
1334
+ });
1335
+ } catch (error) {
1336
+ skipped2.push({ url, reason: "create_failed" });
1337
+ }
1338
+ }
1339
+ if (!urls.length && seedTabs.length) {
1340
+ const tab = seedTabs[0];
1341
+ created2.push({
1342
+ tabId: tab.id,
1343
+ windowId: tab.windowId,
1344
+ index: tab.index,
1345
+ url: tab.url,
1346
+ title: tab.title
1347
+ });
1348
+ }
1349
+ if (seedTabId && created2.length > 0 && urls.length > 0) {
1350
+ try {
1351
+ await chrome.tabs.remove(seedTabId);
1352
+ } catch (error) {
1353
+ deps2.log("Failed to remove seed tab", error);
1354
+ }
1355
+ }
1356
+ let groupId2 = null;
1357
+ if (groupTitle && created2.length > 0) {
1358
+ try {
1359
+ const tabIds = created2.map((tab) => tab.tabId).filter((id) => typeof id === "number");
1360
+ if (tabIds.length > 0) {
1361
+ groupId2 = await chrome.tabs.group({ tabIds, createProperties: { windowId: windowId2 } });
1362
+ const update = { title: groupTitle };
1363
+ if (groupColor) {
1364
+ update.color = groupColor;
1365
+ }
1366
+ await chrome.tabGroups.update(groupId2, update);
1367
+ }
1368
+ } catch (error) {
1369
+ deps2.log("Failed to create group", error);
1370
+ groupId2 = null;
1371
+ }
1372
+ }
1373
+ return {
1374
+ windowId: windowId2,
1375
+ groupId: groupId2,
1376
+ groupTitle: groupTitle || null,
1377
+ afterGroupTitle: null,
1378
+ insertIndex: null,
1379
+ created: created2,
1380
+ skipped: skipped2,
1381
+ summary: {
1382
+ createdTabs: created2.length,
1383
+ skippedUrls: skipped2.length,
1384
+ grouped: Boolean(groupId2)
1385
+ }
1386
+ };
1387
+ }
1388
+ const snapshot = await deps2.getTabSnapshot();
1389
+ let openParams = params;
1390
+ if (params.windowId == null && (beforeTabId != null || afterTabId != null)) {
1391
+ const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
1392
+ const anchorWindow = snapshot.windows.find((win) => win.tabs.some((tab) => tab.tabId === anchorId));
1393
+ if (anchorWindow) {
1394
+ openParams = { ...params, windowId: anchorWindow.windowId };
1395
+ }
1396
+ }
1397
+ const selection = resolveOpenWindow(snapshot, openParams);
1398
+ if (selection.error) {
1399
+ throw selection.error;
1400
+ }
1401
+ const windowId = selection.windowId;
1402
+ const windowSnapshot = snapshot.windows.find((win) => win.windowId === windowId);
1403
+ if (!windowSnapshot) {
1404
+ throw new Error("Window snapshot unavailable");
1405
+ }
1406
+ const created = [];
1407
+ const skipped = [];
1408
+ let insertIndex = null;
1409
+ if (afterGroupTitle) {
1410
+ const targetGroup = windowSnapshot.groups.find((group) => group.title === afterGroupTitle);
1411
+ if (!targetGroup) {
1412
+ throw new Error("Group not found in target window");
1413
+ }
1414
+ const groupTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === targetGroup.groupId);
1415
+ if (!groupTabs.length) {
1416
+ throw new Error("Group has no tabs to anchor insertion");
1417
+ }
1418
+ const indices = groupTabs.map((tab) => normalizeTabIndex2(tab.index)).filter((value) => value != null);
1419
+ if (!indices.length) {
1420
+ throw new Error("Group tabs missing indices");
1421
+ }
1422
+ insertIndex = Math.max(...indices) + 1;
1423
+ }
1424
+ if (beforeTabId != null || afterTabId != null) {
1425
+ if (afterGroupTitle) {
1426
+ throw new Error("Only one target position is allowed");
1427
+ }
1428
+ const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
1429
+ const anchorTab = windowSnapshot.tabs.find((tab) => tab.tabId === anchorId);
1430
+ if (!anchorTab) {
1431
+ throw new Error("Anchor tab not found in target window");
1432
+ }
1433
+ const anchorIndex = normalizeTabIndex2(anchorTab.index);
1434
+ if (!Number.isFinite(anchorIndex)) {
1435
+ throw new Error("Anchor tab index unavailable");
1436
+ }
1437
+ insertIndex = beforeTabId != null ? anchorIndex : anchorIndex + 1;
1438
+ }
1439
+ let nextIndex = insertIndex;
1440
+ for (const url of urls) {
1441
+ try {
1442
+ const createOptions = { windowId, url, active: false };
1443
+ if (nextIndex != null) {
1444
+ createOptions.index = nextIndex;
1445
+ nextIndex += 1;
1446
+ }
1447
+ const tab = await chrome.tabs.create(createOptions);
1448
+ created.push({
1449
+ tabId: tab.id,
1450
+ windowId: tab.windowId,
1451
+ index: tab.index,
1452
+ url: tab.url,
1453
+ title: tab.title
1454
+ });
1455
+ } catch (error) {
1456
+ skipped.push({ url, reason: "create_failed" });
1457
+ }
1458
+ }
1459
+ let groupId = null;
1460
+ if (groupTitle && created.length > 0) {
1461
+ try {
1462
+ const tabIds = created.map((tab) => tab.tabId).filter((id) => typeof id === "number");
1463
+ if (tabIds.length > 0) {
1464
+ groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
1465
+ const update = { title: groupTitle };
1466
+ if (groupColor) {
1467
+ update.color = groupColor;
1468
+ }
1469
+ await chrome.tabGroups.update(groupId, update);
1470
+ }
1471
+ } catch (error) {
1472
+ deps2.log("Failed to create group", error);
1473
+ groupId = null;
1474
+ }
1475
+ }
1476
+ return {
1477
+ windowId,
1478
+ groupId,
1479
+ groupTitle: groupTitle || null,
1480
+ afterGroupTitle: afterGroupTitle || null,
1481
+ insertIndex,
1482
+ created,
1483
+ skipped,
1484
+ summary: {
1485
+ createdTabs: created.length,
1486
+ skippedUrls: skipped.length,
1487
+ grouped: Boolean(groupId)
1488
+ }
1489
+ };
1490
+ }
1491
+ }
1492
+ });
1493
+
1494
+ // dist/extension/lib/move.js
1495
+ var require_move = __commonJS({
1496
+ "dist/extension/lib/move.js"(exports) {
1497
+ "use strict";
1498
+ Object.defineProperty(exports, "__esModule", { value: true });
1499
+ exports.resolveMoveTarget = resolveMoveTarget;
1500
+ exports.moveTab = moveTab;
1501
+ exports.moveGroup = moveGroup;
1502
+ var tabs2 = require_tabs();
1503
+ var { normalizeTabIndex: normalizeTabIndex2 } = tabs2;
1504
+ function resolveMoveTarget(snapshot, params, deps2) {
1505
+ const beforeTabId = Number(params.beforeTabId);
1506
+ const afterTabId = Number(params.afterTabId);
1507
+ const beforeGroupTitle = typeof params.beforeGroupTitle === "string" ? params.beforeGroupTitle.trim() : "";
1508
+ const afterGroupTitle = typeof params.afterGroupTitle === "string" ? params.afterGroupTitle.trim() : "";
1509
+ const targets = [
1510
+ Number.isFinite(beforeTabId) ? "before-tab" : null,
1511
+ Number.isFinite(afterTabId) ? "after-tab" : null,
1512
+ beforeGroupTitle ? "before-group" : null,
1513
+ afterGroupTitle ? "after-group" : null
1514
+ ].filter(Boolean);
1515
+ if (targets.length === 0) {
1516
+ return { error: { message: "Missing target position (--before/--after)" } };
1517
+ }
1518
+ if (targets.length > 1) {
1519
+ return { error: { message: "Only one target position is allowed" } };
1520
+ }
1521
+ const windows = snapshot.windows;
1522
+ const findTab = (tabId) => {
1523
+ for (const win of windows) {
1524
+ const tab = win.tabs.find((entry) => entry.tabId === tabId);
1525
+ if (tab) {
1526
+ return { tab, windowId: win.windowId };
1527
+ }
1528
+ }
1529
+ return null;
1530
+ };
1531
+ if (targets[0] === "before-tab" || targets[0] === "after-tab") {
1532
+ const tabId = targets[0] === "before-tab" ? beforeTabId : afterTabId;
1533
+ if (!Number.isFinite(tabId)) {
1534
+ return { error: { message: "Invalid tab target" } };
1535
+ }
1536
+ const match2 = findTab(tabId);
1537
+ if (!match2) {
1538
+ return { error: { message: "Target tab not found" } };
1539
+ }
1540
+ const index = normalizeTabIndex2(match2.tab.index);
1541
+ if (!Number.isFinite(index)) {
1542
+ return { error: { message: "Target tab index unavailable" } };
1543
+ }
1544
+ return {
1545
+ windowId: match2.windowId,
1546
+ index: targets[0] === "before-tab" ? index : index + 1,
1547
+ anchor: { type: "tab", tabId }
1548
+ };
1549
+ }
1550
+ const groupTitle = targets[0] === "before-group" ? beforeGroupTitle : afterGroupTitle;
1551
+ const windowId = Number.isFinite(params.windowId) ? Number(params.windowId) : void 0;
1552
+ const resolved = deps2.resolveGroupByTitle(snapshot, groupTitle, windowId);
1553
+ if (resolved.error) {
1554
+ return resolved;
1555
+ }
1556
+ const match = resolved.match;
1557
+ if (!match.tabs.length) {
1558
+ return { error: { message: "Target group has no tabs" } };
1559
+ }
1560
+ const indices = match.tabs.map((tab) => normalizeTabIndex2(tab.index)).filter((value) => value != null);
1561
+ if (!indices.length) {
1562
+ return { error: { message: "Target group indices unavailable" } };
1563
+ }
1564
+ const minIndex = Math.min(...indices);
1565
+ const maxIndex = Math.max(...indices);
1566
+ return {
1567
+ windowId: match.windowId,
1568
+ index: targets[0] === "before-group" ? minIndex : maxIndex + 1,
1569
+ anchor: { type: "group", groupId: match.group.groupId, groupTitle }
1570
+ };
1571
+ }
1572
+ async function moveTab(params, deps2) {
1573
+ const tabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
1574
+ const tabId = Number.isFinite(params.tabId) ? Number(params.tabId) : tabIds.length ? Number(tabIds[0]) : null;
1575
+ if (!tabId) {
1576
+ throw new Error("Missing tabId");
1577
+ }
1578
+ const snapshot = await deps2.getTabSnapshot();
1579
+ const windows = snapshot.windows;
1580
+ const sourceWindow = windows.find((win) => win.tabs.some((tab) => tab.tabId === tabId));
1581
+ if (!sourceWindow) {
1582
+ throw new Error("Source tab not found");
1583
+ }
1584
+ const sourceTab = sourceWindow.tabs.find((tab) => tab.tabId === tabId);
1585
+ if (!sourceTab) {
1586
+ throw new Error("Source tab not found");
1587
+ }
1588
+ const newWindow = params.newWindow === true;
1589
+ const hasTarget = Number.isFinite(params.beforeTabId) || Number.isFinite(params.afterTabId) || typeof params.beforeGroupTitle === "string" && params.beforeGroupTitle.trim() || typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim();
1590
+ if (newWindow) {
1591
+ if (hasTarget) {
1592
+ throw new Error("Cannot combine --new-window with --before/--after");
1593
+ }
1594
+ const createdWindow = await chrome.windows.create({ tabId, focused: false });
1595
+ const targetWindowId2 = createdWindow.id;
1596
+ let targetIndex2 = 0;
1597
+ const createdTab = createdWindow.tabs?.find((tab) => tab.id === tabId) || null;
1598
+ if (createdTab && Number.isFinite(createdTab.index)) {
1599
+ targetIndex2 = createdTab.index;
1600
+ } else {
1601
+ try {
1602
+ const updated = await chrome.tabs.get(tabId);
1603
+ targetIndex2 = updated.index;
1604
+ } catch {
1605
+ targetIndex2 = 0;
1606
+ }
1607
+ }
1608
+ return {
1609
+ tabId,
1610
+ from: { windowId: sourceWindow.windowId, index: sourceTab.index },
1611
+ to: { windowId: targetWindowId2, index: targetIndex2 },
1612
+ summary: { movedTabs: 1 },
1613
+ undo: {
1614
+ action: "move-tab",
1615
+ tabId,
1616
+ from: {
1617
+ windowId: sourceWindow.windowId,
1618
+ index: sourceTab.index,
1619
+ groupId: sourceTab.groupId,
1620
+ groupTitle: sourceTab.groupTitle,
1621
+ groupColor: sourceTab.groupColor,
1622
+ groupCollapsed: sourceTab.groupCollapsed ?? null
1623
+ },
1624
+ to: {
1625
+ windowId: targetWindowId2,
1626
+ index: targetIndex2
1627
+ }
1628
+ },
1629
+ txid: params.txid || null
1630
+ };
1631
+ }
1632
+ let normalizedParams = params;
1633
+ if (params.windowId != null) {
1634
+ const resolvedWindowId = deps2.resolveWindowIdFromParams(snapshot, params.windowId);
1635
+ normalizedParams = { ...params, windowId: resolvedWindowId ?? void 0 };
1636
+ }
1637
+ const target = resolveMoveTarget(snapshot, normalizedParams, deps2);
1638
+ if (target.error) {
1639
+ throw target.error;
1640
+ }
1641
+ const targetWindowId = target.windowId;
1642
+ let targetIndex = target.index;
1643
+ const sourceIndex = normalizeTabIndex2(sourceTab.index);
1644
+ if (Number.isFinite(sourceIndex) && sourceWindow.windowId === targetWindowId && sourceIndex < targetIndex) {
1645
+ targetIndex -= 1;
1646
+ }
1647
+ const moved = await chrome.tabs.move(tabId, { windowId: targetWindowId, index: targetIndex });
1648
+ return {
1649
+ tabId,
1650
+ from: { windowId: sourceWindow.windowId, index: sourceTab.index },
1651
+ to: { windowId: targetWindowId, index: moved.index },
1652
+ summary: { movedTabs: 1 },
1653
+ undo: {
1654
+ action: "move-tab",
1655
+ tabId,
1656
+ from: {
1657
+ windowId: sourceWindow.windowId,
1658
+ index: sourceTab.index,
1659
+ groupId: sourceTab.groupId,
1660
+ groupTitle: sourceTab.groupTitle,
1661
+ groupColor: sourceTab.groupColor,
1662
+ groupCollapsed: sourceTab.groupCollapsed ?? null
1663
+ },
1664
+ to: {
1665
+ windowId: targetWindowId,
1666
+ index: moved.index
1667
+ }
1668
+ },
1669
+ txid: params.txid || null
1670
+ };
1671
+ }
1672
+ async function moveGroup(params, deps2) {
1673
+ const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
1674
+ const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
1675
+ if (!groupId && !groupTitle) {
1676
+ throw new Error("Missing group identifier");
1677
+ }
1678
+ const snapshot = await deps2.getTabSnapshot();
1679
+ const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) ?? void 0 : void 0;
1680
+ const resolvedGroup = groupId != null ? deps2.resolveGroupById(snapshot, groupId) : deps2.resolveGroupByTitle(snapshot, groupTitle, windowIdParam);
1681
+ if (resolvedGroup.error) {
1682
+ throw resolvedGroup.error;
1683
+ }
1684
+ const source = resolvedGroup.match;
1685
+ if (!source.tabs.length) {
1686
+ throw new Error("Group has no tabs to move");
1687
+ }
1688
+ const newWindow = params.newWindow === true;
1689
+ const hasTarget = Number.isFinite(params.beforeTabId) || Number.isFinite(params.afterTabId) || typeof params.beforeGroupTitle === "string" && params.beforeGroupTitle.trim() || typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim();
1690
+ if (newWindow) {
1691
+ if (hasTarget) {
1692
+ throw new Error("Cannot combine --new-window with --before/--after");
1693
+ }
1694
+ const tabIds2 = source.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
1695
+ const [firstTabId, ...restTabIds] = tabIds2;
1696
+ if (!firstTabId) {
1697
+ throw new Error("Group has no tabs to move");
1698
+ }
1699
+ const createdWindow = await chrome.windows.create({ tabId: firstTabId, focused: false });
1700
+ const targetWindowId2 = createdWindow.id;
1701
+ if (restTabIds.length > 0) {
1702
+ await chrome.tabs.move(restTabIds, { windowId: targetWindowId2, index: -1 });
1703
+ }
1704
+ let newGroupId2 = null;
1705
+ try {
1706
+ newGroupId2 = await chrome.tabs.group({ tabIds: tabIds2, createProperties: { windowId: targetWindowId2 } });
1707
+ await chrome.tabGroups.update(newGroupId2, {
1708
+ title: source.group.title || "",
1709
+ color: source.group.color || "grey",
1710
+ collapsed: source.group.collapsed || false
1711
+ });
1712
+ } catch (error) {
1713
+ deps2.log("Failed to regroup tabs", error);
1714
+ }
1715
+ const undoTabs2 = source.tabs.map((tab) => ({
1716
+ tabId: tab.tabId,
1717
+ windowId: tab.windowId,
1718
+ index: tab.index,
1719
+ groupId: tab.groupId,
1720
+ groupTitle: tab.groupTitle,
1721
+ groupColor: tab.groupColor,
1722
+ groupCollapsed: source.group.collapsed ?? null
1723
+ })).filter((tab) => typeof tab.tabId === "number");
1724
+ return {
1725
+ groupId: source.group.groupId,
1726
+ windowId: source.windowId,
1727
+ movedToWindowId: targetWindowId2,
1728
+ newGroupId: newGroupId2,
1729
+ summary: { movedTabs: tabIds2.length },
1730
+ undo: {
1731
+ action: "move-group",
1732
+ groupId: source.group.groupId,
1733
+ windowId: source.windowId,
1734
+ movedToWindowId: targetWindowId2,
1735
+ groupTitle: source.group.title ?? null,
1736
+ groupColor: source.group.color ?? null,
1737
+ groupCollapsed: source.group.collapsed ?? null,
1738
+ tabs: undoTabs2
1739
+ },
1740
+ txid: params.txid || null
1741
+ };
1742
+ }
1743
+ const target = resolveMoveTarget(snapshot, params, deps2);
1744
+ if (target.error) {
1745
+ throw target.error;
1746
+ }
1747
+ if (target.anchor?.type === "tab") {
1748
+ const anchorTabId = target.anchor.tabId;
1749
+ if (source.tabs.some((tab) => tab.tabId === anchorTabId)) {
1750
+ throw new Error("Target tab is within the source group");
1751
+ }
1752
+ }
1753
+ if (target.anchor?.type === "group") {
1754
+ const anchorGroupId = target.anchor.groupId;
1755
+ if (anchorGroupId === source.group.groupId && source.windowId === target.windowId) {
1756
+ throw new Error("Target group matches source group");
1757
+ }
1758
+ }
1759
+ const tabIds = source.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
1760
+ const indices = source.tabs.map((tab) => normalizeTabIndex2(tab.index)).filter((value) => value != null);
1761
+ const minIndex = Math.min(...indices);
1762
+ const maxIndex = Math.max(...indices);
1763
+ const targetWindowId = target.windowId;
1764
+ let targetIndex = target.index;
1765
+ if (source.windowId === targetWindowId && targetIndex > maxIndex) {
1766
+ targetIndex -= tabIds.length;
1767
+ }
1768
+ const moved = await chrome.tabs.move(tabIds, { windowId: targetWindowId, index: targetIndex });
1769
+ const movedList = Array.isArray(moved) ? moved : [moved];
1770
+ let newGroupId = null;
1771
+ if (targetWindowId !== source.windowId) {
1772
+ try {
1773
+ const movedIds = movedList.map((tab) => tab.id).filter((id) => typeof id === "number");
1774
+ if (movedIds.length > 0) {
1775
+ newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: targetWindowId } });
1776
+ await chrome.tabGroups.update(newGroupId, {
1777
+ title: source.group.title || "",
1778
+ color: source.group.color || "grey",
1779
+ collapsed: source.group.collapsed || false
1780
+ });
1781
+ }
1782
+ } catch (error) {
1783
+ deps2.log("Failed to regroup tabs", error);
1784
+ }
1785
+ }
1786
+ const undoTabs = source.tabs.map((tab) => ({
1787
+ tabId: tab.tabId,
1788
+ windowId: tab.windowId,
1789
+ index: tab.index,
1790
+ groupId: tab.groupId,
1791
+ groupTitle: tab.groupTitle,
1792
+ groupColor: tab.groupColor,
1793
+ groupCollapsed: source.group.collapsed ?? null
1794
+ })).filter((tab) => typeof tab.tabId === "number");
1795
+ return {
1796
+ groupId: source.group.groupId,
1797
+ windowId: source.windowId,
1798
+ movedToWindowId: targetWindowId,
1799
+ newGroupId,
1800
+ summary: { movedTabs: tabIds.length },
1801
+ undo: {
1802
+ action: "move-group",
1803
+ groupId: source.group.groupId,
1804
+ windowId: source.windowId,
1805
+ movedToWindowId: targetWindowId,
1806
+ groupTitle: source.group.title ?? null,
1807
+ groupColor: source.group.color ?? null,
1808
+ groupCollapsed: source.group.collapsed ?? null,
1809
+ tabs: undoTabs
1810
+ },
1811
+ txid: params.txid || null
1812
+ };
1813
+ }
1814
+ }
1815
+ });
1816
+
1817
+ // dist/extension/lib/inspect.js
1818
+ var require_inspect = __commonJS({
1819
+ "dist/extension/lib/inspect.js"(exports) {
1820
+ "use strict";
1821
+ Object.defineProperty(exports, "__esModule", { value: true });
1822
+ exports.SELECTOR_VALUE_MAX_LENGTH = exports.DESCRIPTION_MAX_LENGTH = exports.DEFAULT_STALE_DAYS = void 0;
1823
+ exports.analyzeTabs = analyzeTabs;
1824
+ exports.inspectTabs = inspectTabs;
1825
+ var content2 = require_content();
1826
+ var { isScriptableUrl: isScriptableUrl2, isGitHubIssueOrPr: isGitHubIssueOrPr2, detectGitHubState: detectGitHubState2, extractPageMeta: extractPageMeta2, extractSelectorSignal: extractSelectorSignal2, waitForSettle: waitForSettle2, waitForTabReady: waitForTabReady2 } = content2;
1827
+ var tabs2 = require_tabs();
1828
+ var { normalizeUrl } = tabs2;
1829
+ exports.DEFAULT_STALE_DAYS = 30;
1830
+ exports.DESCRIPTION_MAX_LENGTH = 250;
1831
+ exports.SELECTOR_VALUE_MAX_LENGTH = 500;
1832
+ async function analyzeTabs(params, requestId, deps2) {
1833
+ const staleDays = Number.isFinite(params.staleDays) ? params.staleDays : exports.DEFAULT_STALE_DAYS;
1834
+ const checkGitHub = params.checkGitHub === true;
1835
+ const githubConcurrencyRaw = Number(params.githubConcurrency);
1836
+ const githubConcurrency = Number.isFinite(githubConcurrencyRaw) && githubConcurrencyRaw > 0 ? Math.min(10, Math.floor(githubConcurrencyRaw)) : 4;
1837
+ const githubTimeoutRaw = Number(params.githubTimeoutMs);
1838
+ const githubTimeoutMs = Number.isFinite(githubTimeoutRaw) && githubTimeoutRaw > 0 ? Math.floor(githubTimeoutRaw) : 4e3;
1839
+ const progressEnabled = params.progress === true;
1840
+ const snapshot = await deps2.getTabSnapshot();
1841
+ const selection = deps2.selectTabsByScope(snapshot, params);
1842
+ if (selection.error) {
1843
+ throw selection.error;
1844
+ }
1845
+ const selectedTabs = selection.tabs;
1846
+ const scopeTabs = selectedTabs;
1847
+ const now = Date.now();
1848
+ const startedAt = Date.now();
1849
+ let githubChecked = 0;
1850
+ let githubTotal = 0;
1851
+ let githubMatched = 0;
1852
+ const normalizedMap = /* @__PURE__ */ new Map();
1853
+ const duplicates = /* @__PURE__ */ new Map();
1854
+ for (const tab of scopeTabs) {
1855
+ const normalized = normalizeUrl(tab.url);
1856
+ if (!normalized) {
1857
+ continue;
1858
+ }
1859
+ if (normalizedMap.has(normalized)) {
1860
+ const existing = normalizedMap.get(normalized);
1861
+ duplicates.set(tab.tabId, existing.tabId);
1862
+ } else {
1863
+ normalizedMap.set(normalized, tab);
1864
+ }
1865
+ }
1866
+ const candidateMap = /* @__PURE__ */ new Map();
1867
+ const addReason = (tab, reason) => {
1868
+ const tabId = tab.tabId;
1869
+ const entry = candidateMap.get(tabId) || { tab, reasons: [] };
1870
+ entry.reasons.push(reason);
1871
+ candidateMap.set(tabId, entry);
1872
+ };
1873
+ for (const tab of selectedTabs) {
1874
+ if (duplicates.has(tab.tabId)) {
1875
+ addReason(tab, {
1876
+ type: "duplicate",
1877
+ detail: `Matches tab ${duplicates.get(tab.tabId)}`
1878
+ });
1879
+ }
1880
+ if (tab.lastFocusedAt) {
1881
+ const ageDays = (now - tab.lastFocusedAt) / (24 * 60 * 60 * 1e3);
1882
+ if (ageDays >= staleDays) {
1883
+ addReason(tab, {
1884
+ type: "stale",
1885
+ detail: `Last focused ${Math.floor(ageDays)} days ago`
1886
+ });
1887
+ }
1888
+ }
1889
+ }
1890
+ const githubTabs = checkGitHub ? selectedTabs.filter((tab) => isGitHubIssueOrPr2(tab.url) && isScriptableUrl2(tab.url)) : [];
1891
+ githubTotal = githubTabs.length;
1892
+ if (checkGitHub && githubTabs.length > 0) {
1893
+ let index = 0;
1894
+ const total = githubTabs.length;
1895
+ const workers = Array.from({ length: Math.min(githubConcurrency, total) }, async () => {
1896
+ while (true) {
1897
+ const currentIndex = index;
1898
+ if (currentIndex >= total) {
1899
+ return;
1900
+ }
1901
+ index += 1;
1902
+ const tab = githubTabs[currentIndex];
1903
+ const state2 = await detectGitHubState2(tab.tabId, githubTimeoutMs);
1904
+ githubChecked += 1;
1905
+ if (state2 === "closed" || state2 === "merged") {
1906
+ githubMatched += 1;
1907
+ addReason(tab, {
1908
+ type: "closed_issue",
1909
+ detail: `GitHub state: ${state2}`
1910
+ });
1911
+ }
1912
+ if (progressEnabled) {
1913
+ deps2.sendProgress(requestId, {
1914
+ phase: "github",
1915
+ processed: githubChecked,
1916
+ total,
1917
+ matched: githubMatched,
1918
+ tabId: tab.tabId,
1919
+ timeoutMs: githubTimeoutMs
1920
+ });
1921
+ }
1922
+ }
1923
+ });
1924
+ await Promise.all(workers);
1925
+ }
1926
+ const candidates = Array.from(candidateMap.values()).map((entry) => {
1927
+ const reasons = entry.reasons;
1928
+ const severity = reasons.some((reason) => reason.type === "duplicate" || reason.type === "closed_issue") ? "high" : "medium";
1929
+ return {
1930
+ tabId: entry.tab.tabId,
1931
+ windowId: entry.tab.windowId,
1932
+ groupId: entry.tab.groupId,
1933
+ url: entry.tab.url,
1934
+ title: entry.tab.title,
1935
+ lastFocusedAt: entry.tab.lastFocusedAt,
1936
+ reasons,
1937
+ severity
1938
+ };
1939
+ });
1940
+ return {
1941
+ generatedAt: Date.now(),
1942
+ staleDays,
1943
+ totals: {
1944
+ tabs: scopeTabs.length,
1945
+ analyzed: selectedTabs.length,
1946
+ candidates: candidates.length
1947
+ },
1948
+ meta: {
1949
+ durationMs: Date.now() - startedAt,
1950
+ githubChecked,
1951
+ githubTotal,
1952
+ githubMatched,
1953
+ githubTimeoutMs
1954
+ },
1955
+ candidates
1956
+ };
1957
+ }
1958
+ async function inspectTabs(params, requestId, deps2) {
1959
+ const signalList = Array.isArray(params.signals) && params.signals.length > 0 ? params.signals.map(String) : ["page-meta"];
1960
+ const signalConcurrencyRaw = Number(params.signalConcurrency);
1961
+ const signalConcurrency = Number.isFinite(signalConcurrencyRaw) && signalConcurrencyRaw > 0 ? Math.min(10, Math.floor(signalConcurrencyRaw)) : 4;
1962
+ const signalTimeoutRaw = Number(params.signalTimeoutMs);
1963
+ const signalTimeoutMs = Number.isFinite(signalTimeoutRaw) && signalTimeoutRaw > 0 ? Math.floor(signalTimeoutRaw) : 4e3;
1964
+ const progressEnabled = params.progress === true;
1965
+ const waitFor = typeof params.waitFor === "string" ? params.waitFor.trim().toLowerCase() : "";
1966
+ if (waitFor === "settle" && Array.isArray(params.tabIds) && params.tabIds.length > 0) {
1967
+ const waitTimeoutRaw = Number(params.waitTimeoutMs);
1968
+ const waitTimeoutMs = Number.isFinite(waitTimeoutRaw) && waitTimeoutRaw > 0 ? Math.floor(waitTimeoutRaw) : signalTimeoutMs;
1969
+ const tabIds = params.tabIds.map(Number).filter(Number.isFinite);
1970
+ await Promise.all(tabIds.map((id) => waitForSettle2(id, waitTimeoutMs)));
1971
+ }
1972
+ const snapshot = await deps2.getTabSnapshot();
1973
+ const selection = deps2.selectTabsByScope(snapshot, params);
1974
+ if (selection.error) {
1975
+ throw selection.error;
1976
+ }
1977
+ const tabs3 = selection.tabs;
1978
+ const startedAt = Date.now();
1979
+ const selectorSpecs = [];
1980
+ if (Array.isArray(params.selectorSpecs)) {
1981
+ selectorSpecs.push(...params.selectorSpecs);
1982
+ }
1983
+ if (params.signalConfig && typeof params.signalConfig === "object") {
1984
+ const config = params.signalConfig;
1985
+ if (Array.isArray(config.selectors)) {
1986
+ selectorSpecs.push(...config.selectors);
1987
+ }
1988
+ if (config.signals && typeof config.signals === "object") {
1989
+ const signals = config.signals;
1990
+ const selectorConfig = signals.selector;
1991
+ if (selectorConfig && Array.isArray(selectorConfig.selectors)) {
1992
+ selectorSpecs.push(...selectorConfig.selectors);
1993
+ }
1994
+ }
1995
+ }
1996
+ const normalizedSelectors = selectorSpecs.filter((spec) => spec && typeof spec.selector === "string" && spec.selector.length > 0).map((spec) => ({
1997
+ name: typeof spec.name === "string" ? spec.name : void 0,
1998
+ selector: spec.selector,
1999
+ attr: typeof spec.attr === "string" ? spec.attr : "text",
2000
+ all: Boolean(spec.all),
2001
+ text: typeof spec.text === "string" && spec.text.trim() ? spec.text.trim() : void 0,
2002
+ textMode: typeof spec.textMode === "string" ? spec.textMode.trim().toLowerCase() : void 0
2003
+ }));
2004
+ const selectorWarnings = normalizedSelectors.filter((spec) => typeof spec.selector === "string" && spec.selector.includes(":contains(")).map((spec) => ({
2005
+ name: spec.name || spec.selector,
2006
+ hint: "CSS :contains() is not supported; use selector text filters or a different selector."
2007
+ }));
2008
+ const signalDefs = [];
2009
+ for (const signalId of signalList) {
2010
+ if (signalId === "github-state") {
2011
+ signalDefs.push({
2012
+ id: signalId,
2013
+ match: (tab) => isGitHubIssueOrPr2(tab.url) && isScriptableUrl2(tab.url),
2014
+ run: async (tabId) => {
2015
+ const state2 = await detectGitHubState2(tabId, signalTimeoutMs);
2016
+ return state2 ? { state: state2 } : null;
2017
+ }
2018
+ });
2019
+ } else if (signalId === "page-meta") {
2020
+ signalDefs.push({
2021
+ id: signalId,
2022
+ match: (tab) => isScriptableUrl2(tab.url),
2023
+ run: async (tabId) => extractPageMeta2(tabId, signalTimeoutMs, exports.DESCRIPTION_MAX_LENGTH)
2024
+ });
2025
+ } else if (signalId === "selector") {
2026
+ signalDefs.push({
2027
+ id: signalId,
2028
+ match: (tab) => isScriptableUrl2(tab.url),
2029
+ run: async (tabId) => extractSelectorSignal2(tabId, normalizedSelectors, signalTimeoutMs, exports.SELECTOR_VALUE_MAX_LENGTH)
2030
+ });
2031
+ }
2032
+ }
2033
+ const tasks = [];
2034
+ for (const tab of tabs3) {
2035
+ for (const signal of signalDefs) {
2036
+ if (signal.match(tab)) {
2037
+ tasks.push({ tab, signal });
2038
+ }
2039
+ }
2040
+ }
2041
+ const totalTasks = tasks.length;
2042
+ let completedTasks = 0;
2043
+ const entryMap = /* @__PURE__ */ new Map();
2044
+ const workerCount = Math.min(signalConcurrency, totalTasks || 1);
2045
+ let index = 0;
2046
+ const workers = Array.from({ length: workerCount }, async () => {
2047
+ while (true) {
2048
+ const currentIndex = index;
2049
+ if (currentIndex >= totalTasks) {
2050
+ return;
2051
+ }
2052
+ index += 1;
2053
+ const task = tasks[currentIndex];
2054
+ const tabId = task.tab.tabId;
2055
+ let result = null;
2056
+ let error = null;
2057
+ const started = Date.now();
2058
+ try {
2059
+ await waitForTabReady2(tabId, params, signalTimeoutMs);
2060
+ result = await task.signal.run(tabId);
2061
+ } catch (err) {
2062
+ const message = err instanceof Error ? err.message : "signal_error";
2063
+ error = message;
2064
+ }
2065
+ const durationMs = Date.now() - started;
2066
+ const entry = entryMap.get(tabId) || { tab: task.tab, signals: {} };
2067
+ entry.signals[task.signal.id] = {
2068
+ ok: error === null,
2069
+ durationMs,
2070
+ data: result,
2071
+ error
2072
+ };
2073
+ entryMap.set(tabId, entry);
2074
+ completedTasks += 1;
2075
+ if (progressEnabled) {
2076
+ deps2.sendProgress(requestId, {
2077
+ phase: "inspect",
2078
+ processed: completedTasks,
2079
+ total: totalTasks,
2080
+ signalId: task.signal.id,
2081
+ tabId
2082
+ });
2083
+ }
2084
+ }
2085
+ });
2086
+ await Promise.all(workers);
2087
+ const entries = Array.from(entryMap.values()).map((entry) => ({
2088
+ tabId: entry.tab.tabId,
2089
+ windowId: entry.tab.windowId,
2090
+ groupId: entry.tab.groupId,
2091
+ url: entry.tab.url,
2092
+ title: entry.tab.title,
2093
+ signals: entry.signals
2094
+ }));
2095
+ return {
2096
+ generatedAt: Date.now(),
2097
+ totals: {
2098
+ tabs: tabs3.length,
2099
+ signals: signalDefs.length,
2100
+ tasks: totalTasks
2101
+ },
2102
+ meta: {
2103
+ durationMs: Date.now() - startedAt,
2104
+ signalTimeoutMs,
2105
+ selectorCount: normalizedSelectors.length,
2106
+ selectorWarnings: selectorWarnings.length > 0 ? selectorWarnings : void 0
2107
+ },
2108
+ entries
2109
+ };
2110
+ }
2111
+ }
2112
+ });
2113
+
2114
+ // dist/extension/lib/undo-handlers.js
2115
+ var require_undo_handlers = __commonJS({
2116
+ "dist/extension/lib/undo-handlers.js"(exports) {
2117
+ "use strict";
2118
+ Object.defineProperty(exports, "__esModule", { value: true });
2119
+ exports.undoGroupUpdate = undoGroupUpdate;
2120
+ exports.undoGroupUngroup = undoGroupUngroup;
2121
+ exports.undoGroupAssign = undoGroupAssign;
2122
+ exports.undoMoveTab = undoMoveTab;
2123
+ exports.undoMoveGroup = undoMoveGroup;
2124
+ exports.undoMergeWindow = undoMergeWindow;
2125
+ exports.undoArchive = undoArchive;
2126
+ exports.undoClose = undoClose;
2127
+ exports.undoTransaction = undoTransaction;
2128
+ async function ensureWindow(windowId) {
2129
+ if (windowId) {
2130
+ try {
2131
+ const existing = await chrome.windows.get(windowId);
2132
+ if (existing) {
2133
+ return windowId;
2134
+ }
2135
+ } catch {
2136
+ }
2137
+ }
2138
+ const created = await chrome.windows.create({ focused: false });
2139
+ return created.id;
2140
+ }
2141
+ async function restoreTabsFromUndo(entries, deps2) {
2142
+ const skipped = [];
2143
+ const restored = [];
2144
+ const windowMap = /* @__PURE__ */ new Map();
2145
+ for (const entry of entries) {
2146
+ const tabId = Number(entry.tabId);
2147
+ if (!Number.isFinite(tabId)) {
2148
+ skipped.push({ tabId: entry.tabId, reason: "missing_tab" });
2149
+ continue;
2150
+ }
2151
+ const sourceWindowId = Number(entry.windowId);
2152
+ if (!Number.isFinite(sourceWindowId)) {
2153
+ skipped.push({ tabId, reason: "missing_window" });
2154
+ continue;
2155
+ }
2156
+ let targetWindowId = windowMap.get(sourceWindowId);
2157
+ if (!targetWindowId) {
2158
+ targetWindowId = await ensureWindow(sourceWindowId);
2159
+ windowMap.set(sourceWindowId, targetWindowId);
2160
+ }
2161
+ try {
2162
+ await chrome.tabs.move(tabId, { windowId: targetWindowId, index: -1 });
2163
+ restored.push({ tabId, entry, targetWindowId });
2164
+ } catch {
2165
+ skipped.push({ tabId, reason: "move_failed" });
2166
+ }
2167
+ }
2168
+ const groupsByWindow = /* @__PURE__ */ new Map();
2169
+ for (const item of restored) {
2170
+ const entry = item.entry;
2171
+ const rawGroupId = typeof entry.groupId === "number" ? entry.groupId : null;
2172
+ const groupId = rawGroupId != null && rawGroupId !== -1 ? rawGroupId : null;
2173
+ const groupTitle = typeof entry.groupTitle === "string" ? entry.groupTitle : null;
2174
+ if (!groupId && !groupTitle) {
2175
+ continue;
2176
+ }
2177
+ const groupKey = groupId != null ? `id:${groupId}` : `title:${groupTitle}`;
2178
+ const key = `${item.targetWindowId}:${groupKey}`;
2179
+ if (!groupsByWindow.has(key)) {
2180
+ groupsByWindow.set(key, {
2181
+ windowId: item.targetWindowId,
2182
+ groupId,
2183
+ groupTitle,
2184
+ groupColor: typeof entry.groupColor === "string" ? entry.groupColor : null,
2185
+ groupCollapsed: typeof entry.groupCollapsed === "boolean" ? entry.groupCollapsed : null,
2186
+ tabIds: []
2187
+ });
2188
+ }
2189
+ groupsByWindow.get(key)?.tabIds.push(item.tabId);
2190
+ }
2191
+ for (const group of groupsByWindow.values()) {
2192
+ if (group.tabIds.length === 0) {
2193
+ continue;
2194
+ }
2195
+ let targetGroupId = null;
2196
+ if (group.groupId != null) {
2197
+ try {
2198
+ targetGroupId = await chrome.tabs.group({ groupId: group.groupId, tabIds: group.tabIds });
2199
+ } catch {
2200
+ targetGroupId = null;
2201
+ }
2202
+ }
2203
+ if (targetGroupId == null) {
2204
+ try {
2205
+ targetGroupId = await chrome.tabs.group({
2206
+ tabIds: group.tabIds,
2207
+ createProperties: { windowId: group.windowId }
2208
+ });
2209
+ } catch (error) {
2210
+ deps2.log("Failed to regroup tabs", error);
2211
+ continue;
2212
+ }
2213
+ }
2214
+ const update = {};
2215
+ if (typeof group.groupTitle === "string") {
2216
+ update.title = group.groupTitle;
2217
+ }
2218
+ if (typeof group.groupColor === "string" && group.groupColor) {
2219
+ update.color = group.groupColor;
2220
+ }
2221
+ if (typeof group.groupCollapsed === "boolean") {
2222
+ update.collapsed = group.groupCollapsed;
2223
+ }
2224
+ if (Object.keys(update).length > 0) {
2225
+ try {
2226
+ await chrome.tabGroups.update(targetGroupId, update);
2227
+ } catch (error) {
2228
+ deps2.log("Failed to update restored group", error);
2229
+ }
2230
+ }
2231
+ }
2232
+ const orderByWindow = /* @__PURE__ */ new Map();
2233
+ for (const item of restored) {
2234
+ const index = Number(item.entry.index);
2235
+ if (!Number.isFinite(index)) {
2236
+ continue;
2237
+ }
2238
+ if (!orderByWindow.has(item.targetWindowId)) {
2239
+ orderByWindow.set(item.targetWindowId, []);
2240
+ }
2241
+ orderByWindow.get(item.targetWindowId)?.push({ tabId: item.tabId, index });
2242
+ }
2243
+ for (const [targetWindowId, items] of orderByWindow.entries()) {
2244
+ const ordered = [...items].sort((a, b) => a.index - b.index);
2245
+ for (const item of ordered) {
2246
+ try {
2247
+ await chrome.tabs.move(item.tabId, { windowId: targetWindowId, index: item.index });
2248
+ } catch {
2249
+ }
2250
+ }
2251
+ }
2252
+ return {
2253
+ summary: {
2254
+ restoredTabs: restored.length,
2255
+ skippedTabs: skipped.length
2256
+ },
2257
+ skipped
2258
+ };
2259
+ }
2260
+ async function undoGroupUpdate(undo) {
2261
+ const groupId = Number(undo.groupId);
2262
+ if (!Number.isFinite(groupId)) {
2263
+ return {
2264
+ summary: { restoredGroups: 0, skippedGroups: 1 },
2265
+ skipped: [{ groupId: undo.groupId, reason: "missing_group" }]
2266
+ };
2267
+ }
2268
+ const previous = undo.previous || {};
2269
+ const update = {};
2270
+ if (typeof previous.title === "string") {
2271
+ update.title = previous.title;
2272
+ }
2273
+ if (typeof previous.color === "string" && previous.color) {
2274
+ update.color = previous.color;
2275
+ }
2276
+ if (typeof previous.collapsed === "boolean") {
2277
+ update.collapsed = previous.collapsed;
2278
+ }
2279
+ if (!Object.keys(update).length) {
2280
+ return {
2281
+ summary: { restoredGroups: 0, skippedGroups: 1 },
2282
+ skipped: [{ groupId, reason: "missing_values" }]
2283
+ };
2284
+ }
2285
+ try {
2286
+ await chrome.tabGroups.update(groupId, update);
2287
+ return {
2288
+ summary: { restoredGroups: 1, skippedGroups: 0 },
2289
+ skipped: []
2290
+ };
2291
+ } catch {
2292
+ return {
2293
+ summary: { restoredGroups: 0, skippedGroups: 1 },
2294
+ skipped: [{ groupId, reason: "update_failed" }]
2295
+ };
2296
+ }
2297
+ }
2298
+ async function undoGroupUngroup(undo, deps2) {
2299
+ const tabs2 = undo.tabs || [];
2300
+ return await restoreTabsFromUndo(tabs2, deps2);
2301
+ }
2302
+ async function undoGroupAssign(undo, deps2) {
2303
+ const tabs2 = undo.tabs || [];
2304
+ return await restoreTabsFromUndo(tabs2, deps2);
2305
+ }
2306
+ async function undoMoveTab(undo, deps2) {
2307
+ const from = undo.from || {};
2308
+ const entry = {
2309
+ tabId: undo.tabId,
2310
+ windowId: from.windowId,
2311
+ index: from.index,
2312
+ groupId: from.groupId,
2313
+ groupTitle: from.groupTitle,
2314
+ groupColor: from.groupColor,
2315
+ groupCollapsed: from.groupCollapsed
2316
+ };
2317
+ return await restoreTabsFromUndo([entry], deps2);
2318
+ }
2319
+ async function undoMoveGroup(undo, deps2) {
2320
+ const tabs2 = undo.tabs || [];
2321
+ return await restoreTabsFromUndo(tabs2, deps2);
2322
+ }
2323
+ async function undoMergeWindow(undo, deps2) {
2324
+ const tabs2 = undo.tabs || [];
2325
+ return await restoreTabsFromUndo(tabs2, deps2);
2326
+ }
2327
+ async function undoArchive(undo, deps2) {
2328
+ const tabs2 = undo.tabs || [];
2329
+ const restored = [];
2330
+ const skipped = [];
2331
+ const windowMap = /* @__PURE__ */ new Map();
2332
+ for (const entry of tabs2) {
2333
+ if (!entry.tabId) {
2334
+ skipped.push({ tabId: entry.tabId, reason: "missing_tab" });
2335
+ continue;
2336
+ }
2337
+ let targetWindowId = windowMap.get(entry.from.windowId);
2338
+ if (!targetWindowId) {
2339
+ targetWindowId = await ensureWindow(entry.from.windowId);
2340
+ windowMap.set(entry.from.windowId, targetWindowId);
2341
+ }
2342
+ try {
2343
+ await chrome.tabs.move(entry.tabId, { windowId: targetWindowId, index: -1 });
2344
+ restored.push({ tabId: entry.tabId, targetWindowId });
2345
+ } catch {
2346
+ skipped.push({ tabId: entry.tabId, reason: "move_failed" });
2347
+ }
2348
+ }
2349
+ const restoredSet = new Set(restored.map((item) => item.tabId));
2350
+ const groupsByWindow = /* @__PURE__ */ new Map();
2351
+ for (const entry of tabs2) {
2352
+ if (!restoredSet.has(entry.tabId)) {
2353
+ continue;
2354
+ }
2355
+ const targetWindowId = windowMap.get(entry.from.windowId) || entry.from.windowId;
2356
+ const hasGroupId = entry.from.groupId != null && entry.from.groupId !== -1;
2357
+ const hasGroupTitle = entry.from.groupTitle != null;
2358
+ if (!hasGroupId && !hasGroupTitle) {
2359
+ continue;
2360
+ }
2361
+ const groupKey = hasGroupId ? entry.from.groupId : entry.from.groupTitle;
2362
+ const key = `${targetWindowId}:${groupKey}`;
2363
+ if (!groupsByWindow.has(key)) {
2364
+ groupsByWindow.set(key, []);
2365
+ }
2366
+ groupsByWindow.get(key)?.push(entry);
2367
+ }
2368
+ for (const [key, groupTabs] of groupsByWindow.entries()) {
2369
+ const [windowIdPart] = key.split(":");
2370
+ const targetWindowId = Number(windowIdPart);
2371
+ const tabIds = groupTabs.map((entry) => entry.tabId).filter(Boolean);
2372
+ if (!tabIds.length) {
2373
+ continue;
2374
+ }
2375
+ try {
2376
+ const groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId: targetWindowId } });
2377
+ await chrome.tabGroups.update(groupId, {
2378
+ title: groupTabs[0].from.groupTitle || "",
2379
+ color: groupTabs[0].from.groupColor || "grey",
2380
+ collapsed: groupTabs[0].from.groupCollapsed || false
2381
+ });
2382
+ } catch (error) {
2383
+ deps2.log("Failed to recreate group", error);
2384
+ }
2385
+ }
2386
+ for (const [originalWindowId, targetWindowId] of windowMap.entries()) {
2387
+ const windowTabs = tabs2.filter((entry) => entry.from.windowId === originalWindowId && restoredSet.has(entry.tabId)).sort((a, b) => a.from.index - b.from.index);
2388
+ for (const entry of windowTabs) {
2389
+ if (!entry.tabId) {
2390
+ continue;
2391
+ }
2392
+ try {
2393
+ await chrome.tabs.move(entry.tabId, { windowId: targetWindowId, index: entry.from.index });
2394
+ } catch {
2395
+ }
2396
+ }
2397
+ const activeTab = windowTabs.find((entry) => entry.active);
2398
+ if (activeTab && activeTab.tabId) {
2399
+ try {
2400
+ await chrome.tabs.update(activeTab.tabId, { active: true });
2401
+ } catch {
2402
+ }
2403
+ }
2404
+ }
2405
+ return {
2406
+ summary: {
2407
+ restoredTabs: restored.length,
2408
+ skippedTabs: skipped.length
2409
+ },
2410
+ skipped
2411
+ };
2412
+ }
2413
+ async function undoClose(undo, deps2) {
2414
+ const tabs2 = undo.tabs || [];
2415
+ const restored = [];
2416
+ const skipped = [];
2417
+ const windowMap = /* @__PURE__ */ new Map();
2418
+ for (const entry of tabs2) {
2419
+ if (!entry.url) {
2420
+ skipped.push({ url: entry.url, reason: "missing_url" });
2421
+ continue;
2422
+ }
2423
+ let targetWindowId = windowMap.get(entry.from.windowId);
2424
+ if (!targetWindowId) {
2425
+ targetWindowId = await ensureWindow(entry.from.windowId);
2426
+ windowMap.set(entry.from.windowId, targetWindowId);
2427
+ }
2428
+ try {
2429
+ const created = await chrome.tabs.create({
2430
+ windowId: targetWindowId,
2431
+ url: entry.url,
2432
+ active: false,
2433
+ pinned: entry.pinned
2434
+ });
2435
+ restored.push({ tabId: created.id, entry });
2436
+ } catch {
2437
+ skipped.push({ url: entry.url, reason: "create_failed" });
2438
+ }
2439
+ }
2440
+ const groupsByWindow = /* @__PURE__ */ new Map();
2441
+ for (const item of restored) {
2442
+ const entry = item.entry;
2443
+ const targetWindowId = windowMap.get(entry.from.windowId) || entry.from.windowId;
2444
+ const hasGroupId = entry.from.groupId != null && entry.from.groupId !== -1;
2445
+ const hasGroupTitle = entry.from.groupTitle != null;
2446
+ if (!hasGroupId && !hasGroupTitle) {
2447
+ continue;
2448
+ }
2449
+ const groupKey = hasGroupId ? entry.from.groupId : entry.from.groupTitle;
2450
+ const key = `${targetWindowId}:${groupKey}`;
2451
+ if (!groupsByWindow.has(key)) {
2452
+ groupsByWindow.set(key, []);
2453
+ }
2454
+ groupsByWindow.get(key)?.push({ tabId: item.tabId, entry });
2455
+ }
2456
+ for (const [key, groupTabs] of groupsByWindow.entries()) {
2457
+ const [windowIdPart] = key.split(":");
2458
+ const targetWindowId = Number(windowIdPart);
2459
+ const tabIds = groupTabs.map((item) => item.tabId).filter(Boolean);
2460
+ if (!tabIds.length) {
2461
+ continue;
2462
+ }
2463
+ try {
2464
+ const groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId: targetWindowId } });
2465
+ await chrome.tabGroups.update(groupId, {
2466
+ title: groupTabs[0].entry.from.groupTitle || "",
2467
+ color: groupTabs[0].entry.from.groupColor || "grey",
2468
+ collapsed: groupTabs[0].entry.from.groupCollapsed || false
2469
+ });
2470
+ } catch (error) {
2471
+ deps2.log("Failed to recreate group", error);
2472
+ }
2473
+ }
2474
+ for (const [originalWindowId, targetWindowId] of windowMap.entries()) {
2475
+ const windowTabs = restored.map((item) => ({ tabId: item.tabId, entry: item.entry })).filter((item) => item.entry.from.windowId === originalWindowId).sort((a, b) => a.entry.from.index - b.entry.from.index);
2476
+ for (const item of windowTabs) {
2477
+ try {
2478
+ await chrome.tabs.move(item.tabId, { windowId: targetWindowId, index: item.entry.from.index });
2479
+ } catch {
2480
+ }
2481
+ }
2482
+ const activeTab = windowTabs.find((item) => item.entry.active);
2483
+ if (activeTab) {
2484
+ try {
2485
+ await chrome.tabs.update(activeTab.tabId, { active: true });
2486
+ } catch {
2487
+ }
2488
+ }
2489
+ }
2490
+ return {
2491
+ summary: {
2492
+ restoredTabs: restored.length,
2493
+ skippedTabs: skipped.length
2494
+ },
2495
+ skipped
2496
+ };
2497
+ }
2498
+ async function undoTransaction(params, deps2) {
2499
+ if (!params.record || !params.record.undo) {
2500
+ throw new Error("Undo record missing");
2501
+ }
2502
+ const undo = params.record.undo;
2503
+ if (undo.action === "archive") {
2504
+ return await undoArchive(undo, deps2);
2505
+ }
2506
+ if (undo.action === "close") {
2507
+ return await undoClose(undo, deps2);
2508
+ }
2509
+ if (undo.action === "group-update") {
2510
+ return await undoGroupUpdate(undo);
2511
+ }
2512
+ if (undo.action === "group-ungroup") {
2513
+ return await undoGroupUngroup(undo, deps2);
2514
+ }
2515
+ if (undo.action === "group-assign") {
2516
+ return await undoGroupAssign(undo, deps2);
2517
+ }
2518
+ if (undo.action === "move-tab") {
2519
+ return await undoMoveTab(undo, deps2);
2520
+ }
2521
+ if (undo.action === "move-group") {
2522
+ return await undoMoveGroup(undo, deps2);
2523
+ }
2524
+ if (undo.action === "merge-window") {
2525
+ return await undoMergeWindow(undo, deps2);
2526
+ }
2527
+ throw new Error(`Unknown undo action: ${undo.action}`);
2528
+ }
2529
+ }
2530
+ });
2531
+
2532
+ // dist/extension/lib/archive.js
2533
+ var require_archive = __commonJS({
2534
+ "dist/extension/lib/archive.js"(exports) {
2535
+ "use strict";
2536
+ Object.defineProperty(exports, "__esModule", { value: true });
2537
+ exports.ensureArchiveWindow = ensureArchiveWindow;
2538
+ exports.archiveTabs = archiveTabs;
2539
+ exports.getTabsByIds = getTabsByIds;
2540
+ exports.closeTabs = closeTabs;
2541
+ exports.mergeWindow = mergeWindow;
2542
+ async function ensureArchiveWindow(deps2) {
2543
+ const archiveWindowId = await deps2.getArchiveWindowId();
2544
+ if (archiveWindowId) {
2545
+ try {
2546
+ await chrome.windows.get(archiveWindowId);
2547
+ return archiveWindowId;
2548
+ } catch {
2549
+ await deps2.setArchiveWindowId(null);
2550
+ }
2551
+ }
2552
+ const created = await chrome.windows.create({ focused: false });
2553
+ await deps2.setArchiveWindowId(created.id);
2554
+ return created.id;
2555
+ }
2556
+ async function archiveTabs(params, deps2) {
2557
+ const snapshot = await deps2.getTabSnapshot();
2558
+ const windowLabels = deps2.buildWindowLabels(snapshot);
2559
+ let windowsToProcess = snapshot.windows;
2560
+ if (params.windowId) {
2561
+ const resolvedWindowId = deps2.resolveWindowIdFromParams(snapshot, params.windowId);
2562
+ windowsToProcess = resolvedWindowId != null ? windowsToProcess.filter((win) => win.windowId === resolvedWindowId) : [];
2563
+ } else if (!params.all) {
2564
+ const focused = windowsToProcess.find((win) => win.focused);
2565
+ windowsToProcess = focused ? [focused] : [];
2566
+ }
2567
+ if (params.groupTitle || params.groupId || params.tabIds) {
2568
+ const selected = deps2.selectTabsByScope(snapshot, params);
2569
+ if (selected.error) {
2570
+ throw selected.error;
2571
+ }
2572
+ windowsToProcess = snapshot.windows.map((win) => ({
2573
+ windowId: win.windowId,
2574
+ focused: win.focused,
2575
+ state: win.state,
2576
+ tabs: win.tabs.filter((tab) => selected.tabs.some((sel) => sel.tabId === tab.tabId)),
2577
+ groups: win.groups
2578
+ })).filter((win) => win.tabs.length > 0);
2579
+ }
2580
+ if (windowsToProcess.length === 0) {
2581
+ return {
2582
+ txid: params.txid || null,
2583
+ summary: { movedTabs: 0, movedGroups: 0, skippedTabs: 0 },
2584
+ archiveWindowId: null,
2585
+ skipped: [],
2586
+ undo: { action: "archive", tabs: [] }
2587
+ };
2588
+ }
2589
+ const archiveWindowId = await ensureArchiveWindow(deps2);
2590
+ const undoTabs = [];
2591
+ const skipped = [];
2592
+ let movedGroups = 0;
2593
+ let movedTabs = 0;
2594
+ for (const window2 of windowsToProcess) {
2595
+ const groupsById = /* @__PURE__ */ new Map();
2596
+ for (const group of window2.groups) {
2597
+ groupsById.set(group.groupId, group);
2598
+ }
2599
+ const groupedTabs = /* @__PURE__ */ new Map();
2600
+ const ungroupedTabs = [];
2601
+ for (const tab of window2.tabs) {
2602
+ if (tab.tabId == null) {
2603
+ continue;
2604
+ }
2605
+ if (tab.groupId === -1) {
2606
+ ungroupedTabs.push(tab);
2607
+ } else {
2608
+ if (!groupedTabs.has(tab.groupId)) {
2609
+ groupedTabs.set(tab.groupId, []);
2610
+ }
2611
+ groupedTabs.get(tab.groupId)?.push(tab);
2612
+ }
2613
+ }
2614
+ const windowLabel = windowLabels.get(window2.windowId) || `W${window2.windowId}`;
2615
+ const plans = [];
2616
+ for (const [groupId, tabs2] of groupedTabs.entries()) {
2617
+ const group = groupsById.get(groupId) || null;
2618
+ plans.push({
2619
+ windowId: window2.windowId,
2620
+ windowLabel,
2621
+ group,
2622
+ tabs: tabs2,
2623
+ isUngrouped: false
2624
+ });
2625
+ }
2626
+ if (ungroupedTabs.length > 0) {
2627
+ plans.push({
2628
+ windowId: window2.windowId,
2629
+ windowLabel,
2630
+ group: null,
2631
+ tabs: ungroupedTabs,
2632
+ isUngrouped: true
2633
+ });
2634
+ }
2635
+ for (const plan of plans) {
2636
+ const tabIds = plan.tabs.map((tab) => tab.tabId);
2637
+ if (tabIds.length === 0) {
2638
+ continue;
2639
+ }
2640
+ const tabById = /* @__PURE__ */ new Map();
2641
+ for (const tab of plan.tabs) {
2642
+ tabById.set(tab.tabId, tab);
2643
+ }
2644
+ let moved;
2645
+ try {
2646
+ moved = await chrome.tabs.move(tabIds, { windowId: archiveWindowId, index: -1 });
2647
+ } catch (error) {
2648
+ for (const tabId of tabIds) {
2649
+ skipped.push({ tabId, reason: "move_failed" });
2650
+ }
2651
+ deps2.log("Failed to move tabs", error);
2652
+ continue;
2653
+ }
2654
+ const movedList = Array.isArray(moved) ? moved : [moved];
2655
+ const movedIds = movedList.map((tab) => tab.id);
2656
+ movedTabs += movedIds.length;
2657
+ for (const movedId of movedIds) {
2658
+ const tab = tabById.get(movedId);
2659
+ if (!tab) {
2660
+ continue;
2661
+ }
2662
+ undoTabs.push({
2663
+ tabId: tab.tabId,
2664
+ url: tab.url,
2665
+ title: tab.title,
2666
+ pinned: tab.pinned,
2667
+ active: tab.active,
2668
+ from: {
2669
+ windowId: tab.windowId,
2670
+ index: tab.index,
2671
+ groupId: tab.groupId,
2672
+ groupTitle: plan.group ? plan.group.title : null,
2673
+ groupColor: plan.group ? plan.group.color : null,
2674
+ groupCollapsed: plan.group ? plan.group.collapsed : null
2675
+ }
2676
+ });
2677
+ }
2678
+ const titleBase = plan.group && plan.group.title ? plan.group.title : plan.isUngrouped ? "Ungrouped" : "Group";
2679
+ const archiveTitle = `${plan.windowLabel} - ${titleBase}`;
2680
+ const groupColor = plan.group && plan.group.color ? plan.group.color : "grey";
2681
+ try {
2682
+ const newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: archiveWindowId } });
2683
+ await chrome.tabGroups.update(newGroupId, { title: archiveTitle, color: groupColor });
2684
+ movedGroups += 1;
2685
+ } catch (error) {
2686
+ deps2.log("Failed to group archived tabs", error);
2687
+ }
2688
+ }
2689
+ }
2690
+ return {
2691
+ txid: params.txid || null,
2692
+ summary: {
2693
+ movedTabs,
2694
+ movedGroups,
2695
+ skippedTabs: skipped.length
2696
+ },
2697
+ archiveWindowId,
2698
+ skipped,
2699
+ undo: {
2700
+ action: "archive",
2701
+ tabs: undoTabs
2702
+ }
2703
+ };
2704
+ }
2705
+ async function getTabsByIds(tabIds) {
2706
+ const results = [];
2707
+ for (const tabId of tabIds) {
2708
+ try {
2709
+ const tab = await chrome.tabs.get(tabId);
2710
+ results.push(tab);
2711
+ } catch {
2712
+ results.push(null);
2713
+ }
2714
+ }
2715
+ return results;
2716
+ }
2717
+ async function closeTabs(params, deps2) {
2718
+ const mode = params.mode || "direct";
2719
+ if (mode === "direct" && !params.confirmed) {
2720
+ throw new Error("Direct close requires confirmation");
2721
+ }
2722
+ let tabIds = params.tabIds || [];
2723
+ if (!tabIds.length && (params.groupTitle || params.groupId || params.windowId)) {
2724
+ const snapshot = await deps2.getTabSnapshot();
2725
+ const selection = deps2.selectTabsByScope(snapshot, params);
2726
+ if (selection.error) {
2727
+ throw selection.error;
2728
+ }
2729
+ tabIds = selection.tabs.map((tab) => tab.tabId);
2730
+ }
2731
+ if (!tabIds.length) {
2732
+ return {
2733
+ txid: params.txid || null,
2734
+ summary: { closedTabs: 0, skippedTabs: 0 },
2735
+ skipped: [],
2736
+ undo: { action: "close", tabs: [] }
2737
+ };
2738
+ }
2739
+ const expectedUrls = params.expectedUrls || {};
2740
+ const tabInfos = await getTabsByIds(tabIds);
2741
+ const validTabs = [];
2742
+ const skipped = [];
2743
+ const groups2 = await chrome.tabGroups.query({});
2744
+ const groupById = new Map(groups2.map((group) => [group.id, group]));
2745
+ for (let i = 0; i < tabIds.length; i += 1) {
2746
+ const tabId = tabIds[i];
2747
+ const tab = tabInfos[i];
2748
+ if (!tab) {
2749
+ skipped.push({ tabId, reason: "not_found" });
2750
+ continue;
2751
+ }
2752
+ const expected = expectedUrls[String(tabId)];
2753
+ if (expected && tab.url !== expected) {
2754
+ skipped.push({ tabId, reason: "url_mismatch" });
2755
+ continue;
2756
+ }
2757
+ const group = tab.groupId !== -1 ? groupById.get(tab.groupId) : null;
2758
+ validTabs.push({
2759
+ tabId,
2760
+ url: tab.url,
2761
+ title: tab.title,
2762
+ pinned: tab.pinned,
2763
+ active: tab.active,
2764
+ from: {
2765
+ windowId: tab.windowId,
2766
+ index: tab.index,
2767
+ groupId: tab.groupId,
2768
+ groupTitle: group ? group.title : null,
2769
+ groupColor: group ? group.color : null,
2770
+ groupCollapsed: group ? group.collapsed : null
2771
+ }
2772
+ });
2773
+ }
2774
+ if (validTabs.length > 0) {
2775
+ await chrome.tabs.remove(validTabs.map((tab) => tab.tabId));
2776
+ }
2777
+ return {
2778
+ txid: params.txid || null,
2779
+ summary: {
2780
+ closedTabs: validTabs.length,
2781
+ skippedTabs: skipped.length
2782
+ },
2783
+ skipped,
2784
+ undo: {
2785
+ action: "close",
2786
+ tabs: validTabs.map((tab) => ({
2787
+ url: tab.url,
2788
+ title: tab.title,
2789
+ pinned: tab.pinned,
2790
+ active: tab.active,
2791
+ from: tab.from
2792
+ }))
2793
+ }
2794
+ };
2795
+ }
2796
+ async function mergeWindow(params, deps2) {
2797
+ const fromWindowId = Number.isFinite(params.fromWindowId) ? Number(params.fromWindowId) : Number(params.windowId);
2798
+ const toWindowId = Number.isFinite(params.toWindowId) ? Number(params.toWindowId) : null;
2799
+ if (!Number.isFinite(fromWindowId) || !Number.isFinite(toWindowId)) {
2800
+ throw new Error("Missing source or target window id");
2801
+ }
2802
+ if (fromWindowId === toWindowId) {
2803
+ throw new Error("Source and target windows must differ");
2804
+ }
2805
+ const snapshot = await deps2.getTabSnapshot();
2806
+ const windows = snapshot.windows;
2807
+ const sourceWindow = windows.find((win) => win.windowId === fromWindowId);
2808
+ if (!sourceWindow) {
2809
+ throw new Error("Source window not found");
2810
+ }
2811
+ const targetWindow = windows.find((win) => win.windowId === toWindowId);
2812
+ if (!targetWindow) {
2813
+ throw new Error("Target window not found");
2814
+ }
2815
+ const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
2816
+ const tabIdSet = new Set(rawTabIds.filter((id) => Number.isFinite(id)));
2817
+ const skipped = [];
2818
+ let selectedTabs = sourceWindow.tabs;
2819
+ if (tabIdSet.size > 0) {
2820
+ const sourceTabIds = new Set(sourceWindow.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number"));
2821
+ for (const tabId of tabIdSet) {
2822
+ if (!sourceTabIds.has(tabId)) {
2823
+ skipped.push({ tabId, reason: "not_in_source" });
2824
+ }
2825
+ }
2826
+ selectedTabs = sourceWindow.tabs.filter((tab) => tabIdSet.has(tab.tabId));
2827
+ }
2828
+ if (selectedTabs.length === 0) {
2829
+ return {
2830
+ fromWindowId,
2831
+ toWindowId,
2832
+ summary: { movedTabs: 0, movedGroups: 0, skippedTabs: skipped.length, closedSource: false },
2833
+ skipped,
2834
+ groups: [],
2835
+ undo: {
2836
+ action: "merge-window",
2837
+ fromWindowId,
2838
+ toWindowId,
2839
+ closedSource: false,
2840
+ tabs: []
2841
+ }
2842
+ };
2843
+ }
2844
+ const orderedTabs = [...selectedTabs].sort((a, b) => {
2845
+ const aIndex = Number(a.index);
2846
+ const bIndex = Number(b.index);
2847
+ if (!Number.isFinite(aIndex) && !Number.isFinite(bIndex)) {
2848
+ return 0;
2849
+ }
2850
+ if (!Number.isFinite(aIndex)) {
2851
+ return 1;
2852
+ }
2853
+ if (!Number.isFinite(bIndex)) {
2854
+ return -1;
2855
+ }
2856
+ return aIndex - bIndex;
2857
+ });
2858
+ const groupById = /* @__PURE__ */ new Map();
2859
+ for (const group of sourceWindow.groups) {
2860
+ groupById.set(group.groupId, group);
2861
+ }
2862
+ const plans = [];
2863
+ let currentPlan = null;
2864
+ for (const tab of orderedTabs) {
2865
+ const rawGroupId = tab.groupId;
2866
+ const groupId = typeof rawGroupId === "number" && rawGroupId !== -1 ? rawGroupId : null;
2867
+ if (!currentPlan || currentPlan.groupId !== groupId) {
2868
+ currentPlan = { groupId, tabs: [] };
2869
+ plans.push(currentPlan);
2870
+ }
2871
+ currentPlan.tabs.push(tab);
2872
+ }
2873
+ let movedTabs = 0;
2874
+ let movedGroups = 0;
2875
+ const groups2 = [];
2876
+ const undoTabs = [];
2877
+ for (const plan of plans) {
2878
+ const tabIds = plan.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
2879
+ if (!tabIds.length) {
2880
+ continue;
2881
+ }
2882
+ let moved;
2883
+ try {
2884
+ moved = await chrome.tabs.move(tabIds, { windowId: toWindowId, index: -1 });
2885
+ } catch (error) {
2886
+ for (const tabId of tabIds) {
2887
+ skipped.push({ tabId, reason: "move_failed" });
2888
+ }
2889
+ deps2.log("Failed to move tabs", error);
2890
+ continue;
2891
+ }
2892
+ const movedList = Array.isArray(moved) ? moved : [moved];
2893
+ const movedIds = movedList.map((tab) => tab.id).filter((id) => typeof id === "number");
2894
+ movedTabs += movedIds.length;
2895
+ for (const entry of plan.tabs) {
2896
+ if (typeof entry.tabId !== "number") {
2897
+ continue;
2898
+ }
2899
+ const meta = groupById.get(entry.groupId);
2900
+ undoTabs.push({
2901
+ tabId: entry.tabId,
2902
+ windowId: entry.windowId,
2903
+ index: entry.index,
2904
+ groupId: entry.groupId,
2905
+ groupTitle: entry.groupTitle,
2906
+ groupColor: entry.groupColor,
2907
+ groupCollapsed: meta ? meta.collapsed : null
2908
+ });
2909
+ }
2910
+ if (plan.groupId != null && movedIds.length > 0) {
2911
+ movedGroups += 1;
2912
+ let newGroupId = null;
2913
+ try {
2914
+ newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: toWindowId } });
2915
+ const meta = groupById.get(plan.groupId);
2916
+ if (meta) {
2917
+ await chrome.tabGroups.update(newGroupId, {
2918
+ title: meta.title || "",
2919
+ color: meta.color || "grey",
2920
+ collapsed: meta.collapsed || false
2921
+ });
2922
+ }
2923
+ } catch (error) {
2924
+ deps2.log("Failed to regroup tabs", error);
2925
+ }
2926
+ groups2.push({ sourceGroupId: plan.groupId, newGroupId });
2927
+ }
2928
+ }
2929
+ let closedSource = false;
2930
+ if (params.closeSource === true) {
2931
+ try {
2932
+ const remainingTabs = await chrome.tabs.query({ windowId: fromWindowId });
2933
+ if (remainingTabs.length === 0) {
2934
+ await chrome.windows.remove(fromWindowId);
2935
+ closedSource = true;
2936
+ }
2937
+ } catch (error) {
2938
+ deps2.log("Failed to close source window", error);
2939
+ }
2940
+ }
2941
+ return {
2942
+ fromWindowId,
2943
+ toWindowId,
2944
+ summary: { movedTabs, movedGroups, skippedTabs: skipped.length, closedSource },
2945
+ skipped,
2946
+ groups: groups2,
2947
+ undo: {
2948
+ action: "merge-window",
2949
+ fromWindowId,
2950
+ toWindowId,
2951
+ closedSource,
2952
+ tabs: undoTabs
2953
+ },
2954
+ txid: params.txid || null
2955
+ };
2956
+ }
2957
+ }
2958
+ });
2959
+
2960
+ // dist/extension/background.js
2961
+ var HOST_NAME = "com.erwinkroon.tabctl";
2962
+ var manifest = chrome.runtime.getManifest();
2963
+ var MANIFEST_VERSION = manifest.version || "0.0.0";
2964
+ var MANIFEST_VERSION_NAME = manifest.version_name || MANIFEST_VERSION;
2965
+ function parseVersionName(versionName) {
2966
+ const match = versionName.match(/-dev\.([0-9a-f]+)(\.dirty)?$/i);
2967
+ if (!match) {
2968
+ return { gitSha: null, dirty: false };
2969
+ }
2970
+ return { gitSha: match[1] || null, dirty: Boolean(match[2]) };
2971
+ }
2972
+ var parsed = parseVersionName(MANIFEST_VERSION_NAME);
2973
+ var VERSION_INFO = {
2974
+ version: MANIFEST_VERSION_NAME,
2975
+ baseVersion: MANIFEST_VERSION,
2976
+ gitSha: parsed.gitSha,
2977
+ dirty: parsed.dirty
2978
+ };
2979
+ var KEEPALIVE_ALARM = "tabctl-keepalive";
2980
+ var KEEPALIVE_INTERVAL_MINUTES = 1;
2981
+ var screenshot = require_screenshot();
2982
+ var content = require_content();
2983
+ var { delay, executeWithTimeout, isScriptableUrl, isGitHubIssueOrPr, detectGitHubState, extractPageMeta, extractSelectorSignal, waitForTabLoad, waitForDomReady, waitForSettle, waitForTabReady, SETTLE_STABILITY_MS, SETTLE_POLL_INTERVAL_MS } = content;
2984
+ var groups = require_groups();
2985
+ var tabs = require_tabs();
2986
+ var { getMostRecentFocusedWindowId, normalizeTabIndex } = tabs;
2987
+ var move = require_move();
2988
+ var inspect = require_inspect();
2989
+ var { DESCRIPTION_MAX_LENGTH } = inspect;
2990
+ var undoHandlers = require_undo_handlers();
2991
+ var archive = require_archive();
2992
+ var state = {
2993
+ port: null,
2994
+ archiveWindowId: null,
2995
+ archiveWindowIdLoaded: false,
2996
+ lastFocused: {},
2997
+ lastFocusedLoaded: false
2998
+ };
2999
+ function log(...args) {
3000
+ console.log("[tabctl]", ...args);
3001
+ }
3002
+ function sendResponse(id, ok, payload) {
3003
+ if (!state.port) {
3004
+ return;
3005
+ }
3006
+ if (ok) {
3007
+ const data = typeof payload === "object" && payload !== null ? { ...payload, component: "extension", version: VERSION_INFO.version, baseVersion: VERSION_INFO.baseVersion } : { payload, component: "extension", version: VERSION_INFO.version, baseVersion: VERSION_INFO.baseVersion };
3008
+ state.port.postMessage({ id, ok: true, data });
3009
+ return;
3010
+ }
3011
+ const error = payload instanceof Error ? { message: payload.message, stack: payload.stack } : payload;
3012
+ state.port.postMessage({ id, ok: false, error });
3013
+ }
3014
+ function connectNative() {
3015
+ if (state.port) {
3016
+ return;
3017
+ }
3018
+ try {
3019
+ const port = chrome.runtime.connectNative(HOST_NAME);
3020
+ state.port = port;
3021
+ port.onMessage.addListener(handleNativeMessage);
3022
+ port.onDisconnect.addListener(() => {
3023
+ const lastError = chrome.runtime.lastError;
3024
+ if (lastError) {
3025
+ log("Native host disconnected:", lastError.message);
3026
+ } else {
3027
+ log("Native host disconnected");
3028
+ }
3029
+ state.port = null;
3030
+ });
3031
+ log("Native host connected");
3032
+ } catch (error) {
3033
+ log("Native host connection failed", error);
3034
+ }
3035
+ }
3036
+ chrome.runtime.onInstalled.addListener(() => {
3037
+ connectNative();
3038
+ chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
3039
+ });
3040
+ chrome.runtime.onStartup.addListener(() => {
3041
+ connectNative();
3042
+ chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
3043
+ });
3044
+ chrome.alarms.onAlarm.addListener((alarm) => {
3045
+ if (alarm.name === KEEPALIVE_ALARM) {
3046
+ connectNative();
3047
+ }
3048
+ });
3049
+ async function ensureLastFocusedLoaded() {
3050
+ if (state.lastFocusedLoaded) {
3051
+ return;
3052
+ }
3053
+ const stored = await chrome.storage.local.get("lastFocused");
3054
+ state.lastFocused = stored.lastFocused || {};
3055
+ state.lastFocusedLoaded = true;
3056
+ }
3057
+ async function ensureArchiveWindowIdLoaded() {
3058
+ if (state.archiveWindowIdLoaded) {
3059
+ return;
3060
+ }
3061
+ const stored = await chrome.storage.local.get("archiveWindowId");
3062
+ state.archiveWindowId = stored.archiveWindowId || null;
3063
+ state.archiveWindowIdLoaded = true;
3064
+ }
3065
+ async function setLastFocused(tabId) {
3066
+ await ensureLastFocusedLoaded();
3067
+ state.lastFocused[String(tabId)] = Date.now();
3068
+ await chrome.storage.local.set({ lastFocused: state.lastFocused });
3069
+ }
3070
+ chrome.tabs.onActivated.addListener((info) => {
3071
+ setLastFocused(info.tabId).catch((error) => log("Failed to set last focused", error));
3072
+ });
3073
+ chrome.tabs.onRemoved.addListener((tabId) => {
3074
+ ensureLastFocusedLoaded().then(() => {
3075
+ const key = String(tabId);
3076
+ if (state.lastFocused[key]) {
3077
+ delete state.lastFocused[key];
3078
+ chrome.storage.local.set({ lastFocused: state.lastFocused });
3079
+ }
3080
+ }).catch((error) => log("Failed to prune last focused", error));
3081
+ });
3082
+ chrome.windows.onFocusChanged.addListener((windowId) => {
3083
+ if (windowId === chrome.windows.WINDOW_ID_NONE) {
3084
+ return;
3085
+ }
3086
+ chrome.tabs.query({ windowId, active: true }).then((tabs2) => {
3087
+ if (tabs2[0] && tabs2[0].id != null) {
3088
+ setLastFocused(tabs2[0].id).catch((error) => log("Failed to set last focused", error));
3089
+ }
3090
+ }).catch((error) => log("Failed to query active tab", error));
3091
+ });
3092
+ async function handleNativeMessage(message) {
3093
+ if (!message || typeof message !== "object") {
3094
+ return;
3095
+ }
3096
+ const { id, action, params } = message;
3097
+ if (!id || !action) {
3098
+ return;
3099
+ }
3100
+ try {
3101
+ const data = await handleAction(action, params || {}, id);
3102
+ sendResponse(id, true, data);
3103
+ } catch (error) {
3104
+ sendResponse(id, false, error);
3105
+ }
3106
+ }
3107
+ function sendProgress(id, payload) {
3108
+ if (!state.port) {
3109
+ return;
3110
+ }
3111
+ state.port.postMessage({ id, progress: true, data: payload });
3112
+ }
3113
+ async function handleAction(action, params, requestId) {
3114
+ switch (action) {
3115
+ case "ping":
3116
+ return {
3117
+ now: Date.now(),
3118
+ version: VERSION_INFO.version,
3119
+ baseVersion: VERSION_INFO.baseVersion,
3120
+ gitSha: VERSION_INFO.gitSha,
3121
+ dirty: VERSION_INFO.dirty,
3122
+ component: "extension"
3123
+ };
3124
+ case "version":
3125
+ return {
3126
+ version: VERSION_INFO.version,
3127
+ baseVersion: VERSION_INFO.baseVersion,
3128
+ gitSha: VERSION_INFO.gitSha,
3129
+ dirty: VERSION_INFO.dirty,
3130
+ component: "extension"
3131
+ };
3132
+ case "list":
3133
+ return await getTabSnapshot();
3134
+ case "analyze":
3135
+ return await inspect.analyzeTabs(params, requestId, deps);
3136
+ case "inspect":
3137
+ return await inspect.inspectTabs(params, requestId, deps);
3138
+ case "focus":
3139
+ return await tabs.focusTab(params);
3140
+ case "refresh":
3141
+ return await tabs.refreshTabs(params);
3142
+ case "open":
3143
+ return await tabs.openTabs(params, deps);
3144
+ case "group-list":
3145
+ return await listGroups(params);
3146
+ case "group-update":
3147
+ return await groupUpdate(params);
3148
+ case "group-ungroup":
3149
+ return await groupUngroup(params);
3150
+ case "group-assign":
3151
+ return await groupAssign(params);
3152
+ case "move-tab":
3153
+ return await move.moveTab(params, deps);
3154
+ case "move-group":
3155
+ return await move.moveGroup(params, deps);
3156
+ case "merge-window":
3157
+ return await archive.mergeWindow(params, deps);
3158
+ case "archive":
3159
+ return await archive.archiveTabs(params, deps);
3160
+ case "close":
3161
+ return await archive.closeTabs(params, deps);
3162
+ case "report":
3163
+ return await reportTabs(params);
3164
+ case "screenshot":
3165
+ return await screenshot.screenshotTabs(params, requestId, deps);
3166
+ case "undo":
3167
+ return await undoHandlers.undoTransaction(params, deps);
3168
+ default:
3169
+ throw new Error(`Unknown action: ${action}`);
3170
+ }
3171
+ }
3172
+ function resolveWindowIdFromParams(snapshot, value) {
3173
+ if (typeof value === "number" && Number.isFinite(value)) {
3174
+ return value;
3175
+ }
3176
+ if (typeof value === "string") {
3177
+ const normalized = value.trim().toLowerCase();
3178
+ if (normalized === "active") {
3179
+ const focused = snapshot.windows.find((win) => win.focused);
3180
+ return focused ? focused.windowId : null;
3181
+ }
3182
+ if (normalized === "last-focused") {
3183
+ return getMostRecentFocusedWindowId(snapshot.windows);
3184
+ }
3185
+ const parsed3 = Number(normalized);
3186
+ return Number.isFinite(parsed3) ? parsed3 : null;
3187
+ }
3188
+ const parsed2 = Number(value);
3189
+ return Number.isFinite(parsed2) ? parsed2 : null;
3190
+ }
3191
+ async function getTabSnapshot() {
3192
+ await ensureLastFocusedLoaded();
3193
+ const windows = await chrome.windows.getAll({ populate: true, windowTypes: ["normal"] });
3194
+ const groups2 = await chrome.tabGroups.query({});
3195
+ const groupById = new Map(groups2.map((group) => [group.id, group]));
3196
+ const snapshot = windows.map((win) => {
3197
+ const tabs2 = (win.tabs || []).map((tab) => {
3198
+ const group = tab.groupId !== -1 ? groupById.get(tab.groupId) : null;
3199
+ return {
3200
+ tabId: tab.id,
3201
+ windowId: win.id,
3202
+ index: tab.index,
3203
+ url: tab.url,
3204
+ title: tab.title,
3205
+ active: tab.active,
3206
+ pinned: tab.pinned,
3207
+ groupId: tab.groupId,
3208
+ groupTitle: group ? group.title : null,
3209
+ groupColor: group ? group.color : null,
3210
+ groupCollapsed: group ? group.collapsed : null,
3211
+ lastFocusedAt: state.lastFocused[String(tab.id)] || null
3212
+ };
3213
+ });
3214
+ const windowGroups = groups2.filter((group) => group.windowId === win.id).map((group) => ({
3215
+ groupId: group.id,
3216
+ title: group.title,
3217
+ color: group.color,
3218
+ collapsed: group.collapsed
3219
+ }));
3220
+ return {
3221
+ windowId: win.id,
3222
+ focused: win.focused,
3223
+ state: win.state,
3224
+ tabs: tabs2,
3225
+ groups: windowGroups
3226
+ };
3227
+ });
3228
+ return {
3229
+ generatedAt: Date.now(),
3230
+ windows: snapshot
3231
+ };
3232
+ }
3233
+ function flattenTabs(snapshot) {
3234
+ const result = [];
3235
+ for (const win of snapshot.windows) {
3236
+ for (const tab of win.tabs) {
3237
+ result.push(tab);
3238
+ }
3239
+ }
3240
+ return result;
3241
+ }
3242
+ function resolveGroupByTitle(snapshot, groupTitle, windowId) {
3243
+ return groups.resolveGroupByTitle(snapshot, buildWindowLabels, groupTitle, windowId);
3244
+ }
3245
+ function resolveGroupById(snapshot, groupId) {
3246
+ return groups.resolveGroupById(snapshot, buildWindowLabels, groupId);
3247
+ }
3248
+ function buildWindowLabels(snapshot) {
3249
+ const labels = /* @__PURE__ */ new Map();
3250
+ snapshot.windows.forEach((win, index) => {
3251
+ labels.set(win.windowId, `W${index + 1}`);
3252
+ });
3253
+ return labels;
3254
+ }
3255
+ function selectTabsByScope(snapshot, params) {
3256
+ const allTabs = flattenTabs(snapshot);
3257
+ if (params.tabIds && params.tabIds.length) {
3258
+ const idSet = new Set(params.tabIds.map(Number));
3259
+ return { tabs: allTabs.filter((tab) => idSet.has(tab.tabId)) };
3260
+ }
3261
+ if (params.groupId) {
3262
+ const groupId = Number(params.groupId);
3263
+ return { tabs: allTabs.filter((tab) => tab.groupId === groupId) };
3264
+ }
3265
+ if (params.groupTitle) {
3266
+ const windowId = params.windowId != null ? resolveWindowIdFromParams(snapshot, params.windowId) ?? void 0 : void 0;
3267
+ const resolved = resolveGroupByTitle(snapshot, params.groupTitle, windowId);
3268
+ if (resolved.error) {
3269
+ return { tabs: [], error: resolved.error };
3270
+ }
3271
+ const match = resolved.match;
3272
+ return {
3273
+ tabs: allTabs.filter((tab) => tab.groupId === match.group.groupId && tab.windowId === match.windowId)
3274
+ };
3275
+ }
3276
+ if (params.windowId) {
3277
+ const windowId = resolveWindowIdFromParams(snapshot, params.windowId);
3278
+ if (!Number.isFinite(windowId)) {
3279
+ return { tabs: [] };
3280
+ }
3281
+ return { tabs: allTabs.filter((tab) => tab.windowId === windowId) };
3282
+ }
3283
+ if (params.all) {
3284
+ return { tabs: allTabs };
3285
+ }
3286
+ const focusedWindow = snapshot.windows.find((win) => win.focused);
3287
+ if (!focusedWindow) {
3288
+ return { tabs: [] };
3289
+ }
3290
+ return { tabs: focusedWindow.tabs };
3291
+ }
3292
+ var deps = {
3293
+ getTabSnapshot,
3294
+ selectTabsByScope,
3295
+ sendProgress,
3296
+ log,
3297
+ resolveWindowIdFromParams,
3298
+ resolveGroupByTitle,
3299
+ resolveGroupById,
3300
+ buildWindowLabels,
3301
+ getArchiveWindowId,
3302
+ setArchiveWindowId,
3303
+ delay,
3304
+ executeWithTimeout,
3305
+ isScriptableUrl,
3306
+ waitForTabReady
3307
+ };
3308
+ async function listGroups(params) {
3309
+ return groups.listGroups(params, deps);
3310
+ }
3311
+ async function groupUpdate(params) {
3312
+ return groups.groupUpdate(params, deps);
3313
+ }
3314
+ async function groupUngroup(params) {
3315
+ return groups.groupUngroup(params, deps);
3316
+ }
3317
+ async function groupAssign(params) {
3318
+ return groups.groupAssign(params, deps);
3319
+ }
3320
+ async function getArchiveWindowId() {
3321
+ await ensureArchiveWindowIdLoaded();
3322
+ return state.archiveWindowId;
3323
+ }
3324
+ async function setArchiveWindowId(id) {
3325
+ state.archiveWindowId = id;
3326
+ state.archiveWindowIdLoaded = true;
3327
+ await chrome.storage.local.set({ archiveWindowId: id });
3328
+ }
3329
+ async function extractDescription(tabId) {
3330
+ try {
3331
+ const [{ result }] = await chrome.scripting.executeScript({
3332
+ target: { tabId },
3333
+ func: () => {
3334
+ const pickContent = (selector) => {
3335
+ const el = document.querySelector(selector);
3336
+ if (!el) {
3337
+ return "";
3338
+ }
3339
+ const content2 = el.getAttribute("content") || el.textContent || "";
3340
+ return content2.trim();
3341
+ };
3342
+ let description = pickContent("meta[name='description']") || pickContent("meta[property='og:description']") || pickContent("meta[name='twitter:description']");
3343
+ if (!description) {
3344
+ const h1 = document.querySelector("h1");
3345
+ const h1Text = h1 ? h1.textContent?.trim() : "";
3346
+ const paragraphs = Array.from(document.querySelectorAll("p")).map((p) => p.textContent?.replace(/\s+/g, " ").trim() || "").filter((text) => text.length > 40);
3347
+ const pText = paragraphs.length ? paragraphs[0] : "";
3348
+ if (h1Text && pText) {
3349
+ description = `${h1Text} - ${pText}`;
3350
+ } else {
3351
+ description = h1Text || pText;
3352
+ }
3353
+ }
3354
+ return description.replace(/\s+/g, " ").trim();
3355
+ }
3356
+ });
3357
+ if (!result) {
3358
+ return "";
3359
+ }
3360
+ return String(result).slice(0, DESCRIPTION_MAX_LENGTH);
3361
+ } catch {
3362
+ return "";
3363
+ }
3364
+ }
3365
+ async function reportTabs(params) {
3366
+ const snapshot = await getTabSnapshot();
3367
+ const selection = selectTabsByScope(snapshot, params);
3368
+ if (selection.error) {
3369
+ throw selection.error;
3370
+ }
3371
+ const windowLabels = buildWindowLabels(snapshot);
3372
+ const tabs2 = selection.tabs;
3373
+ const entries = [];
3374
+ for (const tab of tabs2) {
3375
+ const description = isScriptableUrl(tab.url) ? await extractDescription(tab.tabId) : "";
3376
+ entries.push({
3377
+ tabId: tab.tabId,
3378
+ windowId: tab.windowId,
3379
+ windowLabel: windowLabels.get(tab.windowId) || `W${tab.windowId}`,
3380
+ groupId: tab.groupId,
3381
+ groupTitle: tab.groupTitle,
3382
+ groupColor: tab.groupColor,
3383
+ url: tab.url,
3384
+ title: tab.title,
3385
+ description,
3386
+ lastFocusedAt: tab.lastFocusedAt
3387
+ });
3388
+ }
3389
+ return {
3390
+ generatedAt: Date.now(),
3391
+ entries,
3392
+ totals: {
3393
+ tabs: entries.length
3394
+ }
3395
+ };
3396
+ }
3397
+ self.__tabctl = { state, connectNative };
3398
+ })();