tabctl 0.5.3 → 0.6.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +135 -35
  2. package/dist/extension/background.js +179 -3155
  3. package/dist/extension/lib/content.js +0 -115
  4. package/dist/extension/lib/screenshot.js +0 -93
  5. package/dist/extension/manifest.json +2 -2
  6. package/package.json +13 -5
  7. package/dist/cli/lib/args.js +0 -141
  8. package/dist/cli/lib/client.js +0 -83
  9. package/dist/cli/lib/commands/doctor.js +0 -134
  10. package/dist/cli/lib/commands/index.js +0 -51
  11. package/dist/cli/lib/commands/list.js +0 -159
  12. package/dist/cli/lib/commands/meta.js +0 -229
  13. package/dist/cli/lib/commands/params-groups.js +0 -48
  14. package/dist/cli/lib/commands/params-move.js +0 -44
  15. package/dist/cli/lib/commands/params.js +0 -314
  16. package/dist/cli/lib/commands/profile.js +0 -91
  17. package/dist/cli/lib/commands/setup.js +0 -294
  18. package/dist/cli/lib/constants.js +0 -30
  19. package/dist/cli/lib/help.js +0 -205
  20. package/dist/cli/lib/options-commands.js +0 -274
  21. package/dist/cli/lib/options-groups.js +0 -41
  22. package/dist/cli/lib/options.js +0 -125
  23. package/dist/cli/lib/output.js +0 -147
  24. package/dist/cli/lib/pagination.js +0 -55
  25. package/dist/cli/lib/policy-filter.js +0 -202
  26. package/dist/cli/lib/policy.js +0 -91
  27. package/dist/cli/lib/report.js +0 -61
  28. package/dist/cli/lib/response.js +0 -235
  29. package/dist/cli/lib/scope.js +0 -250
  30. package/dist/cli/lib/snapshot.js +0 -216
  31. package/dist/cli/lib/types.js +0 -2
  32. package/dist/cli/tabctl.js +0 -475
  33. package/dist/extension/lib/archive.js +0 -444
  34. package/dist/extension/lib/deps.js +0 -4
  35. package/dist/extension/lib/groups.js +0 -529
  36. package/dist/extension/lib/inspect.js +0 -252
  37. package/dist/extension/lib/move.js +0 -342
  38. package/dist/extension/lib/tabs.js +0 -456
  39. package/dist/extension/lib/undo-handlers.js +0 -447
  40. package/dist/host/host.bundle.js +0 -670
  41. package/dist/host/host.js +0 -143
  42. package/dist/host/host.sh +0 -5
  43. package/dist/host/launcher/go.mod +0 -3
  44. package/dist/host/launcher/main.go +0 -109
  45. package/dist/host/lib/handlers.js +0 -327
  46. package/dist/host/lib/undo.js +0 -60
  47. package/dist/shared/config.js +0 -134
  48. package/dist/shared/extension-sync.js +0 -170
  49. package/dist/shared/profiles.js +0 -78
  50. package/dist/shared/version.js +0 -8
  51. package/dist/shared/wrapper-health.js +0 -132
@@ -1,252 +0,0 @@
1
- "use strict";
2
- // Analysis/inspection pipeline — extracted from tabs.ts (pure structural refactor).
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.SELECTOR_VALUE_MAX_LENGTH = exports.DESCRIPTION_MAX_LENGTH = exports.DEFAULT_STALE_DAYS = void 0;
5
- exports.analyzeTabs = analyzeTabs;
6
- exports.inspectTabs = inspectTabs;
7
- const content = require("./content");
8
- const { isScriptableUrl, extractPageMeta, extractSelectorSignal, waitForSettle, waitForTabReady } = content;
9
- const tabs = require("./tabs");
10
- const { normalizeUrl } = tabs;
11
- exports.DEFAULT_STALE_DAYS = 30;
12
- exports.DESCRIPTION_MAX_LENGTH = 250;
13
- exports.SELECTOR_VALUE_MAX_LENGTH = 500;
14
- async function analyzeTabs(params, requestId, deps) {
15
- const staleDays = Number.isFinite(params.staleDays) ? params.staleDays : exports.DEFAULT_STALE_DAYS;
16
- const progressEnabled = params.progress === true;
17
- const snapshot = await deps.getTabSnapshot();
18
- const selection = deps.selectTabsByScope(snapshot, params);
19
- if (selection.error) {
20
- throw selection.error;
21
- }
22
- const selectedTabs = selection.tabs;
23
- const scopeTabs = selectedTabs;
24
- const now = Date.now();
25
- const startedAt = Date.now();
26
- const normalizedMap = new Map();
27
- const duplicates = new Map();
28
- for (const tab of scopeTabs) {
29
- const normalized = normalizeUrl(tab.url);
30
- if (!normalized) {
31
- continue;
32
- }
33
- if (normalizedMap.has(normalized)) {
34
- const existing = normalizedMap.get(normalized);
35
- duplicates.set(tab.tabId, existing.tabId);
36
- }
37
- else {
38
- normalizedMap.set(normalized, tab);
39
- }
40
- }
41
- const candidateMap = new Map();
42
- const addReason = (tab, reason) => {
43
- const tabId = tab.tabId;
44
- const entry = candidateMap.get(tabId) || { tab, reasons: [] };
45
- entry.reasons.push(reason);
46
- candidateMap.set(tabId, entry);
47
- };
48
- for (const tab of selectedTabs) {
49
- if (duplicates.has(tab.tabId)) {
50
- addReason(tab, {
51
- type: "duplicate",
52
- detail: `Matches tab ${duplicates.get(tab.tabId)}`,
53
- });
54
- }
55
- if (tab.lastFocusedAt) {
56
- const ageDays = (now - tab.lastFocusedAt) / (24 * 60 * 60 * 1000);
57
- if (ageDays >= staleDays) {
58
- addReason(tab, {
59
- type: "stale",
60
- detail: `Last focused ${Math.floor(ageDays)} days ago`,
61
- });
62
- }
63
- }
64
- }
65
- const candidates = Array.from(candidateMap.values()).map((entry) => {
66
- const reasons = entry.reasons;
67
- const severity = reasons.some((reason) => reason.type === "duplicate")
68
- ? "high"
69
- : "medium";
70
- return {
71
- tabId: entry.tab.tabId,
72
- windowId: entry.tab.windowId,
73
- groupId: entry.tab.groupId,
74
- url: entry.tab.url,
75
- title: entry.tab.title,
76
- lastFocusedAt: entry.tab.lastFocusedAt,
77
- reasons,
78
- severity,
79
- };
80
- });
81
- return {
82
- generatedAt: Date.now(),
83
- staleDays,
84
- totals: {
85
- tabs: scopeTabs.length,
86
- analyzed: selectedTabs.length,
87
- candidates: candidates.length,
88
- },
89
- meta: {
90
- durationMs: Date.now() - startedAt,
91
- },
92
- candidates,
93
- };
94
- }
95
- async function inspectTabs(params, requestId, deps) {
96
- const signalList = Array.isArray(params.signals) && params.signals.length > 0
97
- ? params.signals.map(String)
98
- : ["page-meta"];
99
- const signalConcurrencyRaw = Number(params.signalConcurrency);
100
- const signalConcurrency = Number.isFinite(signalConcurrencyRaw) && signalConcurrencyRaw > 0
101
- ? Math.min(10, Math.floor(signalConcurrencyRaw))
102
- : 4;
103
- const signalTimeoutRaw = Number(params.signalTimeoutMs);
104
- const signalTimeoutMs = Number.isFinite(signalTimeoutRaw) && signalTimeoutRaw > 0
105
- ? Math.floor(signalTimeoutRaw)
106
- : 4000;
107
- const progressEnabled = params.progress === true;
108
- // For settle mode with specific tab IDs, wait BEFORE taking snapshot
109
- // This ensures the tab URL is available for isScriptableUrl() checks
110
- const waitFor = typeof params.waitFor === "string" ? params.waitFor.trim().toLowerCase() : "";
111
- if (waitFor === "settle" && Array.isArray(params.tabIds) && params.tabIds.length > 0) {
112
- const waitTimeoutRaw = Number(params.waitTimeoutMs);
113
- const waitTimeoutMs = Number.isFinite(waitTimeoutRaw) && waitTimeoutRaw > 0 ? Math.floor(waitTimeoutRaw) : signalTimeoutMs;
114
- const tabIds = params.tabIds.map(Number).filter(Number.isFinite);
115
- await Promise.all(tabIds.map((id) => waitForSettle(id, waitTimeoutMs)));
116
- }
117
- const snapshot = await deps.getTabSnapshot();
118
- const selection = deps.selectTabsByScope(snapshot, params);
119
- if (selection.error) {
120
- throw selection.error;
121
- }
122
- const tabs = selection.tabs;
123
- const startedAt = Date.now();
124
- const selectorSpecs = [];
125
- if (Array.isArray(params.selectorSpecs)) {
126
- selectorSpecs.push(...params.selectorSpecs);
127
- }
128
- if (params.signalConfig && typeof params.signalConfig === "object") {
129
- const config = params.signalConfig;
130
- if (Array.isArray(config.selectors)) {
131
- selectorSpecs.push(...config.selectors);
132
- }
133
- if (config.signals && typeof config.signals === "object") {
134
- const signals = config.signals;
135
- const selectorConfig = signals.selector;
136
- if (selectorConfig && Array.isArray(selectorConfig.selectors)) {
137
- selectorSpecs.push(...selectorConfig.selectors);
138
- }
139
- }
140
- }
141
- const normalizedSelectors = selectorSpecs
142
- .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
- }));
151
- const selectorWarnings = normalizedSelectors
152
- .filter((spec) => typeof spec.selector === "string" && spec.selector.includes(":contains("))
153
- .map((spec) => ({
154
- name: spec.name || spec.selector,
155
- hint: "CSS :contains() is not supported; use selector text filters or a different selector.",
156
- }));
157
- const signalDefs = [];
158
- for (const signalId of signalList) {
159
- if (signalId === "page-meta") {
160
- signalDefs.push({
161
- id: signalId,
162
- match: (tab) => isScriptableUrl(tab.url),
163
- run: async (tabId) => extractPageMeta(tabId, signalTimeoutMs, exports.DESCRIPTION_MAX_LENGTH),
164
- });
165
- }
166
- else if (signalId === "selector") {
167
- signalDefs.push({
168
- id: signalId,
169
- match: (tab) => isScriptableUrl(tab.url),
170
- run: async (tabId) => extractSelectorSignal(tabId, normalizedSelectors, signalTimeoutMs, exports.SELECTOR_VALUE_MAX_LENGTH),
171
- });
172
- }
173
- }
174
- const tasks = [];
175
- for (const tab of tabs) {
176
- for (const signal of signalDefs) {
177
- if (signal.match(tab)) {
178
- tasks.push({ tab, signal });
179
- }
180
- }
181
- }
182
- const totalTasks = tasks.length;
183
- let completedTasks = 0;
184
- const entryMap = new Map();
185
- const workerCount = Math.min(signalConcurrency, totalTasks || 1);
186
- let index = 0;
187
- const workers = Array.from({ length: workerCount }, async () => {
188
- while (true) {
189
- const currentIndex = index;
190
- if (currentIndex >= totalTasks) {
191
- return;
192
- }
193
- index += 1;
194
- const task = tasks[currentIndex];
195
- const tabId = task.tab.tabId;
196
- let result = null;
197
- let error = null;
198
- const started = Date.now();
199
- try {
200
- await waitForTabReady(tabId, params, signalTimeoutMs);
201
- result = await task.signal.run(tabId);
202
- }
203
- catch (err) {
204
- const message = err instanceof Error ? err.message : "signal_error";
205
- error = message;
206
- }
207
- const durationMs = Date.now() - started;
208
- const entry = entryMap.get(tabId) || { tab: task.tab, signals: {} };
209
- entry.signals[task.signal.id] = {
210
- ok: error === null,
211
- durationMs,
212
- data: result,
213
- error,
214
- };
215
- entryMap.set(tabId, entry);
216
- completedTasks += 1;
217
- if (progressEnabled) {
218
- deps.sendProgress(requestId, {
219
- phase: "inspect",
220
- processed: completedTasks,
221
- total: totalTasks,
222
- signalId: task.signal.id,
223
- tabId,
224
- });
225
- }
226
- }
227
- });
228
- await Promise.all(workers);
229
- const entries = Array.from(entryMap.values()).map((entry) => ({
230
- tabId: entry.tab.tabId,
231
- windowId: entry.tab.windowId,
232
- groupId: entry.tab.groupId,
233
- url: entry.tab.url,
234
- title: entry.tab.title,
235
- signals: entry.signals,
236
- }));
237
- return {
238
- generatedAt: Date.now(),
239
- totals: {
240
- tabs: tabs.length,
241
- signals: signalDefs.length,
242
- tasks: totalTasks,
243
- },
244
- meta: {
245
- durationMs: Date.now() - startedAt,
246
- signalTimeoutMs,
247
- selectorCount: normalizedSelectors.length,
248
- selectorWarnings: selectorWarnings.length > 0 ? selectorWarnings : undefined,
249
- },
250
- entries,
251
- };
252
- }
@@ -1,342 +0,0 @@
1
- "use strict";
2
- // Tab/group movement — extracted from tabs.ts (pure structural refactor).
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.resolveMoveTarget = resolveMoveTarget;
5
- exports.moveTab = moveTab;
6
- exports.moveGroup = moveGroup;
7
- const tabs = require("./tabs");
8
- const { normalizeTabIndex } = tabs;
9
- function resolveMoveTarget(snapshot, params, deps) {
10
- const beforeTabId = Number(params.beforeTabId);
11
- const afterTabId = Number(params.afterTabId);
12
- const beforeGroupTitle = typeof params.beforeGroupTitle === "string" ? params.beforeGroupTitle.trim() : "";
13
- const afterGroupTitle = typeof params.afterGroupTitle === "string" ? params.afterGroupTitle.trim() : "";
14
- const targets = [
15
- Number.isFinite(beforeTabId) ? "before-tab" : null,
16
- Number.isFinite(afterTabId) ? "after-tab" : null,
17
- beforeGroupTitle ? "before-group" : null,
18
- afterGroupTitle ? "after-group" : null,
19
- ].filter(Boolean);
20
- if (targets.length === 0) {
21
- return { error: { message: "Missing target position (--before/--after)" } };
22
- }
23
- if (targets.length > 1) {
24
- return { error: { message: "Only one target position is allowed" } };
25
- }
26
- const windows = snapshot.windows;
27
- const findTab = (tabId) => {
28
- for (const win of windows) {
29
- const tab = win.tabs.find((entry) => entry.tabId === tabId);
30
- if (tab) {
31
- return { tab, windowId: win.windowId };
32
- }
33
- }
34
- return null;
35
- };
36
- if (targets[0] === "before-tab" || targets[0] === "after-tab") {
37
- const tabId = targets[0] === "before-tab" ? beforeTabId : afterTabId;
38
- if (!Number.isFinite(tabId)) {
39
- return { error: { message: "Invalid tab target" } };
40
- }
41
- const match = findTab(tabId);
42
- if (!match) {
43
- return { error: { message: "Target tab not found" } };
44
- }
45
- const index = normalizeTabIndex(match.tab.index);
46
- if (!Number.isFinite(index)) {
47
- return { error: { message: "Target tab index unavailable" } };
48
- }
49
- return {
50
- windowId: match.windowId,
51
- index: targets[0] === "before-tab" ? index : index + 1,
52
- anchor: { type: "tab", tabId },
53
- };
54
- }
55
- const groupTitle = targets[0] === "before-group" ? beforeGroupTitle : afterGroupTitle;
56
- const windowId = Number.isFinite(params.windowId) ? Number(params.windowId) : undefined;
57
- const resolved = deps.resolveGroupByTitle(snapshot, groupTitle, windowId);
58
- if (resolved.error) {
59
- return resolved;
60
- }
61
- const match = resolved.match;
62
- if (!match.tabs.length) {
63
- return { error: { message: "Target group has no tabs" } };
64
- }
65
- const indices = match.tabs
66
- .map((tab) => normalizeTabIndex(tab.index))
67
- .filter((value) => value != null);
68
- if (!indices.length) {
69
- return { error: { message: "Target group indices unavailable" } };
70
- }
71
- const minIndex = Math.min(...indices);
72
- const maxIndex = Math.max(...indices);
73
- return {
74
- windowId: match.windowId,
75
- index: targets[0] === "before-group" ? minIndex : maxIndex + 1,
76
- anchor: { type: "group", groupId: match.group.groupId, groupTitle },
77
- };
78
- }
79
- async function moveTab(params, deps) {
80
- const tabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
81
- const tabId = Number.isFinite(params.tabId)
82
- ? Number(params.tabId)
83
- : tabIds.length
84
- ? Number(tabIds[0])
85
- : null;
86
- if (!tabId) {
87
- throw new Error("Missing tabId");
88
- }
89
- const snapshot = await deps.getTabSnapshot();
90
- const windows = snapshot.windows;
91
- const sourceWindow = windows.find((win) => win.tabs.some((tab) => tab.tabId === tabId));
92
- if (!sourceWindow) {
93
- throw new Error("Source tab not found");
94
- }
95
- const sourceTab = sourceWindow.tabs.find((tab) => tab.tabId === tabId);
96
- if (!sourceTab) {
97
- throw new Error("Source tab not found");
98
- }
99
- const newWindow = params.newWindow === true;
100
- const hasTarget = Number.isFinite(params.beforeTabId)
101
- || Number.isFinite(params.afterTabId)
102
- || (typeof params.beforeGroupTitle === "string" && params.beforeGroupTitle.trim())
103
- || (typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim());
104
- if (newWindow) {
105
- if (hasTarget) {
106
- throw new Error("Cannot combine --new-window with --before/--after");
107
- }
108
- const createdWindow = await chrome.windows.create({ tabId, focused: false });
109
- const targetWindowId = createdWindow.id;
110
- let targetIndex = 0;
111
- const createdTab = createdWindow.tabs?.find((tab) => tab.id === tabId) || null;
112
- if (createdTab && Number.isFinite(createdTab.index)) {
113
- targetIndex = createdTab.index;
114
- }
115
- else {
116
- try {
117
- const updated = await chrome.tabs.get(tabId);
118
- targetIndex = updated.index;
119
- }
120
- catch {
121
- targetIndex = 0;
122
- }
123
- }
124
- return {
125
- tabId,
126
- from: { windowId: sourceWindow.windowId, index: sourceTab.index },
127
- to: { windowId: targetWindowId, index: targetIndex },
128
- summary: { movedTabs: 1 },
129
- undo: {
130
- action: "move-tab",
131
- tabId,
132
- from: {
133
- windowId: sourceWindow.windowId,
134
- index: sourceTab.index,
135
- groupId: sourceTab.groupId,
136
- groupTitle: sourceTab.groupTitle,
137
- groupColor: sourceTab.groupColor,
138
- groupCollapsed: sourceTab.groupCollapsed ?? null,
139
- },
140
- to: {
141
- windowId: targetWindowId,
142
- index: targetIndex,
143
- },
144
- },
145
- txid: params.txid || null,
146
- };
147
- }
148
- let normalizedParams = params;
149
- if (params.windowId != null) {
150
- const resolvedWindowId = deps.resolveWindowIdFromParams(snapshot, params.windowId);
151
- normalizedParams = { ...params, windowId: resolvedWindowId ?? undefined };
152
- }
153
- const target = resolveMoveTarget(snapshot, normalizedParams, deps);
154
- if (target.error) {
155
- throw target.error;
156
- }
157
- const targetWindowId = target.windowId;
158
- let targetIndex = target.index;
159
- const sourceIndex = normalizeTabIndex(sourceTab.index);
160
- if (Number.isFinite(sourceIndex) && sourceWindow.windowId === targetWindowId && sourceIndex < targetIndex) {
161
- targetIndex -= 1;
162
- }
163
- const moved = await chrome.tabs.move(tabId, { windowId: targetWindowId, index: targetIndex });
164
- return {
165
- tabId,
166
- from: { windowId: sourceWindow.windowId, index: sourceTab.index },
167
- to: { windowId: targetWindowId, index: moved.index },
168
- summary: { movedTabs: 1 },
169
- undo: {
170
- action: "move-tab",
171
- tabId,
172
- from: {
173
- windowId: sourceWindow.windowId,
174
- index: sourceTab.index,
175
- groupId: sourceTab.groupId,
176
- groupTitle: sourceTab.groupTitle,
177
- groupColor: sourceTab.groupColor,
178
- groupCollapsed: sourceTab.groupCollapsed ?? null,
179
- },
180
- to: {
181
- windowId: targetWindowId,
182
- index: moved.index,
183
- },
184
- },
185
- txid: params.txid || null,
186
- };
187
- }
188
- async function moveGroup(params, deps) {
189
- const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
190
- const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
191
- if (!groupId && !groupTitle) {
192
- throw new Error("Missing group identifier");
193
- }
194
- const snapshot = await deps.getTabSnapshot();
195
- const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) ?? undefined : undefined;
196
- const resolvedGroup = groupId != null
197
- ? deps.resolveGroupById(snapshot, groupId)
198
- : deps.resolveGroupByTitle(snapshot, groupTitle, windowIdParam);
199
- if (resolvedGroup.error) {
200
- throw resolvedGroup.error;
201
- }
202
- const source = resolvedGroup.match;
203
- if (!source.tabs.length) {
204
- throw new Error("Group has no tabs to move");
205
- }
206
- const newWindow = params.newWindow === true;
207
- const hasTarget = Number.isFinite(params.beforeTabId)
208
- || Number.isFinite(params.afterTabId)
209
- || (typeof params.beforeGroupTitle === "string" && params.beforeGroupTitle.trim())
210
- || (typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim());
211
- if (newWindow) {
212
- if (hasTarget) {
213
- throw new Error("Cannot combine --new-window with --before/--after");
214
- }
215
- const tabIds = source.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
216
- const [firstTabId, ...restTabIds] = tabIds;
217
- if (!firstTabId) {
218
- throw new Error("Group has no tabs to move");
219
- }
220
- const createdWindow = await chrome.windows.create({ tabId: firstTabId, focused: false });
221
- const targetWindowId = createdWindow.id;
222
- if (restTabIds.length > 0) {
223
- await chrome.tabs.move(restTabIds, { windowId: targetWindowId, index: -1 });
224
- }
225
- let newGroupId = null;
226
- try {
227
- newGroupId = await chrome.tabs.group({ tabIds, createProperties: { windowId: targetWindowId } });
228
- await chrome.tabGroups.update(newGroupId, {
229
- title: source.group.title || "",
230
- color: source.group.color || "grey",
231
- collapsed: source.group.collapsed || false,
232
- });
233
- }
234
- catch (error) {
235
- deps.log("Failed to regroup tabs", error);
236
- }
237
- const undoTabs = source.tabs
238
- .map((tab) => ({
239
- tabId: tab.tabId,
240
- windowId: tab.windowId,
241
- index: tab.index,
242
- groupId: tab.groupId,
243
- groupTitle: tab.groupTitle,
244
- groupColor: tab.groupColor,
245
- groupCollapsed: source.group.collapsed ?? null,
246
- }))
247
- .filter((tab) => typeof tab.tabId === "number");
248
- return {
249
- groupId: source.group.groupId,
250
- windowId: source.windowId,
251
- movedToWindowId: targetWindowId,
252
- newGroupId,
253
- summary: { movedTabs: tabIds.length },
254
- undo: {
255
- action: "move-group",
256
- groupId: source.group.groupId,
257
- windowId: source.windowId,
258
- movedToWindowId: targetWindowId,
259
- groupTitle: source.group.title ?? null,
260
- groupColor: source.group.color ?? null,
261
- groupCollapsed: source.group.collapsed ?? null,
262
- tabs: undoTabs,
263
- },
264
- txid: params.txid || null,
265
- };
266
- }
267
- const target = resolveMoveTarget(snapshot, params, deps);
268
- if (target.error) {
269
- throw target.error;
270
- }
271
- if (target.anchor?.type === "tab") {
272
- const anchorTabId = target.anchor.tabId;
273
- if (source.tabs.some((tab) => tab.tabId === anchorTabId)) {
274
- throw new Error("Target tab is within the source group");
275
- }
276
- }
277
- if (target.anchor?.type === "group") {
278
- const anchorGroupId = target.anchor.groupId;
279
- if (anchorGroupId === source.group.groupId && source.windowId === target.windowId) {
280
- throw new Error("Target group matches source group");
281
- }
282
- }
283
- const tabIds = source.tabs.map((tab) => tab.tabId).filter((id) => typeof id === "number");
284
- const indices = source.tabs
285
- .map((tab) => normalizeTabIndex(tab.index))
286
- .filter((value) => value != null);
287
- const minIndex = Math.min(...indices);
288
- const maxIndex = Math.max(...indices);
289
- const targetWindowId = target.windowId;
290
- let targetIndex = target.index;
291
- if (source.windowId === targetWindowId && targetIndex > maxIndex) {
292
- targetIndex -= tabIds.length;
293
- }
294
- const moved = await chrome.tabs.move(tabIds, { windowId: targetWindowId, index: targetIndex });
295
- const movedList = Array.isArray(moved) ? moved : [moved];
296
- let newGroupId = null;
297
- if (targetWindowId !== source.windowId) {
298
- try {
299
- const movedIds = movedList.map((tab) => tab.id).filter((id) => typeof id === "number");
300
- if (movedIds.length > 0) {
301
- newGroupId = await chrome.tabs.group({ tabIds: movedIds, createProperties: { windowId: targetWindowId } });
302
- await chrome.tabGroups.update(newGroupId, {
303
- title: source.group.title || "",
304
- color: source.group.color || "grey",
305
- collapsed: source.group.collapsed || false,
306
- });
307
- }
308
- }
309
- catch (error) {
310
- deps.log("Failed to regroup tabs", error);
311
- }
312
- }
313
- const undoTabs = source.tabs
314
- .map((tab) => ({
315
- tabId: tab.tabId,
316
- windowId: tab.windowId,
317
- index: tab.index,
318
- groupId: tab.groupId,
319
- groupTitle: tab.groupTitle,
320
- groupColor: tab.groupColor,
321
- groupCollapsed: source.group.collapsed ?? null,
322
- }))
323
- .filter((tab) => typeof tab.tabId === "number");
324
- return {
325
- groupId: source.group.groupId,
326
- windowId: source.windowId,
327
- movedToWindowId: targetWindowId,
328
- newGroupId,
329
- summary: { movedTabs: tabIds.length },
330
- undo: {
331
- action: "move-group",
332
- groupId: source.group.groupId,
333
- windowId: source.windowId,
334
- movedToWindowId: targetWindowId,
335
- groupTitle: source.group.title ?? null,
336
- groupColor: source.group.color ?? null,
337
- groupCollapsed: source.group.collapsed ?? null,
338
- tabs: undoTabs,
339
- },
340
- txid: params.txid || null,
341
- };
342
- }