tabctl 0.6.0-alpha.6 → 0.6.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -264,7 +264,7 @@ Relevant knobs: `TABCTL_SOCKET`, `TABCTL_TCP_PORT`, `TABCTL_PROFILE`, `TABCTL_DA
264
264
  - Runtime command runs can auto-sync extension files when host/extension versions drift; rerun `tabctl reload` if the browser does not pick up changes immediately.
265
265
  - For local release-like testing while developing, force runtime sync behavior with `TABCTL_AUTO_SYNC_MODE=release-like`.
266
266
  - Disable runtime sync entirely with `TABCTL_AUTO_SYNC_MODE=off`.
267
- - `tabctl ping --json` is the canonical runtime version check (`data.versionsInSync`, `data.hostBaseVersion`, `data.baseVersion`).
267
+ - `tabctl ping --json` is the canonical runtime version check (`versionsInSync`, `hostBaseVersion`, `baseVersion`).
268
268
  - Version metadata is intentionally health-only: regular command payloads (`open`, `list`, etc.) do not include version fields.
269
269
  - `tabctl ping` returns connect errors (`ENOENT`, `ECONNREFUSED`, timeout): ensure extension is loaded and active, rerun `tabctl setup`, and in WSL verify `TABCTL_TCP_PORT` or `<dataDir>/tcp-port` matches a listening localhost port.
270
270
  - `tabctl doctor --fix --json` includes per-profile connectivity diagnostics in `data.profiles[].connectivity`; if ping remains unhealthy after local repairs, follow `manualSteps`.
@@ -409,5 +409,5 @@ Notes:
409
409
  - Selector `attr` supports `href-url`/`src-url` to return absolute http(s) URLs.
410
410
  - `screenshot --out` writes per-tab folders into the target directory.
411
411
  - `tabctl undo` accepts a positional txid, `--txid`, or `--latest`.
412
- - `tabctl history --json` returns a JSON array in `data`.
412
+ - `tabctl history --json` returns a top-level JSON array.
413
413
  - `--format` is only supported by `report` (use `--json` elsewhere).
@@ -318,7 +318,6 @@
318
318
  const tabs2 = selection.tabs;
319
319
  const entries = [];
320
320
  let totalTiles = 0;
321
- const startedAt = Date.now();
322
321
  for (let index = 0; index < tabs2.length; index += 1) {
323
322
  const tab = tabs2[index];
324
323
  const tabId = tab.tabId;
@@ -374,19 +373,11 @@
374
373
  deps2.sendProgress(requestId, { phase: "screenshot", processed: index + 1, total: tabs2.length, tabId });
375
374
  }
376
375
  }
377
- return {
378
- generatedAt: Date.now(),
376
+ const response = {
379
377
  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
378
  entries
389
379
  };
380
+ return response;
390
381
  }
391
382
  }
392
383
  });
@@ -876,7 +867,7 @@
876
867
  throw new Error("Missing group update fields");
877
868
  }
878
869
  const updated = await chrome.tabGroups.update(match.group.groupId, update);
879
- return {
870
+ const fullResult = {
880
871
  groupId: updated.id,
881
872
  windowId: updated.windowId,
882
873
  title: updated.title,
@@ -894,6 +885,13 @@
894
885
  },
895
886
  txid: params.txid || null
896
887
  };
888
+ return {
889
+ groupId: updated.id,
890
+ windowId: updated.windowId,
891
+ summary: { updatedGroups: 1 },
892
+ undo: fullResult.undo,
893
+ txid: fullResult.txid
894
+ };
897
895
  }
898
896
  async function groupUngroup2(params, deps2) {
899
897
  const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
@@ -937,7 +935,7 @@
937
935
  if (tabIds.length) {
938
936
  await chrome.tabs.ungroup(tabIds);
939
937
  }
940
- return {
938
+ const fullResult = {
941
939
  groupId: match.group.groupId,
942
940
  groupTitle: match.group.title || null,
943
941
  windowId: match.windowId,
@@ -955,6 +953,13 @@
955
953
  },
956
954
  txid: params.txid || null
957
955
  };
956
+ return {
957
+ groupId: match.group.groupId,
958
+ windowId: match.windowId,
959
+ summary: fullResult.summary,
960
+ undo: fullResult.undo,
961
+ txid: fullResult.txid
962
+ };
958
963
  }
959
964
  async function groupAssign2(params, deps2) {
960
965
  const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
@@ -1078,7 +1083,7 @@
1078
1083
  }
1079
1084
  created = true;
1080
1085
  }
1081
- return {
1086
+ const fullResult = {
1082
1087
  groupId: assignedGroupId,
1083
1088
  groupTitle: targetTitle || groupTitle || null,
1084
1089
  windowId: targetWindowId,
@@ -1100,6 +1105,15 @@
1100
1105
  },
1101
1106
  txid: params.txid || null
1102
1107
  };
1108
+ return {
1109
+ groupId: assignedGroupId,
1110
+ windowId: targetWindowId,
1111
+ created,
1112
+ summary: fullResult.summary,
1113
+ skipped: fullResult.skipped,
1114
+ undo: fullResult.undo,
1115
+ txid: fullResult.txid
1116
+ };
1103
1117
  }
1104
1118
  async function groupGather2(params, deps2) {
1105
1119
  const snapshot = await deps2.getTabSnapshot();
@@ -1167,7 +1181,7 @@
1167
1181
  });
1168
1182
  }
1169
1183
  }
1170
- return {
1184
+ const fullResult = {
1171
1185
  merged,
1172
1186
  summary: {
1173
1187
  mergedGroups: merged.reduce((sum, m) => sum + m.mergedGroupCount, 0),
@@ -1179,6 +1193,12 @@
1179
1193
  },
1180
1194
  txid: params.txid || null
1181
1195
  };
1196
+ return {
1197
+ merged,
1198
+ summary: fullResult.summary,
1199
+ undo: fullResult.undo,
1200
+ txid: fullResult.txid
1201
+ };
1182
1202
  }
1183
1203
  }
1184
1204
  });
@@ -1460,6 +1480,15 @@
1460
1480
  const index = Number(value);
1461
1481
  return Number.isFinite(index) ? index : null;
1462
1482
  }
1483
+ function shapeOpenResult(result) {
1484
+ return {
1485
+ windowId: result.windowId,
1486
+ groupId: result.groupId,
1487
+ createdTabIds: result.created.map((tab) => tab.tabId).filter((id) => typeof id === "number"),
1488
+ skipped: result.skipped,
1489
+ summary: result.summary
1490
+ };
1491
+ }
1463
1492
  function matchIncludes(value, needle) {
1464
1493
  if (!needle) {
1465
1494
  return false;
@@ -1651,12 +1680,9 @@
1651
1680
  groupId2 = null;
1652
1681
  }
1653
1682
  }
1654
- return {
1683
+ return shapeOpenResult({
1655
1684
  windowId: windowId2,
1656
1685
  groupId: groupId2,
1657
- groupTitle: groupTitle || null,
1658
- afterGroupTitle: null,
1659
- insertIndex: null,
1660
1686
  created: created2,
1661
1687
  skipped: skipped2,
1662
1688
  summary: {
@@ -1664,7 +1690,7 @@
1664
1690
  skippedUrls: skipped2.length,
1665
1691
  grouped: Boolean(groupId2)
1666
1692
  }
1667
- };
1693
+ });
1668
1694
  }
1669
1695
  const snapshot = await deps2.getTabSnapshot();
1670
1696
  let openParams = params;
@@ -1775,6 +1801,7 @@
1775
1801
  skipped.push({ url, reason: "create_failed" });
1776
1802
  }
1777
1803
  }
1804
+ const createdTabIds = new Set(created.map((tab) => tab.tabId).filter((id) => typeof id === "number"));
1778
1805
  let groupId = null;
1779
1806
  if (groupTitle && created.length > 0) {
1780
1807
  try {
@@ -1799,28 +1826,64 @@
1799
1826
  if (groupTitle && created.length === 0 && existingGroupId != null) {
1800
1827
  groupId = existingGroupId;
1801
1828
  }
1829
+ const targetGroupId = groupId ?? existingGroupId;
1830
+ if (targetGroupId != null && created.length > 0) {
1831
+ try {
1832
+ if (createdTabIds.size > 0) {
1833
+ const latestTabs = await chrome.tabs.query({ windowId });
1834
+ const missingGroupTabIds = latestTabs.filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId).map((tab) => tab.id);
1835
+ if (missingGroupTabIds.length > 0) {
1836
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: missingGroupTabIds });
1837
+ }
1838
+ }
1839
+ } catch (error) {
1840
+ deps2.log("Failed to enforce grouping for newly opened tabs", error);
1841
+ }
1842
+ }
1802
1843
  try {
1803
1844
  const freshTabs = await chrome.tabs.query({ windowId });
1804
1845
  freshTabs.sort((a, b) => a.index - b.index);
1805
1846
  const firstUngroupedIndex = freshTabs.findIndex((t) => (t.groupId ?? -1) === -1);
1806
1847
  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
- }
1848
+ let tabIdsToMove = [];
1849
+ if (existingGroupId != null) {
1850
+ tabIdsToMove = freshTabs.filter((tab, i) => i > firstUngroupedIndex && typeof tab.id === "number" && createdTabIds.has(tab.id) && (tab.groupId ?? -1) !== -1).map((tab) => tab.id);
1851
+ } else {
1852
+ tabIdsToMove = freshTabs.filter((tab, i) => i > firstUngroupedIndex && (tab.groupId ?? -1) !== -1).map((tab) => tab.id).filter((id) => typeof id === "number");
1853
+ }
1854
+ if (tabIdsToMove.length > 0) {
1855
+ await chrome.tabs.move(tabIdsToMove, { index: firstUngroupedIndex });
1813
1856
  }
1814
1857
  }
1815
1858
  } catch (err) {
1816
1859
  deps2.log("Failed to reorder groups before ungrouped tabs", err);
1817
1860
  }
1818
- return {
1861
+ if (targetGroupId != null && createdTabIds.size > 0) {
1862
+ try {
1863
+ const latestTabs = await chrome.tabs.query({ windowId });
1864
+ const lateUngroupedTabIds = latestTabs.filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId).map((tab) => tab.id);
1865
+ if (lateUngroupedTabIds.length > 0) {
1866
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: lateUngroupedTabIds });
1867
+ }
1868
+ } catch (error) {
1869
+ deps2.log("Failed post-reorder grouping verification", error);
1870
+ }
1871
+ }
1872
+ if (targetGroupId != null && createdTabIds.size > 0) {
1873
+ try {
1874
+ await deps2.delay(250);
1875
+ const delayedTabs = await chrome.tabs.query({ windowId });
1876
+ const delayedUngroupedTabIds = delayedTabs.filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId).map((tab) => tab.id);
1877
+ if (delayedUngroupedTabIds.length > 0) {
1878
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: delayedUngroupedTabIds });
1879
+ }
1880
+ } catch (error) {
1881
+ deps2.log("Failed delayed grouping verification", error);
1882
+ }
1883
+ }
1884
+ return shapeOpenResult({
1819
1885
  windowId,
1820
1886
  groupId,
1821
- groupTitle: groupTitle || null,
1822
- afterGroupTitle: afterGroupTitle || null,
1823
- insertIndex,
1824
1887
  created,
1825
1888
  skipped,
1826
1889
  summary: {
@@ -1828,7 +1891,7 @@
1828
1891
  skippedUrls: skipped.length,
1829
1892
  grouped: Boolean(groupId)
1830
1893
  }
1831
- };
1894
+ });
1832
1895
  }
1833
1896
  }
1834
1897
  });
@@ -1947,7 +2010,7 @@
1947
2010
  targetIndex2 = 0;
1948
2011
  }
1949
2012
  }
1950
- return {
2013
+ const fullResult2 = {
1951
2014
  tabId,
1952
2015
  from: { windowId: sourceWindow.windowId, index: sourceTab.index },
1953
2016
  to: { windowId: targetWindowId2, index: targetIndex2 },
@@ -1970,6 +2033,14 @@
1970
2033
  },
1971
2034
  txid: params.txid || null
1972
2035
  };
2036
+ return {
2037
+ tabId,
2038
+ fromWindowId: sourceWindow.windowId,
2039
+ toWindowId: targetWindowId2,
2040
+ summary: fullResult2.summary,
2041
+ undo: fullResult2.undo,
2042
+ txid: fullResult2.txid
2043
+ };
1973
2044
  }
1974
2045
  let normalizedParams = params;
1975
2046
  if (params.windowId != null) {
@@ -1987,7 +2058,7 @@
1987
2058
  targetIndex -= 1;
1988
2059
  }
1989
2060
  const moved = await chrome.tabs.move(tabId, { windowId: targetWindowId, index: targetIndex });
1990
- return {
2061
+ const fullResult = {
1991
2062
  tabId,
1992
2063
  from: { windowId: sourceWindow.windowId, index: sourceTab.index },
1993
2064
  to: { windowId: targetWindowId, index: moved.index },
@@ -2010,6 +2081,14 @@
2010
2081
  },
2011
2082
  txid: params.txid || null
2012
2083
  };
2084
+ return {
2085
+ tabId,
2086
+ fromWindowId: sourceWindow.windowId,
2087
+ toWindowId: targetWindowId,
2088
+ summary: fullResult.summary,
2089
+ undo: fullResult.undo,
2090
+ txid: fullResult.txid
2091
+ };
2013
2092
  }
2014
2093
  async function moveGroup(params, deps2) {
2015
2094
  const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
@@ -2027,6 +2106,28 @@
2027
2106
  if (!source.tabs.length) {
2028
2107
  throw new Error("Group has no tabs to move");
2029
2108
  }
2109
+ const ensureMovedTabsAreGrouped = async (movedTabIds, targetWindowId2, targetGroupId) => {
2110
+ if (!targetGroupId || movedTabIds.length === 0) {
2111
+ return;
2112
+ }
2113
+ const movedSet = new Set(movedTabIds);
2114
+ const verify = async (step) => {
2115
+ const tabs3 = await chrome.tabs.query({ windowId: targetWindowId2 });
2116
+ const missingGroupTabIds = tabs3.filter((tab) => typeof tab.id === "number" && movedSet.has(tab.id) && tab.groupId !== targetGroupId).map((tab) => tab.id);
2117
+ if (missingGroupTabIds.length > 0) {
2118
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: missingGroupTabIds });
2119
+ }
2120
+ };
2121
+ try {
2122
+ await verify("group-verify");
2123
+ await new Promise((resolve) => setTimeout(resolve, 250));
2124
+ await verify("group-verify-delayed");
2125
+ await new Promise((resolve) => setTimeout(resolve, 1500));
2126
+ await verify("group-verify-delayed-late");
2127
+ } catch (error) {
2128
+ deps2.log("Failed to enforce moved group integrity", error);
2129
+ }
2130
+ };
2030
2131
  const newWindow = params.newWindow === true;
2031
2132
  const hasTarget = Number.isFinite(params.beforeTabId) || Number.isFinite(params.afterTabId) || typeof params.beforeGroupTitle === "string" && params.beforeGroupTitle.trim() || typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim();
2032
2133
  if (newWindow) {
@@ -2054,6 +2155,7 @@
2054
2155
  } catch (error) {
2055
2156
  deps2.log("Failed to regroup tabs", error);
2056
2157
  }
2158
+ await ensureMovedTabsAreGrouped(tabIds2, targetWindowId2, newGroupId2);
2057
2159
  const undoTabs2 = source.tabs.map((tab) => ({
2058
2160
  tabId: tab.tabId,
2059
2161
  windowId: tab.windowId,
@@ -2063,7 +2165,7 @@
2063
2165
  groupColor: tab.groupColor,
2064
2166
  groupCollapsed: source.group.collapsed ?? null
2065
2167
  })).filter((tab) => typeof tab.tabId === "number");
2066
- return {
2168
+ const fullResult2 = {
2067
2169
  groupId: source.group.groupId,
2068
2170
  windowId: source.windowId,
2069
2171
  movedToWindowId: targetWindowId2,
@@ -2081,6 +2183,15 @@
2081
2183
  },
2082
2184
  txid: params.txid || null
2083
2185
  };
2186
+ return {
2187
+ groupId: source.group.groupId,
2188
+ windowId: source.windowId,
2189
+ movedToWindowId: targetWindowId2,
2190
+ newGroupId: newGroupId2,
2191
+ summary: fullResult2.summary,
2192
+ undo: fullResult2.undo,
2193
+ txid: fullResult2.txid
2194
+ };
2084
2195
  }
2085
2196
  const target = resolveMoveTarget(snapshot, params, deps2);
2086
2197
  if (target.error) {
@@ -2125,6 +2236,7 @@
2125
2236
  deps2.log("Failed to regroup tabs", error);
2126
2237
  }
2127
2238
  }
2239
+ await ensureMovedTabsAreGrouped(tabIds, targetWindowId, targetWindowId === source.windowId ? source.group.groupId : newGroupId);
2128
2240
  const undoTabs = source.tabs.map((tab) => ({
2129
2241
  tabId: tab.tabId,
2130
2242
  windowId: tab.windowId,
@@ -2134,7 +2246,7 @@
2134
2246
  groupColor: tab.groupColor,
2135
2247
  groupCollapsed: source.group.collapsed ?? null
2136
2248
  })).filter((tab) => typeof tab.tabId === "number");
2137
- return {
2249
+ const fullResult = {
2138
2250
  groupId: source.group.groupId,
2139
2251
  windowId: source.windowId,
2140
2252
  movedToWindowId: targetWindowId,
@@ -2152,6 +2264,15 @@
2152
2264
  },
2153
2265
  txid: params.txid || null
2154
2266
  };
2267
+ return {
2268
+ groupId: source.group.groupId,
2269
+ windowId: source.windowId,
2270
+ movedToWindowId: targetWindowId,
2271
+ newGroupId,
2272
+ summary: fullResult.summary,
2273
+ undo: fullResult.undo,
2274
+ txid: fullResult.txid
2275
+ };
2155
2276
  }
2156
2277
  }
2157
2278
  });
@@ -2182,7 +2303,6 @@
2182
2303
  const selectedTabs = selection.tabs;
2183
2304
  const scopeTabs = selectedTabs;
2184
2305
  const now = Date.now();
2185
- const startedAt = Date.now();
2186
2306
  const normalizedMap = /* @__PURE__ */ new Map();
2187
2307
  const duplicates = /* @__PURE__ */ new Map();
2188
2308
  for (const tab of scopeTabs) {
@@ -2235,19 +2355,16 @@
2235
2355
  severity
2236
2356
  };
2237
2357
  });
2238
- return {
2239
- generatedAt: Date.now(),
2358
+ const response = {
2240
2359
  staleDays,
2241
2360
  totals: {
2242
2361
  tabs: scopeTabs.length,
2243
2362
  analyzed: selectedTabs.length,
2244
2363
  candidates: candidates.length
2245
2364
  },
2246
- meta: {
2247
- durationMs: Date.now() - startedAt
2248
- },
2249
2365
  candidates
2250
2366
  };
2367
+ return response;
2251
2368
  }
2252
2369
  async function inspectTabs(params, requestId, deps2) {
2253
2370
  const signalList = Array.isArray(params.signals) && params.signals.length > 0 ? params.signals.map(String) : ["page-meta"];
@@ -2269,7 +2386,6 @@
2269
2386
  throw selection.error;
2270
2387
  }
2271
2388
  const tabs3 = selection.tabs;
2272
- const startedAt = Date.now();
2273
2389
  const selectorSpecs = [];
2274
2390
  if (Array.isArray(params.selectorSpecs)) {
2275
2391
  selectorSpecs.push(...params.selectorSpecs);
@@ -2287,17 +2403,24 @@
2287
2403
  }
2288
2404
  }
2289
2405
  }
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,
2406
+ const normalizedSelectors = selectorSpecs.filter((spec) => spec && typeof spec.selector === "string" && spec.selector.length > 0).map((spec) => {
2407
+ const selector = spec.selector;
2408
+ return {
2409
+ name: typeof spec.name === "string" ? spec.name : void 0,
2410
+ selector,
2411
+ attr: typeof spec.attr === "string" ? spec.attr : "text",
2412
+ all: Boolean(spec.all),
2413
+ text: typeof spec.text === "string" && spec.text.trim() ? spec.text.trim() : void 0,
2414
+ textMode: typeof spec.textMode === "string" ? spec.textMode.trim().toLowerCase() : void 0
2415
+ };
2416
+ });
2417
+ const selectorWarnings = normalizedSelectors.filter((spec) => spec.selector.includes(":contains(")).map((spec) => ({
2418
+ code: "unsupported_selector_syntax",
2419
+ signalId: "selector",
2292
2420
  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
2421
  name: spec.name || spec.selector,
2300
- hint: "CSS :contains() is not supported; use selector text filters or a different selector."
2422
+ message: "Selector uses unsupported CSS :contains() syntax.",
2423
+ hint: "Use selector text filters (text/textMode) or a different selector."
2301
2424
  }));
2302
2425
  const signalDefs = [];
2303
2426
  for (const signalId of signalList) {
@@ -2339,19 +2462,22 @@
2339
2462
  const tabId = task.tab.tabId;
2340
2463
  let result = null;
2341
2464
  let error = null;
2342
- const started = Date.now();
2343
2465
  try {
2344
2466
  await waitForTabReady2(tabId, params, signalTimeoutMs);
2345
2467
  result = await task.signal.run(tabId);
2468
+ if (task.signal.id === "selector" && result && typeof result === "object") {
2469
+ const selectorErrors = Object.keys(result.errors || {});
2470
+ if (selectorErrors.length > 0) {
2471
+ error = `selector failures: ${selectorErrors.join(", ")}`;
2472
+ }
2473
+ }
2346
2474
  } catch (err) {
2347
2475
  const message = err instanceof Error ? err.message : "signal_error";
2348
2476
  error = message;
2349
2477
  }
2350
- const durationMs = Date.now() - started;
2351
2478
  const entry = entryMap.get(tabId) || { tab: task.tab, signals: {} };
2352
2479
  entry.signals[task.signal.id] = {
2353
2480
  ok: error === null,
2354
- durationMs,
2355
2481
  data: result,
2356
2482
  error
2357
2483
  };
@@ -2377,21 +2503,18 @@
2377
2503
  title: entry.tab.title,
2378
2504
  signals: entry.signals
2379
2505
  }));
2380
- return {
2381
- generatedAt: Date.now(),
2506
+ const response = {
2382
2507
  totals: {
2383
2508
  tabs: tabs3.length,
2384
2509
  signals: signalDefs.length,
2385
2510
  tasks: totalTasks
2386
2511
  },
2387
- meta: {
2388
- durationMs: Date.now() - startedAt,
2389
- signalTimeoutMs,
2390
- selectorCount: normalizedSelectors.length,
2391
- selectorWarnings: selectorWarnings.length > 0 ? selectorWarnings : void 0
2392
- },
2393
2512
  entries
2394
2513
  };
2514
+ if (selectorWarnings.length > 0) {
2515
+ response.warnings = selectorWarnings;
2516
+ }
2517
+ return response;
2395
2518
  }
2396
2519
  }
2397
2520
  });
@@ -2871,13 +2994,14 @@
2871
2994
  })).filter((win) => win.tabs.length > 0);
2872
2995
  }
2873
2996
  if (windowsToProcess.length === 0) {
2874
- return {
2997
+ const fullResult2 = {
2875
2998
  txid: params.txid || null,
2876
2999
  summary: { movedTabs: 0, movedGroups: 0, skippedTabs: 0 },
2877
3000
  archiveWindowId: null,
2878
3001
  skipped: [],
2879
3002
  undo: { action: "archive", tabs: [] }
2880
3003
  };
3004
+ return fullResult2;
2881
3005
  }
2882
3006
  const archiveWindowId = await ensureArchiveWindow(deps2);
2883
3007
  const undoTabs = [];
@@ -2980,7 +3104,7 @@
2980
3104
  }
2981
3105
  }
2982
3106
  }
2983
- return {
3107
+ const fullResult = {
2984
3108
  txid: params.txid || null,
2985
3109
  summary: {
2986
3110
  movedTabs,
@@ -2994,6 +3118,13 @@
2994
3118
  tabs: undoTabs
2995
3119
  }
2996
3120
  };
3121
+ return {
3122
+ txid: fullResult.txid,
3123
+ summary: fullResult.summary,
3124
+ archiveWindowId,
3125
+ skipped: fullResult.skipped,
3126
+ undo: fullResult.undo
3127
+ };
2997
3128
  }
2998
3129
  async function getTabsByIds(tabIds) {
2999
3130
  const results = [];
@@ -3022,12 +3153,13 @@
3022
3153
  tabIds = selection.tabs.map((tab) => tab.tabId);
3023
3154
  }
3024
3155
  if (!tabIds.length) {
3025
- return {
3156
+ const fullResult2 = {
3026
3157
  txid: params.txid || null,
3027
3158
  summary: { closedTabs: 0, skippedTabs: 0 },
3028
3159
  skipped: [],
3029
3160
  undo: { action: "close", tabs: [] }
3030
3161
  };
3162
+ return fullResult2;
3031
3163
  }
3032
3164
  const expectedUrls = params.expectedUrls || {};
3033
3165
  const tabInfos = await getTabsByIds(tabIds);
@@ -3067,7 +3199,7 @@
3067
3199
  if (validTabs.length > 0) {
3068
3200
  await chrome.tabs.remove(validTabs.map((tab) => tab.tabId));
3069
3201
  }
3070
- return {
3202
+ const fullResult = {
3071
3203
  txid: params.txid || null,
3072
3204
  summary: {
3073
3205
  closedTabs: validTabs.length,
@@ -3085,6 +3217,12 @@
3085
3217
  }))
3086
3218
  }
3087
3219
  };
3220
+ return {
3221
+ txid: fullResult.txid,
3222
+ summary: fullResult.summary,
3223
+ skipped: fullResult.skipped,
3224
+ undo: fullResult.undo
3225
+ };
3088
3226
  }
3089
3227
  async function mergeWindow(params, deps2) {
3090
3228
  const fromWindowId = Number.isFinite(params.fromWindowId) ? Number(params.fromWindowId) : Number(params.windowId);
@@ -3119,7 +3257,7 @@
3119
3257
  selectedTabs = sourceWindow.tabs.filter((tab) => tabIdSet.has(tab.tabId));
3120
3258
  }
3121
3259
  if (selectedTabs.length === 0) {
3122
- return {
3260
+ const fullResult2 = {
3123
3261
  fromWindowId,
3124
3262
  toWindowId,
3125
3263
  summary: { movedTabs: 0, movedGroups: 0, skippedTabs: skipped.length, closedSource: false },
@@ -3133,6 +3271,7 @@
3133
3271
  tabs: []
3134
3272
  }
3135
3273
  };
3274
+ return fullResult2;
3136
3275
  }
3137
3276
  const orderedTabs = [...selectedTabs].sort((a, b) => {
3138
3277
  const aIndex = Number(a.index);
@@ -3231,7 +3370,7 @@
3231
3370
  deps2.log("Failed to close source window", error);
3232
3371
  }
3233
3372
  }
3234
- return {
3373
+ const fullResult = {
3235
3374
  fromWindowId,
3236
3375
  toWindowId,
3237
3376
  summary: { movedTabs, movedGroups, skippedTabs: skipped.length, closedSource },
@@ -3246,6 +3385,15 @@
3246
3385
  },
3247
3386
  txid: params.txid || null
3248
3387
  };
3388
+ return {
3389
+ fromWindowId,
3390
+ toWindowId,
3391
+ summary: fullResult.summary,
3392
+ skipped: fullResult.skipped,
3393
+ groups: fullResult.groups,
3394
+ undo: fullResult.undo,
3395
+ txid: fullResult.txid
3396
+ };
3249
3397
  }
3250
3398
  }
3251
3399
  });
@@ -3425,7 +3573,7 @@
3425
3573
  component: "extension"
3426
3574
  };
3427
3575
  case "list":
3428
- return await getTabSnapshot();
3576
+ return shapeListSnapshot(await getTabSnapshot());
3429
3577
  case "analyze":
3430
3578
  return await inspect.analyzeTabs(params, requestId, deps);
3431
3579
  case "inspect":
@@ -3469,6 +3617,29 @@
3469
3617
  throw new Error(`Unknown action: ${action}`);
3470
3618
  }
3471
3619
  }
3620
+ function shapeListSnapshot(snapshot) {
3621
+ return {
3622
+ windows: snapshot.windows.map((win) => ({
3623
+ windowId: win.windowId,
3624
+ focused: win.focused,
3625
+ tabs: (win.tabs || []).map((tab) => ({
3626
+ tabId: tab.tabId,
3627
+ windowId: tab.windowId,
3628
+ url: tab.url,
3629
+ title: tab.title,
3630
+ active: tab.active,
3631
+ groupId: tab.groupId,
3632
+ groupTitle: tab.groupTitle
3633
+ })),
3634
+ groups: (win.groups || []).map((group) => ({
3635
+ groupId: group.groupId,
3636
+ title: group.title,
3637
+ color: group.color,
3638
+ collapsed: group.collapsed
3639
+ }))
3640
+ }))
3641
+ };
3642
+ }
3472
3643
  function resolveWindowIdFromParams(snapshot, value) {
3473
3644
  if (typeof value === "number" && Number.isFinite(value)) {
3474
3645
  return value;
@@ -51,13 +51,14 @@ async function archiveTabs(params, deps) {
51
51
  .filter((win) => win.tabs.length > 0);
52
52
  }
53
53
  if (windowsToProcess.length === 0) {
54
- return {
54
+ const fullResult = {
55
55
  txid: params.txid || null,
56
56
  summary: { movedTabs: 0, movedGroups: 0, skippedTabs: 0 },
57
57
  archiveWindowId: null,
58
58
  skipped: [],
59
59
  undo: { action: "archive", tabs: [] },
60
60
  };
61
+ return fullResult;
61
62
  }
62
63
  const archiveWindowId = await ensureArchiveWindow(deps);
63
64
  const undoTabs = [];
@@ -169,7 +170,7 @@ async function archiveTabs(params, deps) {
169
170
  }
170
171
  }
171
172
  }
172
- return {
173
+ const fullResult = {
173
174
  txid: params.txid || null,
174
175
  summary: {
175
176
  movedTabs,
@@ -183,6 +184,13 @@ async function archiveTabs(params, deps) {
183
184
  tabs: undoTabs,
184
185
  },
185
186
  };
187
+ return {
188
+ txid: fullResult.txid,
189
+ summary: fullResult.summary,
190
+ archiveWindowId,
191
+ skipped: fullResult.skipped,
192
+ undo: fullResult.undo,
193
+ };
186
194
  }
187
195
  async function getTabsByIds(tabIds) {
188
196
  const results = [];
@@ -212,12 +220,13 @@ async function closeTabs(params, deps) {
212
220
  tabIds = selection.tabs.map((tab) => tab.tabId);
213
221
  }
214
222
  if (!tabIds.length) {
215
- return {
223
+ const fullResult = {
216
224
  txid: params.txid || null,
217
225
  summary: { closedTabs: 0, skippedTabs: 0 },
218
226
  skipped: [],
219
227
  undo: { action: "close", tabs: [] },
220
228
  };
229
+ return fullResult;
221
230
  }
222
231
  const expectedUrls = params.expectedUrls || {};
223
232
  const tabInfos = await getTabsByIds(tabIds);
@@ -257,7 +266,7 @@ async function closeTabs(params, deps) {
257
266
  if (validTabs.length > 0) {
258
267
  await chrome.tabs.remove(validTabs.map((tab) => tab.tabId));
259
268
  }
260
- return {
269
+ const fullResult = {
261
270
  txid: params.txid || null,
262
271
  summary: {
263
272
  closedTabs: validTabs.length,
@@ -275,6 +284,12 @@ async function closeTabs(params, deps) {
275
284
  })),
276
285
  },
277
286
  };
287
+ return {
288
+ txid: fullResult.txid,
289
+ summary: fullResult.summary,
290
+ skipped: fullResult.skipped,
291
+ undo: fullResult.undo,
292
+ };
278
293
  }
279
294
  async function mergeWindow(params, deps) {
280
295
  const fromWindowId = Number.isFinite(params.fromWindowId)
@@ -311,7 +326,7 @@ async function mergeWindow(params, deps) {
311
326
  selectedTabs = sourceWindow.tabs.filter((tab) => tabIdSet.has(tab.tabId));
312
327
  }
313
328
  if (selectedTabs.length === 0) {
314
- return {
329
+ const fullResult = {
315
330
  fromWindowId,
316
331
  toWindowId,
317
332
  summary: { movedTabs: 0, movedGroups: 0, skippedTabs: skipped.length, closedSource: false },
@@ -325,6 +340,7 @@ async function mergeWindow(params, deps) {
325
340
  tabs: [],
326
341
  },
327
342
  };
343
+ return fullResult;
328
344
  }
329
345
  const orderedTabs = [...selectedTabs].sort((a, b) => {
330
346
  const aIndex = Number(a.index);
@@ -426,7 +442,7 @@ async function mergeWindow(params, deps) {
426
442
  deps.log("Failed to close source window", error);
427
443
  }
428
444
  }
429
- return {
445
+ const fullResult = {
430
446
  fromWindowId,
431
447
  toWindowId,
432
448
  summary: { movedTabs, movedGroups, skippedTabs: skipped.length, closedSource },
@@ -441,4 +457,13 @@ async function mergeWindow(params, deps) {
441
457
  },
442
458
  txid: params.txid || null,
443
459
  };
460
+ return {
461
+ fromWindowId,
462
+ toWindowId,
463
+ summary: fullResult.summary,
464
+ skipped: fullResult.skipped,
465
+ groups: fullResult.groups,
466
+ undo: fullResult.undo,
467
+ txid: fullResult.txid,
468
+ };
444
469
  }
@@ -211,7 +211,7 @@ async function groupUpdate(params, deps) {
211
211
  throw new Error("Missing group update fields");
212
212
  }
213
213
  const updated = await chrome.tabGroups.update(match.group.groupId, update);
214
- return {
214
+ const fullResult = {
215
215
  groupId: updated.id,
216
216
  windowId: updated.windowId,
217
217
  title: updated.title,
@@ -229,6 +229,13 @@ async function groupUpdate(params, deps) {
229
229
  },
230
230
  txid: params.txid || null,
231
231
  };
232
+ return {
233
+ groupId: updated.id,
234
+ windowId: updated.windowId,
235
+ summary: { updatedGroups: 1 },
236
+ undo: fullResult.undo,
237
+ txid: fullResult.txid,
238
+ };
232
239
  }
233
240
  async function groupUngroup(params, deps) {
234
241
  const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
@@ -277,7 +284,7 @@ async function groupUngroup(params, deps) {
277
284
  if (tabIds.length) {
278
285
  await chrome.tabs.ungroup(tabIds);
279
286
  }
280
- return {
287
+ const fullResult = {
281
288
  groupId: match.group.groupId,
282
289
  groupTitle: match.group.title || null,
283
290
  windowId: match.windowId,
@@ -295,6 +302,13 @@ async function groupUngroup(params, deps) {
295
302
  },
296
303
  txid: params.txid || null,
297
304
  };
305
+ return {
306
+ groupId: match.group.groupId,
307
+ windowId: match.windowId,
308
+ summary: fullResult.summary,
309
+ undo: fullResult.undo,
310
+ txid: fullResult.txid,
311
+ };
298
312
  }
299
313
  async function groupAssign(params, deps) {
300
314
  const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
@@ -423,7 +437,7 @@ async function groupAssign(params, deps) {
423
437
  }
424
438
  created = true;
425
439
  }
426
- return {
440
+ const fullResult = {
427
441
  groupId: assignedGroupId,
428
442
  groupTitle: targetTitle || groupTitle || null,
429
443
  windowId: targetWindowId,
@@ -445,6 +459,15 @@ async function groupAssign(params, deps) {
445
459
  },
446
460
  txid: params.txid || null,
447
461
  };
462
+ return {
463
+ groupId: assignedGroupId,
464
+ windowId: targetWindowId,
465
+ created,
466
+ summary: fullResult.summary,
467
+ skipped: fullResult.skipped,
468
+ undo: fullResult.undo,
469
+ txid: fullResult.txid,
470
+ };
448
471
  }
449
472
  async function groupGather(params, deps) {
450
473
  const snapshot = await deps.getTabSnapshot();
@@ -514,7 +537,7 @@ async function groupGather(params, deps) {
514
537
  });
515
538
  }
516
539
  }
517
- return {
540
+ const fullResult = {
518
541
  merged,
519
542
  summary: {
520
543
  mergedGroups: merged.reduce((sum, m) => sum + m.mergedGroupCount, 0),
@@ -526,4 +549,10 @@ async function groupGather(params, deps) {
526
549
  },
527
550
  txid: params.txid || null,
528
551
  };
552
+ return {
553
+ merged,
554
+ summary: fullResult.summary,
555
+ undo: fullResult.undo,
556
+ txid: fullResult.txid,
557
+ };
529
558
  }
@@ -22,7 +22,6 @@ async function analyzeTabs(params, requestId, deps) {
22
22
  const selectedTabs = selection.tabs;
23
23
  const scopeTabs = selectedTabs;
24
24
  const now = Date.now();
25
- const startedAt = Date.now();
26
25
  const normalizedMap = new Map();
27
26
  const duplicates = new Map();
28
27
  for (const tab of scopeTabs) {
@@ -78,19 +77,16 @@ async function analyzeTabs(params, requestId, deps) {
78
77
  severity,
79
78
  };
80
79
  });
81
- return {
82
- generatedAt: Date.now(),
80
+ const response = {
83
81
  staleDays,
84
82
  totals: {
85
83
  tabs: scopeTabs.length,
86
84
  analyzed: selectedTabs.length,
87
85
  candidates: candidates.length,
88
86
  },
89
- meta: {
90
- durationMs: Date.now() - startedAt,
91
- },
92
87
  candidates,
93
88
  };
89
+ return response;
94
90
  }
95
91
  async function inspectTabs(params, requestId, deps) {
96
92
  const signalList = Array.isArray(params.signals) && params.signals.length > 0
@@ -120,7 +116,6 @@ async function inspectTabs(params, requestId, deps) {
120
116
  throw selection.error;
121
117
  }
122
118
  const tabs = selection.tabs;
123
- const startedAt = Date.now();
124
119
  const selectorSpecs = [];
125
120
  if (Array.isArray(params.selectorSpecs)) {
126
121
  selectorSpecs.push(...params.selectorSpecs);
@@ -140,19 +135,26 @@ async function inspectTabs(params, requestId, deps) {
140
135
  }
141
136
  const normalizedSelectors = selectorSpecs
142
137
  .filter((spec) => spec && typeof spec.selector === "string" && spec.selector.length > 0)
143
- .map((spec) => ({
144
- name: typeof spec.name === "string" ? spec.name : undefined,
145
- selector: spec.selector,
146
- attr: typeof spec.attr === "string" ? spec.attr : "text",
147
- all: Boolean(spec.all),
148
- text: typeof spec.text === "string" && spec.text.trim() ? spec.text.trim() : undefined,
149
- textMode: typeof spec.textMode === "string" ? spec.textMode.trim().toLowerCase() : undefined,
150
- }));
138
+ .map((spec) => {
139
+ const selector = spec.selector;
140
+ return {
141
+ name: typeof spec.name === "string" ? spec.name : undefined,
142
+ selector,
143
+ attr: typeof spec.attr === "string" ? spec.attr : "text",
144
+ all: Boolean(spec.all),
145
+ text: typeof spec.text === "string" && spec.text.trim() ? spec.text.trim() : undefined,
146
+ textMode: typeof spec.textMode === "string" ? spec.textMode.trim().toLowerCase() : undefined,
147
+ };
148
+ });
151
149
  const selectorWarnings = normalizedSelectors
152
- .filter((spec) => typeof spec.selector === "string" && spec.selector.includes(":contains("))
150
+ .filter((spec) => spec.selector.includes(":contains("))
153
151
  .map((spec) => ({
152
+ code: "unsupported_selector_syntax",
153
+ signalId: "selector",
154
+ selector: spec.selector,
154
155
  name: spec.name || spec.selector,
155
- hint: "CSS :contains() is not supported; use selector text filters or a different selector.",
156
+ message: "Selector uses unsupported CSS :contains() syntax.",
157
+ hint: "Use selector text filters (text/textMode) or a different selector.",
156
158
  }));
157
159
  const signalDefs = [];
158
160
  for (const signalId of signalList) {
@@ -195,20 +197,23 @@ async function inspectTabs(params, requestId, deps) {
195
197
  const tabId = task.tab.tabId;
196
198
  let result = null;
197
199
  let error = null;
198
- const started = Date.now();
199
200
  try {
200
201
  await waitForTabReady(tabId, params, signalTimeoutMs);
201
202
  result = await task.signal.run(tabId);
203
+ if (task.signal.id === "selector" && result && typeof result === "object") {
204
+ const selectorErrors = Object.keys(result.errors || {});
205
+ if (selectorErrors.length > 0) {
206
+ error = `selector failures: ${selectorErrors.join(", ")}`;
207
+ }
208
+ }
202
209
  }
203
210
  catch (err) {
204
211
  const message = err instanceof Error ? err.message : "signal_error";
205
212
  error = message;
206
213
  }
207
- const durationMs = Date.now() - started;
208
214
  const entry = entryMap.get(tabId) || { tab: task.tab, signals: {} };
209
215
  entry.signals[task.signal.id] = {
210
216
  ok: error === null,
211
- durationMs,
212
217
  data: result,
213
218
  error,
214
219
  };
@@ -234,19 +239,16 @@ async function inspectTabs(params, requestId, deps) {
234
239
  title: entry.tab.title,
235
240
  signals: entry.signals,
236
241
  }));
237
- return {
238
- generatedAt: Date.now(),
242
+ const response = {
239
243
  totals: {
240
244
  tabs: tabs.length,
241
245
  signals: signalDefs.length,
242
246
  tasks: totalTasks,
243
247
  },
244
- meta: {
245
- durationMs: Date.now() - startedAt,
246
- signalTimeoutMs,
247
- selectorCount: normalizedSelectors.length,
248
- selectorWarnings: selectorWarnings.length > 0 ? selectorWarnings : undefined,
249
- },
250
248
  entries,
251
249
  };
250
+ if (selectorWarnings.length > 0) {
251
+ response.warnings = selectorWarnings;
252
+ }
253
+ return response;
252
254
  }
@@ -121,7 +121,7 @@ async function moveTab(params, deps) {
121
121
  targetIndex = 0;
122
122
  }
123
123
  }
124
- return {
124
+ const fullResult = {
125
125
  tabId,
126
126
  from: { windowId: sourceWindow.windowId, index: sourceTab.index },
127
127
  to: { windowId: targetWindowId, index: targetIndex },
@@ -144,6 +144,14 @@ async function moveTab(params, deps) {
144
144
  },
145
145
  txid: params.txid || null,
146
146
  };
147
+ return {
148
+ tabId,
149
+ fromWindowId: sourceWindow.windowId,
150
+ toWindowId: targetWindowId,
151
+ summary: fullResult.summary,
152
+ undo: fullResult.undo,
153
+ txid: fullResult.txid,
154
+ };
147
155
  }
148
156
  let normalizedParams = params;
149
157
  if (params.windowId != null) {
@@ -161,7 +169,7 @@ async function moveTab(params, deps) {
161
169
  targetIndex -= 1;
162
170
  }
163
171
  const moved = await chrome.tabs.move(tabId, { windowId: targetWindowId, index: targetIndex });
164
- return {
172
+ const fullResult = {
165
173
  tabId,
166
174
  from: { windowId: sourceWindow.windowId, index: sourceTab.index },
167
175
  to: { windowId: targetWindowId, index: moved.index },
@@ -184,6 +192,14 @@ async function moveTab(params, deps) {
184
192
  },
185
193
  txid: params.txid || null,
186
194
  };
195
+ return {
196
+ tabId,
197
+ fromWindowId: sourceWindow.windowId,
198
+ toWindowId: targetWindowId,
199
+ summary: fullResult.summary,
200
+ undo: fullResult.undo,
201
+ txid: fullResult.txid,
202
+ };
187
203
  }
188
204
  async function moveGroup(params, deps) {
189
205
  const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
@@ -203,6 +219,31 @@ async function moveGroup(params, deps) {
203
219
  if (!source.tabs.length) {
204
220
  throw new Error("Group has no tabs to move");
205
221
  }
222
+ const ensureMovedTabsAreGrouped = async (movedTabIds, targetWindowId, targetGroupId) => {
223
+ if (!targetGroupId || movedTabIds.length === 0) {
224
+ return;
225
+ }
226
+ const movedSet = new Set(movedTabIds);
227
+ const verify = async (step) => {
228
+ const tabs = await chrome.tabs.query({ windowId: targetWindowId });
229
+ const missingGroupTabIds = tabs
230
+ .filter((tab) => typeof tab.id === "number" && movedSet.has(tab.id) && tab.groupId !== targetGroupId)
231
+ .map((tab) => tab.id);
232
+ if (missingGroupTabIds.length > 0) {
233
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: missingGroupTabIds });
234
+ }
235
+ };
236
+ try {
237
+ await verify("group-verify");
238
+ await new Promise((resolve) => setTimeout(resolve, 250));
239
+ await verify("group-verify-delayed");
240
+ await new Promise((resolve) => setTimeout(resolve, 1500));
241
+ await verify("group-verify-delayed-late");
242
+ }
243
+ catch (error) {
244
+ deps.log("Failed to enforce moved group integrity", error);
245
+ }
246
+ };
206
247
  const newWindow = params.newWindow === true;
207
248
  const hasTarget = Number.isFinite(params.beforeTabId)
208
249
  || Number.isFinite(params.afterTabId)
@@ -234,6 +275,7 @@ async function moveGroup(params, deps) {
234
275
  catch (error) {
235
276
  deps.log("Failed to regroup tabs", error);
236
277
  }
278
+ await ensureMovedTabsAreGrouped(tabIds, targetWindowId, newGroupId);
237
279
  const undoTabs = source.tabs
238
280
  .map((tab) => ({
239
281
  tabId: tab.tabId,
@@ -245,7 +287,7 @@ async function moveGroup(params, deps) {
245
287
  groupCollapsed: source.group.collapsed ?? null,
246
288
  }))
247
289
  .filter((tab) => typeof tab.tabId === "number");
248
- return {
290
+ const fullResult = {
249
291
  groupId: source.group.groupId,
250
292
  windowId: source.windowId,
251
293
  movedToWindowId: targetWindowId,
@@ -263,6 +305,15 @@ async function moveGroup(params, deps) {
263
305
  },
264
306
  txid: params.txid || null,
265
307
  };
308
+ return {
309
+ groupId: source.group.groupId,
310
+ windowId: source.windowId,
311
+ movedToWindowId: targetWindowId,
312
+ newGroupId,
313
+ summary: fullResult.summary,
314
+ undo: fullResult.undo,
315
+ txid: fullResult.txid,
316
+ };
266
317
  }
267
318
  const target = resolveMoveTarget(snapshot, params, deps);
268
319
  if (target.error) {
@@ -310,6 +361,7 @@ async function moveGroup(params, deps) {
310
361
  deps.log("Failed to regroup tabs", error);
311
362
  }
312
363
  }
364
+ await ensureMovedTabsAreGrouped(tabIds, targetWindowId, targetWindowId === source.windowId ? source.group.groupId : newGroupId);
313
365
  const undoTabs = source.tabs
314
366
  .map((tab) => ({
315
367
  tabId: tab.tabId,
@@ -321,7 +373,7 @@ async function moveGroup(params, deps) {
321
373
  groupCollapsed: source.group.collapsed ?? null,
322
374
  }))
323
375
  .filter((tab) => typeof tab.tabId === "number");
324
- return {
376
+ const fullResult = {
325
377
  groupId: source.group.groupId,
326
378
  windowId: source.windowId,
327
379
  movedToWindowId: targetWindowId,
@@ -339,4 +391,13 @@ async function moveGroup(params, deps) {
339
391
  },
340
392
  txid: params.txid || null,
341
393
  };
394
+ return {
395
+ groupId: source.group.groupId,
396
+ windowId: source.windowId,
397
+ movedToWindowId: targetWindowId,
398
+ newGroupId,
399
+ summary: fullResult.summary,
400
+ undo: fullResult.undo,
401
+ txid: fullResult.txid,
402
+ };
342
403
  }
@@ -293,7 +293,6 @@ async function screenshotTabs(params, requestId, deps) {
293
293
  const tabs = selection.tabs;
294
294
  const entries = [];
295
295
  let totalTiles = 0;
296
- const startedAt = Date.now();
297
296
  for (let index = 0; index < tabs.length; index += 1) {
298
297
  const tab = tabs[index];
299
298
  const tabId = tab.tabId;
@@ -351,17 +350,9 @@ async function screenshotTabs(params, requestId, deps) {
351
350
  deps.sendProgress(requestId, { phase: "screenshot", processed: index + 1, total: tabs.length, tabId });
352
351
  }
353
352
  }
354
- return {
355
- generatedAt: Date.now(),
353
+ const response = {
356
354
  totals: { tabs: tabs.length, tiles: totalTiles },
357
- meta: {
358
- durationMs: Date.now() - startedAt,
359
- mode,
360
- format,
361
- quality: format === "jpeg" ? quality : null,
362
- tileMaxDim: adjustedTileMaxDim,
363
- maxBytes: adjustedMaxBytes,
364
- },
365
355
  entries,
366
356
  };
357
+ return response;
367
358
  }
@@ -58,6 +58,17 @@ function normalizeTabIndex(value) {
58
58
  const index = Number(value);
59
59
  return Number.isFinite(index) ? index : null;
60
60
  }
61
+ function shapeOpenResult(result) {
62
+ return {
63
+ windowId: result.windowId,
64
+ groupId: result.groupId,
65
+ createdTabIds: result.created
66
+ .map((tab) => tab.tabId)
67
+ .filter((id) => typeof id === "number"),
68
+ skipped: result.skipped,
69
+ summary: result.summary,
70
+ };
71
+ }
61
72
  function matchIncludes(value, needle) {
62
73
  if (!needle) {
63
74
  return false;
@@ -260,12 +271,9 @@ async function openTabs(params, deps) {
260
271
  groupId = null;
261
272
  }
262
273
  }
263
- return {
274
+ return shapeOpenResult({
264
275
  windowId,
265
276
  groupId,
266
- groupTitle: groupTitle || null,
267
- afterGroupTitle: null,
268
- insertIndex: null,
269
277
  created,
270
278
  skipped,
271
279
  summary: {
@@ -273,7 +281,7 @@ async function openTabs(params, deps) {
273
281
  skippedUrls: skipped.length,
274
282
  grouped: Boolean(groupId),
275
283
  },
276
- };
284
+ });
277
285
  }
278
286
  const snapshot = await deps.getTabSnapshot();
279
287
  let openParams = params;
@@ -393,6 +401,9 @@ async function openTabs(params, deps) {
393
401
  skipped.push({ url, reason: "create_failed" });
394
402
  }
395
403
  }
404
+ const createdTabIds = new Set(created
405
+ .map((tab) => tab.tabId)
406
+ .filter((id) => typeof id === "number"));
396
407
  let groupId = null;
397
408
  if (groupTitle && created.length > 0) {
398
409
  try {
@@ -421,30 +432,81 @@ async function openTabs(params, deps) {
421
432
  if (groupTitle && created.length === 0 && existingGroupId != null) {
422
433
  groupId = existingGroupId;
423
434
  }
424
- // Reorder: groups before ungrouped tabs
435
+ const targetGroupId = groupId ?? existingGroupId;
436
+ if (targetGroupId != null && created.length > 0) {
437
+ try {
438
+ if (createdTabIds.size > 0) {
439
+ const latestTabs = await chrome.tabs.query({ windowId });
440
+ const missingGroupTabIds = latestTabs
441
+ .filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId)
442
+ .map((tab) => tab.id);
443
+ if (missingGroupTabIds.length > 0) {
444
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: missingGroupTabIds });
445
+ }
446
+ }
447
+ }
448
+ catch (error) {
449
+ deps.log("Failed to enforce grouping for newly opened tabs", error);
450
+ }
451
+ }
425
452
  try {
426
453
  const freshTabs = await chrome.tabs.query({ windowId });
427
454
  freshTabs.sort((a, b) => a.index - b.index);
428
455
  const firstUngroupedIndex = freshTabs.findIndex(t => (t.groupId ?? -1) === -1);
429
456
  if (firstUngroupedIndex >= 0) {
430
- const groupedAfterUngrouped = freshTabs.filter((t, i) => i > firstUngroupedIndex && (t.groupId ?? -1) !== -1);
431
- if (groupedAfterUngrouped.length > 0) {
432
- const tabIdsToMove = groupedAfterUngrouped.map(t => t.id).filter(id => typeof id === "number");
433
- if (tabIdsToMove.length > 0) {
434
- await chrome.tabs.move(tabIdsToMove, { index: firstUngroupedIndex });
435
- }
457
+ let tabIdsToMove = [];
458
+ if (existingGroupId != null) {
459
+ // In reuse mode, only reorder tabs created by this operation.
460
+ tabIdsToMove = freshTabs
461
+ .filter((tab, i) => i > firstUngroupedIndex && typeof tab.id === "number" && createdTabIds.has(tab.id) && (tab.groupId ?? -1) !== -1)
462
+ .map((tab) => tab.id);
463
+ }
464
+ else {
465
+ tabIdsToMove = freshTabs
466
+ .filter((tab, i) => i > firstUngroupedIndex && (tab.groupId ?? -1) !== -1)
467
+ .map((tab) => tab.id)
468
+ .filter((id) => typeof id === "number");
469
+ }
470
+ if (tabIdsToMove.length > 0) {
471
+ await chrome.tabs.move(tabIdsToMove, { index: firstUngroupedIndex });
436
472
  }
437
473
  }
438
474
  }
439
475
  catch (err) {
440
476
  deps.log("Failed to reorder groups before ungrouped tabs", err);
441
477
  }
442
- return {
478
+ if (targetGroupId != null && createdTabIds.size > 0) {
479
+ try {
480
+ const latestTabs = await chrome.tabs.query({ windowId });
481
+ const lateUngroupedTabIds = latestTabs
482
+ .filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId)
483
+ .map((tab) => tab.id);
484
+ if (lateUngroupedTabIds.length > 0) {
485
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: lateUngroupedTabIds });
486
+ }
487
+ }
488
+ catch (error) {
489
+ deps.log("Failed post-reorder grouping verification", error);
490
+ }
491
+ }
492
+ if (targetGroupId != null && createdTabIds.size > 0) {
493
+ try {
494
+ await deps.delay(250);
495
+ const delayedTabs = await chrome.tabs.query({ windowId });
496
+ const delayedUngroupedTabIds = delayedTabs
497
+ .filter((tab) => typeof tab.id === "number" && createdTabIds.has(tab.id) && tab.groupId !== targetGroupId)
498
+ .map((tab) => tab.id);
499
+ if (delayedUngroupedTabIds.length > 0) {
500
+ await chrome.tabs.group({ groupId: targetGroupId, tabIds: delayedUngroupedTabIds });
501
+ }
502
+ }
503
+ catch (error) {
504
+ deps.log("Failed delayed grouping verification", error);
505
+ }
506
+ }
507
+ return shapeOpenResult({
443
508
  windowId,
444
509
  groupId,
445
- groupTitle: groupTitle || null,
446
- afterGroupTitle: afterGroupTitle || null,
447
- insertIndex,
448
510
  created,
449
511
  skipped,
450
512
  summary: {
@@ -452,5 +514,5 @@ async function openTabs(params, deps) {
452
514
  skippedUrls: skipped.length,
453
515
  grouped: Boolean(groupId),
454
516
  },
455
- };
517
+ });
456
518
  }
@@ -19,5 +19,5 @@
19
19
  "background": {
20
20
  "service_worker": "background.js"
21
21
  },
22
- "version_name": "0.6.0-alpha.6"
22
+ "version_name": "0.6.0-alpha.8"
23
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabctl",
3
- "version": "0.6.0-alpha.6",
3
+ "version": "0.6.0-alpha.8",
4
4
  "description": "CLI tool to manage and analyze browser tabs",
5
5
  "license": "MIT",
6
6
  "repository": {