tabctl 0.5.3 → 0.6.0-alpha.10

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