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.
- package/README.md +135 -35
- package/dist/extension/background.js +179 -3155
- package/dist/extension/lib/content.js +0 -115
- package/dist/extension/lib/screenshot.js +0 -93
- package/dist/extension/manifest.json +2 -2
- package/package.json +13 -5
- package/dist/cli/lib/args.js +0 -141
- package/dist/cli/lib/client.js +0 -83
- package/dist/cli/lib/commands/doctor.js +0 -134
- package/dist/cli/lib/commands/index.js +0 -51
- package/dist/cli/lib/commands/list.js +0 -159
- package/dist/cli/lib/commands/meta.js +0 -229
- package/dist/cli/lib/commands/params-groups.js +0 -48
- package/dist/cli/lib/commands/params-move.js +0 -44
- package/dist/cli/lib/commands/params.js +0 -314
- package/dist/cli/lib/commands/profile.js +0 -91
- package/dist/cli/lib/commands/setup.js +0 -294
- package/dist/cli/lib/constants.js +0 -30
- package/dist/cli/lib/help.js +0 -205
- package/dist/cli/lib/options-commands.js +0 -274
- package/dist/cli/lib/options-groups.js +0 -41
- package/dist/cli/lib/options.js +0 -125
- package/dist/cli/lib/output.js +0 -147
- package/dist/cli/lib/pagination.js +0 -55
- package/dist/cli/lib/policy-filter.js +0 -202
- package/dist/cli/lib/policy.js +0 -91
- package/dist/cli/lib/report.js +0 -61
- package/dist/cli/lib/response.js +0 -235
- package/dist/cli/lib/scope.js +0 -250
- package/dist/cli/lib/snapshot.js +0 -216
- package/dist/cli/lib/types.js +0 -2
- package/dist/cli/tabctl.js +0 -475
- package/dist/extension/lib/archive.js +0 -444
- package/dist/extension/lib/deps.js +0 -4
- package/dist/extension/lib/groups.js +0 -529
- package/dist/extension/lib/inspect.js +0 -252
- package/dist/extension/lib/move.js +0 -342
- package/dist/extension/lib/tabs.js +0 -456
- package/dist/extension/lib/undo-handlers.js +0 -447
- package/dist/host/host.bundle.js +0 -670
- package/dist/host/host.js +0 -143
- package/dist/host/host.sh +0 -5
- package/dist/host/launcher/go.mod +0 -3
- package/dist/host/launcher/main.go +0 -109
- package/dist/host/lib/handlers.js +0 -327
- package/dist/host/lib/undo.js +0 -60
- package/dist/shared/config.js +0 -134
- package/dist/shared/extension-sync.js +0 -170
- package/dist/shared/profiles.js +0 -78
- package/dist/shared/version.js +0 -8
- package/dist/shared/wrapper-health.js +0 -132
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Group management — extracted from background.ts (pure structural refactor).
|
|
3
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.getGroupTabs = getGroupTabs;
|
|
5
|
-
exports.listGroupSummaries = listGroupSummaries;
|
|
6
|
-
exports.summarizeGroupMatch = summarizeGroupMatch;
|
|
7
|
-
exports.findGroupMatches = findGroupMatches;
|
|
8
|
-
exports.resolveGroupByTitle = resolveGroupByTitle;
|
|
9
|
-
exports.resolveGroupById = resolveGroupById;
|
|
10
|
-
exports.listGroups = listGroups;
|
|
11
|
-
exports.groupUpdate = groupUpdate;
|
|
12
|
-
exports.groupUngroup = groupUngroup;
|
|
13
|
-
exports.groupAssign = groupAssign;
|
|
14
|
-
exports.groupGather = groupGather;
|
|
15
|
-
function getGroupTabs(windowSnapshot, groupId) {
|
|
16
|
-
return windowSnapshot.tabs
|
|
17
|
-
.filter((tab) => tab.groupId === groupId)
|
|
18
|
-
.sort((a, b) => {
|
|
19
|
-
const ai = Number(a.index);
|
|
20
|
-
const bi = Number(b.index);
|
|
21
|
-
return (Number.isFinite(ai) ? ai : 0) - (Number.isFinite(bi) ? bi : 0);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
function listGroupSummaries(snapshot, buildWindowLabels, windowId) {
|
|
25
|
-
const windowLabels = buildWindowLabels(snapshot);
|
|
26
|
-
const summaries = [];
|
|
27
|
-
const windows = snapshot.windows;
|
|
28
|
-
for (const win of windows) {
|
|
29
|
-
if (windowId && win.windowId !== windowId) {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
for (const group of win.groups) {
|
|
33
|
-
summaries.push({
|
|
34
|
-
windowId: win.windowId,
|
|
35
|
-
windowLabel: windowLabels.get(win.windowId) ?? null,
|
|
36
|
-
groupId: group.groupId,
|
|
37
|
-
title: typeof group.title === "string" ? group.title : null,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return summaries;
|
|
42
|
-
}
|
|
43
|
-
function summarizeGroupMatch(match, windowLabels) {
|
|
44
|
-
return {
|
|
45
|
-
windowId: match.windowId,
|
|
46
|
-
windowLabel: windowLabels.get(match.windowId) ?? null,
|
|
47
|
-
groupId: match.group.groupId,
|
|
48
|
-
title: typeof match.group.title === "string" ? match.group.title : null,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
function findGroupMatches(snapshot, groupTitle, windowId) {
|
|
52
|
-
const matches = [];
|
|
53
|
-
const windows = snapshot.windows;
|
|
54
|
-
for (const win of windows) {
|
|
55
|
-
if (windowId && win.windowId !== windowId) {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
for (const group of win.groups) {
|
|
59
|
-
if (group.title === groupTitle) {
|
|
60
|
-
matches.push({
|
|
61
|
-
windowId: win.windowId,
|
|
62
|
-
group,
|
|
63
|
-
tabs: getGroupTabs(win, group.groupId),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return matches;
|
|
69
|
-
}
|
|
70
|
-
function resolveGroupByTitle(snapshot, buildWindowLabels, groupTitle, windowId) {
|
|
71
|
-
const windowLabels = buildWindowLabels(snapshot);
|
|
72
|
-
const allMatches = findGroupMatches(snapshot, groupTitle);
|
|
73
|
-
const matches = windowId ? allMatches.filter((match) => match.windowId === windowId) : allMatches;
|
|
74
|
-
const availableGroups = listGroupSummaries(snapshot, buildWindowLabels);
|
|
75
|
-
if (matches.length === 0) {
|
|
76
|
-
const message = windowId && allMatches.length > 0
|
|
77
|
-
? "Group title not found in specified window"
|
|
78
|
-
: "No matching group title found";
|
|
79
|
-
return {
|
|
80
|
-
error: {
|
|
81
|
-
message,
|
|
82
|
-
hint: "Use tabctl group-list to see existing groups.",
|
|
83
|
-
matches: allMatches.map((match) => summarizeGroupMatch(match, windowLabels)),
|
|
84
|
-
availableGroups,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
if (matches.length > 1) {
|
|
89
|
-
return {
|
|
90
|
-
error: {
|
|
91
|
-
message: `Ambiguous group title: found ${matches.length} groups named "${groupTitle}". Use group-gather to merge duplicates, --group-id to target by ID, or --window to narrow scope.`,
|
|
92
|
-
hint: "Use group-gather to merge duplicates, --group-id to target by ID, or --window to narrow scope.",
|
|
93
|
-
matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
|
|
94
|
-
availableGroups,
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
return { match: matches[0] };
|
|
99
|
-
}
|
|
100
|
-
function resolveGroupById(snapshot, buildWindowLabels, groupId) {
|
|
101
|
-
const windows = snapshot.windows;
|
|
102
|
-
const matches = [];
|
|
103
|
-
for (const win of windows) {
|
|
104
|
-
const group = win.groups.find((entry) => entry.groupId === groupId);
|
|
105
|
-
if (group) {
|
|
106
|
-
matches.push({
|
|
107
|
-
windowId: win.windowId,
|
|
108
|
-
group,
|
|
109
|
-
tabs: getGroupTabs(win, groupId),
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (matches.length === 0) {
|
|
114
|
-
return {
|
|
115
|
-
error: {
|
|
116
|
-
message: "Group not found",
|
|
117
|
-
hint: "Use tabctl group-list to see existing groups.",
|
|
118
|
-
availableGroups: listGroupSummaries(snapshot, buildWindowLabels),
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (matches.length > 1) {
|
|
123
|
-
const windowLabels = buildWindowLabels(snapshot);
|
|
124
|
-
return {
|
|
125
|
-
error: {
|
|
126
|
-
message: "Group id is ambiguous. Provide a windowId.",
|
|
127
|
-
hint: "Use --window to disambiguate group ids.",
|
|
128
|
-
matches: matches.map((match) => summarizeGroupMatch(match, windowLabels)),
|
|
129
|
-
availableGroups: listGroupSummaries(snapshot, buildWindowLabels),
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
return { match: matches[0] };
|
|
134
|
-
}
|
|
135
|
-
async function listGroups(params, deps) {
|
|
136
|
-
const snapshot = await deps.getTabSnapshot();
|
|
137
|
-
const windows = snapshot.windows;
|
|
138
|
-
const windowLabels = deps.buildWindowLabels(snapshot);
|
|
139
|
-
const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
140
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
141
|
-
throw new Error("Window not found");
|
|
142
|
-
}
|
|
143
|
-
const groups = [];
|
|
144
|
-
for (const win of windows) {
|
|
145
|
-
if (windowIdParam && win.windowId !== windowIdParam) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const counts = new Map();
|
|
149
|
-
for (const tab of win.tabs) {
|
|
150
|
-
const groupId = tab.groupId;
|
|
151
|
-
if (typeof groupId === "number" && groupId !== -1) {
|
|
152
|
-
counts.set(groupId, (counts.get(groupId) || 0) + 1);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
for (const group of win.groups) {
|
|
156
|
-
const groupId = group.groupId;
|
|
157
|
-
groups.push({
|
|
158
|
-
windowId: win.windowId,
|
|
159
|
-
windowLabel: windowLabels.get(win.windowId) ?? null,
|
|
160
|
-
groupId,
|
|
161
|
-
title: group.title ?? null,
|
|
162
|
-
color: group.color ?? null,
|
|
163
|
-
collapsed: group.collapsed ?? null,
|
|
164
|
-
tabCount: counts.get(groupId) || 0,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return { groups };
|
|
169
|
-
}
|
|
170
|
-
async function groupUpdate(params, deps) {
|
|
171
|
-
const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
|
|
172
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
173
|
-
if (!groupId && !groupTitle) {
|
|
174
|
-
throw new Error("Missing group identifier");
|
|
175
|
-
}
|
|
176
|
-
const snapshot = await deps.getTabSnapshot();
|
|
177
|
-
const windows = snapshot.windows;
|
|
178
|
-
const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
179
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
180
|
-
throw new Error("Window not found");
|
|
181
|
-
}
|
|
182
|
-
let match;
|
|
183
|
-
if (groupId != null) {
|
|
184
|
-
const resolved = resolveGroupById(snapshot, deps.buildWindowLabels, groupId);
|
|
185
|
-
if (resolved.error) {
|
|
186
|
-
throw resolved.error;
|
|
187
|
-
}
|
|
188
|
-
match = resolved.match;
|
|
189
|
-
if (windowIdParam && windowIdParam !== match.windowId) {
|
|
190
|
-
throw new Error("Group is not in the specified window");
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
const resolved = resolveGroupByTitle(snapshot, deps.buildWindowLabels, groupTitle, windowIdParam || undefined);
|
|
195
|
-
if (resolved.error) {
|
|
196
|
-
throw resolved.error;
|
|
197
|
-
}
|
|
198
|
-
match = resolved.match;
|
|
199
|
-
}
|
|
200
|
-
const update = {};
|
|
201
|
-
if (typeof params.title === "string") {
|
|
202
|
-
update.title = params.title;
|
|
203
|
-
}
|
|
204
|
-
if (typeof params.color === "string" && params.color.trim()) {
|
|
205
|
-
update.color = params.color.trim();
|
|
206
|
-
}
|
|
207
|
-
if (typeof params.collapsed === "boolean") {
|
|
208
|
-
update.collapsed = params.collapsed;
|
|
209
|
-
}
|
|
210
|
-
if (!Object.keys(update).length) {
|
|
211
|
-
throw new Error("Missing group update fields");
|
|
212
|
-
}
|
|
213
|
-
const updated = await chrome.tabGroups.update(match.group.groupId, update);
|
|
214
|
-
return {
|
|
215
|
-
groupId: updated.id,
|
|
216
|
-
windowId: updated.windowId,
|
|
217
|
-
title: updated.title,
|
|
218
|
-
color: updated.color,
|
|
219
|
-
collapsed: updated.collapsed,
|
|
220
|
-
undo: {
|
|
221
|
-
action: "group-update",
|
|
222
|
-
groupId: updated.id,
|
|
223
|
-
windowId: match.windowId,
|
|
224
|
-
previous: {
|
|
225
|
-
title: match.group.title ?? null,
|
|
226
|
-
color: match.group.color ?? null,
|
|
227
|
-
collapsed: match.group.collapsed ?? null,
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
txid: params.txid || null,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
async function groupUngroup(params, deps) {
|
|
234
|
-
const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
|
|
235
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
236
|
-
if (!groupId && !groupTitle) {
|
|
237
|
-
throw new Error("Missing group identifier");
|
|
238
|
-
}
|
|
239
|
-
const snapshot = await deps.getTabSnapshot();
|
|
240
|
-
const windows = snapshot.windows;
|
|
241
|
-
const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
242
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
243
|
-
throw new Error("Window not found");
|
|
244
|
-
}
|
|
245
|
-
let match;
|
|
246
|
-
if (groupId != null) {
|
|
247
|
-
const resolved = resolveGroupById(snapshot, deps.buildWindowLabels, groupId);
|
|
248
|
-
if (resolved.error) {
|
|
249
|
-
throw resolved.error;
|
|
250
|
-
}
|
|
251
|
-
match = resolved.match;
|
|
252
|
-
if (windowIdParam && windowIdParam !== match.windowId) {
|
|
253
|
-
throw new Error("Group is not in the specified window");
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
const resolved = resolveGroupByTitle(snapshot, deps.buildWindowLabels, groupTitle, windowIdParam || undefined);
|
|
258
|
-
if (resolved.error) {
|
|
259
|
-
throw resolved.error;
|
|
260
|
-
}
|
|
261
|
-
match = resolved.match;
|
|
262
|
-
}
|
|
263
|
-
const undoTabs = match.tabs
|
|
264
|
-
.map((tab) => ({
|
|
265
|
-
tabId: tab.tabId,
|
|
266
|
-
windowId: tab.windowId,
|
|
267
|
-
index: tab.index,
|
|
268
|
-
groupId: tab.groupId,
|
|
269
|
-
groupTitle: tab.groupTitle,
|
|
270
|
-
groupColor: tab.groupColor,
|
|
271
|
-
groupCollapsed: match.group.collapsed ?? null,
|
|
272
|
-
}))
|
|
273
|
-
.filter((tab) => typeof tab.tabId === "number");
|
|
274
|
-
const tabIds = match.tabs
|
|
275
|
-
.map((tab) => tab.tabId)
|
|
276
|
-
.filter((tabId) => typeof tabId === "number");
|
|
277
|
-
if (tabIds.length) {
|
|
278
|
-
await chrome.tabs.ungroup(tabIds);
|
|
279
|
-
}
|
|
280
|
-
return {
|
|
281
|
-
groupId: match.group.groupId,
|
|
282
|
-
groupTitle: match.group.title || null,
|
|
283
|
-
windowId: match.windowId,
|
|
284
|
-
summary: {
|
|
285
|
-
ungroupedTabs: tabIds.length,
|
|
286
|
-
},
|
|
287
|
-
undo: {
|
|
288
|
-
action: "group-ungroup",
|
|
289
|
-
groupId: match.group.groupId,
|
|
290
|
-
windowId: match.windowId,
|
|
291
|
-
groupTitle: match.group.title || null,
|
|
292
|
-
groupColor: match.group.color || null,
|
|
293
|
-
groupCollapsed: match.group.collapsed ?? null,
|
|
294
|
-
tabs: undoTabs,
|
|
295
|
-
},
|
|
296
|
-
txid: params.txid || null,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
async function groupAssign(params, deps) {
|
|
300
|
-
const rawTabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
|
|
301
|
-
const tabIds = rawTabIds.filter((id) => Number.isFinite(id));
|
|
302
|
-
if (!tabIds.length) {
|
|
303
|
-
throw new Error("Missing tabIds");
|
|
304
|
-
}
|
|
305
|
-
const groupId = Number.isFinite(params.groupId) ? Number(params.groupId) : null;
|
|
306
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
307
|
-
if (!groupId && !groupTitle) {
|
|
308
|
-
throw new Error("Missing group identifier");
|
|
309
|
-
}
|
|
310
|
-
const snapshot = await deps.getTabSnapshot();
|
|
311
|
-
const windows = snapshot.windows;
|
|
312
|
-
const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
313
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
314
|
-
throw new Error("Window not found");
|
|
315
|
-
}
|
|
316
|
-
const tabIndex = new Map();
|
|
317
|
-
for (const win of windows) {
|
|
318
|
-
for (const tab of win.tabs) {
|
|
319
|
-
if (typeof tab.tabId === "number") {
|
|
320
|
-
tabIndex.set(tab.tabId, { tab, windowId: win.windowId });
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
const skipped = [];
|
|
325
|
-
const resolvedTabIds = [];
|
|
326
|
-
const sourceWindows = new Set();
|
|
327
|
-
const undoTabs = [];
|
|
328
|
-
for (const tabId of tabIds) {
|
|
329
|
-
const entry = tabIndex.get(tabId);
|
|
330
|
-
if (!entry) {
|
|
331
|
-
skipped.push({ tabId, reason: "not_found" });
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
resolvedTabIds.push(tabId);
|
|
335
|
-
sourceWindows.add(entry.windowId);
|
|
336
|
-
const tab = entry.tab;
|
|
337
|
-
undoTabs.push({
|
|
338
|
-
tabId,
|
|
339
|
-
windowId: entry.windowId,
|
|
340
|
-
index: tab.index,
|
|
341
|
-
groupId: tab.groupId,
|
|
342
|
-
groupTitle: tab.groupTitle,
|
|
343
|
-
groupColor: tab.groupColor,
|
|
344
|
-
groupCollapsed: tab.groupCollapsed ?? null,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
if (!resolvedTabIds.length) {
|
|
348
|
-
throw new Error("No matching tabs found");
|
|
349
|
-
}
|
|
350
|
-
let targetGroupId = null;
|
|
351
|
-
let targetWindowId = null;
|
|
352
|
-
let targetTitle = null;
|
|
353
|
-
let created = false;
|
|
354
|
-
if (groupId != null) {
|
|
355
|
-
const resolved = resolveGroupById(snapshot, deps.buildWindowLabels, groupId);
|
|
356
|
-
if (resolved.error) {
|
|
357
|
-
throw resolved.error;
|
|
358
|
-
}
|
|
359
|
-
const match = resolved.match;
|
|
360
|
-
targetGroupId = match.group.groupId;
|
|
361
|
-
targetWindowId = match.windowId;
|
|
362
|
-
targetTitle = typeof match.group.title === "string" ? match.group.title : null;
|
|
363
|
-
if (windowIdParam && windowIdParam !== targetWindowId) {
|
|
364
|
-
throw new Error("Group is not in the specified window");
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
const resolved = resolveGroupByTitle(snapshot, deps.buildWindowLabels, groupTitle, windowIdParam || undefined);
|
|
369
|
-
if (resolved.error) {
|
|
370
|
-
const error = resolved.error;
|
|
371
|
-
if (error.message === "No matching group title found" && params.create === true) {
|
|
372
|
-
targetWindowId = windowIdParam || (sourceWindows.size === 1 ? Array.from(sourceWindows)[0] : null);
|
|
373
|
-
if (!targetWindowId) {
|
|
374
|
-
throw new Error("Multiple source windows. Provide --window to create a new group.");
|
|
375
|
-
}
|
|
376
|
-
targetTitle = groupTitle;
|
|
377
|
-
created = true;
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
const match = resolved.match;
|
|
385
|
-
targetGroupId = match.group.groupId;
|
|
386
|
-
targetWindowId = match.windowId;
|
|
387
|
-
targetTitle = typeof match.group.title === "string" && match.group.title ? match.group.title : groupTitle;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (!targetWindowId) {
|
|
391
|
-
throw new Error("Target window not found");
|
|
392
|
-
}
|
|
393
|
-
const moveIds = resolvedTabIds.filter((tabId) => {
|
|
394
|
-
const entry = tabIndex.get(tabId);
|
|
395
|
-
return entry && entry.windowId !== targetWindowId;
|
|
396
|
-
});
|
|
397
|
-
if (moveIds.length > 0) {
|
|
398
|
-
await chrome.tabs.move(moveIds, { windowId: targetWindowId, index: -1 });
|
|
399
|
-
}
|
|
400
|
-
let assignedGroupId = targetGroupId;
|
|
401
|
-
if (targetGroupId != null) {
|
|
402
|
-
await chrome.tabs.group({ groupId: targetGroupId, tabIds: resolvedTabIds });
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
assignedGroupId = await chrome.tabs.group({ tabIds: resolvedTabIds, createProperties: { windowId: targetWindowId } });
|
|
406
|
-
const update = {};
|
|
407
|
-
if (targetTitle) {
|
|
408
|
-
update.title = targetTitle;
|
|
409
|
-
}
|
|
410
|
-
if (typeof params.color === "string" && params.color.trim()) {
|
|
411
|
-
update.color = params.color.trim();
|
|
412
|
-
}
|
|
413
|
-
if (typeof params.collapsed === "boolean") {
|
|
414
|
-
update.collapsed = params.collapsed;
|
|
415
|
-
}
|
|
416
|
-
if (Object.keys(update).length > 0) {
|
|
417
|
-
try {
|
|
418
|
-
await chrome.tabGroups.update(assignedGroupId, update);
|
|
419
|
-
}
|
|
420
|
-
catch (error) {
|
|
421
|
-
deps.log("Failed to update group", error);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
created = true;
|
|
425
|
-
}
|
|
426
|
-
return {
|
|
427
|
-
groupId: assignedGroupId,
|
|
428
|
-
groupTitle: targetTitle || groupTitle || null,
|
|
429
|
-
windowId: targetWindowId,
|
|
430
|
-
created,
|
|
431
|
-
summary: {
|
|
432
|
-
movedTabs: moveIds.length,
|
|
433
|
-
groupedTabs: resolvedTabIds.length,
|
|
434
|
-
skippedTabs: skipped.length,
|
|
435
|
-
},
|
|
436
|
-
skipped,
|
|
437
|
-
undo: {
|
|
438
|
-
action: "group-assign",
|
|
439
|
-
groupId: assignedGroupId,
|
|
440
|
-
groupTitle: targetTitle || groupTitle || null,
|
|
441
|
-
groupColor: typeof params.color === "string" && params.color.trim() ? params.color.trim() : null,
|
|
442
|
-
groupCollapsed: typeof params.collapsed === "boolean" ? params.collapsed : null,
|
|
443
|
-
created,
|
|
444
|
-
tabs: undoTabs,
|
|
445
|
-
},
|
|
446
|
-
txid: params.txid || null,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
async function groupGather(params, deps) {
|
|
450
|
-
const snapshot = await deps.getTabSnapshot();
|
|
451
|
-
const windows = snapshot.windows;
|
|
452
|
-
const windowIdParam = params.windowId != null ? deps.resolveWindowIdFromParams(snapshot, params.windowId) : null;
|
|
453
|
-
if (windowIdParam && !windows.some((win) => win.windowId === windowIdParam)) {
|
|
454
|
-
throw new Error("Window not found");
|
|
455
|
-
}
|
|
456
|
-
const groupTitleFilter = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
457
|
-
const merged = [];
|
|
458
|
-
const undoEntries = [];
|
|
459
|
-
for (const win of windows) {
|
|
460
|
-
if (windowIdParam && win.windowId !== windowIdParam)
|
|
461
|
-
continue;
|
|
462
|
-
const byTitle = new Map();
|
|
463
|
-
for (const group of win.groups) {
|
|
464
|
-
const title = typeof group.title === "string" ? group.title : "";
|
|
465
|
-
if (!title)
|
|
466
|
-
continue;
|
|
467
|
-
if (groupTitleFilter && title !== groupTitleFilter)
|
|
468
|
-
continue;
|
|
469
|
-
if (!byTitle.has(title))
|
|
470
|
-
byTitle.set(title, []);
|
|
471
|
-
byTitle.get(title).push(group);
|
|
472
|
-
}
|
|
473
|
-
for (const [title, titleGroups] of byTitle) {
|
|
474
|
-
if (titleGroups.length < 2)
|
|
475
|
-
continue;
|
|
476
|
-
const groupsWithIndex = titleGroups.map((g) => {
|
|
477
|
-
const tabs = win.tabs.filter((t) => t.groupId === g.groupId);
|
|
478
|
-
const minIndex = Math.min(...tabs.map((t) => {
|
|
479
|
-
const idx = Number(t.index);
|
|
480
|
-
return Number.isFinite(idx) ? idx : Infinity;
|
|
481
|
-
}));
|
|
482
|
-
return { group: g, tabs, minIndex };
|
|
483
|
-
});
|
|
484
|
-
groupsWithIndex.sort((a, b) => a.minIndex - b.minIndex);
|
|
485
|
-
const primary = groupsWithIndex[0];
|
|
486
|
-
const duplicates = groupsWithIndex.slice(1);
|
|
487
|
-
let movedTabs = 0;
|
|
488
|
-
for (const dup of duplicates) {
|
|
489
|
-
const tabIds = dup.tabs
|
|
490
|
-
.map((t) => t.tabId)
|
|
491
|
-
.filter((id) => typeof id === "number");
|
|
492
|
-
if (tabIds.length > 0) {
|
|
493
|
-
for (const tab of dup.tabs) {
|
|
494
|
-
undoEntries.push({
|
|
495
|
-
tabId: tab.tabId,
|
|
496
|
-
windowId: win.windowId,
|
|
497
|
-
index: tab.index,
|
|
498
|
-
groupId: tab.groupId,
|
|
499
|
-
groupTitle: tab.groupTitle,
|
|
500
|
-
groupColor: tab.groupColor,
|
|
501
|
-
groupCollapsed: dup.group.collapsed ?? null,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
await chrome.tabs.group({ groupId: primary.group.groupId, tabIds });
|
|
505
|
-
movedTabs += tabIds.length;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
merged.push({
|
|
509
|
-
windowId: win.windowId,
|
|
510
|
-
groupTitle: title,
|
|
511
|
-
primaryGroupId: primary.group.groupId,
|
|
512
|
-
mergedGroupCount: duplicates.length,
|
|
513
|
-
movedTabs,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return {
|
|
518
|
-
merged,
|
|
519
|
-
summary: {
|
|
520
|
-
mergedGroups: merged.reduce((sum, m) => sum + m.mergedGroupCount, 0),
|
|
521
|
-
movedTabs: merged.reduce((sum, m) => sum + m.movedTabs, 0),
|
|
522
|
-
},
|
|
523
|
-
undo: {
|
|
524
|
-
action: "group-gather",
|
|
525
|
-
tabs: undoEntries,
|
|
526
|
-
},
|
|
527
|
-
txid: params.txid || null,
|
|
528
|
-
};
|
|
529
|
-
}
|