tabctl 0.6.0-alpha.7 → 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;
@@ -1855,12 +1881,9 @@
1855
1881
  deps2.log("Failed delayed grouping verification", error);
1856
1882
  }
1857
1883
  }
1858
- return {
1884
+ return shapeOpenResult({
1859
1885
  windowId,
1860
1886
  groupId,
1861
- groupTitle: groupTitle || null,
1862
- afterGroupTitle: afterGroupTitle || null,
1863
- insertIndex,
1864
1887
  created,
1865
1888
  skipped,
1866
1889
  summary: {
@@ -1868,7 +1891,7 @@
1868
1891
  skippedUrls: skipped.length,
1869
1892
  grouped: Boolean(groupId)
1870
1893
  }
1871
- };
1894
+ });
1872
1895
  }
1873
1896
  }
1874
1897
  });
@@ -1987,7 +2010,7 @@
1987
2010
  targetIndex2 = 0;
1988
2011
  }
1989
2012
  }
1990
- return {
2013
+ const fullResult2 = {
1991
2014
  tabId,
1992
2015
  from: { windowId: sourceWindow.windowId, index: sourceTab.index },
1993
2016
  to: { windowId: targetWindowId2, index: targetIndex2 },
@@ -2010,6 +2033,14 @@
2010
2033
  },
2011
2034
  txid: params.txid || null
2012
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
+ };
2013
2044
  }
2014
2045
  let normalizedParams = params;
2015
2046
  if (params.windowId != null) {
@@ -2027,7 +2058,7 @@
2027
2058
  targetIndex -= 1;
2028
2059
  }
2029
2060
  const moved = await chrome.tabs.move(tabId, { windowId: targetWindowId, index: targetIndex });
2030
- return {
2061
+ const fullResult = {
2031
2062
  tabId,
2032
2063
  from: { windowId: sourceWindow.windowId, index: sourceTab.index },
2033
2064
  to: { windowId: targetWindowId, index: moved.index },
@@ -2050,6 +2081,14 @@
2050
2081
  },
2051
2082
  txid: params.txid || null
2052
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
+ };
2053
2092
  }
2054
2093
  async function moveGroup(params, deps2) {
2055
2094
  const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
@@ -2126,7 +2165,7 @@
2126
2165
  groupColor: tab.groupColor,
2127
2166
  groupCollapsed: source.group.collapsed ?? null
2128
2167
  })).filter((tab) => typeof tab.tabId === "number");
2129
- return {
2168
+ const fullResult2 = {
2130
2169
  groupId: source.group.groupId,
2131
2170
  windowId: source.windowId,
2132
2171
  movedToWindowId: targetWindowId2,
@@ -2144,6 +2183,15 @@
2144
2183
  },
2145
2184
  txid: params.txid || null
2146
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
+ };
2147
2195
  }
2148
2196
  const target = resolveMoveTarget(snapshot, params, deps2);
2149
2197
  if (target.error) {
@@ -2198,7 +2246,7 @@
2198
2246
  groupColor: tab.groupColor,
2199
2247
  groupCollapsed: source.group.collapsed ?? null
2200
2248
  })).filter((tab) => typeof tab.tabId === "number");
2201
- return {
2249
+ const fullResult = {
2202
2250
  groupId: source.group.groupId,
2203
2251
  windowId: source.windowId,
2204
2252
  movedToWindowId: targetWindowId,
@@ -2216,6 +2264,15 @@
2216
2264
  },
2217
2265
  txid: params.txid || null
2218
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
+ };
2219
2276
  }
2220
2277
  }
2221
2278
  });
@@ -2246,7 +2303,6 @@
2246
2303
  const selectedTabs = selection.tabs;
2247
2304
  const scopeTabs = selectedTabs;
2248
2305
  const now = Date.now();
2249
- const startedAt = Date.now();
2250
2306
  const normalizedMap = /* @__PURE__ */ new Map();
2251
2307
  const duplicates = /* @__PURE__ */ new Map();
2252
2308
  for (const tab of scopeTabs) {
@@ -2299,19 +2355,16 @@
2299
2355
  severity
2300
2356
  };
2301
2357
  });
2302
- return {
2303
- generatedAt: Date.now(),
2358
+ const response = {
2304
2359
  staleDays,
2305
2360
  totals: {
2306
2361
  tabs: scopeTabs.length,
2307
2362
  analyzed: selectedTabs.length,
2308
2363
  candidates: candidates.length
2309
2364
  },
2310
- meta: {
2311
- durationMs: Date.now() - startedAt
2312
- },
2313
2365
  candidates
2314
2366
  };
2367
+ return response;
2315
2368
  }
2316
2369
  async function inspectTabs(params, requestId, deps2) {
2317
2370
  const signalList = Array.isArray(params.signals) && params.signals.length > 0 ? params.signals.map(String) : ["page-meta"];
@@ -2333,7 +2386,6 @@
2333
2386
  throw selection.error;
2334
2387
  }
2335
2388
  const tabs3 = selection.tabs;
2336
- const startedAt = Date.now();
2337
2389
  const selectorSpecs = [];
2338
2390
  if (Array.isArray(params.selectorSpecs)) {
2339
2391
  selectorSpecs.push(...params.selectorSpecs);
@@ -2351,17 +2403,24 @@
2351
2403
  }
2352
2404
  }
2353
2405
  }
2354
- const normalizedSelectors = selectorSpecs.filter((spec) => spec && typeof spec.selector === "string" && spec.selector.length > 0).map((spec) => ({
2355
- 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",
2356
2420
  selector: spec.selector,
2357
- attr: typeof spec.attr === "string" ? spec.attr : "text",
2358
- all: Boolean(spec.all),
2359
- text: typeof spec.text === "string" && spec.text.trim() ? spec.text.trim() : void 0,
2360
- textMode: typeof spec.textMode === "string" ? spec.textMode.trim().toLowerCase() : void 0
2361
- }));
2362
- const selectorWarnings = normalizedSelectors.filter((spec) => typeof spec.selector === "string" && spec.selector.includes(":contains(")).map((spec) => ({
2363
2421
  name: spec.name || spec.selector,
2364
- 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."
2365
2424
  }));
2366
2425
  const signalDefs = [];
2367
2426
  for (const signalId of signalList) {
@@ -2403,19 +2462,22 @@
2403
2462
  const tabId = task.tab.tabId;
2404
2463
  let result = null;
2405
2464
  let error = null;
2406
- const started = Date.now();
2407
2465
  try {
2408
2466
  await waitForTabReady2(tabId, params, signalTimeoutMs);
2409
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
+ }
2410
2474
  } catch (err) {
2411
2475
  const message = err instanceof Error ? err.message : "signal_error";
2412
2476
  error = message;
2413
2477
  }
2414
- const durationMs = Date.now() - started;
2415
2478
  const entry = entryMap.get(tabId) || { tab: task.tab, signals: {} };
2416
2479
  entry.signals[task.signal.id] = {
2417
2480
  ok: error === null,
2418
- durationMs,
2419
2481
  data: result,
2420
2482
  error
2421
2483
  };
@@ -2441,21 +2503,18 @@
2441
2503
  title: entry.tab.title,
2442
2504
  signals: entry.signals
2443
2505
  }));
2444
- return {
2445
- generatedAt: Date.now(),
2506
+ const response = {
2446
2507
  totals: {
2447
2508
  tabs: tabs3.length,
2448
2509
  signals: signalDefs.length,
2449
2510
  tasks: totalTasks
2450
2511
  },
2451
- meta: {
2452
- durationMs: Date.now() - startedAt,
2453
- signalTimeoutMs,
2454
- selectorCount: normalizedSelectors.length,
2455
- selectorWarnings: selectorWarnings.length > 0 ? selectorWarnings : void 0
2456
- },
2457
2512
  entries
2458
2513
  };
2514
+ if (selectorWarnings.length > 0) {
2515
+ response.warnings = selectorWarnings;
2516
+ }
2517
+ return response;
2459
2518
  }
2460
2519
  }
2461
2520
  });
@@ -2935,13 +2994,14 @@
2935
2994
  })).filter((win) => win.tabs.length > 0);
2936
2995
  }
2937
2996
  if (windowsToProcess.length === 0) {
2938
- return {
2997
+ const fullResult2 = {
2939
2998
  txid: params.txid || null,
2940
2999
  summary: { movedTabs: 0, movedGroups: 0, skippedTabs: 0 },
2941
3000
  archiveWindowId: null,
2942
3001
  skipped: [],
2943
3002
  undo: { action: "archive", tabs: [] }
2944
3003
  };
3004
+ return fullResult2;
2945
3005
  }
2946
3006
  const archiveWindowId = await ensureArchiveWindow(deps2);
2947
3007
  const undoTabs = [];
@@ -3044,7 +3104,7 @@
3044
3104
  }
3045
3105
  }
3046
3106
  }
3047
- return {
3107
+ const fullResult = {
3048
3108
  txid: params.txid || null,
3049
3109
  summary: {
3050
3110
  movedTabs,
@@ -3058,6 +3118,13 @@
3058
3118
  tabs: undoTabs
3059
3119
  }
3060
3120
  };
3121
+ return {
3122
+ txid: fullResult.txid,
3123
+ summary: fullResult.summary,
3124
+ archiveWindowId,
3125
+ skipped: fullResult.skipped,
3126
+ undo: fullResult.undo
3127
+ };
3061
3128
  }
3062
3129
  async function getTabsByIds(tabIds) {
3063
3130
  const results = [];
@@ -3086,12 +3153,13 @@
3086
3153
  tabIds = selection.tabs.map((tab) => tab.tabId);
3087
3154
  }
3088
3155
  if (!tabIds.length) {
3089
- return {
3156
+ const fullResult2 = {
3090
3157
  txid: params.txid || null,
3091
3158
  summary: { closedTabs: 0, skippedTabs: 0 },
3092
3159
  skipped: [],
3093
3160
  undo: { action: "close", tabs: [] }
3094
3161
  };
3162
+ return fullResult2;
3095
3163
  }
3096
3164
  const expectedUrls = params.expectedUrls || {};
3097
3165
  const tabInfos = await getTabsByIds(tabIds);
@@ -3131,7 +3199,7 @@
3131
3199
  if (validTabs.length > 0) {
3132
3200
  await chrome.tabs.remove(validTabs.map((tab) => tab.tabId));
3133
3201
  }
3134
- return {
3202
+ const fullResult = {
3135
3203
  txid: params.txid || null,
3136
3204
  summary: {
3137
3205
  closedTabs: validTabs.length,
@@ -3149,6 +3217,12 @@
3149
3217
  }))
3150
3218
  }
3151
3219
  };
3220
+ return {
3221
+ txid: fullResult.txid,
3222
+ summary: fullResult.summary,
3223
+ skipped: fullResult.skipped,
3224
+ undo: fullResult.undo
3225
+ };
3152
3226
  }
3153
3227
  async function mergeWindow(params, deps2) {
3154
3228
  const fromWindowId = Number.isFinite(params.fromWindowId) ? Number(params.fromWindowId) : Number(params.windowId);
@@ -3183,7 +3257,7 @@
3183
3257
  selectedTabs = sourceWindow.tabs.filter((tab) => tabIdSet.has(tab.tabId));
3184
3258
  }
3185
3259
  if (selectedTabs.length === 0) {
3186
- return {
3260
+ const fullResult2 = {
3187
3261
  fromWindowId,
3188
3262
  toWindowId,
3189
3263
  summary: { movedTabs: 0, movedGroups: 0, skippedTabs: skipped.length, closedSource: false },
@@ -3197,6 +3271,7 @@
3197
3271
  tabs: []
3198
3272
  }
3199
3273
  };
3274
+ return fullResult2;
3200
3275
  }
3201
3276
  const orderedTabs = [...selectedTabs].sort((a, b) => {
3202
3277
  const aIndex = Number(a.index);
@@ -3295,7 +3370,7 @@
3295
3370
  deps2.log("Failed to close source window", error);
3296
3371
  }
3297
3372
  }
3298
- return {
3373
+ const fullResult = {
3299
3374
  fromWindowId,
3300
3375
  toWindowId,
3301
3376
  summary: { movedTabs, movedGroups, skippedTabs: skipped.length, closedSource },
@@ -3310,6 +3385,15 @@
3310
3385
  },
3311
3386
  txid: params.txid || null
3312
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
+ };
3313
3397
  }
3314
3398
  }
3315
3399
  });
@@ -3489,7 +3573,7 @@
3489
3573
  component: "extension"
3490
3574
  };
3491
3575
  case "list":
3492
- return await getTabSnapshot();
3576
+ return shapeListSnapshot(await getTabSnapshot());
3493
3577
  case "analyze":
3494
3578
  return await inspect.analyzeTabs(params, requestId, deps);
3495
3579
  case "inspect":
@@ -3533,6 +3617,29 @@
3533
3617
  throw new Error(`Unknown action: ${action}`);
3534
3618
  }
3535
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
+ }
3536
3643
  function resolveWindowIdFromParams(snapshot, value) {
3537
3644
  if (typeof value === "number" && Number.isFinite(value)) {
3538
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;
@@ -271,7 +287,7 @@ async function moveGroup(params, deps) {
271
287
  groupCollapsed: source.group.collapsed ?? null,
272
288
  }))
273
289
  .filter((tab) => typeof tab.tabId === "number");
274
- return {
290
+ const fullResult = {
275
291
  groupId: source.group.groupId,
276
292
  windowId: source.windowId,
277
293
  movedToWindowId: targetWindowId,
@@ -289,6 +305,15 @@ async function moveGroup(params, deps) {
289
305
  },
290
306
  txid: params.txid || null,
291
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
+ };
292
317
  }
293
318
  const target = resolveMoveTarget(snapshot, params, deps);
294
319
  if (target.error) {
@@ -348,7 +373,7 @@ async function moveGroup(params, deps) {
348
373
  groupCollapsed: source.group.collapsed ?? null,
349
374
  }))
350
375
  .filter((tab) => typeof tab.tabId === "number");
351
- return {
376
+ const fullResult = {
352
377
  groupId: source.group.groupId,
353
378
  windowId: source.windowId,
354
379
  movedToWindowId: targetWindowId,
@@ -366,4 +391,13 @@ async function moveGroup(params, deps) {
366
391
  },
367
392
  txid: params.txid || null,
368
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
+ };
369
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;
@@ -496,12 +504,9 @@ async function openTabs(params, deps) {
496
504
  deps.log("Failed delayed grouping verification", error);
497
505
  }
498
506
  }
499
- return {
507
+ return shapeOpenResult({
500
508
  windowId,
501
509
  groupId,
502
- groupTitle: groupTitle || null,
503
- afterGroupTitle: afterGroupTitle || null,
504
- insertIndex,
505
510
  created,
506
511
  skipped,
507
512
  summary: {
@@ -509,5 +514,5 @@ async function openTabs(params, deps) {
509
514
  skippedUrls: skipped.length,
510
515
  grouped: Boolean(groupId),
511
516
  },
512
- };
517
+ });
513
518
  }
@@ -19,5 +19,5 @@
19
19
  "background": {
20
20
  "service_worker": "background.js"
21
21
  },
22
- "version_name": "0.6.0-alpha.7"
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.7",
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": {