tabctl 0.3.1 → 0.4.0

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.
@@ -242,6 +242,10 @@ async function main() {
242
242
  action = "group-assign";
243
243
  params = (0, commands_2.buildGroupAssignParams)(options);
244
244
  break;
245
+ case "group-gather":
246
+ action = "group-gather";
247
+ params = (0, commands_2.buildGroupGatherParams)(options);
248
+ break;
245
249
  case "move-tab":
246
250
  action = "move-tab";
247
251
  params = (0, commands_2.buildMoveTabParams)(options);
@@ -303,7 +307,7 @@ async function main() {
303
307
  (0, output_1.errorOut)("merge-window --close-source requires --confirm");
304
308
  }
305
309
  }
306
- if (enforcePolicy && ["analyze", "inspect", "report", "screenshot", "close", "archive", "focus", "refresh", "move-tab", "move-group", "group-assign", "group-update", "group-ungroup", "merge-window"].includes(command)) {
310
+ if (enforcePolicy && ["analyze", "inspect", "report", "screenshot", "close", "archive", "focus", "refresh", "move-tab", "move-group", "group-assign", "group-update", "group-ungroup", "group-gather", "merge-window"].includes(command)) {
307
311
  if (command === "close" && options.apply) {
308
312
  (0, output_1.errorOut)("Policy blocks close --apply; use explicit tab targets.");
309
313
  }
@@ -1,8 +1,27 @@
1
1
  (() => {
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
2
4
  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
+ };
3
9
  var __commonJS = (cb, mod) => function __require() {
4
10
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
5
11
  };
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);
6
25
 
7
26
  // dist/extension/lib/screenshot.js
8
27
  var require_screenshot = __commonJS({
@@ -690,8 +709,13 @@
690
709
  exports.groupUpdate = groupUpdate2;
691
710
  exports.groupUngroup = groupUngroup2;
692
711
  exports.groupAssign = groupAssign2;
712
+ exports.groupGather = groupGather2;
693
713
  function getGroupTabs(windowSnapshot, groupId) {
694
- return windowSnapshot.tabs.filter((tab) => tab.groupId === groupId).sort((a, b) => (Number(a.index) || 0) - (Number(b.index) || 0));
714
+ return windowSnapshot.tabs.filter((tab) => tab.groupId === groupId).sort((a, b) => {
715
+ const ai = Number(a.index);
716
+ const bi = Number(b.index);
717
+ return (Number.isFinite(ai) ? ai : 0) - (Number.isFinite(bi) ? bi : 0);
718
+ });
695
719
  }
696
720
  function listGroupSummaries(snapshot, buildWindowLabels2, windowId) {
697
721
  const windowLabels = buildWindowLabels2(snapshot);
@@ -758,8 +782,8 @@
758
782
  if (matches.length > 1) {
759
783
  return {
760
784
  error: {
761
- message: "Group title is ambiguous. Provide a windowId.",
762
- hint: "Use --window to disambiguate group titles.",
785
+ 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.`,
786
+ hint: "Use group-gather to merge duplicates, --group-id to target by ID, or --window to narrow scope.",
763
787
  matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
764
788
  availableGroups
765
789
  }
@@ -1105,6 +1129,301 @@
1105
1129
  txid: params.txid || null
1106
1130
  };
1107
1131
  }
1132
+ async function groupGather2(params, deps2) {
1133
+ const snapshot = await deps2.getTabSnapshot();
1134
+ const windows = snapshot.windows;
1135
+ const windowIdParam = params.windowId != null ? deps2.resolveWindowIdFromParams(snapshot, params.windowId) : null;
1136
+ if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
1137
+ throw new Error("Window not found");
1138
+ }
1139
+ const groupTitleFilter = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
1140
+ const merged = [];
1141
+ const undoEntries = [];
1142
+ for (const win of windows) {
1143
+ if (windowIdParam && win.windowId !== windowIdParam)
1144
+ continue;
1145
+ const byTitle = /* @__PURE__ */ new Map();
1146
+ for (const group of win.groups) {
1147
+ const title = typeof group.title === "string" ? group.title : "";
1148
+ if (!title)
1149
+ continue;
1150
+ if (groupTitleFilter && title !== groupTitleFilter)
1151
+ continue;
1152
+ if (!byTitle.has(title))
1153
+ byTitle.set(title, []);
1154
+ byTitle.get(title).push(group);
1155
+ }
1156
+ for (const [title, titleGroups] of byTitle) {
1157
+ if (titleGroups.length < 2)
1158
+ continue;
1159
+ const groupsWithIndex = titleGroups.map((g) => {
1160
+ const tabs2 = win.tabs.filter((t) => t.groupId === g.groupId);
1161
+ const minIndex = Math.min(...tabs2.map((t) => {
1162
+ const idx = Number(t.index);
1163
+ return Number.isFinite(idx) ? idx : Infinity;
1164
+ }));
1165
+ return { group: g, tabs: tabs2, minIndex };
1166
+ });
1167
+ groupsWithIndex.sort((a, b) => a.minIndex - b.minIndex);
1168
+ const primary = groupsWithIndex[0];
1169
+ const duplicates = groupsWithIndex.slice(1);
1170
+ let movedTabs = 0;
1171
+ for (const dup of duplicates) {
1172
+ const tabIds = dup.tabs.map((t) => t.tabId).filter((id) => typeof id === "number");
1173
+ if (tabIds.length > 0) {
1174
+ for (const tab of dup.tabs) {
1175
+ undoEntries.push({
1176
+ tabId: tab.tabId,
1177
+ windowId: win.windowId,
1178
+ index: tab.index,
1179
+ groupId: tab.groupId,
1180
+ groupTitle: tab.groupTitle,
1181
+ groupColor: tab.groupColor,
1182
+ groupCollapsed: dup.group.collapsed ?? null
1183
+ });
1184
+ }
1185
+ await chrome.tabs.group({ groupId: primary.group.groupId, tabIds });
1186
+ movedTabs += tabIds.length;
1187
+ }
1188
+ }
1189
+ merged.push({
1190
+ windowId: win.windowId,
1191
+ groupTitle: title,
1192
+ primaryGroupId: primary.group.groupId,
1193
+ mergedGroupCount: duplicates.length,
1194
+ movedTabs
1195
+ });
1196
+ }
1197
+ }
1198
+ return {
1199
+ merged,
1200
+ summary: {
1201
+ mergedGroups: merged.reduce((sum, m) => sum + m.mergedGroupCount, 0),
1202
+ movedTabs: merged.reduce((sum, m) => sum + m.movedTabs, 0)
1203
+ },
1204
+ undo: {
1205
+ action: "group-gather",
1206
+ tabs: undoEntries
1207
+ },
1208
+ txid: params.txid || null
1209
+ };
1210
+ }
1211
+ }
1212
+ });
1213
+
1214
+ // node_modules/normalize-url/index.js
1215
+ var normalize_url_exports = {};
1216
+ __export(normalize_url_exports, {
1217
+ default: () => normalizeUrl
1218
+ });
1219
+ function normalizeUrl(urlString, options) {
1220
+ options = {
1221
+ defaultProtocol: "http",
1222
+ normalizeProtocol: true,
1223
+ forceHttp: false,
1224
+ forceHttps: false,
1225
+ stripAuthentication: true,
1226
+ stripHash: false,
1227
+ stripTextFragment: true,
1228
+ stripWWW: true,
1229
+ removeQueryParameters: [/^utm_\w+/i],
1230
+ removeTrailingSlash: true,
1231
+ removeSingleSlash: true,
1232
+ removeDirectoryIndex: false,
1233
+ removeExplicitPort: false,
1234
+ sortQueryParameters: true,
1235
+ removePath: false,
1236
+ transformPath: false,
1237
+ ...options
1238
+ };
1239
+ if (typeof options.defaultProtocol === "string" && !options.defaultProtocol.endsWith(":")) {
1240
+ options.defaultProtocol = `${options.defaultProtocol}:`;
1241
+ }
1242
+ urlString = urlString.trim();
1243
+ if (/^data:/i.test(urlString)) {
1244
+ return normalizeDataURL(urlString, options);
1245
+ }
1246
+ if (hasCustomProtocol(urlString)) {
1247
+ return urlString;
1248
+ }
1249
+ const hasRelativeProtocol = urlString.startsWith("//");
1250
+ const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
1251
+ if (!isRelativeUrl) {
1252
+ urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
1253
+ }
1254
+ const urlObject = new URL(urlString);
1255
+ if (options.forceHttp && options.forceHttps) {
1256
+ throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");
1257
+ }
1258
+ if (options.forceHttp && urlObject.protocol === "https:") {
1259
+ urlObject.protocol = "http:";
1260
+ }
1261
+ if (options.forceHttps && urlObject.protocol === "http:") {
1262
+ urlObject.protocol = "https:";
1263
+ }
1264
+ if (options.stripAuthentication) {
1265
+ urlObject.username = "";
1266
+ urlObject.password = "";
1267
+ }
1268
+ if (options.stripHash) {
1269
+ urlObject.hash = "";
1270
+ } else if (options.stripTextFragment) {
1271
+ urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, "");
1272
+ }
1273
+ if (urlObject.pathname) {
1274
+ const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g;
1275
+ let lastIndex = 0;
1276
+ let result = "";
1277
+ for (; ; ) {
1278
+ const match = protocolRegex.exec(urlObject.pathname);
1279
+ if (!match) {
1280
+ break;
1281
+ }
1282
+ const protocol = match[0];
1283
+ const protocolAtIndex = match.index;
1284
+ const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex);
1285
+ result += intermediate.replace(/\/{2,}/g, "/");
1286
+ result += protocol;
1287
+ lastIndex = protocolAtIndex + protocol.length;
1288
+ }
1289
+ const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length);
1290
+ result += remnant.replace(/\/{2,}/g, "/");
1291
+ urlObject.pathname = result;
1292
+ }
1293
+ if (urlObject.pathname) {
1294
+ try {
1295
+ urlObject.pathname = decodeURI(urlObject.pathname).replace(/\\/g, "%5C");
1296
+ } catch {
1297
+ }
1298
+ }
1299
+ if (options.removeDirectoryIndex === true) {
1300
+ options.removeDirectoryIndex = [/^index\.[a-z]+$/];
1301
+ }
1302
+ if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
1303
+ const pathComponents = urlObject.pathname.split("/").filter(Boolean);
1304
+ const lastComponent = pathComponents.at(-1);
1305
+ if (lastComponent && testParameter(lastComponent, options.removeDirectoryIndex)) {
1306
+ pathComponents.pop();
1307
+ urlObject.pathname = pathComponents.length > 0 ? `/${pathComponents.join("/")}/` : "/";
1308
+ }
1309
+ }
1310
+ if (options.removePath) {
1311
+ urlObject.pathname = "/";
1312
+ }
1313
+ if (options.transformPath && typeof options.transformPath === "function") {
1314
+ const pathComponents = urlObject.pathname.split("/").filter(Boolean);
1315
+ const newComponents = options.transformPath(pathComponents);
1316
+ urlObject.pathname = newComponents?.length > 0 ? `/${newComponents.join("/")}` : "/";
1317
+ }
1318
+ if (urlObject.hostname) {
1319
+ urlObject.hostname = urlObject.hostname.replace(/\.$/, "");
1320
+ if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) {
1321
+ urlObject.hostname = urlObject.hostname.replace(/^www\./, "");
1322
+ }
1323
+ }
1324
+ if (Array.isArray(options.removeQueryParameters)) {
1325
+ for (const key of [...urlObject.searchParams.keys()]) {
1326
+ if (testParameter(key, options.removeQueryParameters)) {
1327
+ urlObject.searchParams.delete(key);
1328
+ }
1329
+ }
1330
+ }
1331
+ if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {
1332
+ urlObject.search = "";
1333
+ }
1334
+ if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
1335
+ for (const key of [...urlObject.searchParams.keys()]) {
1336
+ if (!testParameter(key, options.keepQueryParameters)) {
1337
+ urlObject.searchParams.delete(key);
1338
+ }
1339
+ }
1340
+ }
1341
+ if (options.sortQueryParameters) {
1342
+ const originalSearch = urlObject.search;
1343
+ urlObject.searchParams.sort();
1344
+ try {
1345
+ urlObject.search = decodeURIComponent(urlObject.search);
1346
+ } catch {
1347
+ }
1348
+ const partsWithoutEquals = originalSearch.slice(1).split("&").filter((p) => p && !p.includes("="));
1349
+ for (const part of partsWithoutEquals) {
1350
+ const decoded = decodeURIComponent(part);
1351
+ urlObject.search = urlObject.search.replace(`?${decoded}=`, `?${decoded}`).replace(`&${decoded}=`, `&${decoded}`);
1352
+ }
1353
+ }
1354
+ if (options.removeTrailingSlash) {
1355
+ urlObject.pathname = urlObject.pathname.replace(/\/$/, "");
1356
+ }
1357
+ if (options.removeExplicitPort && urlObject.port) {
1358
+ urlObject.port = "";
1359
+ }
1360
+ const oldUrlString = urlString;
1361
+ urlString = urlObject.toString();
1362
+ if (!options.removeSingleSlash && urlObject.pathname === "/" && !oldUrlString.endsWith("/") && urlObject.hash === "") {
1363
+ urlString = urlString.replace(/\/$/, "");
1364
+ }
1365
+ if ((options.removeTrailingSlash || urlObject.pathname === "/") && urlObject.hash === "" && options.removeSingleSlash) {
1366
+ urlString = urlString.replace(/\/$/, "");
1367
+ }
1368
+ if (hasRelativeProtocol && !options.normalizeProtocol) {
1369
+ urlString = urlString.replace(/^http:\/\//, "//");
1370
+ }
1371
+ if (options.stripProtocol) {
1372
+ urlString = urlString.replace(/^(?:https?:)?\/\//, "");
1373
+ }
1374
+ return urlString;
1375
+ }
1376
+ var DATA_URL_DEFAULT_MIME_TYPE, DATA_URL_DEFAULT_CHARSET, testParameter, supportedProtocols, hasCustomProtocol, normalizeDataURL;
1377
+ var init_normalize_url = __esm({
1378
+ "node_modules/normalize-url/index.js"() {
1379
+ DATA_URL_DEFAULT_MIME_TYPE = "text/plain";
1380
+ DATA_URL_DEFAULT_CHARSET = "us-ascii";
1381
+ testParameter = (name, filters) => filters.some((filter) => filter instanceof RegExp ? filter.test(name) : filter === name);
1382
+ supportedProtocols = /* @__PURE__ */ new Set([
1383
+ "https:",
1384
+ "http:",
1385
+ "file:"
1386
+ ]);
1387
+ hasCustomProtocol = (urlString) => {
1388
+ try {
1389
+ const { protocol } = new URL(urlString);
1390
+ return protocol.endsWith(":") && !protocol.includes(".") && !supportedProtocols.has(protocol);
1391
+ } catch {
1392
+ return false;
1393
+ }
1394
+ };
1395
+ normalizeDataURL = (urlString, { stripHash }) => {
1396
+ const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString);
1397
+ if (!match) {
1398
+ throw new Error(`Invalid URL: ${urlString}`);
1399
+ }
1400
+ const { type, data, hash } = match.groups;
1401
+ const mediaType = type.split(";");
1402
+ const isBase64 = mediaType.at(-1) === "base64";
1403
+ if (isBase64) {
1404
+ mediaType.pop();
1405
+ }
1406
+ const mimeType = mediaType.shift()?.toLowerCase() ?? "";
1407
+ const attributes = mediaType.map((attribute) => {
1408
+ let [key, value = ""] = attribute.split("=").map((string) => string.trim());
1409
+ if (key === "charset") {
1410
+ value = value.toLowerCase();
1411
+ if (value === DATA_URL_DEFAULT_CHARSET) {
1412
+ return "";
1413
+ }
1414
+ }
1415
+ return `${key}${value ? `=${value}` : ""}`;
1416
+ }).filter(Boolean);
1417
+ const normalizedMediaType = [...attributes];
1418
+ if (isBase64) {
1419
+ normalizedMediaType.push("base64");
1420
+ }
1421
+ if (normalizedMediaType.length > 0 || mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE) {
1422
+ normalizedMediaType.unshift(mimeType);
1423
+ }
1424
+ const hashPart = stripHash || !hash ? "" : `#${hash}`;
1425
+ return `data:${normalizedMediaType.join(";")},${isBase64 ? data.trim() : data}${hashPart}`;
1426
+ };
1108
1427
  }
1109
1428
  });
1110
1429
 
@@ -1112,14 +1431,18 @@
1112
1431
  var require_tabs = __commonJS({
1113
1432
  "dist/extension/lib/tabs.js"(exports) {
1114
1433
  "use strict";
1434
+ var __importDefault = exports && exports.__importDefault || function(mod) {
1435
+ return mod && mod.__esModule ? mod : { "default": mod };
1436
+ };
1115
1437
  Object.defineProperty(exports, "__esModule", { value: true });
1116
1438
  exports.getMostRecentFocusedWindowId = getMostRecentFocusedWindowId2;
1117
- exports.normalizeUrl = normalizeUrl;
1439
+ exports.normalizeUrl = normalizeUrl2;
1118
1440
  exports.normalizeTabIndex = normalizeTabIndex2;
1119
1441
  exports.resolveOpenWindow = resolveOpenWindow;
1120
1442
  exports.focusTab = focusTab;
1121
1443
  exports.refreshTabs = refreshTabs;
1122
1444
  exports.openTabs = openTabs;
1445
+ var normalize_url_1 = __importDefault((init_normalize_url(), __toCommonJS(normalize_url_exports)));
1123
1446
  function getMostRecentFocusedWindowId2(windows) {
1124
1447
  let bestWindowId = null;
1125
1448
  let bestFocusedAt = -Infinity;
@@ -1137,45 +1460,29 @@
1137
1460
  }
1138
1461
  return bestWindowId;
1139
1462
  }
1140
- function normalizeUrl(rawUrl) {
1463
+ function normalizeUrl2(rawUrl) {
1141
1464
  if (!rawUrl || typeof rawUrl !== "string") {
1142
1465
  return null;
1143
1466
  }
1144
- let url;
1145
1467
  try {
1146
- url = new URL(rawUrl);
1468
+ return (0, normalize_url_1.default)(rawUrl, {
1469
+ stripHash: true,
1470
+ removeQueryParameters: [
1471
+ /^utm_\w+$/i,
1472
+ "fbclid",
1473
+ "gclid",
1474
+ "igshid",
1475
+ "mc_cid",
1476
+ "mc_eid",
1477
+ "ref",
1478
+ "ref_src",
1479
+ "ref_url",
1480
+ "si"
1481
+ ]
1482
+ });
1147
1483
  } catch {
1148
1484
  return null;
1149
1485
  }
1150
- if (url.protocol !== "http:" && url.protocol !== "https:") {
1151
- return null;
1152
- }
1153
- url.hash = "";
1154
- const dropKeys = /* @__PURE__ */ new Set([
1155
- "fbclid",
1156
- "gclid",
1157
- "igshid",
1158
- "mc_cid",
1159
- "mc_eid",
1160
- "ref",
1161
- "ref_src",
1162
- "ref_url",
1163
- "utm_campaign",
1164
- "utm_content",
1165
- "utm_medium",
1166
- "utm_source",
1167
- "utm_term",
1168
- "utm_name",
1169
- "si"
1170
- ]);
1171
- for (const key of Array.from(url.searchParams.keys())) {
1172
- if (key.startsWith("utm_") || dropKeys.has(key)) {
1173
- url.searchParams.delete(key);
1174
- }
1175
- }
1176
- const search = url.searchParams.toString();
1177
- url.search = search ? `?${search}` : "";
1178
- return url.toString();
1179
1486
  }
1180
1487
  function normalizeTabIndex2(value) {
1181
1488
  const index = Number(value);
@@ -1303,6 +1610,8 @@
1303
1610
  throw new Error("Only one target position is allowed");
1304
1611
  }
1305
1612
  const newWindow = params.newWindow === true;
1613
+ const forceNewGroup = params.newGroup === true;
1614
+ const allowDuplicates = params.allowDuplicates === true;
1306
1615
  if (!urls.length && !newWindow) {
1307
1616
  throw new Error("No URLs provided");
1308
1617
  }
@@ -1387,6 +1696,12 @@
1387
1696
  }
1388
1697
  const snapshot = await deps2.getTabSnapshot();
1389
1698
  let openParams = params;
1699
+ if (groupTitle && !forceNewGroup && openParams.windowId == null && !openParams.windowGroupTitle && !openParams.windowTabId && !openParams.windowUrl) {
1700
+ const groupWindows = snapshot.windows.filter((win) => win.groups.some((g) => g.title === groupTitle));
1701
+ if (groupWindows.length === 1) {
1702
+ openParams = { ...openParams, windowId: groupWindows[0].windowId };
1703
+ }
1704
+ }
1390
1705
  if (params.windowId == null && (beforeTabId != null || afterTabId != null)) {
1391
1706
  const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
1392
1707
  const anchorWindow = snapshot.windows.find((win) => win.tabs.some((tab) => tab.tabId === anchorId));
@@ -1403,6 +1718,24 @@
1403
1718
  if (!windowSnapshot) {
1404
1719
  throw new Error("Window snapshot unavailable");
1405
1720
  }
1721
+ let existingGroupId = null;
1722
+ const existingUrlSet = /* @__PURE__ */ new Set();
1723
+ if (groupTitle && !forceNewGroup) {
1724
+ const matchingGroups = windowSnapshot.groups.filter((g) => g.title === groupTitle);
1725
+ if (matchingGroups.length > 1) {
1726
+ 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.`);
1727
+ }
1728
+ if (matchingGroups.length === 1) {
1729
+ existingGroupId = matchingGroups[0].groupId;
1730
+ const existingTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === existingGroupId);
1731
+ for (const tab of existingTabs) {
1732
+ const norm = normalizeUrl2(tab.url);
1733
+ if (norm) {
1734
+ existingUrlSet.add(norm);
1735
+ }
1736
+ }
1737
+ }
1738
+ }
1406
1739
  const created = [];
1407
1740
  const skipped = [];
1408
1741
  let insertIndex = null;
@@ -1436,8 +1769,22 @@
1436
1769
  }
1437
1770
  insertIndex = beforeTabId != null ? anchorIndex : anchorIndex + 1;
1438
1771
  }
1772
+ if (existingGroupId != null && insertIndex == null && beforeTabId == null && afterTabId == null) {
1773
+ const groupTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === existingGroupId);
1774
+ const indices = groupTabs.map((tab) => normalizeTabIndex2(tab.index)).filter((value) => value != null);
1775
+ if (indices.length) {
1776
+ insertIndex = Math.max(...indices) + 1;
1777
+ }
1778
+ }
1439
1779
  let nextIndex = insertIndex;
1440
1780
  for (const url of urls) {
1781
+ if (!allowDuplicates && existingGroupId != null) {
1782
+ const norm = normalizeUrl2(url);
1783
+ if (norm && existingUrlSet.has(norm)) {
1784
+ skipped.push({ url, reason: "duplicate" });
1785
+ continue;
1786
+ }
1787
+ }
1441
1788
  try {
1442
1789
  const createOptions = { windowId, url, active: false };
1443
1790
  if (nextIndex != null) {
@@ -1461,18 +1808,41 @@
1461
1808
  try {
1462
1809
  const tabIds = created.map((tab) => tab.tabId).filter((id) => typeof id === "number");
1463
1810
  if (tabIds.length > 0) {
1464
- groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
1465
- const update = { title: groupTitle };
1466
- if (groupColor) {
1467
- update.color = groupColor;
1811
+ if (existingGroupId != null) {
1812
+ groupId = await chrome.tabs.group({ groupId: existingGroupId, tabIds });
1813
+ } else {
1814
+ groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
1815
+ const update = { title: groupTitle };
1816
+ if (groupColor) {
1817
+ update.color = groupColor;
1818
+ }
1819
+ await chrome.tabGroups.update(groupId, update);
1468
1820
  }
1469
- await chrome.tabGroups.update(groupId, update);
1470
1821
  }
1471
1822
  } catch (error) {
1472
1823
  deps2.log("Failed to create group", error);
1473
1824
  groupId = null;
1474
1825
  }
1475
1826
  }
1827
+ if (groupTitle && created.length === 0 && existingGroupId != null) {
1828
+ groupId = existingGroupId;
1829
+ }
1830
+ try {
1831
+ const freshTabs = await chrome.tabs.query({ windowId });
1832
+ freshTabs.sort((a, b) => a.index - b.index);
1833
+ const firstUngroupedIndex = freshTabs.findIndex((t) => (t.groupId ?? -1) === -1);
1834
+ if (firstUngroupedIndex >= 0) {
1835
+ const groupedAfterUngrouped = freshTabs.filter((t, i) => i > firstUngroupedIndex && (t.groupId ?? -1) !== -1);
1836
+ if (groupedAfterUngrouped.length > 0) {
1837
+ const tabIdsToMove = groupedAfterUngrouped.map((t) => t.id).filter((id) => typeof id === "number");
1838
+ if (tabIdsToMove.length > 0) {
1839
+ await chrome.tabs.move(tabIdsToMove, { index: firstUngroupedIndex });
1840
+ }
1841
+ }
1842
+ }
1843
+ } catch (err) {
1844
+ deps2.log("Failed to reorder groups before ungrouped tabs", err);
1845
+ }
1476
1846
  return {
1477
1847
  windowId,
1478
1848
  groupId,
@@ -1825,7 +2195,7 @@
1825
2195
  var content2 = require_content();
1826
2196
  var { isScriptableUrl: isScriptableUrl2, isGitHubIssueOrPr: isGitHubIssueOrPr2, detectGitHubState: detectGitHubState2, extractPageMeta: extractPageMeta2, extractSelectorSignal: extractSelectorSignal2, waitForSettle: waitForSettle2, waitForTabReady: waitForTabReady2 } = content2;
1827
2197
  var tabs2 = require_tabs();
1828
- var { normalizeUrl } = tabs2;
2198
+ var { normalizeUrl: normalizeUrl2 } = tabs2;
1829
2199
  exports.DEFAULT_STALE_DAYS = 30;
1830
2200
  exports.DESCRIPTION_MAX_LENGTH = 250;
1831
2201
  exports.SELECTOR_VALUE_MAX_LENGTH = 500;
@@ -1852,7 +2222,7 @@
1852
2222
  const normalizedMap = /* @__PURE__ */ new Map();
1853
2223
  const duplicates = /* @__PURE__ */ new Map();
1854
2224
  for (const tab of scopeTabs) {
1855
- const normalized = normalizeUrl(tab.url);
2225
+ const normalized = normalizeUrl2(tab.url);
1856
2226
  if (!normalized) {
1857
2227
  continue;
1858
2228
  }
@@ -2119,6 +2489,7 @@
2119
2489
  exports.undoGroupUpdate = undoGroupUpdate;
2120
2490
  exports.undoGroupUngroup = undoGroupUngroup;
2121
2491
  exports.undoGroupAssign = undoGroupAssign;
2492
+ exports.undoGroupGather = undoGroupGather;
2122
2493
  exports.undoMoveTab = undoMoveTab;
2123
2494
  exports.undoMoveGroup = undoMoveGroup;
2124
2495
  exports.undoMergeWindow = undoMergeWindow;
@@ -2303,6 +2674,10 @@
2303
2674
  const tabs2 = undo.tabs || [];
2304
2675
  return await restoreTabsFromUndo(tabs2, deps2);
2305
2676
  }
2677
+ async function undoGroupGather(undo, deps2) {
2678
+ const tabs2 = undo.tabs || [];
2679
+ return await restoreTabsFromUndo(tabs2, deps2);
2680
+ }
2306
2681
  async function undoMoveTab(undo, deps2) {
2307
2682
  const from = undo.from || {};
2308
2683
  const entry = {
@@ -2515,6 +2890,9 @@
2515
2890
  if (undo.action === "group-assign") {
2516
2891
  return await undoGroupAssign(undo, deps2);
2517
2892
  }
2893
+ if (undo.action === "group-gather") {
2894
+ return await undoGroupGather(undo, deps2);
2895
+ }
2518
2896
  if (undo.action === "move-tab") {
2519
2897
  return await undoMoveTab(undo, deps2);
2520
2898
  }
@@ -3149,6 +3527,8 @@
3149
3527
  return await groupUngroup(params);
3150
3528
  case "group-assign":
3151
3529
  return await groupAssign(params);
3530
+ case "group-gather":
3531
+ return await groupGather(params);
3152
3532
  case "move-tab":
3153
3533
  return await move.moveTab(params, deps);
3154
3534
  case "move-group":
@@ -3320,6 +3700,9 @@
3320
3700
  async function groupAssign(params) {
3321
3701
  return groups.groupAssign(params, deps);
3322
3702
  }
3703
+ async function groupGather(params) {
3704
+ return groups.groupGather(params, deps);
3705
+ }
3323
3706
  async function getArchiveWindowId() {
3324
3707
  await ensureArchiveWindowIdLoaded();
3325
3708
  return state.archiveWindowId;