tabctl 0.6.0-alpha.8 → 0.6.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extension/background.js +178 -3327
- package/dist/extension/lib/content.js +0 -115
- package/dist/extension/lib/screenshot.js +0 -84
- package/dist/extension/manifest.json +1 -1
- package/package.json +2 -2
- package/dist/extension/lib/archive.js +0 -469
- package/dist/extension/lib/deps.js +0 -4
- package/dist/extension/lib/groups.js +0 -558
- package/dist/extension/lib/inspect.js +0 -254
- package/dist/extension/lib/move.js +0 -403
- package/dist/extension/lib/tabs.js +0 -518
- package/dist/extension/lib/undo-handlers.js +0 -447
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __esm = (fn, res) => function __init() {
|
|
7
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
-
};
|
|
9
3
|
var __commonJS = (cb, mod) => function __require() {
|
|
10
4
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
5
|
};
|
|
12
|
-
var __export = (target, all) => {
|
|
13
|
-
for (var name in all)
|
|
14
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
-
};
|
|
16
|
-
var __copyProps = (to, from, except, desc) => {
|
|
17
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
-
for (let key of __getOwnPropNames(from))
|
|
19
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
-
}
|
|
22
|
-
return to;
|
|
23
|
-
};
|
|
24
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
25
6
|
|
|
26
7
|
// dist/extension/lib/screenshot.js
|
|
27
8
|
var require_screenshot = __commonJS({
|
|
@@ -40,7 +21,6 @@
|
|
|
40
21
|
exports.getPageMetrics = getPageMetrics;
|
|
41
22
|
exports.scrollToPosition = scrollToPosition;
|
|
42
23
|
exports.captureTabTiles = captureTabTiles;
|
|
43
|
-
exports.screenshotTabs = screenshotTabs;
|
|
44
24
|
exports.SCREENSHOT_TILE_MAX_DIM = 2e3;
|
|
45
25
|
exports.SCREENSHOT_MAX_BYTES = 2e6;
|
|
46
26
|
exports.SCREENSHOT_QUALITY = 80;
|
|
@@ -185,8 +165,8 @@
|
|
|
185
165
|
}
|
|
186
166
|
return chrome.tabs.captureVisibleTab(windowId, options);
|
|
187
167
|
}
|
|
188
|
-
async function getPageMetrics(tabId, timeoutMs,
|
|
189
|
-
const result = await
|
|
168
|
+
async function getPageMetrics(tabId, timeoutMs, deps) {
|
|
169
|
+
const result = await deps.executeWithTimeout(tabId, timeoutMs, () => {
|
|
190
170
|
const doc = document.documentElement;
|
|
191
171
|
const body = document.body;
|
|
192
172
|
const pageWidth = Math.max(doc.scrollWidth, doc.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);
|
|
@@ -206,8 +186,8 @@
|
|
|
206
186
|
}
|
|
207
187
|
return result;
|
|
208
188
|
}
|
|
209
|
-
async function scrollToPosition(tabId, timeoutMs, x, y,
|
|
210
|
-
const result = await
|
|
189
|
+
async function scrollToPosition(tabId, timeoutMs, x, y, deps) {
|
|
190
|
+
const result = await deps.executeWithTimeout(tabId, timeoutMs, (scrollX, scrollY) => {
|
|
211
191
|
window.scrollTo(scrollX, scrollY);
|
|
212
192
|
return {
|
|
213
193
|
scrollX: window.scrollX || window.pageXOffset || 0,
|
|
@@ -219,13 +199,13 @@
|
|
|
219
199
|
}
|
|
220
200
|
return result;
|
|
221
201
|
}
|
|
222
|
-
async function captureTabTiles(tab, options,
|
|
202
|
+
async function captureTabTiles(tab, options, deps) {
|
|
223
203
|
const tabId = tab.tabId;
|
|
224
204
|
const windowId = tab.windowId;
|
|
225
205
|
if (!Number.isFinite(tabId) || !Number.isFinite(windowId)) {
|
|
226
206
|
throw new Error("Missing tab/window id");
|
|
227
207
|
}
|
|
228
|
-
const metrics = await getPageMetrics(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS,
|
|
208
|
+
const metrics = await getPageMetrics(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, deps);
|
|
229
209
|
if (!metrics) {
|
|
230
210
|
throw new Error("Failed to read page metrics");
|
|
231
211
|
}
|
|
@@ -243,7 +223,7 @@
|
|
|
243
223
|
let tileIndex = 0;
|
|
244
224
|
const captureTile = async (x, y, width, height, total) => {
|
|
245
225
|
if (tileIndex > 0) {
|
|
246
|
-
await
|
|
226
|
+
await deps.delay(exports.SCREENSHOT_CAPTURE_DELAY_MS);
|
|
247
227
|
}
|
|
248
228
|
const rawDataUrl = await captureVisible(windowId, options.format, options.quality);
|
|
249
229
|
if (!rawDataUrl) {
|
|
@@ -278,8 +258,8 @@
|
|
|
278
258
|
const tileCount = Math.ceil(maxX / stepX) * Math.ceil(maxY / stepY);
|
|
279
259
|
for (let y = 0; y < maxY; y += stepY) {
|
|
280
260
|
for (let x = 0; x < maxX; x += stepX) {
|
|
281
|
-
await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, x, y,
|
|
282
|
-
await
|
|
261
|
+
await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, x, y, deps);
|
|
262
|
+
await deps.delay(exports.SCREENSHOT_SCROLL_DELAY_MS);
|
|
283
263
|
const width = Math.min(stepX, maxX - x);
|
|
284
264
|
const height = Math.min(stepY, maxY - y);
|
|
285
265
|
try {
|
|
@@ -287,7 +267,7 @@
|
|
|
287
267
|
} catch (err) {
|
|
288
268
|
const message = err instanceof Error ? err.message : "capture_failed";
|
|
289
269
|
if (message.includes("MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND")) {
|
|
290
|
-
await
|
|
270
|
+
await deps.delay(1e3);
|
|
291
271
|
await captureTile(x, y, width, height, tileCount);
|
|
292
272
|
} else {
|
|
293
273
|
throw err;
|
|
@@ -295,90 +275,9 @@
|
|
|
295
275
|
}
|
|
296
276
|
}
|
|
297
277
|
}
|
|
298
|
-
await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, startScrollX, startScrollY,
|
|
278
|
+
await scrollToPosition(tabId, exports.SCREENSHOT_PROCESS_TIMEOUT_MS, startScrollX, startScrollY, deps);
|
|
299
279
|
return tiles;
|
|
300
280
|
}
|
|
301
|
-
async function screenshotTabs(params, requestId, deps2) {
|
|
302
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
303
|
-
const selection = deps2.selectTabsByScope(snapshot, params);
|
|
304
|
-
if (selection.error) {
|
|
305
|
-
throw selection.error;
|
|
306
|
-
}
|
|
307
|
-
const mode = params.mode === "full" ? "full" : "viewport";
|
|
308
|
-
const format = params.format === "jpeg" ? "jpeg" : "png";
|
|
309
|
-
const qualityRaw = Number(params.quality);
|
|
310
|
-
const quality = Number.isFinite(qualityRaw) ? Math.min(100, Math.max(0, Math.floor(qualityRaw))) : exports.SCREENSHOT_QUALITY;
|
|
311
|
-
const tileMaxDimRaw = Number(params.tileMaxDim);
|
|
312
|
-
const tileMaxDim = Number.isFinite(tileMaxDimRaw) && tileMaxDimRaw > 0 ? Math.floor(tileMaxDimRaw) : exports.SCREENSHOT_TILE_MAX_DIM;
|
|
313
|
-
const adjustedTileMaxDim = tileMaxDim < 50 ? 50 : tileMaxDim;
|
|
314
|
-
const maxBytesRaw = Number(params.maxBytes);
|
|
315
|
-
const maxBytes = Number.isFinite(maxBytesRaw) && maxBytesRaw > 0 ? Math.floor(maxBytesRaw) : exports.SCREENSHOT_MAX_BYTES;
|
|
316
|
-
const adjustedMaxBytes = maxBytes < 5e4 ? 5e4 : maxBytes;
|
|
317
|
-
const progressEnabled = params.progress === true;
|
|
318
|
-
const tabs2 = selection.tabs;
|
|
319
|
-
const entries = [];
|
|
320
|
-
let totalTiles = 0;
|
|
321
|
-
for (let index = 0; index < tabs2.length; index += 1) {
|
|
322
|
-
const tab = tabs2[index];
|
|
323
|
-
const tabId = tab.tabId;
|
|
324
|
-
const url = tab.url;
|
|
325
|
-
if (!deps2.isScriptableUrl(url)) {
|
|
326
|
-
entries.push({
|
|
327
|
-
tabId,
|
|
328
|
-
windowId: tab.windowId,
|
|
329
|
-
groupId: tab.groupId,
|
|
330
|
-
url: tab.url,
|
|
331
|
-
title: tab.title,
|
|
332
|
-
error: { message: "unsupported_url" },
|
|
333
|
-
tiles: []
|
|
334
|
-
});
|
|
335
|
-
if (progressEnabled) {
|
|
336
|
-
deps2.sendProgress(requestId, { phase: "screenshot", processed: index + 1, total: tabs2.length, tabId });
|
|
337
|
-
}
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
let tiles = [];
|
|
341
|
-
let error = null;
|
|
342
|
-
try {
|
|
343
|
-
const windowId = tab.windowId;
|
|
344
|
-
const activeTabs = await chrome.tabs.query({ windowId, active: true });
|
|
345
|
-
const activeTabId = activeTabs[0]?.id ?? null;
|
|
346
|
-
if (activeTabId && activeTabId !== tabId) {
|
|
347
|
-
await chrome.tabs.update(tabId, { active: true });
|
|
348
|
-
await deps2.delay(exports.SCREENSHOT_SCROLL_DELAY_MS);
|
|
349
|
-
}
|
|
350
|
-
try {
|
|
351
|
-
await deps2.waitForTabReady(tabId, params, exports.SCREENSHOT_PROCESS_TIMEOUT_MS);
|
|
352
|
-
tiles = await captureTabTiles(tab, { mode, format, quality, tileMaxDim: adjustedTileMaxDim, maxBytes: adjustedMaxBytes }, deps2);
|
|
353
|
-
} finally {
|
|
354
|
-
if (activeTabId && activeTabId !== tabId) {
|
|
355
|
-
await chrome.tabs.update(activeTabId, { active: true });
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
} catch (err) {
|
|
359
|
-
const message = err instanceof Error ? err.message : "capture_failed";
|
|
360
|
-
error = { message };
|
|
361
|
-
}
|
|
362
|
-
totalTiles += tiles.length;
|
|
363
|
-
entries.push({
|
|
364
|
-
tabId: tab.tabId,
|
|
365
|
-
windowId: tab.windowId,
|
|
366
|
-
groupId: tab.groupId,
|
|
367
|
-
url: tab.url,
|
|
368
|
-
title: tab.title,
|
|
369
|
-
tiles,
|
|
370
|
-
...error ? { error } : {}
|
|
371
|
-
});
|
|
372
|
-
if (progressEnabled) {
|
|
373
|
-
deps2.sendProgress(requestId, { phase: "screenshot", processed: index + 1, total: tabs2.length, tabId });
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const response = {
|
|
377
|
-
totals: { tabs: tabs2.length, tiles: totalTiles },
|
|
378
|
-
entries
|
|
379
|
-
};
|
|
380
|
-
return response;
|
|
381
|
-
}
|
|
382
281
|
}
|
|
383
282
|
});
|
|
384
283
|
|
|
@@ -387,21 +286,10 @@
|
|
|
387
286
|
"dist/extension/lib/content.js"(exports) {
|
|
388
287
|
"use strict";
|
|
389
288
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
390
|
-
exports.SETTLE_POLL_INTERVAL_MS = exports.SETTLE_STABILITY_MS = void 0;
|
|
391
|
-
exports.isScriptableUrl = isScriptableUrl2;
|
|
392
289
|
exports.delay = delay2;
|
|
393
290
|
exports.executeWithTimeout = executeWithTimeout2;
|
|
394
|
-
exports.extractPageMeta =
|
|
395
|
-
exports.extractSelectorSignal =
|
|
396
|
-
exports.waitForTabLoad = waitForTabLoad2;
|
|
397
|
-
exports.waitForDomReady = waitForDomReady2;
|
|
398
|
-
exports.waitForSettle = waitForSettle2;
|
|
399
|
-
exports.waitForTabReady = waitForTabReady2;
|
|
400
|
-
exports.SETTLE_STABILITY_MS = 500;
|
|
401
|
-
exports.SETTLE_POLL_INTERVAL_MS = 50;
|
|
402
|
-
function isScriptableUrl2(url) {
|
|
403
|
-
return typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"));
|
|
404
|
-
}
|
|
291
|
+
exports.extractPageMeta = extractPageMeta;
|
|
292
|
+
exports.extractSelectorSignal = extractSelectorSignal;
|
|
405
293
|
function delay2(ms) {
|
|
406
294
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
407
295
|
}
|
|
@@ -428,7 +316,7 @@
|
|
|
428
316
|
return null;
|
|
429
317
|
}
|
|
430
318
|
}
|
|
431
|
-
async function
|
|
319
|
+
async function extractPageMeta(tabId, timeoutMs, descriptionMaxLength) {
|
|
432
320
|
const result = await executeWithTimeout2(tabId, timeoutMs, () => {
|
|
433
321
|
const pickContent = (selector) => {
|
|
434
322
|
const el = document.querySelector(selector);
|
|
@@ -455,7 +343,7 @@
|
|
|
455
343
|
h1: (meta.h1 || "").slice(0, descriptionMaxLength)
|
|
456
344
|
};
|
|
457
345
|
}
|
|
458
|
-
async function
|
|
346
|
+
async function extractSelectorSignal(tabId, specs, timeoutMs, selectorValueMaxLength) {
|
|
459
347
|
if (!specs.length) {
|
|
460
348
|
return null;
|
|
461
349
|
}
|
|
@@ -558,2951 +446,103 @@
|
|
|
558
446
|
}
|
|
559
447
|
return result;
|
|
560
448
|
}
|
|
561
|
-
function waitForTabLoad2(tabId, timeoutMs) {
|
|
562
|
-
return new Promise((resolve) => {
|
|
563
|
-
let settled = false;
|
|
564
|
-
const done = () => {
|
|
565
|
-
if (settled) {
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
settled = true;
|
|
569
|
-
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
570
|
-
resolve();
|
|
571
|
-
};
|
|
572
|
-
const onUpdated = (updatedTabId, info) => {
|
|
573
|
-
if (updatedTabId === tabId && info.status === "complete") {
|
|
574
|
-
done();
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
578
|
-
chrome.tabs.get(tabId).then((tab) => {
|
|
579
|
-
if (tab.status === "complete") {
|
|
580
|
-
done();
|
|
581
|
-
}
|
|
582
|
-
}).catch(() => {
|
|
583
|
-
done();
|
|
584
|
-
});
|
|
585
|
-
setTimeout(done, timeoutMs);
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
async function waitForDomReady2(tabId, timeoutMs) {
|
|
589
|
-
const result = await executeWithTimeout2(tabId, timeoutMs, () => {
|
|
590
|
-
if (document.readyState === "interactive" || document.readyState === "complete") {
|
|
591
|
-
return true;
|
|
592
|
-
}
|
|
593
|
-
return new Promise((resolve) => {
|
|
594
|
-
const onReady = () => {
|
|
595
|
-
document.removeEventListener("DOMContentLoaded", onReady);
|
|
596
|
-
resolve(true);
|
|
597
|
-
};
|
|
598
|
-
document.addEventListener("DOMContentLoaded", onReady, { once: true });
|
|
599
|
-
setTimeout(() => {
|
|
600
|
-
document.removeEventListener("DOMContentLoaded", onReady);
|
|
601
|
-
resolve(false);
|
|
602
|
-
}, Math.max(0, timeoutMs - 50));
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
|
-
if (result === null) {
|
|
606
|
-
await delay2(Math.min(200, Math.max(50, Math.floor(timeoutMs / 10))));
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
async function waitForSettle2(tabId, timeoutMs) {
|
|
610
|
-
const startTime = Date.now();
|
|
611
|
-
let lastUrl = "";
|
|
612
|
-
let lastTitle = "";
|
|
613
|
-
let stableStart = Date.now();
|
|
614
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
615
|
-
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
616
|
-
if (!tab)
|
|
617
|
-
return;
|
|
618
|
-
const currentUrl = tab.url || "";
|
|
619
|
-
const currentTitle = tab.title || "";
|
|
620
|
-
if (currentUrl !== lastUrl || currentTitle !== lastTitle) {
|
|
621
|
-
lastUrl = currentUrl;
|
|
622
|
-
lastTitle = currentTitle;
|
|
623
|
-
stableStart = Date.now();
|
|
624
|
-
} else if (isScriptableUrl2(currentUrl) && tab.status === "complete" && Date.now() - stableStart >= exports.SETTLE_STABILITY_MS) {
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
await delay2(exports.SETTLE_POLL_INTERVAL_MS);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
async function waitForTabReady2(tabId, params, fallbackTimeoutMs) {
|
|
631
|
-
const waitFor = typeof params.waitFor === "string" ? params.waitFor.trim().toLowerCase() : "";
|
|
632
|
-
if (!waitFor || waitFor === "none") {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
const timeoutRaw = Number(params.waitTimeoutMs);
|
|
636
|
-
const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.floor(timeoutRaw) : fallbackTimeoutMs;
|
|
637
|
-
if (waitFor === "settle") {
|
|
638
|
-
await waitForSettle2(tabId, timeoutMs);
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
try {
|
|
642
|
-
const tab = await chrome.tabs.get(tabId);
|
|
643
|
-
if (!isScriptableUrl2(tab.url)) {
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
} catch {
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
if (waitFor === "load") {
|
|
650
|
-
await waitForTabLoad2(tabId, timeoutMs);
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
if (waitFor === "dom") {
|
|
654
|
-
await waitForDomReady2(tabId, timeoutMs);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
// dist/extension/lib/groups.js
|
|
661
|
-
var require_groups = __commonJS({
|
|
662
|
-
"dist/extension/lib/groups.js"(exports) {
|
|
663
|
-
"use strict";
|
|
664
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
665
|
-
exports.getGroupTabs = getGroupTabs;
|
|
666
|
-
exports.listGroupSummaries = listGroupSummaries;
|
|
667
|
-
exports.summarizeGroupMatch = summarizeGroupMatch;
|
|
668
|
-
exports.findGroupMatches = findGroupMatches;
|
|
669
|
-
exports.resolveGroupByTitle = resolveGroupByTitle2;
|
|
670
|
-
exports.resolveGroupById = resolveGroupById2;
|
|
671
|
-
exports.listGroups = listGroups2;
|
|
672
|
-
exports.groupUpdate = groupUpdate2;
|
|
673
|
-
exports.groupUngroup = groupUngroup2;
|
|
674
|
-
exports.groupAssign = groupAssign2;
|
|
675
|
-
exports.groupGather = groupGather2;
|
|
676
|
-
function getGroupTabs(windowSnapshot, groupId) {
|
|
677
|
-
return windowSnapshot.tabs.filter((tab) => tab.groupId === groupId).sort((a, b) => {
|
|
678
|
-
const ai = Number(a.index);
|
|
679
|
-
const bi = Number(b.index);
|
|
680
|
-
return (Number.isFinite(ai) ? ai : 0) - (Number.isFinite(bi) ? bi : 0);
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
function listGroupSummaries(snapshot, buildWindowLabels2, windowId) {
|
|
684
|
-
const windowLabels = buildWindowLabels2(snapshot);
|
|
685
|
-
const summaries = [];
|
|
686
|
-
const windows = snapshot.windows;
|
|
687
|
-
for (const win of windows) {
|
|
688
|
-
if (windowId && win.windowId !== windowId) {
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
for (const group of win.groups) {
|
|
692
|
-
summaries.push({
|
|
693
|
-
windowId: win.windowId,
|
|
694
|
-
windowLabel: windowLabels.get(win.windowId) ?? null,
|
|
695
|
-
groupId: group.groupId,
|
|
696
|
-
title: typeof group.title === "string" ? group.title : null
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return summaries;
|
|
701
|
-
}
|
|
702
|
-
function summarizeGroupMatch(match, windowLabels) {
|
|
703
|
-
return {
|
|
704
|
-
windowId: match.windowId,
|
|
705
|
-
windowLabel: windowLabels.get(match.windowId) ?? null,
|
|
706
|
-
groupId: match.group.groupId,
|
|
707
|
-
title: typeof match.group.title === "string" ? match.group.title : null
|
|
708
|
-
};
|
|
709
|
-
}
|
|
710
|
-
function findGroupMatches(snapshot, groupTitle, windowId) {
|
|
711
|
-
const matches = [];
|
|
712
|
-
const windows = snapshot.windows;
|
|
713
|
-
for (const win of windows) {
|
|
714
|
-
if (windowId && win.windowId !== windowId) {
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
for (const group of win.groups) {
|
|
718
|
-
if (group.title === groupTitle) {
|
|
719
|
-
matches.push({
|
|
720
|
-
windowId: win.windowId,
|
|
721
|
-
group,
|
|
722
|
-
tabs: getGroupTabs(win, group.groupId)
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
return matches;
|
|
728
|
-
}
|
|
729
|
-
function resolveGroupByTitle2(snapshot, buildWindowLabels2, groupTitle, windowId) {
|
|
730
|
-
const windowLabels = buildWindowLabels2(snapshot);
|
|
731
|
-
const allMatches = findGroupMatches(snapshot, groupTitle);
|
|
732
|
-
const matches = windowId ? allMatches.filter((match) => match.windowId === windowId) : allMatches;
|
|
733
|
-
const availableGroups = listGroupSummaries(snapshot, buildWindowLabels2);
|
|
734
|
-
if (matches.length === 0) {
|
|
735
|
-
const message = windowId && allMatches.length > 0 ? "Group title not found in specified window" : "No matching group title found";
|
|
736
|
-
return {
|
|
737
|
-
error: {
|
|
738
|
-
message,
|
|
739
|
-
hint: "Use tabctl group-list to see existing groups.",
|
|
740
|
-
matches: allMatches.map((match) => summarizeGroupMatch(match, windowLabels)),
|
|
741
|
-
availableGroups
|
|
742
|
-
}
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
if (matches.length > 1) {
|
|
746
|
-
return {
|
|
747
|
-
error: {
|
|
748
|
-
message: `Ambiguous group title: found ${matches.length} groups named "${groupTitle}". Use group-gather to merge duplicates, --group-id to target by ID, or --window to narrow scope.`,
|
|
749
|
-
hint: "Use group-gather to merge duplicates, --group-id to target by ID, or --window to narrow scope.",
|
|
750
|
-
matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
|
|
751
|
-
availableGroups
|
|
752
|
-
}
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
return { match: matches[0] };
|
|
756
|
-
}
|
|
757
|
-
function resolveGroupById2(snapshot, buildWindowLabels2, groupId) {
|
|
758
|
-
const windows = snapshot.windows;
|
|
759
|
-
const matches = [];
|
|
760
|
-
for (const win of windows) {
|
|
761
|
-
const group = win.groups.find((entry) => entry.groupId === groupId);
|
|
762
|
-
if (group) {
|
|
763
|
-
matches.push({
|
|
764
|
-
windowId: win.windowId,
|
|
765
|
-
group,
|
|
766
|
-
tabs: getGroupTabs(win, groupId)
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
if (matches.length === 0) {
|
|
771
|
-
return {
|
|
772
|
-
error: {
|
|
773
|
-
message: "Group not found",
|
|
774
|
-
hint: "Use tabctl group-list to see existing groups.",
|
|
775
|
-
availableGroups: listGroupSummaries(snapshot, buildWindowLabels2)
|
|
776
|
-
}
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
if (matches.length > 1) {
|
|
780
|
-
const windowLabels = buildWindowLabels2(snapshot);
|
|
781
|
-
return {
|
|
782
|
-
error: {
|
|
783
|
-
message: "Group id is ambiguous. Provide a windowId.",
|
|
784
|
-
hint: "Use --window to disambiguate group ids.",
|
|
785
|
-
matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
|
|
786
|
-
availableGroups: listGroupSummaries(snapshot, buildWindowLabels2)
|
|
787
|
-
}
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
return { match: matches[0] };
|
|
791
|
-
}
|
|
792
|
-
async function listGroups2(params, deps2) {
|
|
793
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
794
|
-
const windows = snapshot.windows;
|
|
795
|
-
const windowLabels = deps2.buildWindowLabels(snapshot);
|
|
796
|
-
const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
797
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
798
|
-
throw new Error("Window not found");
|
|
799
|
-
}
|
|
800
|
-
const groups2 = [];
|
|
801
|
-
for (const win of windows) {
|
|
802
|
-
if (windowIdParam && win.windowId !== windowIdParam) {
|
|
803
|
-
continue;
|
|
804
|
-
}
|
|
805
|
-
const counts = /* @__PURE__ */ new Map();
|
|
806
|
-
for (const tab of win.tabs) {
|
|
807
|
-
const groupId = tab.groupId;
|
|
808
|
-
if (typeof groupId === "number" && groupId !== -1) {
|
|
809
|
-
counts.set(groupId, (counts.get(groupId) || 0) + 1);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
for (const group of win.groups) {
|
|
813
|
-
const groupId = group.groupId;
|
|
814
|
-
groups2.push({
|
|
815
|
-
windowId: win.windowId,
|
|
816
|
-
windowLabel: windowLabels.get(win.windowId) ?? null,
|
|
817
|
-
groupId,
|
|
818
|
-
title: group.title ?? null,
|
|
819
|
-
color: group.color ?? null,
|
|
820
|
-
collapsed: group.collapsed ?? null,
|
|
821
|
-
tabCount: counts.get(groupId) || 0
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
return { groups: groups2 };
|
|
826
|
-
}
|
|
827
|
-
async function groupUpdate2(params, deps2) {
|
|
828
|
-
const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
|
|
829
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
830
|
-
if (!groupId && !groupTitle) {
|
|
831
|
-
throw new Error("Missing group identifier");
|
|
832
|
-
}
|
|
833
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
834
|
-
const windows = snapshot.windows;
|
|
835
|
-
const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
836
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
837
|
-
throw new Error("Window not found");
|
|
838
|
-
}
|
|
839
|
-
let match;
|
|
840
|
-
if (groupId != null) {
|
|
841
|
-
const resolved = resolveGroupById2(snapshot, deps2.buildWindowLabels, groupId);
|
|
842
|
-
if (resolved.error) {
|
|
843
|
-
throw resolved.error;
|
|
844
|
-
}
|
|
845
|
-
match = resolved.match;
|
|
846
|
-
if (windowIdParam && windowIdParam !== match.windowId) {
|
|
847
|
-
throw new Error("Group is not in the specified window");
|
|
848
|
-
}
|
|
849
|
-
} else {
|
|
850
|
-
const resolved = resolveGroupByTitle2(snapshot, deps2.buildWindowLabels, groupTitle, windowIdParam || void 0);
|
|
851
|
-
if (resolved.error) {
|
|
852
|
-
throw resolved.error;
|
|
853
|
-
}
|
|
854
|
-
match = resolved.match;
|
|
855
|
-
}
|
|
856
|
-
const update = {};
|
|
857
|
-
if (typeof params.title === "string") {
|
|
858
|
-
update.title = params.title;
|
|
859
|
-
}
|
|
860
|
-
if (typeof params.color === "string" && params.color.trim()) {
|
|
861
|
-
update.color = params.color.trim();
|
|
862
|
-
}
|
|
863
|
-
if (typeof params.collapsed === "boolean") {
|
|
864
|
-
update.collapsed = params.collapsed;
|
|
865
|
-
}
|
|
866
|
-
if (!Object.keys(update).length) {
|
|
867
|
-
throw new Error("Missing group update fields");
|
|
868
|
-
}
|
|
869
|
-
const updated = await chrome.tabGroups.update(match.group.groupId, update);
|
|
870
|
-
const fullResult = {
|
|
871
|
-
groupId: updated.id,
|
|
872
|
-
windowId: updated.windowId,
|
|
873
|
-
title: updated.title,
|
|
874
|
-
color: updated.color,
|
|
875
|
-
collapsed: updated.collapsed,
|
|
876
|
-
undo: {
|
|
877
|
-
action: "group-update",
|
|
878
|
-
groupId: updated.id,
|
|
879
|
-
windowId: match.windowId,
|
|
880
|
-
previous: {
|
|
881
|
-
title: match.group.title ?? null,
|
|
882
|
-
color: match.group.color ?? null,
|
|
883
|
-
collapsed: match.group.collapsed ?? null
|
|
884
|
-
}
|
|
885
|
-
},
|
|
886
|
-
txid: params.txid || null
|
|
887
|
-
};
|
|
888
|
-
return {
|
|
889
|
-
groupId: updated.id,
|
|
890
|
-
windowId: updated.windowId,
|
|
891
|
-
summary: { updatedGroups: 1 },
|
|
892
|
-
undo: fullResult.undo,
|
|
893
|
-
txid: fullResult.txid
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
async function groupUngroup2(params, deps2) {
|
|
897
|
-
const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
|
|
898
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
899
|
-
if (!groupId && !groupTitle) {
|
|
900
|
-
throw new Error("Missing group identifier");
|
|
901
|
-
}
|
|
902
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
903
|
-
const windows = snapshot.windows;
|
|
904
|
-
const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
905
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
906
|
-
throw new Error("Window not found");
|
|
907
|
-
}
|
|
908
|
-
let match;
|
|
909
|
-
if (groupId != null) {
|
|
910
|
-
const resolved = resolveGroupById2(snapshot, deps2.buildWindowLabels, groupId);
|
|
911
|
-
if (resolved.error) {
|
|
912
|
-
throw resolved.error;
|
|
913
|
-
}
|
|
914
|
-
match = resolved.match;
|
|
915
|
-
if (windowIdParam && windowIdParam !== match.windowId) {
|
|
916
|
-
throw new Error("Group is not in the specified window");
|
|
917
|
-
}
|
|
918
|
-
} else {
|
|
919
|
-
const resolved = resolveGroupByTitle2(snapshot, deps2.buildWindowLabels, groupTitle, windowIdParam || void 0);
|
|
920
|
-
if (resolved.error) {
|
|
921
|
-
throw resolved.error;
|
|
922
|
-
}
|
|
923
|
-
match = resolved.match;
|
|
924
|
-
}
|
|
925
|
-
const undoTabs = match.tabs.map((tab) => ({
|
|
926
|
-
tabId: tab.tabId,
|
|
927
|
-
windowId: tab.windowId,
|
|
928
|
-
index: tab.index,
|
|
929
|
-
groupId: tab.groupId,
|
|
930
|
-
groupTitle: tab.groupTitle,
|
|
931
|
-
groupColor: tab.groupColor,
|
|
932
|
-
groupCollapsed: match.group.collapsed ?? null
|
|
933
|
-
})).filter((tab) => typeof tab.tabId === "number");
|
|
934
|
-
const tabIds = match.tabs.map((tab) => tab.tabId).filter((tabId) => typeof tabId === "number");
|
|
935
|
-
if (tabIds.length) {
|
|
936
|
-
await chrome.tabs.ungroup(tabIds);
|
|
937
|
-
}
|
|
938
|
-
const fullResult = {
|
|
939
|
-
groupId: match.group.groupId,
|
|
940
|
-
groupTitle: match.group.title || null,
|
|
941
|
-
windowId: match.windowId,
|
|
942
|
-
summary: {
|
|
943
|
-
ungroupedTabs: tabIds.length
|
|
944
|
-
},
|
|
945
|
-
undo: {
|
|
946
|
-
action: "group-ungroup",
|
|
947
|
-
groupId: match.group.groupId,
|
|
948
|
-
windowId: match.windowId,
|
|
949
|
-
groupTitle: match.group.title || null,
|
|
950
|
-
groupColor: match.group.color || null,
|
|
951
|
-
groupCollapsed: match.group.collapsed ?? null,
|
|
952
|
-
tabs: undoTabs
|
|
953
|
-
},
|
|
954
|
-
txid: params.txid || null
|
|
955
|
-
};
|
|
956
|
-
return {
|
|
957
|
-
groupId: match.group.groupId,
|
|
958
|
-
windowId: match.windowId,
|
|
959
|
-
summary: fullResult.summary,
|
|
960
|
-
undo: fullResult.undo,
|
|
961
|
-
txid: fullResult.txid
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
async function groupAssign2(params, deps2) {
|
|
965
|
-
const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
|
|
966
|
-
const tabIds = rawTabIds.filter((id) => Number.isFinite(id));
|
|
967
|
-
if (!tabIds.length) {
|
|
968
|
-
throw new Error("Missing tabIds");
|
|
969
|
-
}
|
|
970
|
-
const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
|
|
971
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
972
|
-
if (!groupId && !groupTitle) {
|
|
973
|
-
throw new Error("Missing group identifier");
|
|
974
|
-
}
|
|
975
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
976
|
-
const windows = snapshot.windows;
|
|
977
|
-
const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
978
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
979
|
-
throw new Error("Window not found");
|
|
980
|
-
}
|
|
981
|
-
const tabIndex = /* @__PURE__ */ new Map();
|
|
982
|
-
for (const win of windows) {
|
|
983
|
-
for (const tab of win.tabs) {
|
|
984
|
-
if (typeof tab.tabId === "number") {
|
|
985
|
-
tabIndex.set(tab.tabId, { tab, windowId: win.windowId });
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
const skipped = [];
|
|
990
|
-
const resolvedTabIds = [];
|
|
991
|
-
const sourceWindows = /* @__PURE__ */ new Set();
|
|
992
|
-
const undoTabs = [];
|
|
993
|
-
for (const tabId of tabIds) {
|
|
994
|
-
const entry = tabIndex.get(tabId);
|
|
995
|
-
if (!entry) {
|
|
996
|
-
skipped.push({ tabId, reason: "not_found" });
|
|
997
|
-
continue;
|
|
998
|
-
}
|
|
999
|
-
resolvedTabIds.push(tabId);
|
|
1000
|
-
sourceWindows.add(entry.windowId);
|
|
1001
|
-
const tab = entry.tab;
|
|
1002
|
-
undoTabs.push({
|
|
1003
|
-
tabId,
|
|
1004
|
-
windowId: entry.windowId,
|
|
1005
|
-
index: tab.index,
|
|
1006
|
-
groupId: tab.groupId,
|
|
1007
|
-
groupTitle: tab.groupTitle,
|
|
1008
|
-
groupColor: tab.groupColor,
|
|
1009
|
-
groupCollapsed: tab.groupCollapsed ?? null
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
if (!resolvedTabIds.length) {
|
|
1013
|
-
throw new Error("No matching tabs found");
|
|
1014
|
-
}
|
|
1015
|
-
let targetGroupId = null;
|
|
1016
|
-
let targetWindowId = null;
|
|
1017
|
-
let targetTitle = null;
|
|
1018
|
-
let created = false;
|
|
1019
|
-
if (groupId != null) {
|
|
1020
|
-
const resolved = resolveGroupById2(snapshot, deps2.buildWindowLabels, groupId);
|
|
1021
|
-
if (resolved.error) {
|
|
1022
|
-
throw resolved.error;
|
|
1023
|
-
}
|
|
1024
|
-
const match = resolved.match;
|
|
1025
|
-
targetGroupId = match.group.groupId;
|
|
1026
|
-
targetWindowId = match.windowId;
|
|
1027
|
-
targetTitle = typeof match.group.title === "string" ? match.group.title : null;
|
|
1028
|
-
if (windowIdParam && windowIdParam !== targetWindowId) {
|
|
1029
|
-
throw new Error("Group is not in the specified window");
|
|
1030
|
-
}
|
|
1031
|
-
} else {
|
|
1032
|
-
const resolved = resolveGroupByTitle2(snapshot, deps2.buildWindowLabels, groupTitle, windowIdParam || void 0);
|
|
1033
|
-
if (resolved.error) {
|
|
1034
|
-
const error = resolved.error;
|
|
1035
|
-
if (error.message === "No matching group title found" && params.create === true) {
|
|
1036
|
-
targetWindowId = windowIdParam || (sourceWindows.size === 1 ? Array.from(sourceWindows)[0] : null);
|
|
1037
|
-
if (!targetWindowId) {
|
|
1038
|
-
throw new Error("Multiple source windows. Provide --window to create a new group.");
|
|
1039
|
-
}
|
|
1040
|
-
targetTitle = groupTitle;
|
|
1041
|
-
created = true;
|
|
1042
|
-
} else {
|
|
1043
|
-
throw error;
|
|
1044
|
-
}
|
|
1045
|
-
} else {
|
|
1046
|
-
const match = resolved.match;
|
|
1047
|
-
targetGroupId = match.group.groupId;
|
|
1048
|
-
targetWindowId = match.windowId;
|
|
1049
|
-
targetTitle = typeof match.group.title === "string" && match.group.title ? match.group.title : groupTitle;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
if (!targetWindowId) {
|
|
1053
|
-
throw new Error("Target window not found");
|
|
1054
|
-
}
|
|
1055
|
-
const moveIds = resolvedTabIds.filter((tabId) => {
|
|
1056
|
-
const entry = tabIndex.get(tabId);
|
|
1057
|
-
return entry && entry.windowId !== targetWindowId;
|
|
1058
|
-
});
|
|
1059
|
-
if (moveIds.length > 0) {
|
|
1060
|
-
await chrome.tabs.move(moveIds, { windowId: targetWindowId, index: -1 });
|
|
1061
|
-
}
|
|
1062
|
-
let assignedGroupId = targetGroupId;
|
|
1063
|
-
if (targetGroupId != null) {
|
|
1064
|
-
await chrome.tabs.group({ groupId: targetGroupId, tabIds: resolvedTabIds });
|
|
1065
|
-
} else {
|
|
1066
|
-
assignedGroupId = await chrome.tabs.group({ tabIds: resolvedTabIds, createProperties: { windowId: targetWindowId } });
|
|
1067
|
-
const update = {};
|
|
1068
|
-
if (targetTitle) {
|
|
1069
|
-
update.title = targetTitle;
|
|
1070
|
-
}
|
|
1071
|
-
if (typeof params.color === "string" && params.color.trim()) {
|
|
1072
|
-
update.color = params.color.trim();
|
|
1073
|
-
}
|
|
1074
|
-
if (typeof params.collapsed === "boolean") {
|
|
1075
|
-
update.collapsed = params.collapsed;
|
|
1076
|
-
}
|
|
1077
|
-
if (Object.keys(update).length > 0) {
|
|
1078
|
-
try {
|
|
1079
|
-
await chrome.tabGroups.update(assignedGroupId, update);
|
|
1080
|
-
} catch (error) {
|
|
1081
|
-
deps2.log("Failed to update group", error);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
created = true;
|
|
1085
|
-
}
|
|
1086
|
-
const fullResult = {
|
|
1087
|
-
groupId: assignedGroupId,
|
|
1088
|
-
groupTitle: targetTitle || groupTitle || null,
|
|
1089
|
-
windowId: targetWindowId,
|
|
1090
|
-
created,
|
|
1091
|
-
summary: {
|
|
1092
|
-
movedTabs: moveIds.length,
|
|
1093
|
-
groupedTabs: resolvedTabIds.length,
|
|
1094
|
-
skippedTabs: skipped.length
|
|
1095
|
-
},
|
|
1096
|
-
skipped,
|
|
1097
|
-
undo: {
|
|
1098
|
-
action: "group-assign",
|
|
1099
|
-
groupId: assignedGroupId,
|
|
1100
|
-
groupTitle: targetTitle || groupTitle || null,
|
|
1101
|
-
groupColor: typeof params.color === "string" && params.color.trim() ? params.color.trim() : null,
|
|
1102
|
-
groupCollapsed: typeof params.collapsed === "boolean" ? params.collapsed : null,
|
|
1103
|
-
created,
|
|
1104
|
-
tabs: undoTabs
|
|
1105
|
-
},
|
|
1106
|
-
txid: params.txid || null
|
|
1107
|
-
};
|
|
1108
|
-
return {
|
|
1109
|
-
groupId: assignedGroupId,
|
|
1110
|
-
windowId: targetWindowId,
|
|
1111
|
-
created,
|
|
1112
|
-
summary: fullResult.summary,
|
|
1113
|
-
skipped: fullResult.skipped,
|
|
1114
|
-
undo: fullResult.undo,
|
|
1115
|
-
txid: fullResult.txid
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
async function groupGather2(params, deps2) {
|
|
1119
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
1120
|
-
const windows = snapshot.windows;
|
|
1121
|
-
const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
1122
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
1123
|
-
throw new Error("Window not found");
|
|
1124
|
-
}
|
|
1125
|
-
const groupTitleFilter = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
1126
|
-
const merged = [];
|
|
1127
|
-
const undoEntries = [];
|
|
1128
|
-
for (const win of windows) {
|
|
1129
|
-
if (windowIdParam && win.windowId !== windowIdParam)
|
|
1130
|
-
continue;
|
|
1131
|
-
const byTitle = /* @__PURE__ */ new Map();
|
|
1132
|
-
for (const group of win.groups) {
|
|
1133
|
-
const title = typeof group.title === "string" ? group.title : "";
|
|
1134
|
-
if (!title)
|
|
1135
|
-
continue;
|
|
1136
|
-
if (groupTitleFilter && title !== groupTitleFilter)
|
|
1137
|
-
continue;
|
|
1138
|
-
if (!byTitle.has(title))
|
|
1139
|
-
byTitle.set(title, []);
|
|
1140
|
-
byTitle.get(title).push(group);
|
|
1141
|
-
}
|
|
1142
|
-
for (const [title, titleGroups] of byTitle) {
|
|
1143
|
-
if (titleGroups.length < 2)
|
|
1144
|
-
continue;
|
|
1145
|
-
const groupsWithIndex = titleGroups.map((g) => {
|
|
1146
|
-
const tabs2 = win.tabs.filter((t) => t.groupId === g.groupId);
|
|
1147
|
-
const minIndex = Math.min(...tabs2.map((t) => {
|
|
1148
|
-
const idx = Number(t.index);
|
|
1149
|
-
return Number.isFinite(idx) ? idx : Infinity;
|
|
1150
|
-
}));
|
|
1151
|
-
return { group: g, tabs: tabs2, minIndex };
|
|
1152
|
-
});
|
|
1153
|
-
groupsWithIndex.sort((a, b) => a.minIndex - b.minIndex);
|
|
1154
|
-
const primary = groupsWithIndex[0];
|
|
1155
|
-
const duplicates = groupsWithIndex.slice(1);
|
|
1156
|
-
let movedTabs = 0;
|
|
1157
|
-
for (const dup of duplicates) {
|
|
1158
|
-
const tabIds = dup.tabs.map((t) => t.tabId).filter((id) => typeof id === "number");
|
|
1159
|
-
if (tabIds.length > 0) {
|
|
1160
|
-
for (const tab of dup.tabs) {
|
|
1161
|
-
undoEntries.push({
|
|
1162
|
-
tabId: tab.tabId,
|
|
1163
|
-
windowId: win.windowId,
|
|
1164
|
-
index: tab.index,
|
|
1165
|
-
groupId: tab.groupId,
|
|
1166
|
-
groupTitle: tab.groupTitle,
|
|
1167
|
-
groupColor: tab.groupColor,
|
|
1168
|
-
groupCollapsed: dup.group.collapsed ?? null
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
await chrome.tabs.group({ groupId: primary.group.groupId, tabIds });
|
|
1172
|
-
movedTabs += tabIds.length;
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
merged.push({
|
|
1176
|
-
windowId: win.windowId,
|
|
1177
|
-
groupTitle: title,
|
|
1178
|
-
primaryGroupId: primary.group.groupId,
|
|
1179
|
-
mergedGroupCount: duplicates.length,
|
|
1180
|
-
movedTabs
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
const fullResult = {
|
|
1185
|
-
merged,
|
|
1186
|
-
summary: {
|
|
1187
|
-
mergedGroups: merged.reduce((sum, m) => sum + m.mergedGroupCount, 0),
|
|
1188
|
-
movedTabs: merged.reduce((sum, m) => sum + m.movedTabs, 0)
|
|
1189
|
-
},
|
|
1190
|
-
undo: {
|
|
1191
|
-
action: "group-gather",
|
|
1192
|
-
tabs: undoEntries
|
|
1193
|
-
},
|
|
1194
|
-
txid: params.txid || null
|
|
1195
|
-
};
|
|
1196
|
-
return {
|
|
1197
|
-
merged,
|
|
1198
|
-
summary: fullResult.summary,
|
|
1199
|
-
undo: fullResult.undo,
|
|
1200
|
-
txid: fullResult.txid
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
449
|
}
|
|
1204
450
|
});
|
|
1205
451
|
|
|
1206
|
-
//
|
|
1207
|
-
var
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
function
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
forceHttp: false,
|
|
1216
|
-
forceHttps: false,
|
|
1217
|
-
stripAuthentication: true,
|
|
1218
|
-
stripHash: false,
|
|
1219
|
-
stripTextFragment: true,
|
|
1220
|
-
stripWWW: true,
|
|
1221
|
-
removeQueryParameters: [/^utm_\w+/i],
|
|
1222
|
-
removeTrailingSlash: true,
|
|
1223
|
-
removeSingleSlash: true,
|
|
1224
|
-
removeDirectoryIndex: false,
|
|
1225
|
-
removeExplicitPort: false,
|
|
1226
|
-
sortQueryParameters: true,
|
|
1227
|
-
removePath: false,
|
|
1228
|
-
transformPath: false,
|
|
1229
|
-
...options
|
|
1230
|
-
};
|
|
1231
|
-
if (typeof options.defaultProtocol === "string" && !options.defaultProtocol.endsWith(":")) {
|
|
1232
|
-
options.defaultProtocol = `${options.defaultProtocol}:`;
|
|
1233
|
-
}
|
|
1234
|
-
urlString = urlString.trim();
|
|
1235
|
-
if (/^data:/i.test(urlString)) {
|
|
1236
|
-
return normalizeDataURL(urlString, options);
|
|
1237
|
-
}
|
|
1238
|
-
if (hasCustomProtocol(urlString)) {
|
|
1239
|
-
return urlString;
|
|
1240
|
-
}
|
|
1241
|
-
const hasRelativeProtocol = urlString.startsWith("//");
|
|
1242
|
-
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
|
|
1243
|
-
if (!isRelativeUrl) {
|
|
1244
|
-
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
|
|
1245
|
-
}
|
|
1246
|
-
const urlObject = new URL(urlString);
|
|
1247
|
-
if (options.forceHttp && options.forceHttps) {
|
|
1248
|
-
throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");
|
|
1249
|
-
}
|
|
1250
|
-
if (options.forceHttp && urlObject.protocol === "https:") {
|
|
1251
|
-
urlObject.protocol = "http:";
|
|
452
|
+
// dist/extension/background.js
|
|
453
|
+
var HOST_NAME = "com.erwinkroon.tabctl";
|
|
454
|
+
var manifest = chrome.runtime.getManifest();
|
|
455
|
+
var MANIFEST_VERSION = manifest.version || "0.0.0";
|
|
456
|
+
var MANIFEST_VERSION_NAME = manifest.version_name || MANIFEST_VERSION;
|
|
457
|
+
function parseVersionName(versionName) {
|
|
458
|
+
const match = versionName.match(/-dev\.([0-9a-f]+)(\.dirty)?$/i);
|
|
459
|
+
if (!match) {
|
|
460
|
+
return { gitSha: null, dirty: false };
|
|
1252
461
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
462
|
+
return { gitSha: match[1] || null, dirty: Boolean(match[2]) };
|
|
463
|
+
}
|
|
464
|
+
var parsed = parseVersionName(MANIFEST_VERSION_NAME);
|
|
465
|
+
var VERSION_INFO = {
|
|
466
|
+
version: MANIFEST_VERSION_NAME,
|
|
467
|
+
baseVersion: MANIFEST_VERSION,
|
|
468
|
+
gitSha: parsed.gitSha,
|
|
469
|
+
dirty: parsed.dirty
|
|
470
|
+
};
|
|
471
|
+
var KEEPALIVE_ALARM = "tabctl-keepalive";
|
|
472
|
+
var KEEPALIVE_INTERVAL_MINUTES = 1;
|
|
473
|
+
var screenshot = require_screenshot();
|
|
474
|
+
var content = require_content();
|
|
475
|
+
var { delay, executeWithTimeout } = content;
|
|
476
|
+
var DESCRIPTION_MAX_LENGTH = 250;
|
|
477
|
+
function requireFiniteId(value, name) {
|
|
478
|
+
const n = Number(value);
|
|
479
|
+
if (!Number.isFinite(n))
|
|
480
|
+
throw new Error(`${name} must be a finite number, got: ${String(value)}`);
|
|
481
|
+
return n;
|
|
482
|
+
}
|
|
483
|
+
var state = {
|
|
484
|
+
port: null,
|
|
485
|
+
lastFocused: {},
|
|
486
|
+
lastFocusedLoaded: false
|
|
487
|
+
};
|
|
488
|
+
function log(...args) {
|
|
489
|
+
console.log("[tabctl]", ...args);
|
|
490
|
+
}
|
|
491
|
+
function sendResponse(id, ok, payload) {
|
|
492
|
+
if (!state.port) {
|
|
493
|
+
return;
|
|
1255
494
|
}
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
1258
|
-
|
|
495
|
+
if (ok) {
|
|
496
|
+
const data = typeof payload === "object" && payload !== null ? payload : { payload };
|
|
497
|
+
state.port.postMessage({ id, ok: true, data });
|
|
498
|
+
return;
|
|
1259
499
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
500
|
+
const error = payload instanceof Error ? { message: payload.message, stack: payload.stack } : payload;
|
|
501
|
+
state.port.postMessage({ id, ok: false, error });
|
|
502
|
+
}
|
|
503
|
+
function connectNative() {
|
|
504
|
+
if (state.port) {
|
|
505
|
+
return;
|
|
1264
506
|
}
|
|
1265
|
-
|
|
1266
|
-
const
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
const
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
507
|
+
try {
|
|
508
|
+
const port = chrome.runtime.connectNative(HOST_NAME);
|
|
509
|
+
state.port = port;
|
|
510
|
+
port.onMessage.addListener(handleNativeMessage);
|
|
511
|
+
port.onDisconnect.addListener(() => {
|
|
512
|
+
const lastError = chrome.runtime.lastError;
|
|
513
|
+
if (lastError) {
|
|
514
|
+
log("Native host disconnected:", lastError.message);
|
|
515
|
+
} else {
|
|
516
|
+
log("Native host disconnected");
|
|
1273
517
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
lastIndex = protocolAtIndex + protocol.length;
|
|
1280
|
-
}
|
|
1281
|
-
const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length);
|
|
1282
|
-
result += remnant.replace(/\/{2,}/g, "/");
|
|
1283
|
-
urlObject.pathname = result;
|
|
1284
|
-
}
|
|
1285
|
-
if (urlObject.pathname) {
|
|
1286
|
-
try {
|
|
1287
|
-
urlObject.pathname = decodeURI(urlObject.pathname).replace(/\\/g, "%5C");
|
|
1288
|
-
} catch {
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
if (options.removeDirectoryIndex === true) {
|
|
1292
|
-
options.removeDirectoryIndex = [/^index\.[a-z]+$/];
|
|
1293
|
-
}
|
|
1294
|
-
if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
|
|
1295
|
-
const pathComponents = urlObject.pathname.split("/").filter(Boolean);
|
|
1296
|
-
const lastComponent = pathComponents.at(-1);
|
|
1297
|
-
if (lastComponent && testParameter(lastComponent, options.removeDirectoryIndex)) {
|
|
1298
|
-
pathComponents.pop();
|
|
1299
|
-
urlObject.pathname = pathComponents.length > 0 ? `/${pathComponents.join("/")}/` : "/";
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
if (options.removePath) {
|
|
1303
|
-
urlObject.pathname = "/";
|
|
518
|
+
state.port = null;
|
|
519
|
+
});
|
|
520
|
+
log("Native host connected");
|
|
521
|
+
} catch (error) {
|
|
522
|
+
log("Native host connection failed", error);
|
|
1304
523
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
524
|
+
}
|
|
525
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
526
|
+
connectNative();
|
|
527
|
+
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
|
|
528
|
+
});
|
|
529
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
530
|
+
connectNative();
|
|
531
|
+
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
|
|
532
|
+
});
|
|
533
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
534
|
+
if (alarm.name === KEEPALIVE_ALARM) {
|
|
535
|
+
connectNative();
|
|
1309
536
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
if (Array.isArray(options.removeQueryParameters)) {
|
|
1317
|
-
for (const key of [...urlObject.searchParams.keys()]) {
|
|
1318
|
-
if (testParameter(key, options.removeQueryParameters)) {
|
|
1319
|
-
urlObject.searchParams.delete(key);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {
|
|
1324
|
-
urlObject.search = "";
|
|
1325
|
-
}
|
|
1326
|
-
if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
|
|
1327
|
-
for (const key of [...urlObject.searchParams.keys()]) {
|
|
1328
|
-
if (!testParameter(key, options.keepQueryParameters)) {
|
|
1329
|
-
urlObject.searchParams.delete(key);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
if (options.sortQueryParameters) {
|
|
1334
|
-
const originalSearch = urlObject.search;
|
|
1335
|
-
urlObject.searchParams.sort();
|
|
1336
|
-
try {
|
|
1337
|
-
urlObject.search = decodeURIComponent(urlObject.search);
|
|
1338
|
-
} catch {
|
|
1339
|
-
}
|
|
1340
|
-
const partsWithoutEquals = originalSearch.slice(1).split("&").filter((p) => p && !p.includes("="));
|
|
1341
|
-
for (const part of partsWithoutEquals) {
|
|
1342
|
-
const decoded = decodeURIComponent(part);
|
|
1343
|
-
urlObject.search = urlObject.search.replace(`?${decoded}=`, `?${decoded}`).replace(`&${decoded}=`, `&${decoded}`);
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
if (options.removeTrailingSlash) {
|
|
1347
|
-
urlObject.pathname = urlObject.pathname.replace(/\/$/, "");
|
|
1348
|
-
}
|
|
1349
|
-
if (options.removeExplicitPort && urlObject.port) {
|
|
1350
|
-
urlObject.port = "";
|
|
1351
|
-
}
|
|
1352
|
-
const oldUrlString = urlString;
|
|
1353
|
-
urlString = urlObject.toString();
|
|
1354
|
-
if (!options.removeSingleSlash && urlObject.pathname === "/" && !oldUrlString.endsWith("/") && urlObject.hash === "") {
|
|
1355
|
-
urlString = urlString.replace(/\/$/, "");
|
|
1356
|
-
}
|
|
1357
|
-
if ((options.removeTrailingSlash || urlObject.pathname === "/") && urlObject.hash === "" && options.removeSingleSlash) {
|
|
1358
|
-
urlString = urlString.replace(/\/$/, "");
|
|
1359
|
-
}
|
|
1360
|
-
if (hasRelativeProtocol && !options.normalizeProtocol) {
|
|
1361
|
-
urlString = urlString.replace(/^http:\/\//, "//");
|
|
1362
|
-
}
|
|
1363
|
-
if (options.stripProtocol) {
|
|
1364
|
-
urlString = urlString.replace(/^(?:https?:)?\/\//, "");
|
|
1365
|
-
}
|
|
1366
|
-
return urlString;
|
|
1367
|
-
}
|
|
1368
|
-
var DATA_URL_DEFAULT_MIME_TYPE, DATA_URL_DEFAULT_CHARSET, testParameter, supportedProtocols, hasCustomProtocol, normalizeDataURL;
|
|
1369
|
-
var init_normalize_url = __esm({
|
|
1370
|
-
"node_modules/normalize-url/index.js"() {
|
|
1371
|
-
DATA_URL_DEFAULT_MIME_TYPE = "text/plain";
|
|
1372
|
-
DATA_URL_DEFAULT_CHARSET = "us-ascii";
|
|
1373
|
-
testParameter = (name, filters) => filters.some((filter) => filter instanceof RegExp ? filter.test(name) : filter === name);
|
|
1374
|
-
supportedProtocols = /* @__PURE__ */ new Set([
|
|
1375
|
-
"https:",
|
|
1376
|
-
"http:",
|
|
1377
|
-
"file:"
|
|
1378
|
-
]);
|
|
1379
|
-
hasCustomProtocol = (urlString) => {
|
|
1380
|
-
try {
|
|
1381
|
-
const { protocol } = new URL(urlString);
|
|
1382
|
-
return protocol.endsWith(":") && !protocol.includes(".") && !supportedProtocols.has(protocol);
|
|
1383
|
-
} catch {
|
|
1384
|
-
return false;
|
|
1385
|
-
}
|
|
1386
|
-
};
|
|
1387
|
-
normalizeDataURL = (urlString, { stripHash }) => {
|
|
1388
|
-
const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString);
|
|
1389
|
-
if (!match) {
|
|
1390
|
-
throw new Error(`Invalid URL: ${urlString}`);
|
|
1391
|
-
}
|
|
1392
|
-
const { type, data, hash } = match.groups;
|
|
1393
|
-
const mediaType = type.split(";");
|
|
1394
|
-
const isBase64 = mediaType.at(-1) === "base64";
|
|
1395
|
-
if (isBase64) {
|
|
1396
|
-
mediaType.pop();
|
|
1397
|
-
}
|
|
1398
|
-
const mimeType = mediaType.shift()?.toLowerCase() ?? "";
|
|
1399
|
-
const attributes = mediaType.map((attribute) => {
|
|
1400
|
-
let [key, value = ""] = attribute.split("=").map((string) => string.trim());
|
|
1401
|
-
if (key === "charset") {
|
|
1402
|
-
value = value.toLowerCase();
|
|
1403
|
-
if (value === DATA_URL_DEFAULT_CHARSET) {
|
|
1404
|
-
return "";
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
return `${key}${value ? `=${value}` : ""}`;
|
|
1408
|
-
}).filter(Boolean);
|
|
1409
|
-
const normalizedMediaType = [...attributes];
|
|
1410
|
-
if (isBase64) {
|
|
1411
|
-
normalizedMediaType.push("base64");
|
|
1412
|
-
}
|
|
1413
|
-
if (normalizedMediaType.length > 0 || mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE) {
|
|
1414
|
-
normalizedMediaType.unshift(mimeType);
|
|
1415
|
-
}
|
|
1416
|
-
const hashPart = stripHash || !hash ? "" : `#${hash}`;
|
|
1417
|
-
return `data:${normalizedMediaType.join(";")},${isBase64 ? data.trim() : data}${hashPart}`;
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
});
|
|
1421
|
-
|
|
1422
|
-
// dist/extension/lib/tabs.js
|
|
1423
|
-
var require_tabs = __commonJS({
|
|
1424
|
-
"dist/extension/lib/tabs.js"(exports) {
|
|
1425
|
-
"use strict";
|
|
1426
|
-
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
1427
|
-
return mod && mod.__esModule ? mod : { "default": mod };
|
|
1428
|
-
};
|
|
1429
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1430
|
-
exports.getMostRecentFocusedWindowId = getMostRecentFocusedWindowId2;
|
|
1431
|
-
exports.normalizeUrl = normalizeUrl2;
|
|
1432
|
-
exports.normalizeTabIndex = normalizeTabIndex2;
|
|
1433
|
-
exports.resolveOpenWindow = resolveOpenWindow;
|
|
1434
|
-
exports.focusTab = focusTab;
|
|
1435
|
-
exports.refreshTabs = refreshTabs;
|
|
1436
|
-
exports.openTabs = openTabs;
|
|
1437
|
-
var normalize_url_1 = __importDefault((init_normalize_url(), __toCommonJS(normalize_url_exports)));
|
|
1438
|
-
function getMostRecentFocusedWindowId2(windows) {
|
|
1439
|
-
let bestWindowId = null;
|
|
1440
|
-
let bestFocusedAt = -Infinity;
|
|
1441
|
-
for (const win of windows) {
|
|
1442
|
-
for (const tab of win.tabs) {
|
|
1443
|
-
const focusedAt = Number(tab.lastFocusedAt);
|
|
1444
|
-
if (!Number.isFinite(focusedAt)) {
|
|
1445
|
-
continue;
|
|
1446
|
-
}
|
|
1447
|
-
if (focusedAt > bestFocusedAt) {
|
|
1448
|
-
bestFocusedAt = focusedAt;
|
|
1449
|
-
bestWindowId = win.windowId;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
return bestWindowId;
|
|
1454
|
-
}
|
|
1455
|
-
function normalizeUrl2(rawUrl) {
|
|
1456
|
-
if (!rawUrl || typeof rawUrl !== "string") {
|
|
1457
|
-
return null;
|
|
1458
|
-
}
|
|
1459
|
-
try {
|
|
1460
|
-
return (0, normalize_url_1.default)(rawUrl, {
|
|
1461
|
-
stripHash: true,
|
|
1462
|
-
removeQueryParameters: [
|
|
1463
|
-
/^utm_\w+$/i,
|
|
1464
|
-
"fbclid",
|
|
1465
|
-
"gclid",
|
|
1466
|
-
"igshid",
|
|
1467
|
-
"mc_cid",
|
|
1468
|
-
"mc_eid",
|
|
1469
|
-
"ref",
|
|
1470
|
-
"ref_src",
|
|
1471
|
-
"ref_url",
|
|
1472
|
-
"si"
|
|
1473
|
-
]
|
|
1474
|
-
});
|
|
1475
|
-
} catch {
|
|
1476
|
-
return null;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
function normalizeTabIndex2(value) {
|
|
1480
|
-
const index = Number(value);
|
|
1481
|
-
return Number.isFinite(index) ? index : null;
|
|
1482
|
-
}
|
|
1483
|
-
function shapeOpenResult(result) {
|
|
1484
|
-
return {
|
|
1485
|
-
windowId: result.windowId,
|
|
1486
|
-
groupId: result.groupId,
|
|
1487
|
-
createdTabIds: result.created.map((tab) => tab.tabId).filter((id) => typeof id === "number"),
|
|
1488
|
-
skipped: result.skipped,
|
|
1489
|
-
summary: result.summary
|
|
1490
|
-
};
|
|
1491
|
-
}
|
|
1492
|
-
function matchIncludes(value, needle) {
|
|
1493
|
-
if (!needle) {
|
|
1494
|
-
return false;
|
|
1495
|
-
}
|
|
1496
|
-
return typeof value === "string" && value.toLowerCase().includes(needle);
|
|
1497
|
-
}
|
|
1498
|
-
function resolveOpenWindow(snapshot, params) {
|
|
1499
|
-
const windows = snapshot.windows;
|
|
1500
|
-
if (!windows.length) {
|
|
1501
|
-
return { error: { message: "No windows available" } };
|
|
1502
|
-
}
|
|
1503
|
-
if (params.windowId != null) {
|
|
1504
|
-
if (typeof params.windowId === "string") {
|
|
1505
|
-
const normalized = params.windowId.trim().toLowerCase();
|
|
1506
|
-
if (normalized === "active") {
|
|
1507
|
-
const focused2 = windows.find((win) => win.focused);
|
|
1508
|
-
if (focused2) {
|
|
1509
|
-
return { windowId: focused2.windowId };
|
|
1510
|
-
}
|
|
1511
|
-
return { error: { message: "Active window not found" } };
|
|
1512
|
-
}
|
|
1513
|
-
if (normalized === "last-focused") {
|
|
1514
|
-
const lastFocused2 = getMostRecentFocusedWindowId2(windows);
|
|
1515
|
-
if (lastFocused2 != null) {
|
|
1516
|
-
return { windowId: lastFocused2 };
|
|
1517
|
-
}
|
|
1518
|
-
return { error: { message: "Last focused window not found" } };
|
|
1519
|
-
}
|
|
1520
|
-
if (normalized === "new") {
|
|
1521
|
-
return { error: { message: "--window new is only supported by open" } };
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
const windowId = Number(params.windowId);
|
|
1525
|
-
const found = windows.find((win) => win.windowId === windowId);
|
|
1526
|
-
if (!found) {
|
|
1527
|
-
return { error: { message: "Window not found" } };
|
|
1528
|
-
}
|
|
1529
|
-
return { windowId };
|
|
1530
|
-
}
|
|
1531
|
-
if (params.windowTabId != null) {
|
|
1532
|
-
const tabId = Number(params.windowTabId);
|
|
1533
|
-
const found = windows.find((win) => win.tabs.some((tab) => tab.tabId === tabId));
|
|
1534
|
-
if (!found) {
|
|
1535
|
-
return { error: { message: "Window not found for tab" } };
|
|
1536
|
-
}
|
|
1537
|
-
return { windowId: found.windowId };
|
|
1538
|
-
}
|
|
1539
|
-
let candidates = [...windows];
|
|
1540
|
-
let filtered = false;
|
|
1541
|
-
if (typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim()) {
|
|
1542
|
-
const groupTitle = params.afterGroupTitle.trim();
|
|
1543
|
-
candidates = candidates.filter((win) => win.groups.some((group) => group.title === groupTitle));
|
|
1544
|
-
filtered = true;
|
|
1545
|
-
}
|
|
1546
|
-
if (typeof params.windowGroupTitle === "string" && params.windowGroupTitle.trim()) {
|
|
1547
|
-
const groupTitle = params.windowGroupTitle.trim();
|
|
1548
|
-
candidates = candidates.filter((win) => win.groups.some((group) => group.title === groupTitle));
|
|
1549
|
-
filtered = true;
|
|
1550
|
-
}
|
|
1551
|
-
if (typeof params.windowUrl === "string" && params.windowUrl.trim()) {
|
|
1552
|
-
const needle = params.windowUrl.trim().toLowerCase();
|
|
1553
|
-
candidates = candidates.filter((win) => win.tabs.some((tab) => matchIncludes(tab.url, needle)));
|
|
1554
|
-
filtered = true;
|
|
1555
|
-
}
|
|
1556
|
-
if (filtered) {
|
|
1557
|
-
if (candidates.length === 1) {
|
|
1558
|
-
return { windowId: candidates[0].windowId };
|
|
1559
|
-
}
|
|
1560
|
-
if (candidates.length === 0) {
|
|
1561
|
-
return { error: { message: "No matching window found" } };
|
|
1562
|
-
}
|
|
1563
|
-
return { error: { message: "Multiple windows match selection. Provide --window to disambiguate." } };
|
|
1564
|
-
}
|
|
1565
|
-
const focused = windows.find((win) => win.focused);
|
|
1566
|
-
if (focused) {
|
|
1567
|
-
return { windowId: focused.windowId };
|
|
1568
|
-
}
|
|
1569
|
-
if (windows.length === 1) {
|
|
1570
|
-
return { windowId: windows[0].windowId };
|
|
1571
|
-
}
|
|
1572
|
-
const lastFocused = getMostRecentFocusedWindowId2(windows);
|
|
1573
|
-
if (lastFocused != null) {
|
|
1574
|
-
return { windowId: lastFocused };
|
|
1575
|
-
}
|
|
1576
|
-
return { error: { message: "Multiple windows available. Provide --window to target one." } };
|
|
1577
|
-
}
|
|
1578
|
-
async function focusTab(params) {
|
|
1579
|
-
const tabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
|
|
1580
|
-
const tabId = Number.isFinite(params.tabId) ? Number(params.tabId) : tabIds.length ? Number(tabIds[0]) : null;
|
|
1581
|
-
if (!tabId) {
|
|
1582
|
-
throw new Error("Missing tabId");
|
|
1583
|
-
}
|
|
1584
|
-
const tab = await chrome.tabs.get(tabId);
|
|
1585
|
-
await chrome.windows.update(tab.windowId, { focused: true });
|
|
1586
|
-
await chrome.tabs.update(tabId, { active: true });
|
|
1587
|
-
return {
|
|
1588
|
-
tabId,
|
|
1589
|
-
windowId: tab.windowId
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
async function refreshTabs(params) {
|
|
1593
|
-
const tabId = Number.isFinite(params.tabId) ? Number(params.tabId) : null;
|
|
1594
|
-
if (!tabId) {
|
|
1595
|
-
throw new Error("Missing tabId");
|
|
1596
|
-
}
|
|
1597
|
-
await chrome.tabs.reload(tabId);
|
|
1598
|
-
return {
|
|
1599
|
-
tabId,
|
|
1600
|
-
summary: { refreshedTabs: 1 }
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
async function openTabs(params, deps2) {
|
|
1604
|
-
const urls = Array.isArray(params.urls) ? params.urls.map((url) => typeof url === "string" ? url.trim() : "").filter(Boolean) : [];
|
|
1605
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
1606
|
-
const groupColor = typeof params.color === "string" ? params.color.trim() : "";
|
|
1607
|
-
const afterGroupTitle = typeof params.afterGroupTitle === "string" ? params.afterGroupTitle.trim() : "";
|
|
1608
|
-
const beforeTabId = Number.isFinite(params.beforeTabId) ? Number(params.beforeTabId) : null;
|
|
1609
|
-
const afterTabId = Number.isFinite(params.afterTabId) ? Number(params.afterTabId) : null;
|
|
1610
|
-
if (beforeTabId != null && afterTabId != null) {
|
|
1611
|
-
throw new Error("Only one target position is allowed");
|
|
1612
|
-
}
|
|
1613
|
-
const newWindow = params.newWindow === true;
|
|
1614
|
-
const forceNewGroup = params.newGroup === true;
|
|
1615
|
-
const allowDuplicates = params.allowDuplicates === true;
|
|
1616
|
-
if (!urls.length && !newWindow) {
|
|
1617
|
-
throw new Error("No URLs provided");
|
|
1618
|
-
}
|
|
1619
|
-
if (newWindow) {
|
|
1620
|
-
if (afterGroupTitle || beforeTabId || afterTabId) {
|
|
1621
|
-
throw new Error("Cannot use --before/--after with --new-window");
|
|
1622
|
-
}
|
|
1623
|
-
if (params.windowId != null || params.windowGroupTitle || params.windowTabId != null || params.windowUrl) {
|
|
1624
|
-
throw new Error("Cannot combine --new-window with window selectors");
|
|
1625
|
-
}
|
|
1626
|
-
const created2 = [];
|
|
1627
|
-
const skipped2 = [];
|
|
1628
|
-
const createdWindow = await chrome.windows.create({ focused: false });
|
|
1629
|
-
const windowId2 = createdWindow.id;
|
|
1630
|
-
let seedTabs = createdWindow.tabs;
|
|
1631
|
-
if (!seedTabs) {
|
|
1632
|
-
seedTabs = await chrome.tabs.query({ windowId: windowId2 });
|
|
1633
|
-
}
|
|
1634
|
-
const seedTabId = seedTabs.find((tab) => typeof tab.id === "number")?.id ?? null;
|
|
1635
|
-
for (const url of urls) {
|
|
1636
|
-
try {
|
|
1637
|
-
const tab = await chrome.tabs.create({ windowId: windowId2, url, active: false });
|
|
1638
|
-
created2.push({
|
|
1639
|
-
tabId: tab.id,
|
|
1640
|
-
windowId: tab.windowId,
|
|
1641
|
-
index: tab.index,
|
|
1642
|
-
url: tab.url,
|
|
1643
|
-
title: tab.title
|
|
1644
|
-
});
|
|
1645
|
-
} catch (error) {
|
|
1646
|
-
skipped2.push({ url, reason: "create_failed" });
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
if (!urls.length && seedTabs.length) {
|
|
1650
|
-
const tab = seedTabs[0];
|
|
1651
|
-
created2.push({
|
|
1652
|
-
tabId: tab.id,
|
|
1653
|
-
windowId: tab.windowId,
|
|
1654
|
-
index: tab.index,
|
|
1655
|
-
url: tab.url,
|
|
1656
|
-
title: tab.title
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
|
-
if (seedTabId && created2.length > 0 && urls.length > 0) {
|
|
1660
|
-
try {
|
|
1661
|
-
await chrome.tabs.remove(seedTabId);
|
|
1662
|
-
} catch (error) {
|
|
1663
|
-
deps2.log("Failed to remove seed tab", error);
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
let groupId2 = null;
|
|
1667
|
-
if (groupTitle && created2.length > 0) {
|
|
1668
|
-
try {
|
|
1669
|
-
const tabIds = created2.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
1670
|
-
if (tabIds.length > 0) {
|
|
1671
|
-
groupId2 = await chrome.tabs.group({ tabIds, createProperties: { windowId: windowId2 } });
|
|
1672
|
-
const update = { title: groupTitle };
|
|
1673
|
-
if (groupColor) {
|
|
1674
|
-
update.color = groupColor;
|
|
1675
|
-
}
|
|
1676
|
-
await chrome.tabGroups.update(groupId2, update);
|
|
1677
|
-
}
|
|
1678
|
-
} catch (error) {
|
|
1679
|
-
deps2.log("Failed to create group", error);
|
|
1680
|
-
groupId2 = null;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
return shapeOpenResult({
|
|
1684
|
-
windowId: windowId2,
|
|
1685
|
-
groupId: groupId2,
|
|
1686
|
-
created: created2,
|
|
1687
|
-
skipped: skipped2,
|
|
1688
|
-
summary: {
|
|
1689
|
-
createdTabs: created2.length,
|
|
1690
|
-
skippedUrls: skipped2.length,
|
|
1691
|
-
grouped: Boolean(groupId2)
|
|
1692
|
-
}
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
1696
|
-
let openParams = params;
|
|
1697
|
-
if (groupTitle && !forceNewGroup && openParams.windowId == null && !openParams.windowGroupTitle && !openParams.windowTabId && !openParams.windowUrl) {
|
|
1698
|
-
const groupWindows = snapshot.windows.filter((win) => win.groups.some((g) => g.title === groupTitle));
|
|
1699
|
-
if (groupWindows.length === 1) {
|
|
1700
|
-
openParams = { ...openParams, windowId: groupWindows[0].windowId };
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
if (params.windowId == null && (beforeTabId != null || afterTabId != null)) {
|
|
1704
|
-
const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
|
|
1705
|
-
const anchorWindow = snapshot.windows.find((win) => win.tabs.some((tab) => tab.tabId === anchorId));
|
|
1706
|
-
if (anchorWindow) {
|
|
1707
|
-
openParams = { ...params, windowId: anchorWindow.windowId };
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
const selection = resolveOpenWindow(snapshot, openParams);
|
|
1711
|
-
if (selection.error) {
|
|
1712
|
-
throw selection.error;
|
|
1713
|
-
}
|
|
1714
|
-
const windowId = selection.windowId;
|
|
1715
|
-
const windowSnapshot = snapshot.windows.find((win) => win.windowId === windowId);
|
|
1716
|
-
if (!windowSnapshot) {
|
|
1717
|
-
throw new Error("Window snapshot unavailable");
|
|
1718
|
-
}
|
|
1719
|
-
let existingGroupId = null;
|
|
1720
|
-
const existingUrlSet = /* @__PURE__ */ new Set();
|
|
1721
|
-
if (groupTitle && !forceNewGroup) {
|
|
1722
|
-
const matchingGroups = windowSnapshot.groups.filter((g) => g.title === groupTitle);
|
|
1723
|
-
if (matchingGroups.length > 1) {
|
|
1724
|
-
throw new Error(`Ambiguous group title "${groupTitle}": found ${matchingGroups.length} groups with the same name. Use --new-group to force a new group, group-gather to merge, or --group-id to target by ID.`);
|
|
1725
|
-
}
|
|
1726
|
-
if (matchingGroups.length === 1) {
|
|
1727
|
-
existingGroupId = matchingGroups[0].groupId;
|
|
1728
|
-
const existingTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === existingGroupId);
|
|
1729
|
-
for (const tab of existingTabs) {
|
|
1730
|
-
const norm = normalizeUrl2(tab.url);
|
|
1731
|
-
if (norm) {
|
|
1732
|
-
existingUrlSet.add(norm);
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
const created = [];
|
|
1738
|
-
const skipped = [];
|
|
1739
|
-
let insertIndex = null;
|
|
1740
|
-
if (afterGroupTitle) {
|
|
1741
|
-
const targetGroup = windowSnapshot.groups.find((group) => group.title === afterGroupTitle);
|
|
1742
|
-
if (!targetGroup) {
|
|
1743
|
-
throw new Error("Group not found in target window");
|
|
1744
|
-
}
|
|
1745
|
-
const groupTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === targetGroup.groupId);
|
|
1746
|
-
if (!groupTabs.length) {
|
|
1747
|
-
throw new Error("Group has no tabs to anchor insertion");
|
|
1748
|
-
}
|
|
1749
|
-
const indices = groupTabs.map((tab) => normalizeTabIndex2(tab.index)).filter((value) => value != null);
|
|
1750
|
-
if (!indices.length) {
|
|
1751
|
-
throw new Error("Group tabs missing indices");
|
|
1752
|
-
}
|
|
1753
|
-
insertIndex = Math.max(...indices) + 1;
|
|
1754
|
-
}
|
|
1755
|
-
if (beforeTabId != null || afterTabId != null) {
|
|
1756
|
-
if (afterGroupTitle) {
|
|
1757
|
-
throw new Error("Only one target position is allowed");
|
|
1758
|
-
}
|
|
1759
|
-
const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
|
|
1760
|
-
const anchorTab = windowSnapshot.tabs.find((tab) => tab.tabId === anchorId);
|
|
1761
|
-
if (!anchorTab) {
|
|
1762
|
-
throw new Error("Anchor tab not found in target window");
|
|
1763
|
-
}
|
|
1764
|
-
const anchorIndex = normalizeTabIndex2(anchorTab.index);
|
|
1765
|
-
if (!Number.isFinite(anchorIndex)) {
|
|
1766
|
-
throw new Error("Anchor tab index unavailable");
|
|
1767
|
-
}
|
|
1768
|
-
insertIndex = beforeTabId != null ? anchorIndex : anchorIndex + 1;
|
|
1769
|
-
}
|
|
1770
|
-
if (existingGroupId != null && insertIndex == null && beforeTabId == null && afterTabId == null) {
|
|
1771
|
-
const groupTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === existingGroupId);
|
|
1772
|
-
const indices = groupTabs.map((tab) => normalizeTabIndex2(tab.index)).filter((value) => value != null);
|
|
1773
|
-
if (indices.length) {
|
|
1774
|
-
insertIndex = Math.max(...indices) + 1;
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
let nextIndex = insertIndex;
|
|
1778
|
-
for (const url of urls) {
|
|
1779
|
-
if (!allowDuplicates && existingGroupId != null) {
|
|
1780
|
-
const norm = normalizeUrl2(url);
|
|
1781
|
-
if (norm && existingUrlSet.has(norm)) {
|
|
1782
|
-
skipped.push({ url, reason: "duplicate" });
|
|
1783
|
-
continue;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
try {
|
|
1787
|
-
const createOptions = { windowId, url, active: false };
|
|
1788
|
-
if (nextIndex != null) {
|
|
1789
|
-
createOptions.index = nextIndex;
|
|
1790
|
-
nextIndex += 1;
|
|
1791
|
-
}
|
|
1792
|
-
const tab = await chrome.tabs.create(createOptions);
|
|
1793
|
-
created.push({
|
|
1794
|
-
tabId: tab.id,
|
|
1795
|
-
windowId: tab.windowId,
|
|
1796
|
-
index: tab.index,
|
|
1797
|
-
url: tab.url,
|
|
1798
|
-
title: tab.title
|
|
1799
|
-
});
|
|
1800
|
-
} catch (error) {
|
|
1801
|
-
skipped.push({ url, reason: "create_failed" });
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
const createdTabIds = new Set(created.map((tab) => tab.tabId).filter((id) => typeof id === "number"));
|
|
1805
|
-
let groupId = null;
|
|
1806
|
-
if (groupTitle && created.length > 0) {
|
|
1807
|
-
try {
|
|
1808
|
-
const tabIds = created.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
1809
|
-
if (tabIds.length > 0) {
|
|
1810
|
-
if (existingGroupId != null) {
|
|
1811
|
-
groupId = await chrome.tabs.group({ groupId: existingGroupId, tabIds });
|
|
1812
|
-
} else {
|
|
1813
|
-
groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
|
|
1814
|
-
const update = { title: groupTitle };
|
|
1815
|
-
if (groupColor) {
|
|
1816
|
-
update.color = groupColor;
|
|
1817
|
-
}
|
|
1818
|
-
await chrome.tabGroups.update(groupId, update);
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
} catch (error) {
|
|
1822
|
-
deps2.log("Failed to create group", error);
|
|
1823
|
-
groupId = null;
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
if (groupTitle && created.length === 0 && existingGroupId != null) {
|
|
1827
|
-
groupId = existingGroupId;
|
|
1828
|
-
}
|
|
1829
|
-
const targetGroupId = groupId ?? existingGroupId;
|
|
1830
|
-
if (targetGroupId != null && created.length > 0) {
|
|
1831
|
-
try {
|
|
1832
|
-
if (createdTabIds.size > 0) {
|
|
1833
|
-
const latestTabs = await chrome.tabs.query({ windowId });
|
|
1834
|
-
const missingGroupTabIds = latestTabs.filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId).map((tab) => tab.id);
|
|
1835
|
-
if (missingGroupTabIds.length > 0) {
|
|
1836
|
-
await chrome.tabs.group({ groupId: targetGroupId, tabIds: missingGroupTabIds });
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
} catch (error) {
|
|
1840
|
-
deps2.log("Failed to enforce grouping for newly opened tabs", error);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
try {
|
|
1844
|
-
const freshTabs = await chrome.tabs.query({ windowId });
|
|
1845
|
-
freshTabs.sort((a, b) => a.index - b.index);
|
|
1846
|
-
const firstUngroupedIndex = freshTabs.findIndex((t) => (t.groupId ?? -1) === -1);
|
|
1847
|
-
if (firstUngroupedIndex >= 0) {
|
|
1848
|
-
let tabIdsToMove = [];
|
|
1849
|
-
if (existingGroupId != null) {
|
|
1850
|
-
tabIdsToMove = freshTabs.filter((tab, i) => i > firstUngroupedIndex && typeof tab.id === "number" && createdTabIds.has(tab.id) && (tab.groupId ?? -1) !== -1).map((tab) => tab.id);
|
|
1851
|
-
} else {
|
|
1852
|
-
tabIdsToMove = freshTabs.filter((tab, i) => i > firstUngroupedIndex && (tab.groupId ?? -1) !== -1).map((tab) => tab.id).filter((id) => typeof id === "number");
|
|
1853
|
-
}
|
|
1854
|
-
if (tabIdsToMove.length > 0) {
|
|
1855
|
-
await chrome.tabs.move(tabIdsToMove, { index: firstUngroupedIndex });
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
} catch (err) {
|
|
1859
|
-
deps2.log("Failed to reorder groups before ungrouped tabs", err);
|
|
1860
|
-
}
|
|
1861
|
-
if (targetGroupId != null && createdTabIds.size > 0) {
|
|
1862
|
-
try {
|
|
1863
|
-
const latestTabs = await chrome.tabs.query({ windowId });
|
|
1864
|
-
const lateUngroupedTabIds = latestTabs.filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId).map((tab) => tab.id);
|
|
1865
|
-
if (lateUngroupedTabIds.length > 0) {
|
|
1866
|
-
await chrome.tabs.group({ groupId: targetGroupId, tabIds: lateUngroupedTabIds });
|
|
1867
|
-
}
|
|
1868
|
-
} catch (error) {
|
|
1869
|
-
deps2.log("Failed post-reorder grouping verification", error);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
if (targetGroupId != null && createdTabIds.size > 0) {
|
|
1873
|
-
try {
|
|
1874
|
-
await deps2.delay(250);
|
|
1875
|
-
const delayedTabs = await chrome.tabs.query({ windowId });
|
|
1876
|
-
const delayedUngroupedTabIds = delayedTabs.filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId).map((tab) => tab.id);
|
|
1877
|
-
if (delayedUngroupedTabIds.length > 0) {
|
|
1878
|
-
await chrome.tabs.group({ groupId: targetGroupId, tabIds: delayedUngroupedTabIds });
|
|
1879
|
-
}
|
|
1880
|
-
} catch (error) {
|
|
1881
|
-
deps2.log("Failed delayed grouping verification", error);
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
return shapeOpenResult({
|
|
1885
|
-
windowId,
|
|
1886
|
-
groupId,
|
|
1887
|
-
created,
|
|
1888
|
-
skipped,
|
|
1889
|
-
summary: {
|
|
1890
|
-
createdTabs: created.length,
|
|
1891
|
-
skippedUrls: skipped.length,
|
|
1892
|
-
grouped: Boolean(groupId)
|
|
1893
|
-
}
|
|
1894
|
-
});
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
});
|
|
1898
|
-
|
|
1899
|
-
// dist/extension/lib/move.js
|
|
1900
|
-
var require_move = __commonJS({
|
|
1901
|
-
"dist/extension/lib/move.js"(exports) {
|
|
1902
|
-
"use strict";
|
|
1903
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1904
|
-
exports.resolveMoveTarget = resolveMoveTarget;
|
|
1905
|
-
exports.moveTab = moveTab;
|
|
1906
|
-
exports.moveGroup = moveGroup;
|
|
1907
|
-
var tabs2 = require_tabs();
|
|
1908
|
-
var { normalizeTabIndex: normalizeTabIndex2 } = tabs2;
|
|
1909
|
-
function resolveMoveTarget(snapshot, params, deps2) {
|
|
1910
|
-
const beforeTabId = Number(params.beforeTabId);
|
|
1911
|
-
const afterTabId = Number(params.afterTabId);
|
|
1912
|
-
const beforeGroupTitle = typeof params.beforeGroupTitle === "string" ? params.beforeGroupTitle.trim() : "";
|
|
1913
|
-
const afterGroupTitle = typeof params.afterGroupTitle === "string" ? params.afterGroupTitle.trim() : "";
|
|
1914
|
-
const targets = [
|
|
1915
|
-
Number.isFinite(beforeTabId) ? "before-tab" : null,
|
|
1916
|
-
Number.isFinite(afterTabId) ? "after-tab" : null,
|
|
1917
|
-
beforeGroupTitle ? "before-group" : null,
|
|
1918
|
-
afterGroupTitle ? "after-group" : null
|
|
1919
|
-
].filter(Boolean);
|
|
1920
|
-
if (targets.length === 0) {
|
|
1921
|
-
return { error: { message: "Missing target position (--before/--after)" } };
|
|
1922
|
-
}
|
|
1923
|
-
if (targets.length > 1) {
|
|
1924
|
-
return { error: { message: "Only one target position is allowed" } };
|
|
1925
|
-
}
|
|
1926
|
-
const windows = snapshot.windows;
|
|
1927
|
-
const findTab = (tabId) => {
|
|
1928
|
-
for (const win of windows) {
|
|
1929
|
-
const tab = win.tabs.find((entry) => entry.tabId === tabId);
|
|
1930
|
-
if (tab) {
|
|
1931
|
-
return { tab, windowId: win.windowId };
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
return null;
|
|
1935
|
-
};
|
|
1936
|
-
if (targets[0] === "before-tab" || targets[0] === "after-tab") {
|
|
1937
|
-
const tabId = targets[0] === "before-tab" ? beforeTabId : afterTabId;
|
|
1938
|
-
if (!Number.isFinite(tabId)) {
|
|
1939
|
-
return { error: { message: "Invalid tab target" } };
|
|
1940
|
-
}
|
|
1941
|
-
const match2 = findTab(tabId);
|
|
1942
|
-
if (!match2) {
|
|
1943
|
-
return { error: { message: "Target tab not found" } };
|
|
1944
|
-
}
|
|
1945
|
-
const index = normalizeTabIndex2(match2.tab.index);
|
|
1946
|
-
if (!Number.isFinite(index)) {
|
|
1947
|
-
return { error: { message: "Target tab index unavailable" } };
|
|
1948
|
-
}
|
|
1949
|
-
return {
|
|
1950
|
-
windowId: match2.windowId,
|
|
1951
|
-
index: targets[0] === "before-tab" ? index : index + 1,
|
|
1952
|
-
anchor: { type: "tab", tabId }
|
|
1953
|
-
};
|
|
1954
|
-
}
|
|
1955
|
-
const groupTitle = targets[0] === "before-group" ? beforeGroupTitle : afterGroupTitle;
|
|
1956
|
-
const windowId = Number.isFinite(params.windowId) ? Number(params.windowId) : void 0;
|
|
1957
|
-
const resolved = deps2.resolveGroupByTitle(snapshot, groupTitle, windowId);
|
|
1958
|
-
if (resolved.error) {
|
|
1959
|
-
return resolved;
|
|
1960
|
-
}
|
|
1961
|
-
const match = resolved.match;
|
|
1962
|
-
if (!match.tabs.length) {
|
|
1963
|
-
return { error: { message: "Target group has no tabs" } };
|
|
1964
|
-
}
|
|
1965
|
-
const indices = match.tabs.map((tab) => normalizeTabIndex2(tab.index)).filter((value) => value != null);
|
|
1966
|
-
if (!indices.length) {
|
|
1967
|
-
return { error: { message: "Target group indices unavailable" } };
|
|
1968
|
-
}
|
|
1969
|
-
const minIndex = Math.min(...indices);
|
|
1970
|
-
const maxIndex = Math.max(...indices);
|
|
1971
|
-
return {
|
|
1972
|
-
windowId: match.windowId,
|
|
1973
|
-
index: targets[0] === "before-group" ? minIndex : maxIndex + 1,
|
|
1974
|
-
anchor: { type: "group", groupId: match.group.groupId, groupTitle }
|
|
1975
|
-
};
|
|
1976
|
-
}
|
|
1977
|
-
async function moveTab(params, deps2) {
|
|
1978
|
-
const tabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
|
|
1979
|
-
const tabId = Number.isFinite(params.tabId) ? Number(params.tabId) : tabIds.length ? Number(tabIds[0]) : null;
|
|
1980
|
-
if (!tabId) {
|
|
1981
|
-
throw new Error("Missing tabId");
|
|
1982
|
-
}
|
|
1983
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
1984
|
-
const windows = snapshot.windows;
|
|
1985
|
-
const sourceWindow = windows.find((win) => win.tabs.some((tab) => tab.tabId === tabId));
|
|
1986
|
-
if (!sourceWindow) {
|
|
1987
|
-
throw new Error("Source tab not found");
|
|
1988
|
-
}
|
|
1989
|
-
const sourceTab = sourceWindow.tabs.find((tab) => tab.tabId === tabId);
|
|
1990
|
-
if (!sourceTab) {
|
|
1991
|
-
throw new Error("Source tab not found");
|
|
1992
|
-
}
|
|
1993
|
-
const newWindow = params.newWindow === true;
|
|
1994
|
-
const hasTarget = Number.isFinite(params.beforeTabId) || Number.isFinite(params.afterTabId) || typeof params.beforeGroupTitle === "string" && params.beforeGroupTitle.trim() || typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim();
|
|
1995
|
-
if (newWindow) {
|
|
1996
|
-
if (hasTarget) {
|
|
1997
|
-
throw new Error("Cannot combine --new-window with --before/--after");
|
|
1998
|
-
}
|
|
1999
|
-
const createdWindow = await chrome.windows.create({ tabId, focused: false });
|
|
2000
|
-
const targetWindowId2 = createdWindow.id;
|
|
2001
|
-
let targetIndex2 = 0;
|
|
2002
|
-
const createdTab = createdWindow.tabs?.find((tab) => tab.id === tabId) || null;
|
|
2003
|
-
if (createdTab && Number.isFinite(createdTab.index)) {
|
|
2004
|
-
targetIndex2 = createdTab.index;
|
|
2005
|
-
} else {
|
|
2006
|
-
try {
|
|
2007
|
-
const updated = await chrome.tabs.get(tabId);
|
|
2008
|
-
targetIndex2 = updated.index;
|
|
2009
|
-
} catch {
|
|
2010
|
-
targetIndex2 = 0;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
const fullResult2 = {
|
|
2014
|
-
tabId,
|
|
2015
|
-
from: { windowId: sourceWindow.windowId, index: sourceTab.index },
|
|
2016
|
-
to: { windowId: targetWindowId2, index: targetIndex2 },
|
|
2017
|
-
summary: { movedTabs: 1 },
|
|
2018
|
-
undo: {
|
|
2019
|
-
action: "move-tab",
|
|
2020
|
-
tabId,
|
|
2021
|
-
from: {
|
|
2022
|
-
windowId: sourceWindow.windowId,
|
|
2023
|
-
index: sourceTab.index,
|
|
2024
|
-
groupId: sourceTab.groupId,
|
|
2025
|
-
groupTitle: sourceTab.groupTitle,
|
|
2026
|
-
groupColor: sourceTab.groupColor,
|
|
2027
|
-
groupCollapsed: sourceTab.groupCollapsed ?? null
|
|
2028
|
-
},
|
|
2029
|
-
to: {
|
|
2030
|
-
windowId: targetWindowId2,
|
|
2031
|
-
index: targetIndex2
|
|
2032
|
-
}
|
|
2033
|
-
},
|
|
2034
|
-
txid: params.txid || null
|
|
2035
|
-
};
|
|
2036
|
-
return {
|
|
2037
|
-
tabId,
|
|
2038
|
-
fromWindowId: sourceWindow.windowId,
|
|
2039
|
-
toWindowId: targetWindowId2,
|
|
2040
|
-
summary: fullResult2.summary,
|
|
2041
|
-
undo: fullResult2.undo,
|
|
2042
|
-
txid: fullResult2.txid
|
|
2043
|
-
};
|
|
2044
|
-
}
|
|
2045
|
-
let normalizedParams = params;
|
|
2046
|
-
if (params.windowId != null) {
|
|
2047
|
-
const resolvedWindowId = deps2.resolveWindowIdFromParams(snapshot, params.windowId);
|
|
2048
|
-
normalizedParams = { ...params, windowId: resolvedWindowId ?? void 0 };
|
|
2049
|
-
}
|
|
2050
|
-
const target = resolveMoveTarget(snapshot, normalizedParams, deps2);
|
|
2051
|
-
if (target.error) {
|
|
2052
|
-
throw target.error;
|
|
2053
|
-
}
|
|
2054
|
-
const targetWindowId = target.windowId;
|
|
2055
|
-
let targetIndex = target.index;
|
|
2056
|
-
const sourceIndex = normalizeTabIndex2(sourceTab.index);
|
|
2057
|
-
if (Number.isFinite(sourceIndex) && sourceWindow.windowId === targetWindowId && sourceIndex < targetIndex) {
|
|
2058
|
-
targetIndex -= 1;
|
|
2059
|
-
}
|
|
2060
|
-
const moved = await chrome.tabs.move(tabId, { windowId: targetWindowId, index: targetIndex });
|
|
2061
|
-
const fullResult = {
|
|
2062
|
-
tabId,
|
|
2063
|
-
from: { windowId: sourceWindow.windowId, index: sourceTab.index },
|
|
2064
|
-
to: { windowId: targetWindowId, index: moved.index },
|
|
2065
|
-
summary: { movedTabs: 1 },
|
|
2066
|
-
undo: {
|
|
2067
|
-
action: "move-tab",
|
|
2068
|
-
tabId,
|
|
2069
|
-
from: {
|
|
2070
|
-
windowId: sourceWindow.windowId,
|
|
2071
|
-
index: sourceTab.index,
|
|
2072
|
-
groupId: sourceTab.groupId,
|
|
2073
|
-
groupTitle: sourceTab.groupTitle,
|
|
2074
|
-
groupColor: sourceTab.groupColor,
|
|
2075
|
-
groupCollapsed: sourceTab.groupCollapsed ?? null
|
|
2076
|
-
},
|
|
2077
|
-
to: {
|
|
2078
|
-
windowId: targetWindowId,
|
|
2079
|
-
index: moved.index
|
|
2080
|
-
}
|
|
2081
|
-
},
|
|
2082
|
-
txid: params.txid || null
|
|
2083
|
-
};
|
|
2084
|
-
return {
|
|
2085
|
-
tabId,
|
|
2086
|
-
fromWindowId: sourceWindow.windowId,
|
|
2087
|
-
toWindowId: targetWindowId,
|
|
2088
|
-
summary: fullResult.summary,
|
|
2089
|
-
undo: fullResult.undo,
|
|
2090
|
-
txid: fullResult.txid
|
|
2091
|
-
};
|
|
2092
|
-
}
|
|
2093
|
-
async function moveGroup(params, deps2) {
|
|
2094
|
-
const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
|
|
2095
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
2096
|
-
if (!groupId && !groupTitle) {
|
|
2097
|
-
throw new Error("Missing group identifier");
|
|
2098
|
-
}
|
|
2099
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
2100
|
-
const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) ?? void 0 : void 0;
|
|
2101
|
-
const resolvedGroup = groupId != null ? deps2.resolveGroupById(snapshot, groupId) : deps2.resolveGroupByTitle(snapshot, groupTitle, windowIdParam);
|
|
2102
|
-
if (resolvedGroup.error) {
|
|
2103
|
-
throw resolvedGroup.error;
|
|
2104
|
-
}
|
|
2105
|
-
const source = resolvedGroup.match;
|
|
2106
|
-
if (!source.tabs.length) {
|
|
2107
|
-
throw new Error("Group has no tabs to move");
|
|
2108
|
-
}
|
|
2109
|
-
const ensureMovedTabsAreGrouped = async (movedTabIds, targetWindowId2, targetGroupId) => {
|
|
2110
|
-
if (!targetGroupId || movedTabIds.length === 0) {
|
|
2111
|
-
return;
|
|
2112
|
-
}
|
|
2113
|
-
const movedSet = new Set(movedTabIds);
|
|
2114
|
-
const verify = async (step) => {
|
|
2115
|
-
const tabs3 = await chrome.tabs.query({ windowId: targetWindowId2 });
|
|
2116
|
-
const missingGroupTabIds = tabs3.filter((tab) => typeof tab.id === "number" && movedSet.has(tab.id) && tab.groupId !== targetGroupId).map((tab) => tab.id);
|
|
2117
|
-
if (missingGroupTabIds.length > 0) {
|
|
2118
|
-
await chrome.tabs.group({ groupId: targetGroupId, tabIds: missingGroupTabIds });
|
|
2119
|
-
}
|
|
2120
|
-
};
|
|
2121
|
-
try {
|
|
2122
|
-
await verify("group-verify");
|
|
2123
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
2124
|
-
await verify("group-verify-delayed");
|
|
2125
|
-
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
2126
|
-
await verify("group-verify-delayed-late");
|
|
2127
|
-
} catch (error) {
|
|
2128
|
-
deps2.log("Failed to enforce moved group integrity", error);
|
|
2129
|
-
}
|
|
2130
|
-
};
|
|
2131
|
-
const newWindow = params.newWindow === true;
|
|
2132
|
-
const hasTarget = Number.isFinite(params.beforeTabId) || Number.isFinite(params.afterTabId) || typeof params.beforeGroupTitle === "string" && params.beforeGroupTitle.trim() || typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim();
|
|
2133
|
-
if (newWindow) {
|
|
2134
|
-
if (hasTarget) {
|
|
2135
|
-
throw new Error("Cannot combine --new-window with --before/--after");
|
|
2136
|
-
}
|
|
2137
|
-
const tabIds2 = source.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
2138
|
-
const [firstTabId, ...restTabIds] = tabIds2;
|
|
2139
|
-
if (!firstTabId) {
|
|
2140
|
-
throw new Error("Group has no tabs to move");
|
|
2141
|
-
}
|
|
2142
|
-
const createdWindow = await chrome.windows.create({ tabId: firstTabId, focused: false });
|
|
2143
|
-
const targetWindowId2 = createdWindow.id;
|
|
2144
|
-
if (restTabIds.length > 0) {
|
|
2145
|
-
await chrome.tabs.move(restTabIds, { windowId: targetWindowId2, index: -1 });
|
|
2146
|
-
}
|
|
2147
|
-
let newGroupId2 = null;
|
|
2148
|
-
try {
|
|
2149
|
-
newGroupId2 = await chrome.tabs.group({ tabIds: tabIds2, createProperties: { windowId: targetWindowId2 } });
|
|
2150
|
-
await chrome.tabGroups.update(newGroupId2, {
|
|
2151
|
-
title: source.group.title || "",
|
|
2152
|
-
color: source.group.color || "grey",
|
|
2153
|
-
collapsed: source.group.collapsed || false
|
|
2154
|
-
});
|
|
2155
|
-
} catch (error) {
|
|
2156
|
-
deps2.log("Failed to regroup tabs", error);
|
|
2157
|
-
}
|
|
2158
|
-
await ensureMovedTabsAreGrouped(tabIds2, targetWindowId2, newGroupId2);
|
|
2159
|
-
const undoTabs2 = source.tabs.map((tab) => ({
|
|
2160
|
-
tabId: tab.tabId,
|
|
2161
|
-
windowId: tab.windowId,
|
|
2162
|
-
index: tab.index,
|
|
2163
|
-
groupId: tab.groupId,
|
|
2164
|
-
groupTitle: tab.groupTitle,
|
|
2165
|
-
groupColor: tab.groupColor,
|
|
2166
|
-
groupCollapsed: source.group.collapsed ?? null
|
|
2167
|
-
})).filter((tab) => typeof tab.tabId === "number");
|
|
2168
|
-
const fullResult2 = {
|
|
2169
|
-
groupId: source.group.groupId,
|
|
2170
|
-
windowId: source.windowId,
|
|
2171
|
-
movedToWindowId: targetWindowId2,
|
|
2172
|
-
newGroupId: newGroupId2,
|
|
2173
|
-
summary: { movedTabs: tabIds2.length },
|
|
2174
|
-
undo: {
|
|
2175
|
-
action: "move-group",
|
|
2176
|
-
groupId: source.group.groupId,
|
|
2177
|
-
windowId: source.windowId,
|
|
2178
|
-
movedToWindowId: targetWindowId2,
|
|
2179
|
-
groupTitle: source.group.title ?? null,
|
|
2180
|
-
groupColor: source.group.color ?? null,
|
|
2181
|
-
groupCollapsed: source.group.collapsed ?? null,
|
|
2182
|
-
tabs: undoTabs2
|
|
2183
|
-
},
|
|
2184
|
-
txid: params.txid || null
|
|
2185
|
-
};
|
|
2186
|
-
return {
|
|
2187
|
-
groupId: source.group.groupId,
|
|
2188
|
-
windowId: source.windowId,
|
|
2189
|
-
movedToWindowId: targetWindowId2,
|
|
2190
|
-
newGroupId: newGroupId2,
|
|
2191
|
-
summary: fullResult2.summary,
|
|
2192
|
-
undo: fullResult2.undo,
|
|
2193
|
-
txid: fullResult2.txid
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
const target = resolveMoveTarget(snapshot, params, deps2);
|
|
2197
|
-
if (target.error) {
|
|
2198
|
-
throw target.error;
|
|
2199
|
-
}
|
|
2200
|
-
if (target.anchor?.type === "tab") {
|
|
2201
|
-
const anchorTabId = target.anchor.tabId;
|
|
2202
|
-
if (source.tabs.some((tab) => tab.tabId === anchorTabId)) {
|
|
2203
|
-
throw new Error("Target tab is within the source group");
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
if (target.anchor?.type === "group") {
|
|
2207
|
-
const anchorGroupId = target.anchor.groupId;
|
|
2208
|
-
if (anchorGroupId === source.group.groupId && source.windowId === target.windowId) {
|
|
2209
|
-
throw new Error("Target group matches source group");
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
const tabIds = source.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
2213
|
-
const indices = source.tabs.map((tab) => normalizeTabIndex2(tab.index)).filter((value) => value != null);
|
|
2214
|
-
const minIndex = Math.min(...indices);
|
|
2215
|
-
const maxIndex = Math.max(...indices);
|
|
2216
|
-
const targetWindowId = target.windowId;
|
|
2217
|
-
let targetIndex = target.index;
|
|
2218
|
-
if (source.windowId === targetWindowId && targetIndex > maxIndex) {
|
|
2219
|
-
targetIndex -= tabIds.length;
|
|
2220
|
-
}
|
|
2221
|
-
const moved = await chrome.tabs.move(tabIds, { windowId: targetWindowId, index: targetIndex });
|
|
2222
|
-
const movedList = Array.isArray(moved) ? moved : [moved];
|
|
2223
|
-
let newGroupId = null;
|
|
2224
|
-
if (targetWindowId !== source.windowId) {
|
|
2225
|
-
try {
|
|
2226
|
-
const movedIds = movedList.map((tab) => tab.id).filter((id) => typeof id === "number");
|
|
2227
|
-
if (movedIds.length > 0) {
|
|
2228
|
-
newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: targetWindowId } });
|
|
2229
|
-
await chrome.tabGroups.update(newGroupId, {
|
|
2230
|
-
title: source.group.title || "",
|
|
2231
|
-
color: source.group.color || "grey",
|
|
2232
|
-
collapsed: source.group.collapsed || false
|
|
2233
|
-
});
|
|
2234
|
-
}
|
|
2235
|
-
} catch (error) {
|
|
2236
|
-
deps2.log("Failed to regroup tabs", error);
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
await ensureMovedTabsAreGrouped(tabIds, targetWindowId, targetWindowId === source.windowId ? source.group.groupId : newGroupId);
|
|
2240
|
-
const undoTabs = source.tabs.map((tab) => ({
|
|
2241
|
-
tabId: tab.tabId,
|
|
2242
|
-
windowId: tab.windowId,
|
|
2243
|
-
index: tab.index,
|
|
2244
|
-
groupId: tab.groupId,
|
|
2245
|
-
groupTitle: tab.groupTitle,
|
|
2246
|
-
groupColor: tab.groupColor,
|
|
2247
|
-
groupCollapsed: source.group.collapsed ?? null
|
|
2248
|
-
})).filter((tab) => typeof tab.tabId === "number");
|
|
2249
|
-
const fullResult = {
|
|
2250
|
-
groupId: source.group.groupId,
|
|
2251
|
-
windowId: source.windowId,
|
|
2252
|
-
movedToWindowId: targetWindowId,
|
|
2253
|
-
newGroupId,
|
|
2254
|
-
summary: { movedTabs: tabIds.length },
|
|
2255
|
-
undo: {
|
|
2256
|
-
action: "move-group",
|
|
2257
|
-
groupId: source.group.groupId,
|
|
2258
|
-
windowId: source.windowId,
|
|
2259
|
-
movedToWindowId: targetWindowId,
|
|
2260
|
-
groupTitle: source.group.title ?? null,
|
|
2261
|
-
groupColor: source.group.color ?? null,
|
|
2262
|
-
groupCollapsed: source.group.collapsed ?? null,
|
|
2263
|
-
tabs: undoTabs
|
|
2264
|
-
},
|
|
2265
|
-
txid: params.txid || null
|
|
2266
|
-
};
|
|
2267
|
-
return {
|
|
2268
|
-
groupId: source.group.groupId,
|
|
2269
|
-
windowId: source.windowId,
|
|
2270
|
-
movedToWindowId: targetWindowId,
|
|
2271
|
-
newGroupId,
|
|
2272
|
-
summary: fullResult.summary,
|
|
2273
|
-
undo: fullResult.undo,
|
|
2274
|
-
txid: fullResult.txid
|
|
2275
|
-
};
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
});
|
|
2279
|
-
|
|
2280
|
-
// dist/extension/lib/inspect.js
|
|
2281
|
-
var require_inspect = __commonJS({
|
|
2282
|
-
"dist/extension/lib/inspect.js"(exports) {
|
|
2283
|
-
"use strict";
|
|
2284
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2285
|
-
exports.SELECTOR_VALUE_MAX_LENGTH = exports.DESCRIPTION_MAX_LENGTH = exports.DEFAULT_STALE_DAYS = void 0;
|
|
2286
|
-
exports.analyzeTabs = analyzeTabs;
|
|
2287
|
-
exports.inspectTabs = inspectTabs;
|
|
2288
|
-
var content2 = require_content();
|
|
2289
|
-
var { isScriptableUrl: isScriptableUrl2, extractPageMeta: extractPageMeta2, extractSelectorSignal: extractSelectorSignal2, waitForSettle: waitForSettle2, waitForTabReady: waitForTabReady2 } = content2;
|
|
2290
|
-
var tabs2 = require_tabs();
|
|
2291
|
-
var { normalizeUrl: normalizeUrl2 } = tabs2;
|
|
2292
|
-
exports.DEFAULT_STALE_DAYS = 30;
|
|
2293
|
-
exports.DESCRIPTION_MAX_LENGTH = 250;
|
|
2294
|
-
exports.SELECTOR_VALUE_MAX_LENGTH = 500;
|
|
2295
|
-
async function analyzeTabs(params, requestId, deps2) {
|
|
2296
|
-
const staleDays = Number.isFinite(params.staleDays) ? params.staleDays : exports.DEFAULT_STALE_DAYS;
|
|
2297
|
-
const progressEnabled = params.progress === true;
|
|
2298
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
2299
|
-
const selection = deps2.selectTabsByScope(snapshot, params);
|
|
2300
|
-
if (selection.error) {
|
|
2301
|
-
throw selection.error;
|
|
2302
|
-
}
|
|
2303
|
-
const selectedTabs = selection.tabs;
|
|
2304
|
-
const scopeTabs = selectedTabs;
|
|
2305
|
-
const now = Date.now();
|
|
2306
|
-
const normalizedMap = /* @__PURE__ */ new Map();
|
|
2307
|
-
const duplicates = /* @__PURE__ */ new Map();
|
|
2308
|
-
for (const tab of scopeTabs) {
|
|
2309
|
-
const normalized = normalizeUrl2(tab.url);
|
|
2310
|
-
if (!normalized) {
|
|
2311
|
-
continue;
|
|
2312
|
-
}
|
|
2313
|
-
if (normalizedMap.has(normalized)) {
|
|
2314
|
-
const existing = normalizedMap.get(normalized);
|
|
2315
|
-
duplicates.set(tab.tabId, existing.tabId);
|
|
2316
|
-
} else {
|
|
2317
|
-
normalizedMap.set(normalized, tab);
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
const candidateMap = /* @__PURE__ */ new Map();
|
|
2321
|
-
const addReason = (tab, reason) => {
|
|
2322
|
-
const tabId = tab.tabId;
|
|
2323
|
-
const entry = candidateMap.get(tabId) || { tab, reasons: [] };
|
|
2324
|
-
entry.reasons.push(reason);
|
|
2325
|
-
candidateMap.set(tabId, entry);
|
|
2326
|
-
};
|
|
2327
|
-
for (const tab of selectedTabs) {
|
|
2328
|
-
if (duplicates.has(tab.tabId)) {
|
|
2329
|
-
addReason(tab, {
|
|
2330
|
-
type: "duplicate",
|
|
2331
|
-
detail: `Matches tab ${duplicates.get(tab.tabId)}`
|
|
2332
|
-
});
|
|
2333
|
-
}
|
|
2334
|
-
if (tab.lastFocusedAt) {
|
|
2335
|
-
const ageDays = (now - tab.lastFocusedAt) / (24 * 60 * 60 * 1e3);
|
|
2336
|
-
if (ageDays >= staleDays) {
|
|
2337
|
-
addReason(tab, {
|
|
2338
|
-
type: "stale",
|
|
2339
|
-
detail: `Last focused ${Math.floor(ageDays)} days ago`
|
|
2340
|
-
});
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
const candidates = Array.from(candidateMap.values()).map((entry) => {
|
|
2345
|
-
const reasons = entry.reasons;
|
|
2346
|
-
const severity = reasons.some((reason) => reason.type === "duplicate") ? "high" : "medium";
|
|
2347
|
-
return {
|
|
2348
|
-
tabId: entry.tab.tabId,
|
|
2349
|
-
windowId: entry.tab.windowId,
|
|
2350
|
-
groupId: entry.tab.groupId,
|
|
2351
|
-
url: entry.tab.url,
|
|
2352
|
-
title: entry.tab.title,
|
|
2353
|
-
lastFocusedAt: entry.tab.lastFocusedAt,
|
|
2354
|
-
reasons,
|
|
2355
|
-
severity
|
|
2356
|
-
};
|
|
2357
|
-
});
|
|
2358
|
-
const response = {
|
|
2359
|
-
staleDays,
|
|
2360
|
-
totals: {
|
|
2361
|
-
tabs: scopeTabs.length,
|
|
2362
|
-
analyzed: selectedTabs.length,
|
|
2363
|
-
candidates: candidates.length
|
|
2364
|
-
},
|
|
2365
|
-
candidates
|
|
2366
|
-
};
|
|
2367
|
-
return response;
|
|
2368
|
-
}
|
|
2369
|
-
async function inspectTabs(params, requestId, deps2) {
|
|
2370
|
-
const signalList = Array.isArray(params.signals) && params.signals.length > 0 ? params.signals.map(String) : ["page-meta"];
|
|
2371
|
-
const signalConcurrencyRaw = Number(params.signalConcurrency);
|
|
2372
|
-
const signalConcurrency = Number.isFinite(signalConcurrencyRaw) && signalConcurrencyRaw > 0 ? Math.min(10, Math.floor(signalConcurrencyRaw)) : 4;
|
|
2373
|
-
const signalTimeoutRaw = Number(params.signalTimeoutMs);
|
|
2374
|
-
const signalTimeoutMs = Number.isFinite(signalTimeoutRaw) && signalTimeoutRaw > 0 ? Math.floor(signalTimeoutRaw) : 4e3;
|
|
2375
|
-
const progressEnabled = params.progress === true;
|
|
2376
|
-
const waitFor = typeof params.waitFor === "string" ? params.waitFor.trim().toLowerCase() : "";
|
|
2377
|
-
if (waitFor === "settle" && Array.isArray(params.tabIds) && params.tabIds.length > 0) {
|
|
2378
|
-
const waitTimeoutRaw = Number(params.waitTimeoutMs);
|
|
2379
|
-
const waitTimeoutMs = Number.isFinite(waitTimeoutRaw) && waitTimeoutRaw > 0 ? Math.floor(waitTimeoutRaw) : signalTimeoutMs;
|
|
2380
|
-
const tabIds = params.tabIds.map(Number).filter(Number.isFinite);
|
|
2381
|
-
await Promise.all(tabIds.map((id) => waitForSettle2(id, waitTimeoutMs)));
|
|
2382
|
-
}
|
|
2383
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
2384
|
-
const selection = deps2.selectTabsByScope(snapshot, params);
|
|
2385
|
-
if (selection.error) {
|
|
2386
|
-
throw selection.error;
|
|
2387
|
-
}
|
|
2388
|
-
const tabs3 = selection.tabs;
|
|
2389
|
-
const selectorSpecs = [];
|
|
2390
|
-
if (Array.isArray(params.selectorSpecs)) {
|
|
2391
|
-
selectorSpecs.push(...params.selectorSpecs);
|
|
2392
|
-
}
|
|
2393
|
-
if (params.signalConfig && typeof params.signalConfig === "object") {
|
|
2394
|
-
const config = params.signalConfig;
|
|
2395
|
-
if (Array.isArray(config.selectors)) {
|
|
2396
|
-
selectorSpecs.push(...config.selectors);
|
|
2397
|
-
}
|
|
2398
|
-
if (config.signals && typeof config.signals === "object") {
|
|
2399
|
-
const signals = config.signals;
|
|
2400
|
-
const selectorConfig = signals.selector;
|
|
2401
|
-
if (selectorConfig && Array.isArray(selectorConfig.selectors)) {
|
|
2402
|
-
selectorSpecs.push(...selectorConfig.selectors);
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
const normalizedSelectors = selectorSpecs.filter((spec) => spec && typeof spec.selector === "string" && spec.selector.length > 0).map((spec) => {
|
|
2407
|
-
const selector = spec.selector;
|
|
2408
|
-
return {
|
|
2409
|
-
name: typeof spec.name === "string" ? spec.name : void 0,
|
|
2410
|
-
selector,
|
|
2411
|
-
attr: typeof spec.attr === "string" ? spec.attr : "text",
|
|
2412
|
-
all: Boolean(spec.all),
|
|
2413
|
-
text: typeof spec.text === "string" && spec.text.trim() ? spec.text.trim() : void 0,
|
|
2414
|
-
textMode: typeof spec.textMode === "string" ? spec.textMode.trim().toLowerCase() : void 0
|
|
2415
|
-
};
|
|
2416
|
-
});
|
|
2417
|
-
const selectorWarnings = normalizedSelectors.filter((spec) => spec.selector.includes(":contains(")).map((spec) => ({
|
|
2418
|
-
code: "unsupported_selector_syntax",
|
|
2419
|
-
signalId: "selector",
|
|
2420
|
-
selector: spec.selector,
|
|
2421
|
-
name: spec.name || spec.selector,
|
|
2422
|
-
message: "Selector uses unsupported CSS :contains() syntax.",
|
|
2423
|
-
hint: "Use selector text filters (text/textMode) or a different selector."
|
|
2424
|
-
}));
|
|
2425
|
-
const signalDefs = [];
|
|
2426
|
-
for (const signalId of signalList) {
|
|
2427
|
-
if (signalId === "page-meta") {
|
|
2428
|
-
signalDefs.push({
|
|
2429
|
-
id: signalId,
|
|
2430
|
-
match: (tab) => isScriptableUrl2(tab.url),
|
|
2431
|
-
run: async (tabId) => extractPageMeta2(tabId, signalTimeoutMs, exports.DESCRIPTION_MAX_LENGTH)
|
|
2432
|
-
});
|
|
2433
|
-
} else if (signalId === "selector") {
|
|
2434
|
-
signalDefs.push({
|
|
2435
|
-
id: signalId,
|
|
2436
|
-
match: (tab) => isScriptableUrl2(tab.url),
|
|
2437
|
-
run: async (tabId) => extractSelectorSignal2(tabId, normalizedSelectors, signalTimeoutMs, exports.SELECTOR_VALUE_MAX_LENGTH)
|
|
2438
|
-
});
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
const tasks = [];
|
|
2442
|
-
for (const tab of tabs3) {
|
|
2443
|
-
for (const signal of signalDefs) {
|
|
2444
|
-
if (signal.match(tab)) {
|
|
2445
|
-
tasks.push({ tab, signal });
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
const totalTasks = tasks.length;
|
|
2450
|
-
let completedTasks = 0;
|
|
2451
|
-
const entryMap = /* @__PURE__ */ new Map();
|
|
2452
|
-
const workerCount = Math.min(signalConcurrency, totalTasks || 1);
|
|
2453
|
-
let index = 0;
|
|
2454
|
-
const workers = Array.from({ length: workerCount }, async () => {
|
|
2455
|
-
while (true) {
|
|
2456
|
-
const currentIndex = index;
|
|
2457
|
-
if (currentIndex >= totalTasks) {
|
|
2458
|
-
return;
|
|
2459
|
-
}
|
|
2460
|
-
index += 1;
|
|
2461
|
-
const task = tasks[currentIndex];
|
|
2462
|
-
const tabId = task.tab.tabId;
|
|
2463
|
-
let result = null;
|
|
2464
|
-
let error = null;
|
|
2465
|
-
try {
|
|
2466
|
-
await waitForTabReady2(tabId, params, signalTimeoutMs);
|
|
2467
|
-
result = await task.signal.run(tabId);
|
|
2468
|
-
if (task.signal.id === "selector" && result && typeof result === "object") {
|
|
2469
|
-
const selectorErrors = Object.keys(result.errors || {});
|
|
2470
|
-
if (selectorErrors.length > 0) {
|
|
2471
|
-
error = `selector failures: ${selectorErrors.join(", ")}`;
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
} catch (err) {
|
|
2475
|
-
const message = err instanceof Error ? err.message : "signal_error";
|
|
2476
|
-
error = message;
|
|
2477
|
-
}
|
|
2478
|
-
const entry = entryMap.get(tabId) || { tab: task.tab, signals: {} };
|
|
2479
|
-
entry.signals[task.signal.id] = {
|
|
2480
|
-
ok: error === null,
|
|
2481
|
-
data: result,
|
|
2482
|
-
error
|
|
2483
|
-
};
|
|
2484
|
-
entryMap.set(tabId, entry);
|
|
2485
|
-
completedTasks += 1;
|
|
2486
|
-
if (progressEnabled) {
|
|
2487
|
-
deps2.sendProgress(requestId, {
|
|
2488
|
-
phase: "inspect",
|
|
2489
|
-
processed: completedTasks,
|
|
2490
|
-
total: totalTasks,
|
|
2491
|
-
signalId: task.signal.id,
|
|
2492
|
-
tabId
|
|
2493
|
-
});
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
});
|
|
2497
|
-
await Promise.all(workers);
|
|
2498
|
-
const entries = Array.from(entryMap.values()).map((entry) => ({
|
|
2499
|
-
tabId: entry.tab.tabId,
|
|
2500
|
-
windowId: entry.tab.windowId,
|
|
2501
|
-
groupId: entry.tab.groupId,
|
|
2502
|
-
url: entry.tab.url,
|
|
2503
|
-
title: entry.tab.title,
|
|
2504
|
-
signals: entry.signals
|
|
2505
|
-
}));
|
|
2506
|
-
const response = {
|
|
2507
|
-
totals: {
|
|
2508
|
-
tabs: tabs3.length,
|
|
2509
|
-
signals: signalDefs.length,
|
|
2510
|
-
tasks: totalTasks
|
|
2511
|
-
},
|
|
2512
|
-
entries
|
|
2513
|
-
};
|
|
2514
|
-
if (selectorWarnings.length > 0) {
|
|
2515
|
-
response.warnings = selectorWarnings;
|
|
2516
|
-
}
|
|
2517
|
-
return response;
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
});
|
|
2521
|
-
|
|
2522
|
-
// dist/extension/lib/undo-handlers.js
|
|
2523
|
-
var require_undo_handlers = __commonJS({
|
|
2524
|
-
"dist/extension/lib/undo-handlers.js"(exports) {
|
|
2525
|
-
"use strict";
|
|
2526
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2527
|
-
exports.undoGroupUpdate = undoGroupUpdate;
|
|
2528
|
-
exports.undoGroupUngroup = undoGroupUngroup;
|
|
2529
|
-
exports.undoGroupAssign = undoGroupAssign;
|
|
2530
|
-
exports.undoGroupGather = undoGroupGather;
|
|
2531
|
-
exports.undoMoveTab = undoMoveTab;
|
|
2532
|
-
exports.undoMoveGroup = undoMoveGroup;
|
|
2533
|
-
exports.undoMergeWindow = undoMergeWindow;
|
|
2534
|
-
exports.undoArchive = undoArchive;
|
|
2535
|
-
exports.undoClose = undoClose;
|
|
2536
|
-
exports.undoTransaction = undoTransaction;
|
|
2537
|
-
async function ensureWindow(windowId) {
|
|
2538
|
-
if (windowId) {
|
|
2539
|
-
try {
|
|
2540
|
-
const existing = await chrome.windows.get(windowId);
|
|
2541
|
-
if (existing) {
|
|
2542
|
-
return windowId;
|
|
2543
|
-
}
|
|
2544
|
-
} catch {
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
const created = await chrome.windows.create({ focused: false });
|
|
2548
|
-
return created.id;
|
|
2549
|
-
}
|
|
2550
|
-
async function restoreTabsFromUndo(entries, deps2) {
|
|
2551
|
-
const skipped = [];
|
|
2552
|
-
const restored = [];
|
|
2553
|
-
const windowMap = /* @__PURE__ */ new Map();
|
|
2554
|
-
for (const entry of entries) {
|
|
2555
|
-
const tabId = Number(entry.tabId);
|
|
2556
|
-
if (!Number.isFinite(tabId)) {
|
|
2557
|
-
skipped.push({ tabId: entry.tabId, reason: "missing_tab" });
|
|
2558
|
-
continue;
|
|
2559
|
-
}
|
|
2560
|
-
const sourceWindowId = Number(entry.windowId);
|
|
2561
|
-
if (!Number.isFinite(sourceWindowId)) {
|
|
2562
|
-
skipped.push({ tabId, reason: "missing_window" });
|
|
2563
|
-
continue;
|
|
2564
|
-
}
|
|
2565
|
-
let targetWindowId = windowMap.get(sourceWindowId);
|
|
2566
|
-
if (!targetWindowId) {
|
|
2567
|
-
targetWindowId = await ensureWindow(sourceWindowId);
|
|
2568
|
-
windowMap.set(sourceWindowId, targetWindowId);
|
|
2569
|
-
}
|
|
2570
|
-
try {
|
|
2571
|
-
await chrome.tabs.move(tabId, { windowId: targetWindowId, index: -1 });
|
|
2572
|
-
restored.push({ tabId, entry, targetWindowId });
|
|
2573
|
-
} catch {
|
|
2574
|
-
skipped.push({ tabId, reason: "move_failed" });
|
|
2575
|
-
}
|
|
2576
|
-
}
|
|
2577
|
-
const groupsByWindow = /* @__PURE__ */ new Map();
|
|
2578
|
-
for (const item of restored) {
|
|
2579
|
-
const entry = item.entry;
|
|
2580
|
-
const rawGroupId = typeof entry.groupId === "number" ? entry.groupId : null;
|
|
2581
|
-
const groupId = rawGroupId != null && rawGroupId !== -1 ? rawGroupId : null;
|
|
2582
|
-
const groupTitle = typeof entry.groupTitle === "string" ? entry.groupTitle : null;
|
|
2583
|
-
if (!groupId && !groupTitle) {
|
|
2584
|
-
continue;
|
|
2585
|
-
}
|
|
2586
|
-
const groupKey = groupId != null ? `id:${groupId}` : `title:${groupTitle}`;
|
|
2587
|
-
const key = `${item.targetWindowId}:${groupKey}`;
|
|
2588
|
-
if (!groupsByWindow.has(key)) {
|
|
2589
|
-
groupsByWindow.set(key, {
|
|
2590
|
-
windowId: item.targetWindowId,
|
|
2591
|
-
groupId,
|
|
2592
|
-
groupTitle,
|
|
2593
|
-
groupColor: typeof entry.groupColor === "string" ? entry.groupColor : null,
|
|
2594
|
-
groupCollapsed: typeof entry.groupCollapsed === "boolean" ? entry.groupCollapsed : null,
|
|
2595
|
-
tabIds: []
|
|
2596
|
-
});
|
|
2597
|
-
}
|
|
2598
|
-
groupsByWindow.get(key)?.tabIds.push(item.tabId);
|
|
2599
|
-
}
|
|
2600
|
-
for (const group of groupsByWindow.values()) {
|
|
2601
|
-
if (group.tabIds.length === 0) {
|
|
2602
|
-
continue;
|
|
2603
|
-
}
|
|
2604
|
-
let targetGroupId = null;
|
|
2605
|
-
if (group.groupId != null) {
|
|
2606
|
-
try {
|
|
2607
|
-
targetGroupId = await chrome.tabs.group({ groupId: group.groupId, tabIds: group.tabIds });
|
|
2608
|
-
} catch {
|
|
2609
|
-
targetGroupId = null;
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
if (targetGroupId == null) {
|
|
2613
|
-
try {
|
|
2614
|
-
targetGroupId = await chrome.tabs.group({
|
|
2615
|
-
tabIds: group.tabIds,
|
|
2616
|
-
createProperties: { windowId: group.windowId }
|
|
2617
|
-
});
|
|
2618
|
-
} catch (error) {
|
|
2619
|
-
deps2.log("Failed to regroup tabs", error);
|
|
2620
|
-
continue;
|
|
2621
|
-
}
|
|
2622
|
-
}
|
|
2623
|
-
const update = {};
|
|
2624
|
-
if (typeof group.groupTitle === "string") {
|
|
2625
|
-
update.title = group.groupTitle;
|
|
2626
|
-
}
|
|
2627
|
-
if (typeof group.groupColor === "string" && group.groupColor) {
|
|
2628
|
-
update.color = group.groupColor;
|
|
2629
|
-
}
|
|
2630
|
-
if (typeof group.groupCollapsed === "boolean") {
|
|
2631
|
-
update.collapsed = group.groupCollapsed;
|
|
2632
|
-
}
|
|
2633
|
-
if (Object.keys(update).length > 0) {
|
|
2634
|
-
try {
|
|
2635
|
-
await chrome.tabGroups.update(targetGroupId, update);
|
|
2636
|
-
} catch (error) {
|
|
2637
|
-
deps2.log("Failed to update restored group", error);
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
const orderByWindow = /* @__PURE__ */ new Map();
|
|
2642
|
-
for (const item of restored) {
|
|
2643
|
-
const index = Number(item.entry.index);
|
|
2644
|
-
if (!Number.isFinite(index)) {
|
|
2645
|
-
continue;
|
|
2646
|
-
}
|
|
2647
|
-
if (!orderByWindow.has(item.targetWindowId)) {
|
|
2648
|
-
orderByWindow.set(item.targetWindowId, []);
|
|
2649
|
-
}
|
|
2650
|
-
orderByWindow.get(item.targetWindowId)?.push({ tabId: item.tabId, index });
|
|
2651
|
-
}
|
|
2652
|
-
for (const [targetWindowId, items] of orderByWindow.entries()) {
|
|
2653
|
-
const ordered = [...items].sort((a, b) => a.index - b.index);
|
|
2654
|
-
for (const item of ordered) {
|
|
2655
|
-
try {
|
|
2656
|
-
await chrome.tabs.move(item.tabId, { windowId: targetWindowId, index: item.index });
|
|
2657
|
-
} catch {
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
}
|
|
2661
|
-
return {
|
|
2662
|
-
summary: {
|
|
2663
|
-
restoredTabs: restored.length,
|
|
2664
|
-
skippedTabs: skipped.length
|
|
2665
|
-
},
|
|
2666
|
-
skipped
|
|
2667
|
-
};
|
|
2668
|
-
}
|
|
2669
|
-
async function undoGroupUpdate(undo) {
|
|
2670
|
-
const groupId = Number(undo.groupId);
|
|
2671
|
-
if (!Number.isFinite(groupId)) {
|
|
2672
|
-
return {
|
|
2673
|
-
summary: { restoredGroups: 0, skippedGroups: 1 },
|
|
2674
|
-
skipped: [{ groupId: undo.groupId, reason: "missing_group" }]
|
|
2675
|
-
};
|
|
2676
|
-
}
|
|
2677
|
-
const previous = undo.previous || {};
|
|
2678
|
-
const update = {};
|
|
2679
|
-
if (typeof previous.title === "string") {
|
|
2680
|
-
update.title = previous.title;
|
|
2681
|
-
}
|
|
2682
|
-
if (typeof previous.color === "string" && previous.color) {
|
|
2683
|
-
update.color = previous.color;
|
|
2684
|
-
}
|
|
2685
|
-
if (typeof previous.collapsed === "boolean") {
|
|
2686
|
-
update.collapsed = previous.collapsed;
|
|
2687
|
-
}
|
|
2688
|
-
if (!Object.keys(update).length) {
|
|
2689
|
-
return {
|
|
2690
|
-
summary: { restoredGroups: 0, skippedGroups: 1 },
|
|
2691
|
-
skipped: [{ groupId, reason: "missing_values" }]
|
|
2692
|
-
};
|
|
2693
|
-
}
|
|
2694
|
-
try {
|
|
2695
|
-
await chrome.tabGroups.update(groupId, update);
|
|
2696
|
-
return {
|
|
2697
|
-
summary: { restoredGroups: 1, skippedGroups: 0 },
|
|
2698
|
-
skipped: []
|
|
2699
|
-
};
|
|
2700
|
-
} catch {
|
|
2701
|
-
return {
|
|
2702
|
-
summary: { restoredGroups: 0, skippedGroups: 1 },
|
|
2703
|
-
skipped: [{ groupId, reason: "update_failed" }]
|
|
2704
|
-
};
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
async function undoGroupUngroup(undo, deps2) {
|
|
2708
|
-
const tabs2 = undo.tabs || [];
|
|
2709
|
-
return await restoreTabsFromUndo(tabs2, deps2);
|
|
2710
|
-
}
|
|
2711
|
-
async function undoGroupAssign(undo, deps2) {
|
|
2712
|
-
const tabs2 = undo.tabs || [];
|
|
2713
|
-
return await restoreTabsFromUndo(tabs2, deps2);
|
|
2714
|
-
}
|
|
2715
|
-
async function undoGroupGather(undo, deps2) {
|
|
2716
|
-
const tabs2 = undo.tabs || [];
|
|
2717
|
-
return await restoreTabsFromUndo(tabs2, deps2);
|
|
2718
|
-
}
|
|
2719
|
-
async function undoMoveTab(undo, deps2) {
|
|
2720
|
-
const from = undo.from || {};
|
|
2721
|
-
const entry = {
|
|
2722
|
-
tabId: undo.tabId,
|
|
2723
|
-
windowId: from.windowId,
|
|
2724
|
-
index: from.index,
|
|
2725
|
-
groupId: from.groupId,
|
|
2726
|
-
groupTitle: from.groupTitle,
|
|
2727
|
-
groupColor: from.groupColor,
|
|
2728
|
-
groupCollapsed: from.groupCollapsed
|
|
2729
|
-
};
|
|
2730
|
-
return await restoreTabsFromUndo([entry], deps2);
|
|
2731
|
-
}
|
|
2732
|
-
async function undoMoveGroup(undo, deps2) {
|
|
2733
|
-
const tabs2 = undo.tabs || [];
|
|
2734
|
-
return await restoreTabsFromUndo(tabs2, deps2);
|
|
2735
|
-
}
|
|
2736
|
-
async function undoMergeWindow(undo, deps2) {
|
|
2737
|
-
const tabs2 = undo.tabs || [];
|
|
2738
|
-
return await restoreTabsFromUndo(tabs2, deps2);
|
|
2739
|
-
}
|
|
2740
|
-
async function undoArchive(undo, deps2) {
|
|
2741
|
-
const tabs2 = undo.tabs || [];
|
|
2742
|
-
const restored = [];
|
|
2743
|
-
const skipped = [];
|
|
2744
|
-
const windowMap = /* @__PURE__ */ new Map();
|
|
2745
|
-
for (const entry of tabs2) {
|
|
2746
|
-
if (!entry.tabId) {
|
|
2747
|
-
skipped.push({ tabId: entry.tabId, reason: "missing_tab" });
|
|
2748
|
-
continue;
|
|
2749
|
-
}
|
|
2750
|
-
let targetWindowId = windowMap.get(entry.from.windowId);
|
|
2751
|
-
if (!targetWindowId) {
|
|
2752
|
-
targetWindowId = await ensureWindow(entry.from.windowId);
|
|
2753
|
-
windowMap.set(entry.from.windowId, targetWindowId);
|
|
2754
|
-
}
|
|
2755
|
-
try {
|
|
2756
|
-
await chrome.tabs.move(entry.tabId, { windowId: targetWindowId, index: -1 });
|
|
2757
|
-
restored.push({ tabId: entry.tabId, targetWindowId });
|
|
2758
|
-
} catch {
|
|
2759
|
-
skipped.push({ tabId: entry.tabId, reason: "move_failed" });
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
const restoredSet = new Set(restored.map((item) => item.tabId));
|
|
2763
|
-
const groupsByWindow = /* @__PURE__ */ new Map();
|
|
2764
|
-
for (const entry of tabs2) {
|
|
2765
|
-
if (!restoredSet.has(entry.tabId)) {
|
|
2766
|
-
continue;
|
|
2767
|
-
}
|
|
2768
|
-
const targetWindowId = windowMap.get(entry.from.windowId) || entry.from.windowId;
|
|
2769
|
-
const hasGroupId = entry.from.groupId != null && entry.from.groupId !== -1;
|
|
2770
|
-
const hasGroupTitle = entry.from.groupTitle != null;
|
|
2771
|
-
if (!hasGroupId && !hasGroupTitle) {
|
|
2772
|
-
continue;
|
|
2773
|
-
}
|
|
2774
|
-
const groupKey = hasGroupId ? entry.from.groupId : entry.from.groupTitle;
|
|
2775
|
-
const key = `${targetWindowId}:${groupKey}`;
|
|
2776
|
-
if (!groupsByWindow.has(key)) {
|
|
2777
|
-
groupsByWindow.set(key, []);
|
|
2778
|
-
}
|
|
2779
|
-
groupsByWindow.get(key)?.push(entry);
|
|
2780
|
-
}
|
|
2781
|
-
for (const [key, groupTabs] of groupsByWindow.entries()) {
|
|
2782
|
-
const [windowIdPart] = key.split(":");
|
|
2783
|
-
const targetWindowId = Number(windowIdPart);
|
|
2784
|
-
const tabIds = groupTabs.map((entry) => entry.tabId).filter(Boolean);
|
|
2785
|
-
if (!tabIds.length) {
|
|
2786
|
-
continue;
|
|
2787
|
-
}
|
|
2788
|
-
try {
|
|
2789
|
-
const groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId: targetWindowId } });
|
|
2790
|
-
await chrome.tabGroups.update(groupId, {
|
|
2791
|
-
title: groupTabs[0].from.groupTitle || "",
|
|
2792
|
-
color: groupTabs[0].from.groupColor || "grey",
|
|
2793
|
-
collapsed: groupTabs[0].from.groupCollapsed || false
|
|
2794
|
-
});
|
|
2795
|
-
} catch (error) {
|
|
2796
|
-
deps2.log("Failed to recreate group", error);
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
for (const [originalWindowId, targetWindowId] of windowMap.entries()) {
|
|
2800
|
-
const windowTabs = tabs2.filter((entry) => entry.from.windowId === originalWindowId && restoredSet.has(entry.tabId)).sort((a, b) => a.from.index - b.from.index);
|
|
2801
|
-
for (const entry of windowTabs) {
|
|
2802
|
-
if (!entry.tabId) {
|
|
2803
|
-
continue;
|
|
2804
|
-
}
|
|
2805
|
-
try {
|
|
2806
|
-
await chrome.tabs.move(entry.tabId, { windowId: targetWindowId, index: entry.from.index });
|
|
2807
|
-
} catch {
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
const activeTab = windowTabs.find((entry) => entry.active);
|
|
2811
|
-
if (activeTab && activeTab.tabId) {
|
|
2812
|
-
try {
|
|
2813
|
-
await chrome.tabs.update(activeTab.tabId, { active: true });
|
|
2814
|
-
} catch {
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
return {
|
|
2819
|
-
summary: {
|
|
2820
|
-
restoredTabs: restored.length,
|
|
2821
|
-
skippedTabs: skipped.length
|
|
2822
|
-
},
|
|
2823
|
-
skipped
|
|
2824
|
-
};
|
|
2825
|
-
}
|
|
2826
|
-
async function undoClose(undo, deps2) {
|
|
2827
|
-
const tabs2 = undo.tabs || [];
|
|
2828
|
-
const restored = [];
|
|
2829
|
-
const skipped = [];
|
|
2830
|
-
const windowMap = /* @__PURE__ */ new Map();
|
|
2831
|
-
for (const entry of tabs2) {
|
|
2832
|
-
if (!entry.url) {
|
|
2833
|
-
skipped.push({ url: entry.url, reason: "missing_url" });
|
|
2834
|
-
continue;
|
|
2835
|
-
}
|
|
2836
|
-
let targetWindowId = windowMap.get(entry.from.windowId);
|
|
2837
|
-
if (!targetWindowId) {
|
|
2838
|
-
targetWindowId = await ensureWindow(entry.from.windowId);
|
|
2839
|
-
windowMap.set(entry.from.windowId, targetWindowId);
|
|
2840
|
-
}
|
|
2841
|
-
try {
|
|
2842
|
-
const created = await chrome.tabs.create({
|
|
2843
|
-
windowId: targetWindowId,
|
|
2844
|
-
url: entry.url,
|
|
2845
|
-
active: false,
|
|
2846
|
-
pinned: entry.pinned
|
|
2847
|
-
});
|
|
2848
|
-
restored.push({ tabId: created.id, entry });
|
|
2849
|
-
} catch {
|
|
2850
|
-
skipped.push({ url: entry.url, reason: "create_failed" });
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
const groupsByWindow = /* @__PURE__ */ new Map();
|
|
2854
|
-
for (const item of restored) {
|
|
2855
|
-
const entry = item.entry;
|
|
2856
|
-
const targetWindowId = windowMap.get(entry.from.windowId) || entry.from.windowId;
|
|
2857
|
-
const hasGroupId = entry.from.groupId != null && entry.from.groupId !== -1;
|
|
2858
|
-
const hasGroupTitle = entry.from.groupTitle != null;
|
|
2859
|
-
if (!hasGroupId && !hasGroupTitle) {
|
|
2860
|
-
continue;
|
|
2861
|
-
}
|
|
2862
|
-
const groupKey = hasGroupId ? entry.from.groupId : entry.from.groupTitle;
|
|
2863
|
-
const key = `${targetWindowId}:${groupKey}`;
|
|
2864
|
-
if (!groupsByWindow.has(key)) {
|
|
2865
|
-
groupsByWindow.set(key, []);
|
|
2866
|
-
}
|
|
2867
|
-
groupsByWindow.get(key)?.push({ tabId: item.tabId, entry });
|
|
2868
|
-
}
|
|
2869
|
-
for (const [key, groupTabs] of groupsByWindow.entries()) {
|
|
2870
|
-
const [windowIdPart] = key.split(":");
|
|
2871
|
-
const targetWindowId = Number(windowIdPart);
|
|
2872
|
-
const tabIds = groupTabs.map((item) => item.tabId).filter(Boolean);
|
|
2873
|
-
if (!tabIds.length) {
|
|
2874
|
-
continue;
|
|
2875
|
-
}
|
|
2876
|
-
try {
|
|
2877
|
-
const groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId: targetWindowId } });
|
|
2878
|
-
await chrome.tabGroups.update(groupId, {
|
|
2879
|
-
title: groupTabs[0].entry.from.groupTitle || "",
|
|
2880
|
-
color: groupTabs[0].entry.from.groupColor || "grey",
|
|
2881
|
-
collapsed: groupTabs[0].entry.from.groupCollapsed || false
|
|
2882
|
-
});
|
|
2883
|
-
} catch (error) {
|
|
2884
|
-
deps2.log("Failed to recreate group", error);
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
for (const [originalWindowId, targetWindowId] of windowMap.entries()) {
|
|
2888
|
-
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);
|
|
2889
|
-
for (const item of windowTabs) {
|
|
2890
|
-
try {
|
|
2891
|
-
await chrome.tabs.move(item.tabId, { windowId: targetWindowId, index: item.entry.from.index });
|
|
2892
|
-
} catch {
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
const activeTab = windowTabs.find((item) => item.entry.active);
|
|
2896
|
-
if (activeTab) {
|
|
2897
|
-
try {
|
|
2898
|
-
await chrome.tabs.update(activeTab.tabId, { active: true });
|
|
2899
|
-
} catch {
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
}
|
|
2903
|
-
return {
|
|
2904
|
-
summary: {
|
|
2905
|
-
restoredTabs: restored.length,
|
|
2906
|
-
skippedTabs: skipped.length
|
|
2907
|
-
},
|
|
2908
|
-
skipped
|
|
2909
|
-
};
|
|
2910
|
-
}
|
|
2911
|
-
async function undoTransaction(params, deps2) {
|
|
2912
|
-
if (!params.record || !params.record.undo) {
|
|
2913
|
-
throw new Error("Undo record missing");
|
|
2914
|
-
}
|
|
2915
|
-
const undo = params.record.undo;
|
|
2916
|
-
if (undo.action === "archive") {
|
|
2917
|
-
return await undoArchive(undo, deps2);
|
|
2918
|
-
}
|
|
2919
|
-
if (undo.action === "close") {
|
|
2920
|
-
return await undoClose(undo, deps2);
|
|
2921
|
-
}
|
|
2922
|
-
if (undo.action === "group-update") {
|
|
2923
|
-
return await undoGroupUpdate(undo);
|
|
2924
|
-
}
|
|
2925
|
-
if (undo.action === "group-ungroup") {
|
|
2926
|
-
return await undoGroupUngroup(undo, deps2);
|
|
2927
|
-
}
|
|
2928
|
-
if (undo.action === "group-assign") {
|
|
2929
|
-
return await undoGroupAssign(undo, deps2);
|
|
2930
|
-
}
|
|
2931
|
-
if (undo.action === "group-gather") {
|
|
2932
|
-
return await undoGroupGather(undo, deps2);
|
|
2933
|
-
}
|
|
2934
|
-
if (undo.action === "move-tab") {
|
|
2935
|
-
return await undoMoveTab(undo, deps2);
|
|
2936
|
-
}
|
|
2937
|
-
if (undo.action === "move-group") {
|
|
2938
|
-
return await undoMoveGroup(undo, deps2);
|
|
2939
|
-
}
|
|
2940
|
-
if (undo.action === "merge-window") {
|
|
2941
|
-
return await undoMergeWindow(undo, deps2);
|
|
2942
|
-
}
|
|
2943
|
-
throw new Error(`Unknown undo action: ${undo.action}`);
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
});
|
|
2947
|
-
|
|
2948
|
-
// dist/extension/lib/archive.js
|
|
2949
|
-
var require_archive = __commonJS({
|
|
2950
|
-
"dist/extension/lib/archive.js"(exports) {
|
|
2951
|
-
"use strict";
|
|
2952
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2953
|
-
exports.ensureArchiveWindow = ensureArchiveWindow;
|
|
2954
|
-
exports.archiveTabs = archiveTabs;
|
|
2955
|
-
exports.getTabsByIds = getTabsByIds;
|
|
2956
|
-
exports.closeTabs = closeTabs;
|
|
2957
|
-
exports.mergeWindow = mergeWindow;
|
|
2958
|
-
async function ensureArchiveWindow(deps2) {
|
|
2959
|
-
const archiveWindowId = await deps2.getArchiveWindowId();
|
|
2960
|
-
if (archiveWindowId) {
|
|
2961
|
-
try {
|
|
2962
|
-
await chrome.windows.get(archiveWindowId);
|
|
2963
|
-
return archiveWindowId;
|
|
2964
|
-
} catch {
|
|
2965
|
-
await deps2.setArchiveWindowId(null);
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
|
-
const created = await chrome.windows.create({ focused: false });
|
|
2969
|
-
await deps2.setArchiveWindowId(created.id);
|
|
2970
|
-
return created.id;
|
|
2971
|
-
}
|
|
2972
|
-
async function archiveTabs(params, deps2) {
|
|
2973
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
2974
|
-
const windowLabels = deps2.buildWindowLabels(snapshot);
|
|
2975
|
-
let windowsToProcess = snapshot.windows;
|
|
2976
|
-
if (params.windowId) {
|
|
2977
|
-
const resolvedWindowId = deps2.resolveWindowIdFromParams(snapshot, params.windowId);
|
|
2978
|
-
windowsToProcess = resolvedWindowId != null ? windowsToProcess.filter((win) => win.windowId === resolvedWindowId) : [];
|
|
2979
|
-
} else if (!params.all) {
|
|
2980
|
-
const focused = windowsToProcess.find((win) => win.focused);
|
|
2981
|
-
windowsToProcess = focused ? [focused] : [];
|
|
2982
|
-
}
|
|
2983
|
-
if (params.groupTitle || params.groupId || params.tabIds) {
|
|
2984
|
-
const selected = deps2.selectTabsByScope(snapshot, params);
|
|
2985
|
-
if (selected.error) {
|
|
2986
|
-
throw selected.error;
|
|
2987
|
-
}
|
|
2988
|
-
windowsToProcess = snapshot.windows.map((win) => ({
|
|
2989
|
-
windowId: win.windowId,
|
|
2990
|
-
focused: win.focused,
|
|
2991
|
-
state: win.state,
|
|
2992
|
-
tabs: win.tabs.filter((tab) => selected.tabs.some((sel) => sel.tabId === tab.tabId)),
|
|
2993
|
-
groups: win.groups
|
|
2994
|
-
})).filter((win) => win.tabs.length > 0);
|
|
2995
|
-
}
|
|
2996
|
-
if (windowsToProcess.length === 0) {
|
|
2997
|
-
const fullResult2 = {
|
|
2998
|
-
txid: params.txid || null,
|
|
2999
|
-
summary: { movedTabs: 0, movedGroups: 0, skippedTabs: 0 },
|
|
3000
|
-
archiveWindowId: null,
|
|
3001
|
-
skipped: [],
|
|
3002
|
-
undo: { action: "archive", tabs: [] }
|
|
3003
|
-
};
|
|
3004
|
-
return fullResult2;
|
|
3005
|
-
}
|
|
3006
|
-
const archiveWindowId = await ensureArchiveWindow(deps2);
|
|
3007
|
-
const undoTabs = [];
|
|
3008
|
-
const skipped = [];
|
|
3009
|
-
let movedGroups = 0;
|
|
3010
|
-
let movedTabs = 0;
|
|
3011
|
-
for (const window2 of windowsToProcess) {
|
|
3012
|
-
const groupsById = /* @__PURE__ */ new Map();
|
|
3013
|
-
for (const group of window2.groups) {
|
|
3014
|
-
groupsById.set(group.groupId, group);
|
|
3015
|
-
}
|
|
3016
|
-
const groupedTabs = /* @__PURE__ */ new Map();
|
|
3017
|
-
const ungroupedTabs = [];
|
|
3018
|
-
for (const tab of window2.tabs) {
|
|
3019
|
-
if (tab.tabId == null) {
|
|
3020
|
-
continue;
|
|
3021
|
-
}
|
|
3022
|
-
if (tab.groupId === -1) {
|
|
3023
|
-
ungroupedTabs.push(tab);
|
|
3024
|
-
} else {
|
|
3025
|
-
if (!groupedTabs.has(tab.groupId)) {
|
|
3026
|
-
groupedTabs.set(tab.groupId, []);
|
|
3027
|
-
}
|
|
3028
|
-
groupedTabs.get(tab.groupId)?.push(tab);
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
const windowLabel = windowLabels.get(window2.windowId) || `W${window2.windowId}`;
|
|
3032
|
-
const plans = [];
|
|
3033
|
-
for (const [groupId, tabs2] of groupedTabs.entries()) {
|
|
3034
|
-
const group = groupsById.get(groupId) || null;
|
|
3035
|
-
plans.push({
|
|
3036
|
-
windowId: window2.windowId,
|
|
3037
|
-
windowLabel,
|
|
3038
|
-
group,
|
|
3039
|
-
tabs: tabs2,
|
|
3040
|
-
isUngrouped: false
|
|
3041
|
-
});
|
|
3042
|
-
}
|
|
3043
|
-
if (ungroupedTabs.length > 0) {
|
|
3044
|
-
plans.push({
|
|
3045
|
-
windowId: window2.windowId,
|
|
3046
|
-
windowLabel,
|
|
3047
|
-
group: null,
|
|
3048
|
-
tabs: ungroupedTabs,
|
|
3049
|
-
isUngrouped: true
|
|
3050
|
-
});
|
|
3051
|
-
}
|
|
3052
|
-
for (const plan of plans) {
|
|
3053
|
-
const tabIds = plan.tabs.map((tab) => tab.tabId);
|
|
3054
|
-
if (tabIds.length === 0) {
|
|
3055
|
-
continue;
|
|
3056
|
-
}
|
|
3057
|
-
const tabById = /* @__PURE__ */ new Map();
|
|
3058
|
-
for (const tab of plan.tabs) {
|
|
3059
|
-
tabById.set(tab.tabId, tab);
|
|
3060
|
-
}
|
|
3061
|
-
let moved;
|
|
3062
|
-
try {
|
|
3063
|
-
moved = await chrome.tabs.move(tabIds, { windowId: archiveWindowId, index: -1 });
|
|
3064
|
-
} catch (error) {
|
|
3065
|
-
for (const tabId of tabIds) {
|
|
3066
|
-
skipped.push({ tabId, reason: "move_failed" });
|
|
3067
|
-
}
|
|
3068
|
-
deps2.log("Failed to move tabs", error);
|
|
3069
|
-
continue;
|
|
3070
|
-
}
|
|
3071
|
-
const movedList = Array.isArray(moved) ? moved : [moved];
|
|
3072
|
-
const movedIds = movedList.map((tab) => tab.id);
|
|
3073
|
-
movedTabs += movedIds.length;
|
|
3074
|
-
for (const movedId of movedIds) {
|
|
3075
|
-
const tab = tabById.get(movedId);
|
|
3076
|
-
if (!tab) {
|
|
3077
|
-
continue;
|
|
3078
|
-
}
|
|
3079
|
-
undoTabs.push({
|
|
3080
|
-
tabId: tab.tabId,
|
|
3081
|
-
url: tab.url,
|
|
3082
|
-
title: tab.title,
|
|
3083
|
-
pinned: tab.pinned,
|
|
3084
|
-
active: tab.active,
|
|
3085
|
-
from: {
|
|
3086
|
-
windowId: tab.windowId,
|
|
3087
|
-
index: tab.index,
|
|
3088
|
-
groupId: tab.groupId,
|
|
3089
|
-
groupTitle: plan.group ? plan.group.title : null,
|
|
3090
|
-
groupColor: plan.group ? plan.group.color : null,
|
|
3091
|
-
groupCollapsed: plan.group ? plan.group.collapsed : null
|
|
3092
|
-
}
|
|
3093
|
-
});
|
|
3094
|
-
}
|
|
3095
|
-
const titleBase = plan.group && plan.group.title ? plan.group.title : plan.isUngrouped ? "Ungrouped" : "Group";
|
|
3096
|
-
const archiveTitle = `${plan.windowLabel} - ${titleBase}`;
|
|
3097
|
-
const groupColor = plan.group && plan.group.color ? plan.group.color : "grey";
|
|
3098
|
-
try {
|
|
3099
|
-
const newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: archiveWindowId } });
|
|
3100
|
-
await chrome.tabGroups.update(newGroupId, { title: archiveTitle, color: groupColor });
|
|
3101
|
-
movedGroups += 1;
|
|
3102
|
-
} catch (error) {
|
|
3103
|
-
deps2.log("Failed to group archived tabs", error);
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
}
|
|
3107
|
-
const fullResult = {
|
|
3108
|
-
txid: params.txid || null,
|
|
3109
|
-
summary: {
|
|
3110
|
-
movedTabs,
|
|
3111
|
-
movedGroups,
|
|
3112
|
-
skippedTabs: skipped.length
|
|
3113
|
-
},
|
|
3114
|
-
archiveWindowId,
|
|
3115
|
-
skipped,
|
|
3116
|
-
undo: {
|
|
3117
|
-
action: "archive",
|
|
3118
|
-
tabs: undoTabs
|
|
3119
|
-
}
|
|
3120
|
-
};
|
|
3121
|
-
return {
|
|
3122
|
-
txid: fullResult.txid,
|
|
3123
|
-
summary: fullResult.summary,
|
|
3124
|
-
archiveWindowId,
|
|
3125
|
-
skipped: fullResult.skipped,
|
|
3126
|
-
undo: fullResult.undo
|
|
3127
|
-
};
|
|
3128
|
-
}
|
|
3129
|
-
async function getTabsByIds(tabIds) {
|
|
3130
|
-
const results = [];
|
|
3131
|
-
for (const tabId of tabIds) {
|
|
3132
|
-
try {
|
|
3133
|
-
const tab = await chrome.tabs.get(tabId);
|
|
3134
|
-
results.push(tab);
|
|
3135
|
-
} catch {
|
|
3136
|
-
results.push(null);
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
return results;
|
|
3140
|
-
}
|
|
3141
|
-
async function closeTabs(params, deps2) {
|
|
3142
|
-
const mode = params.mode || "direct";
|
|
3143
|
-
if (mode === "direct" && !params.confirmed) {
|
|
3144
|
-
throw new Error("Direct close requires confirmation");
|
|
3145
|
-
}
|
|
3146
|
-
let tabIds = params.tabIds || [];
|
|
3147
|
-
if (!tabIds.length && (params.groupTitle || params.groupId || params.windowId)) {
|
|
3148
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
3149
|
-
const selection = deps2.selectTabsByScope(snapshot, params);
|
|
3150
|
-
if (selection.error) {
|
|
3151
|
-
throw selection.error;
|
|
3152
|
-
}
|
|
3153
|
-
tabIds = selection.tabs.map((tab) => tab.tabId);
|
|
3154
|
-
}
|
|
3155
|
-
if (!tabIds.length) {
|
|
3156
|
-
const fullResult2 = {
|
|
3157
|
-
txid: params.txid || null,
|
|
3158
|
-
summary: { closedTabs: 0, skippedTabs: 0 },
|
|
3159
|
-
skipped: [],
|
|
3160
|
-
undo: { action: "close", tabs: [] }
|
|
3161
|
-
};
|
|
3162
|
-
return fullResult2;
|
|
3163
|
-
}
|
|
3164
|
-
const expectedUrls = params.expectedUrls || {};
|
|
3165
|
-
const tabInfos = await getTabsByIds(tabIds);
|
|
3166
|
-
const validTabs = [];
|
|
3167
|
-
const skipped = [];
|
|
3168
|
-
const groups2 = await chrome.tabGroups.query({});
|
|
3169
|
-
const groupById = new Map(groups2.map((group) => [group.id, group]));
|
|
3170
|
-
for (let i = 0; i < tabIds.length; i += 1) {
|
|
3171
|
-
const tabId = tabIds[i];
|
|
3172
|
-
const tab = tabInfos[i];
|
|
3173
|
-
if (!tab) {
|
|
3174
|
-
skipped.push({ tabId, reason: "not_found" });
|
|
3175
|
-
continue;
|
|
3176
|
-
}
|
|
3177
|
-
const expected = expectedUrls[String(tabId)];
|
|
3178
|
-
if (expected && tab.url !== expected) {
|
|
3179
|
-
skipped.push({ tabId, reason: "url_mismatch" });
|
|
3180
|
-
continue;
|
|
3181
|
-
}
|
|
3182
|
-
const group = tab.groupId !== -1 ? groupById.get(tab.groupId) : null;
|
|
3183
|
-
validTabs.push({
|
|
3184
|
-
tabId,
|
|
3185
|
-
url: tab.url,
|
|
3186
|
-
title: tab.title,
|
|
3187
|
-
pinned: tab.pinned,
|
|
3188
|
-
active: tab.active,
|
|
3189
|
-
from: {
|
|
3190
|
-
windowId: tab.windowId,
|
|
3191
|
-
index: tab.index,
|
|
3192
|
-
groupId: tab.groupId,
|
|
3193
|
-
groupTitle: group ? group.title : null,
|
|
3194
|
-
groupColor: group ? group.color : null,
|
|
3195
|
-
groupCollapsed: group ? group.collapsed : null
|
|
3196
|
-
}
|
|
3197
|
-
});
|
|
3198
|
-
}
|
|
3199
|
-
if (validTabs.length > 0) {
|
|
3200
|
-
await chrome.tabs.remove(validTabs.map((tab) => tab.tabId));
|
|
3201
|
-
}
|
|
3202
|
-
const fullResult = {
|
|
3203
|
-
txid: params.txid || null,
|
|
3204
|
-
summary: {
|
|
3205
|
-
closedTabs: validTabs.length,
|
|
3206
|
-
skippedTabs: skipped.length
|
|
3207
|
-
},
|
|
3208
|
-
skipped,
|
|
3209
|
-
undo: {
|
|
3210
|
-
action: "close",
|
|
3211
|
-
tabs: validTabs.map((tab) => ({
|
|
3212
|
-
url: tab.url,
|
|
3213
|
-
title: tab.title,
|
|
3214
|
-
pinned: tab.pinned,
|
|
3215
|
-
active: tab.active,
|
|
3216
|
-
from: tab.from
|
|
3217
|
-
}))
|
|
3218
|
-
}
|
|
3219
|
-
};
|
|
3220
|
-
return {
|
|
3221
|
-
txid: fullResult.txid,
|
|
3222
|
-
summary: fullResult.summary,
|
|
3223
|
-
skipped: fullResult.skipped,
|
|
3224
|
-
undo: fullResult.undo
|
|
3225
|
-
};
|
|
3226
|
-
}
|
|
3227
|
-
async function mergeWindow(params, deps2) {
|
|
3228
|
-
const fromWindowId = Number.isFinite(params.fromWindowId) ? Number(params.fromWindowId) : Number(params.windowId);
|
|
3229
|
-
const toWindowId = Number.isFinite(params.toWindowId) ? Number(params.toWindowId) : null;
|
|
3230
|
-
if (!Number.isFinite(fromWindowId) || !Number.isFinite(toWindowId)) {
|
|
3231
|
-
throw new Error("Missing source or target window id");
|
|
3232
|
-
}
|
|
3233
|
-
if (fromWindowId === toWindowId) {
|
|
3234
|
-
throw new Error("Source and target windows must differ");
|
|
3235
|
-
}
|
|
3236
|
-
const snapshot = await deps2.getTabSnapshot();
|
|
3237
|
-
const windows = snapshot.windows;
|
|
3238
|
-
const sourceWindow = windows.find((win) => win.windowId === fromWindowId);
|
|
3239
|
-
if (!sourceWindow) {
|
|
3240
|
-
throw new Error("Source window not found");
|
|
3241
|
-
}
|
|
3242
|
-
const targetWindow = windows.find((win) => win.windowId === toWindowId);
|
|
3243
|
-
if (!targetWindow) {
|
|
3244
|
-
throw new Error("Target window not found");
|
|
3245
|
-
}
|
|
3246
|
-
const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
|
|
3247
|
-
const tabIdSet = new Set(rawTabIds.filter((id) => Number.isFinite(id)));
|
|
3248
|
-
const skipped = [];
|
|
3249
|
-
let selectedTabs = sourceWindow.tabs;
|
|
3250
|
-
if (tabIdSet.size > 0) {
|
|
3251
|
-
const sourceTabIds = new Set(sourceWindow.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number"));
|
|
3252
|
-
for (const tabId of tabIdSet) {
|
|
3253
|
-
if (!sourceTabIds.has(tabId)) {
|
|
3254
|
-
skipped.push({ tabId, reason: "not_in_source" });
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
selectedTabs = sourceWindow.tabs.filter((tab) => tabIdSet.has(tab.tabId));
|
|
3258
|
-
}
|
|
3259
|
-
if (selectedTabs.length === 0) {
|
|
3260
|
-
const fullResult2 = {
|
|
3261
|
-
fromWindowId,
|
|
3262
|
-
toWindowId,
|
|
3263
|
-
summary: { movedTabs: 0, movedGroups: 0, skippedTabs: skipped.length, closedSource: false },
|
|
3264
|
-
skipped,
|
|
3265
|
-
groups: [],
|
|
3266
|
-
undo: {
|
|
3267
|
-
action: "merge-window",
|
|
3268
|
-
fromWindowId,
|
|
3269
|
-
toWindowId,
|
|
3270
|
-
closedSource: false,
|
|
3271
|
-
tabs: []
|
|
3272
|
-
}
|
|
3273
|
-
};
|
|
3274
|
-
return fullResult2;
|
|
3275
|
-
}
|
|
3276
|
-
const orderedTabs = [...selectedTabs].sort((a, b) => {
|
|
3277
|
-
const aIndex = Number(a.index);
|
|
3278
|
-
const bIndex = Number(b.index);
|
|
3279
|
-
if (!Number.isFinite(aIndex) && !Number.isFinite(bIndex)) {
|
|
3280
|
-
return 0;
|
|
3281
|
-
}
|
|
3282
|
-
if (!Number.isFinite(aIndex)) {
|
|
3283
|
-
return 1;
|
|
3284
|
-
}
|
|
3285
|
-
if (!Number.isFinite(bIndex)) {
|
|
3286
|
-
return -1;
|
|
3287
|
-
}
|
|
3288
|
-
return aIndex - bIndex;
|
|
3289
|
-
});
|
|
3290
|
-
const groupById = /* @__PURE__ */ new Map();
|
|
3291
|
-
for (const group of sourceWindow.groups) {
|
|
3292
|
-
groupById.set(group.groupId, group);
|
|
3293
|
-
}
|
|
3294
|
-
const plans = [];
|
|
3295
|
-
let currentPlan = null;
|
|
3296
|
-
for (const tab of orderedTabs) {
|
|
3297
|
-
const rawGroupId = tab.groupId;
|
|
3298
|
-
const groupId = typeof rawGroupId === "number" && rawGroupId !== -1 ? rawGroupId : null;
|
|
3299
|
-
if (!currentPlan || currentPlan.groupId !== groupId) {
|
|
3300
|
-
currentPlan = { groupId, tabs: [] };
|
|
3301
|
-
plans.push(currentPlan);
|
|
3302
|
-
}
|
|
3303
|
-
currentPlan.tabs.push(tab);
|
|
3304
|
-
}
|
|
3305
|
-
let movedTabs = 0;
|
|
3306
|
-
let movedGroups = 0;
|
|
3307
|
-
const groups2 = [];
|
|
3308
|
-
const undoTabs = [];
|
|
3309
|
-
for (const plan of plans) {
|
|
3310
|
-
const tabIds = plan.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
3311
|
-
if (!tabIds.length) {
|
|
3312
|
-
continue;
|
|
3313
|
-
}
|
|
3314
|
-
let moved;
|
|
3315
|
-
try {
|
|
3316
|
-
moved = await chrome.tabs.move(tabIds, { windowId: toWindowId, index: -1 });
|
|
3317
|
-
} catch (error) {
|
|
3318
|
-
for (const tabId of tabIds) {
|
|
3319
|
-
skipped.push({ tabId, reason: "move_failed" });
|
|
3320
|
-
}
|
|
3321
|
-
deps2.log("Failed to move tabs", error);
|
|
3322
|
-
continue;
|
|
3323
|
-
}
|
|
3324
|
-
const movedList = Array.isArray(moved) ? moved : [moved];
|
|
3325
|
-
const movedIds = movedList.map((tab) => tab.id).filter((id) => typeof id === "number");
|
|
3326
|
-
movedTabs += movedIds.length;
|
|
3327
|
-
for (const entry of plan.tabs) {
|
|
3328
|
-
if (typeof entry.tabId !== "number") {
|
|
3329
|
-
continue;
|
|
3330
|
-
}
|
|
3331
|
-
const meta = groupById.get(entry.groupId);
|
|
3332
|
-
undoTabs.push({
|
|
3333
|
-
tabId: entry.tabId,
|
|
3334
|
-
windowId: entry.windowId,
|
|
3335
|
-
index: entry.index,
|
|
3336
|
-
groupId: entry.groupId,
|
|
3337
|
-
groupTitle: entry.groupTitle,
|
|
3338
|
-
groupColor: entry.groupColor,
|
|
3339
|
-
groupCollapsed: meta ? meta.collapsed : null
|
|
3340
|
-
});
|
|
3341
|
-
}
|
|
3342
|
-
if (plan.groupId != null && movedIds.length > 0) {
|
|
3343
|
-
movedGroups += 1;
|
|
3344
|
-
let newGroupId = null;
|
|
3345
|
-
try {
|
|
3346
|
-
newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: toWindowId } });
|
|
3347
|
-
const meta = groupById.get(plan.groupId);
|
|
3348
|
-
if (meta) {
|
|
3349
|
-
await chrome.tabGroups.update(newGroupId, {
|
|
3350
|
-
title: meta.title || "",
|
|
3351
|
-
color: meta.color || "grey",
|
|
3352
|
-
collapsed: meta.collapsed || false
|
|
3353
|
-
});
|
|
3354
|
-
}
|
|
3355
|
-
} catch (error) {
|
|
3356
|
-
deps2.log("Failed to regroup tabs", error);
|
|
3357
|
-
}
|
|
3358
|
-
groups2.push({ sourceGroupId: plan.groupId, newGroupId });
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
3361
|
-
let closedSource = false;
|
|
3362
|
-
if (params.closeSource === true) {
|
|
3363
|
-
try {
|
|
3364
|
-
const remainingTabs = await chrome.tabs.query({ windowId: fromWindowId });
|
|
3365
|
-
if (remainingTabs.length === 0) {
|
|
3366
|
-
await chrome.windows.remove(fromWindowId);
|
|
3367
|
-
closedSource = true;
|
|
3368
|
-
}
|
|
3369
|
-
} catch (error) {
|
|
3370
|
-
deps2.log("Failed to close source window", error);
|
|
3371
|
-
}
|
|
3372
|
-
}
|
|
3373
|
-
const fullResult = {
|
|
3374
|
-
fromWindowId,
|
|
3375
|
-
toWindowId,
|
|
3376
|
-
summary: { movedTabs, movedGroups, skippedTabs: skipped.length, closedSource },
|
|
3377
|
-
skipped,
|
|
3378
|
-
groups: groups2,
|
|
3379
|
-
undo: {
|
|
3380
|
-
action: "merge-window",
|
|
3381
|
-
fromWindowId,
|
|
3382
|
-
toWindowId,
|
|
3383
|
-
closedSource,
|
|
3384
|
-
tabs: undoTabs
|
|
3385
|
-
},
|
|
3386
|
-
txid: params.txid || null
|
|
3387
|
-
};
|
|
3388
|
-
return {
|
|
3389
|
-
fromWindowId,
|
|
3390
|
-
toWindowId,
|
|
3391
|
-
summary: fullResult.summary,
|
|
3392
|
-
skipped: fullResult.skipped,
|
|
3393
|
-
groups: fullResult.groups,
|
|
3394
|
-
undo: fullResult.undo,
|
|
3395
|
-
txid: fullResult.txid
|
|
3396
|
-
};
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
});
|
|
3400
|
-
|
|
3401
|
-
// dist/extension/background.js
|
|
3402
|
-
var HOST_NAME = "com.erwinkroon.tabctl";
|
|
3403
|
-
var manifest = chrome.runtime.getManifest();
|
|
3404
|
-
var MANIFEST_VERSION = manifest.version || "0.0.0";
|
|
3405
|
-
var MANIFEST_VERSION_NAME = manifest.version_name || MANIFEST_VERSION;
|
|
3406
|
-
function parseVersionName(versionName) {
|
|
3407
|
-
const match = versionName.match(/-dev\.([0-9a-f]+)(\.dirty)?$/i);
|
|
3408
|
-
if (!match) {
|
|
3409
|
-
return { gitSha: null, dirty: false };
|
|
3410
|
-
}
|
|
3411
|
-
return { gitSha: match[1] || null, dirty: Boolean(match[2]) };
|
|
3412
|
-
}
|
|
3413
|
-
var parsed = parseVersionName(MANIFEST_VERSION_NAME);
|
|
3414
|
-
var VERSION_INFO = {
|
|
3415
|
-
version: MANIFEST_VERSION_NAME,
|
|
3416
|
-
baseVersion: MANIFEST_VERSION,
|
|
3417
|
-
gitSha: parsed.gitSha,
|
|
3418
|
-
dirty: parsed.dirty
|
|
3419
|
-
};
|
|
3420
|
-
var KEEPALIVE_ALARM = "tabctl-keepalive";
|
|
3421
|
-
var KEEPALIVE_INTERVAL_MINUTES = 1;
|
|
3422
|
-
var screenshot = require_screenshot();
|
|
3423
|
-
var content = require_content();
|
|
3424
|
-
var { delay, executeWithTimeout, isScriptableUrl, extractPageMeta, extractSelectorSignal, waitForTabLoad, waitForDomReady, waitForSettle, waitForTabReady, SETTLE_STABILITY_MS, SETTLE_POLL_INTERVAL_MS } = content;
|
|
3425
|
-
var groups = require_groups();
|
|
3426
|
-
var tabs = require_tabs();
|
|
3427
|
-
var { getMostRecentFocusedWindowId, normalizeTabIndex } = tabs;
|
|
3428
|
-
var move = require_move();
|
|
3429
|
-
var inspect = require_inspect();
|
|
3430
|
-
var { DESCRIPTION_MAX_LENGTH } = inspect;
|
|
3431
|
-
var undoHandlers = require_undo_handlers();
|
|
3432
|
-
var archive = require_archive();
|
|
3433
|
-
var state = {
|
|
3434
|
-
port: null,
|
|
3435
|
-
archiveWindowId: null,
|
|
3436
|
-
archiveWindowIdLoaded: false,
|
|
3437
|
-
lastFocused: {},
|
|
3438
|
-
lastFocusedLoaded: false
|
|
3439
|
-
};
|
|
3440
|
-
function log(...args) {
|
|
3441
|
-
console.log("[tabctl]", ...args);
|
|
3442
|
-
}
|
|
3443
|
-
function sendResponse(id, ok, payload) {
|
|
3444
|
-
if (!state.port) {
|
|
3445
|
-
return;
|
|
3446
|
-
}
|
|
3447
|
-
if (ok) {
|
|
3448
|
-
const data = typeof payload === "object" && payload !== null ? payload : { payload };
|
|
3449
|
-
state.port.postMessage({ id, ok: true, data });
|
|
3450
|
-
return;
|
|
3451
|
-
}
|
|
3452
|
-
const error = payload instanceof Error ? { message: payload.message, stack: payload.stack } : payload;
|
|
3453
|
-
state.port.postMessage({ id, ok: false, error });
|
|
3454
|
-
}
|
|
3455
|
-
function connectNative() {
|
|
3456
|
-
if (state.port) {
|
|
3457
|
-
return;
|
|
3458
|
-
}
|
|
3459
|
-
try {
|
|
3460
|
-
const port = chrome.runtime.connectNative(HOST_NAME);
|
|
3461
|
-
state.port = port;
|
|
3462
|
-
port.onMessage.addListener(handleNativeMessage);
|
|
3463
|
-
port.onDisconnect.addListener(() => {
|
|
3464
|
-
const lastError = chrome.runtime.lastError;
|
|
3465
|
-
if (lastError) {
|
|
3466
|
-
log("Native host disconnected:", lastError.message);
|
|
3467
|
-
} else {
|
|
3468
|
-
log("Native host disconnected");
|
|
3469
|
-
}
|
|
3470
|
-
state.port = null;
|
|
3471
|
-
});
|
|
3472
|
-
log("Native host connected");
|
|
3473
|
-
} catch (error) {
|
|
3474
|
-
log("Native host connection failed", error);
|
|
3475
|
-
}
|
|
3476
|
-
}
|
|
3477
|
-
chrome.runtime.onInstalled.addListener(() => {
|
|
3478
|
-
connectNative();
|
|
3479
|
-
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
|
|
3480
|
-
});
|
|
3481
|
-
chrome.runtime.onStartup.addListener(() => {
|
|
3482
|
-
connectNative();
|
|
3483
|
-
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: KEEPALIVE_INTERVAL_MINUTES });
|
|
3484
|
-
});
|
|
3485
|
-
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
3486
|
-
if (alarm.name === KEEPALIVE_ALARM) {
|
|
3487
|
-
connectNative();
|
|
3488
|
-
}
|
|
3489
|
-
});
|
|
3490
|
-
async function ensureLastFocusedLoaded() {
|
|
3491
|
-
if (state.lastFocusedLoaded) {
|
|
3492
|
-
return;
|
|
537
|
+
});
|
|
538
|
+
async function ensureLastFocusedLoaded() {
|
|
539
|
+
if (state.lastFocusedLoaded) {
|
|
540
|
+
return;
|
|
3493
541
|
}
|
|
3494
542
|
const stored = await chrome.storage.local.get("lastFocused");
|
|
3495
543
|
state.lastFocused = stored.lastFocused || {};
|
|
3496
544
|
state.lastFocusedLoaded = true;
|
|
3497
545
|
}
|
|
3498
|
-
async function ensureArchiveWindowIdLoaded() {
|
|
3499
|
-
if (state.archiveWindowIdLoaded) {
|
|
3500
|
-
return;
|
|
3501
|
-
}
|
|
3502
|
-
const stored = await chrome.storage.local.get("archiveWindowId");
|
|
3503
|
-
state.archiveWindowId = stored.archiveWindowId || null;
|
|
3504
|
-
state.archiveWindowIdLoaded = true;
|
|
3505
|
-
}
|
|
3506
546
|
async function setLastFocused(tabId) {
|
|
3507
547
|
await ensureLastFocusedLoaded();
|
|
3508
548
|
state.lastFocused[String(tabId)] = Date.now();
|
|
@@ -3524,9 +564,9 @@
|
|
|
3524
564
|
if (windowId === chrome.windows.WINDOW_ID_NONE) {
|
|
3525
565
|
return;
|
|
3526
566
|
}
|
|
3527
|
-
chrome.tabs.query({ windowId, active: true }).then((
|
|
3528
|
-
if (
|
|
3529
|
-
setLastFocused(
|
|
567
|
+
chrome.tabs.query({ windowId, active: true }).then((tabs) => {
|
|
568
|
+
if (tabs[0] && tabs[0].id != null) {
|
|
569
|
+
setLastFocused(tabs[0].id).catch((error) => log("Failed to set last focused", error));
|
|
3530
570
|
}
|
|
3531
571
|
}).catch((error) => log("Failed to query active tab", error));
|
|
3532
572
|
});
|
|
@@ -3545,13 +585,11 @@
|
|
|
3545
585
|
sendResponse(id, false, error);
|
|
3546
586
|
}
|
|
3547
587
|
}
|
|
3548
|
-
function sendProgress(id, payload) {
|
|
3549
|
-
if (!state.port) {
|
|
3550
|
-
return;
|
|
3551
|
-
}
|
|
3552
|
-
state.port.postMessage({ id, progress: true, data: payload });
|
|
3553
|
-
}
|
|
3554
588
|
async function handleAction(action, params, requestId) {
|
|
589
|
+
if (action.startsWith("p:")) {
|
|
590
|
+
delete params.client;
|
|
591
|
+
delete params.txid;
|
|
592
|
+
}
|
|
3555
593
|
switch (action) {
|
|
3556
594
|
case "ping":
|
|
3557
595
|
return {
|
|
@@ -3563,109 +601,89 @@
|
|
|
3563
601
|
dirty: VERSION_INFO.dirty,
|
|
3564
602
|
component: "extension"
|
|
3565
603
|
};
|
|
3566
|
-
case "version":
|
|
3567
|
-
return {
|
|
3568
|
-
runtimeId: chrome.runtime.id,
|
|
3569
|
-
version: VERSION_INFO.version,
|
|
3570
|
-
baseVersion: VERSION_INFO.baseVersion,
|
|
3571
|
-
gitSha: VERSION_INFO.gitSha,
|
|
3572
|
-
dirty: VERSION_INFO.dirty,
|
|
3573
|
-
component: "extension"
|
|
3574
|
-
};
|
|
3575
|
-
case "list":
|
|
3576
|
-
return shapeListSnapshot(await getTabSnapshot());
|
|
3577
|
-
case "analyze":
|
|
3578
|
-
return await inspect.analyzeTabs(params, requestId, deps);
|
|
3579
|
-
case "inspect":
|
|
3580
|
-
return await inspect.inspectTabs(params, requestId, deps);
|
|
3581
|
-
case "focus":
|
|
3582
|
-
return await tabs.focusTab(params);
|
|
3583
|
-
case "refresh":
|
|
3584
|
-
return await tabs.refreshTabs(params);
|
|
3585
|
-
case "open":
|
|
3586
|
-
return await tabs.openTabs(params, deps);
|
|
3587
|
-
case "group-list":
|
|
3588
|
-
return await listGroups(params);
|
|
3589
|
-
case "group-update":
|
|
3590
|
-
return await groupUpdate(params);
|
|
3591
|
-
case "group-ungroup":
|
|
3592
|
-
return await groupUngroup(params);
|
|
3593
|
-
case "group-assign":
|
|
3594
|
-
return await groupAssign(params);
|
|
3595
|
-
case "group-gather":
|
|
3596
|
-
return await groupGather(params);
|
|
3597
|
-
case "move-tab":
|
|
3598
|
-
return await move.moveTab(params, deps);
|
|
3599
|
-
case "move-group":
|
|
3600
|
-
return await move.moveGroup(params, deps);
|
|
3601
|
-
case "merge-window":
|
|
3602
|
-
return await archive.mergeWindow(params, deps);
|
|
3603
|
-
case "archive":
|
|
3604
|
-
return await archive.archiveTabs(params, deps);
|
|
3605
|
-
case "close":
|
|
3606
|
-
return await archive.closeTabs(params, deps);
|
|
3607
|
-
case "report":
|
|
3608
|
-
return await reportTabs(params);
|
|
3609
|
-
case "screenshot":
|
|
3610
|
-
return await screenshot.screenshotTabs(params, requestId, deps);
|
|
3611
|
-
case "undo":
|
|
3612
|
-
return await undoHandlers.undoTransaction(params, deps);
|
|
3613
604
|
case "reload":
|
|
3614
605
|
setTimeout(() => chrome.runtime.reload(), 100);
|
|
3615
606
|
return { reloading: true };
|
|
607
|
+
// --- Primitives: thin Chrome API wrappers (p: prefix) ---
|
|
608
|
+
case "p:snapshot":
|
|
609
|
+
return await getTabSnapshot();
|
|
610
|
+
case "p:tab-get":
|
|
611
|
+
return await chrome.tabs.get(requireFiniteId(params.tabId, "tabId"));
|
|
612
|
+
case "p:tab-query":
|
|
613
|
+
return await chrome.tabs.query(params.query);
|
|
614
|
+
case "p:tab-create":
|
|
615
|
+
return await chrome.tabs.create(params);
|
|
616
|
+
case "p:tab-update": {
|
|
617
|
+
const { tabId: rawTabId, ...updateProps } = params;
|
|
618
|
+
return await chrome.tabs.update(requireFiniteId(rawTabId, "tabId"), updateProps);
|
|
619
|
+
}
|
|
620
|
+
case "p:tab-move": {
|
|
621
|
+
const { tabIds: moveTabIds, ...moveProps } = params;
|
|
622
|
+
return await chrome.tabs.move(moveTabIds, moveProps);
|
|
623
|
+
}
|
|
624
|
+
case "p:tab-remove":
|
|
625
|
+
await chrome.tabs.remove(params.tabIds);
|
|
626
|
+
return { removed: true };
|
|
627
|
+
case "p:tab-reload":
|
|
628
|
+
await chrome.tabs.reload(requireFiniteId(params.tabId, "tabId"));
|
|
629
|
+
return { reloaded: true };
|
|
630
|
+
case "p:tab-group":
|
|
631
|
+
return { groupId: await chrome.tabs.group(params) };
|
|
632
|
+
case "p:tab-ungroup":
|
|
633
|
+
await chrome.tabs.ungroup(params.tabIds);
|
|
634
|
+
return { ungrouped: true };
|
|
635
|
+
case "p:group-update": {
|
|
636
|
+
const { groupId: rawGroupId, ...groupProps } = params;
|
|
637
|
+
return await chrome.tabGroups.update(requireFiniteId(rawGroupId, "groupId"), groupProps);
|
|
638
|
+
}
|
|
639
|
+
case "p:window-create":
|
|
640
|
+
return await chrome.windows.create(params);
|
|
641
|
+
case "p:window-remove":
|
|
642
|
+
await chrome.windows.remove(requireFiniteId(params.windowId, "windowId"));
|
|
643
|
+
return { removed: true };
|
|
644
|
+
case "p:window-update": {
|
|
645
|
+
const { windowId: rawWinId, ...winProps } = params;
|
|
646
|
+
return await chrome.windows.update(requireFiniteId(rawWinId, "windowId"), winProps);
|
|
647
|
+
}
|
|
648
|
+
case "p:execute-script": {
|
|
649
|
+
const targetTabId = requireFiniteId(params.tabId, "tabId");
|
|
650
|
+
const funcName = params.func;
|
|
651
|
+
const funcArgs = params.args || [];
|
|
652
|
+
const timeoutMs = Number(params.timeoutMs) || 8e3;
|
|
653
|
+
switch (funcName) {
|
|
654
|
+
case "extractPageMeta":
|
|
655
|
+
return await content.extractPageMeta(targetTabId, timeoutMs, funcArgs[0] || DESCRIPTION_MAX_LENGTH);
|
|
656
|
+
case "extractSelectorSignal": {
|
|
657
|
+
const specs = funcArgs[0];
|
|
658
|
+
if (!Array.isArray(specs))
|
|
659
|
+
throw new Error("extractSelectorSignal requires specs array as first arg");
|
|
660
|
+
const selectorMaxLen = funcArgs[1] || 500;
|
|
661
|
+
return await content.extractSelectorSignal(targetTabId, specs, timeoutMs, selectorMaxLen);
|
|
662
|
+
}
|
|
663
|
+
default:
|
|
664
|
+
throw new Error(`Unknown execute-script func: ${funcName}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
case "p:screenshot-tile": {
|
|
668
|
+
const opts = params.options ?? {};
|
|
669
|
+
const mode = opts.mode === "viewport" || opts.mode === "full" ? opts.mode : "viewport";
|
|
670
|
+
const fmt = opts.format === "png" || opts.format === "jpeg" ? opts.format : "png";
|
|
671
|
+
const quality = typeof opts.quality === "number" ? Math.max(1, Math.min(100, opts.quality)) : 80;
|
|
672
|
+
const tileMaxDim = typeof opts.tileMaxDim === "number" ? Math.max(50, opts.tileMaxDim) : 50;
|
|
673
|
+
const maxBytes = typeof opts.maxBytes === "number" ? Math.max(5e4, opts.maxBytes) : 5e4;
|
|
674
|
+
return await screenshot.captureTabTiles(params.tab, { mode, format: fmt, quality, tileMaxDim, maxBytes }, { delay, executeWithTimeout });
|
|
675
|
+
}
|
|
3616
676
|
default:
|
|
3617
677
|
throw new Error(`Unknown action: ${action}`);
|
|
3618
678
|
}
|
|
3619
679
|
}
|
|
3620
|
-
function shapeListSnapshot(snapshot) {
|
|
3621
|
-
return {
|
|
3622
|
-
windows: snapshot.windows.map((win) => ({
|
|
3623
|
-
windowId: win.windowId,
|
|
3624
|
-
focused: win.focused,
|
|
3625
|
-
tabs: (win.tabs || []).map((tab) => ({
|
|
3626
|
-
tabId: tab.tabId,
|
|
3627
|
-
windowId: tab.windowId,
|
|
3628
|
-
url: tab.url,
|
|
3629
|
-
title: tab.title,
|
|
3630
|
-
active: tab.active,
|
|
3631
|
-
groupId: tab.groupId,
|
|
3632
|
-
groupTitle: tab.groupTitle
|
|
3633
|
-
})),
|
|
3634
|
-
groups: (win.groups || []).map((group) => ({
|
|
3635
|
-
groupId: group.groupId,
|
|
3636
|
-
title: group.title,
|
|
3637
|
-
color: group.color,
|
|
3638
|
-
collapsed: group.collapsed
|
|
3639
|
-
}))
|
|
3640
|
-
}))
|
|
3641
|
-
};
|
|
3642
|
-
}
|
|
3643
|
-
function resolveWindowIdFromParams(snapshot, value) {
|
|
3644
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3645
|
-
return value;
|
|
3646
|
-
}
|
|
3647
|
-
if (typeof value === "string") {
|
|
3648
|
-
const normalized = value.trim().toLowerCase();
|
|
3649
|
-
if (normalized === "active") {
|
|
3650
|
-
const focused = snapshot.windows.find((win) => win.focused);
|
|
3651
|
-
return focused ? focused.windowId : null;
|
|
3652
|
-
}
|
|
3653
|
-
if (normalized === "last-focused") {
|
|
3654
|
-
return getMostRecentFocusedWindowId(snapshot.windows);
|
|
3655
|
-
}
|
|
3656
|
-
const parsed3 = Number(normalized);
|
|
3657
|
-
return Number.isFinite(parsed3) ? parsed3 : null;
|
|
3658
|
-
}
|
|
3659
|
-
const parsed2 = Number(value);
|
|
3660
|
-
return Number.isFinite(parsed2) ? parsed2 : null;
|
|
3661
|
-
}
|
|
3662
680
|
async function getTabSnapshot() {
|
|
3663
681
|
await ensureLastFocusedLoaded();
|
|
3664
682
|
const windows = await chrome.windows.getAll({ populate: true, windowTypes: ["normal"] });
|
|
3665
|
-
const
|
|
3666
|
-
const groupById = new Map(
|
|
683
|
+
const groups = await chrome.tabGroups.query({});
|
|
684
|
+
const groupById = new Map(groups.map((group) => [group.id, group]));
|
|
3667
685
|
const snapshot = windows.map((win) => {
|
|
3668
|
-
const
|
|
686
|
+
const tabs = (win.tabs || []).map((tab) => {
|
|
3669
687
|
const group = tab.groupId !== -1 ? groupById.get(tab.groupId) : null;
|
|
3670
688
|
return {
|
|
3671
689
|
tabId: tab.id,
|
|
@@ -3682,7 +700,7 @@
|
|
|
3682
700
|
lastFocusedAt: state.lastFocused[String(tab.id)] || null
|
|
3683
701
|
};
|
|
3684
702
|
});
|
|
3685
|
-
const windowGroups =
|
|
703
|
+
const windowGroups = groups.filter((group) => group.windowId === win.id).map((group) => ({
|
|
3686
704
|
groupId: group.id,
|
|
3687
705
|
title: group.title,
|
|
3688
706
|
color: group.color,
|
|
@@ -3692,7 +710,7 @@
|
|
|
3692
710
|
windowId: win.id,
|
|
3693
711
|
focused: win.focused,
|
|
3694
712
|
state: win.state,
|
|
3695
|
-
tabs
|
|
713
|
+
tabs,
|
|
3696
714
|
groups: windowGroups
|
|
3697
715
|
};
|
|
3698
716
|
});
|
|
@@ -3701,172 +719,5 @@
|
|
|
3701
719
|
windows: snapshot
|
|
3702
720
|
};
|
|
3703
721
|
}
|
|
3704
|
-
function flattenTabs(snapshot) {
|
|
3705
|
-
const result = [];
|
|
3706
|
-
for (const win of snapshot.windows) {
|
|
3707
|
-
for (const tab of win.tabs) {
|
|
3708
|
-
result.push(tab);
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
return result;
|
|
3712
|
-
}
|
|
3713
|
-
function resolveGroupByTitle(snapshot, groupTitle, windowId) {
|
|
3714
|
-
return groups.resolveGroupByTitle(snapshot, buildWindowLabels, groupTitle, windowId);
|
|
3715
|
-
}
|
|
3716
|
-
function resolveGroupById(snapshot, groupId) {
|
|
3717
|
-
return groups.resolveGroupById(snapshot, buildWindowLabels, groupId);
|
|
3718
|
-
}
|
|
3719
|
-
function buildWindowLabels(snapshot) {
|
|
3720
|
-
const labels = /* @__PURE__ */ new Map();
|
|
3721
|
-
snapshot.windows.forEach((win, index) => {
|
|
3722
|
-
labels.set(win.windowId, `W${index + 1}`);
|
|
3723
|
-
});
|
|
3724
|
-
return labels;
|
|
3725
|
-
}
|
|
3726
|
-
function selectTabsByScope(snapshot, params) {
|
|
3727
|
-
const allTabs = flattenTabs(snapshot);
|
|
3728
|
-
if (params.tabIds && params.tabIds.length) {
|
|
3729
|
-
const idSet = new Set(params.tabIds.map(Number));
|
|
3730
|
-
return { tabs: allTabs.filter((tab) => idSet.has(tab.tabId)) };
|
|
3731
|
-
}
|
|
3732
|
-
if (params.groupId) {
|
|
3733
|
-
const groupId = Number(params.groupId);
|
|
3734
|
-
return { tabs: allTabs.filter((tab) => tab.groupId === groupId) };
|
|
3735
|
-
}
|
|
3736
|
-
if (params.groupTitle) {
|
|
3737
|
-
const windowId = params.windowId != null ? resolveWindowIdFromParams(snapshot, params.windowId) ?? void 0 : void 0;
|
|
3738
|
-
const resolved = resolveGroupByTitle(snapshot, params.groupTitle, windowId);
|
|
3739
|
-
if (resolved.error) {
|
|
3740
|
-
return { tabs: [], error: resolved.error };
|
|
3741
|
-
}
|
|
3742
|
-
const match = resolved.match;
|
|
3743
|
-
return {
|
|
3744
|
-
tabs: allTabs.filter((tab) => tab.groupId === match.group.groupId && tab.windowId === match.windowId)
|
|
3745
|
-
};
|
|
3746
|
-
}
|
|
3747
|
-
if (params.windowId) {
|
|
3748
|
-
const windowId = resolveWindowIdFromParams(snapshot, params.windowId);
|
|
3749
|
-
if (!Number.isFinite(windowId)) {
|
|
3750
|
-
return { tabs: [] };
|
|
3751
|
-
}
|
|
3752
|
-
return { tabs: allTabs.filter((tab) => tab.windowId === windowId) };
|
|
3753
|
-
}
|
|
3754
|
-
if (params.all) {
|
|
3755
|
-
return { tabs: allTabs };
|
|
3756
|
-
}
|
|
3757
|
-
const focusedWindow = snapshot.windows.find((win) => win.focused);
|
|
3758
|
-
if (!focusedWindow) {
|
|
3759
|
-
return { tabs: [] };
|
|
3760
|
-
}
|
|
3761
|
-
return { tabs: focusedWindow.tabs };
|
|
3762
|
-
}
|
|
3763
|
-
var deps = {
|
|
3764
|
-
getTabSnapshot,
|
|
3765
|
-
selectTabsByScope,
|
|
3766
|
-
sendProgress,
|
|
3767
|
-
log,
|
|
3768
|
-
resolveWindowIdFromParams,
|
|
3769
|
-
resolveGroupByTitle,
|
|
3770
|
-
resolveGroupById,
|
|
3771
|
-
buildWindowLabels,
|
|
3772
|
-
getArchiveWindowId,
|
|
3773
|
-
setArchiveWindowId,
|
|
3774
|
-
delay,
|
|
3775
|
-
executeWithTimeout,
|
|
3776
|
-
isScriptableUrl,
|
|
3777
|
-
waitForTabReady
|
|
3778
|
-
};
|
|
3779
|
-
async function listGroups(params) {
|
|
3780
|
-
return groups.listGroups(params, deps);
|
|
3781
|
-
}
|
|
3782
|
-
async function groupUpdate(params) {
|
|
3783
|
-
return groups.groupUpdate(params, deps);
|
|
3784
|
-
}
|
|
3785
|
-
async function groupUngroup(params) {
|
|
3786
|
-
return groups.groupUngroup(params, deps);
|
|
3787
|
-
}
|
|
3788
|
-
async function groupAssign(params) {
|
|
3789
|
-
return groups.groupAssign(params, deps);
|
|
3790
|
-
}
|
|
3791
|
-
async function groupGather(params) {
|
|
3792
|
-
return groups.groupGather(params, deps);
|
|
3793
|
-
}
|
|
3794
|
-
async function getArchiveWindowId() {
|
|
3795
|
-
await ensureArchiveWindowIdLoaded();
|
|
3796
|
-
return state.archiveWindowId;
|
|
3797
|
-
}
|
|
3798
|
-
async function setArchiveWindowId(id) {
|
|
3799
|
-
state.archiveWindowId = id;
|
|
3800
|
-
state.archiveWindowIdLoaded = true;
|
|
3801
|
-
await chrome.storage.local.set({ archiveWindowId: id });
|
|
3802
|
-
}
|
|
3803
|
-
async function extractDescription(tabId) {
|
|
3804
|
-
try {
|
|
3805
|
-
const [{ result }] = await chrome.scripting.executeScript({
|
|
3806
|
-
target: { tabId },
|
|
3807
|
-
func: () => {
|
|
3808
|
-
const pickContent = (selector) => {
|
|
3809
|
-
const el = document.querySelector(selector);
|
|
3810
|
-
if (!el) {
|
|
3811
|
-
return "";
|
|
3812
|
-
}
|
|
3813
|
-
const content2 = el.getAttribute("content") || el.textContent || "";
|
|
3814
|
-
return content2.trim();
|
|
3815
|
-
};
|
|
3816
|
-
let description = pickContent("meta[name='description']") || pickContent("meta[property='og:description']") || pickContent("meta[name='twitter:description']");
|
|
3817
|
-
if (!description) {
|
|
3818
|
-
const h1 = document.querySelector("h1");
|
|
3819
|
-
const h1Text = h1 ? h1.textContent?.trim() : "";
|
|
3820
|
-
const paragraphs = Array.from(document.querySelectorAll("p")).map((p) => p.textContent?.replace(/\s+/g, " ").trim() || "").filter((text) => text.length > 40);
|
|
3821
|
-
const pText = paragraphs.length ? paragraphs[0] : "";
|
|
3822
|
-
if (h1Text && pText) {
|
|
3823
|
-
description = `${h1Text} - ${pText}`;
|
|
3824
|
-
} else {
|
|
3825
|
-
description = h1Text || pText;
|
|
3826
|
-
}
|
|
3827
|
-
}
|
|
3828
|
-
return description.replace(/\s+/g, " ").trim();
|
|
3829
|
-
}
|
|
3830
|
-
});
|
|
3831
|
-
if (!result) {
|
|
3832
|
-
return "";
|
|
3833
|
-
}
|
|
3834
|
-
return String(result).slice(0, DESCRIPTION_MAX_LENGTH);
|
|
3835
|
-
} catch {
|
|
3836
|
-
return "";
|
|
3837
|
-
}
|
|
3838
|
-
}
|
|
3839
|
-
async function reportTabs(params) {
|
|
3840
|
-
const snapshot = await getTabSnapshot();
|
|
3841
|
-
const selection = selectTabsByScope(snapshot, params);
|
|
3842
|
-
if (selection.error) {
|
|
3843
|
-
throw selection.error;
|
|
3844
|
-
}
|
|
3845
|
-
const windowLabels = buildWindowLabels(snapshot);
|
|
3846
|
-
const tabs2 = selection.tabs;
|
|
3847
|
-
const entries = [];
|
|
3848
|
-
for (const tab of tabs2) {
|
|
3849
|
-
const description = isScriptableUrl(tab.url) ? await extractDescription(tab.tabId) : "";
|
|
3850
|
-
entries.push({
|
|
3851
|
-
tabId: tab.tabId,
|
|
3852
|
-
windowId: tab.windowId,
|
|
3853
|
-
windowLabel: windowLabels.get(tab.windowId) || `W${tab.windowId}`,
|
|
3854
|
-
groupId: tab.groupId,
|
|
3855
|
-
groupTitle: tab.groupTitle,
|
|
3856
|
-
groupColor: tab.groupColor,
|
|
3857
|
-
url: tab.url,
|
|
3858
|
-
title: tab.title,
|
|
3859
|
-
description,
|
|
3860
|
-
lastFocusedAt: tab.lastFocusedAt
|
|
3861
|
-
});
|
|
3862
|
-
}
|
|
3863
|
-
return {
|
|
3864
|
-
generatedAt: Date.now(),
|
|
3865
|
-
entries,
|
|
3866
|
-
totals: {
|
|
3867
|
-
tabs: entries.length
|
|
3868
|
-
}
|
|
3869
|
-
};
|
|
3870
|
-
}
|
|
3871
722
|
self.__tabctl = { state, connectNative };
|
|
3872
723
|
})();
|