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,456 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Tab operations — extracted from background.ts (pure structural refactor).
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.getMostRecentFocusedWindowId = getMostRecentFocusedWindowId;
|
|
8
|
-
exports.normalizeUrl = normalizeUrl;
|
|
9
|
-
exports.normalizeTabIndex = normalizeTabIndex;
|
|
10
|
-
exports.resolveOpenWindow = resolveOpenWindow;
|
|
11
|
-
exports.focusTab = focusTab;
|
|
12
|
-
exports.refreshTabs = refreshTabs;
|
|
13
|
-
exports.openTabs = openTabs;
|
|
14
|
-
const normalize_url_1 = __importDefault(require("normalize-url"));
|
|
15
|
-
function getMostRecentFocusedWindowId(windows) {
|
|
16
|
-
let bestWindowId = null;
|
|
17
|
-
let bestFocusedAt = -Infinity;
|
|
18
|
-
for (const win of windows) {
|
|
19
|
-
for (const tab of win.tabs) {
|
|
20
|
-
const focusedAt = Number(tab.lastFocusedAt);
|
|
21
|
-
if (!Number.isFinite(focusedAt)) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (focusedAt > bestFocusedAt) {
|
|
25
|
-
bestFocusedAt = focusedAt;
|
|
26
|
-
bestWindowId = win.windowId;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return bestWindowId;
|
|
31
|
-
}
|
|
32
|
-
function normalizeUrl(rawUrl) {
|
|
33
|
-
if (!rawUrl || typeof rawUrl !== "string") {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
return (0, normalize_url_1.default)(rawUrl, {
|
|
38
|
-
stripHash: true,
|
|
39
|
-
removeQueryParameters: [
|
|
40
|
-
/^utm_\w+$/i,
|
|
41
|
-
"fbclid",
|
|
42
|
-
"gclid",
|
|
43
|
-
"igshid",
|
|
44
|
-
"mc_cid",
|
|
45
|
-
"mc_eid",
|
|
46
|
-
"ref",
|
|
47
|
-
"ref_src",
|
|
48
|
-
"ref_url",
|
|
49
|
-
"si",
|
|
50
|
-
],
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
function normalizeTabIndex(value) {
|
|
58
|
-
const index = Number(value);
|
|
59
|
-
return Number.isFinite(index) ? index : null;
|
|
60
|
-
}
|
|
61
|
-
function matchIncludes(value, needle) {
|
|
62
|
-
if (!needle) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
return typeof value === "string" && value.toLowerCase().includes(needle);
|
|
66
|
-
}
|
|
67
|
-
function resolveOpenWindow(snapshot, params) {
|
|
68
|
-
const windows = snapshot.windows;
|
|
69
|
-
if (!windows.length) {
|
|
70
|
-
return { error: { message: "No windows available" } };
|
|
71
|
-
}
|
|
72
|
-
if (params.windowId != null) {
|
|
73
|
-
if (typeof params.windowId === "string") {
|
|
74
|
-
const normalized = params.windowId.trim().toLowerCase();
|
|
75
|
-
if (normalized === "active") {
|
|
76
|
-
const focused = windows.find((win) => win.focused);
|
|
77
|
-
if (focused) {
|
|
78
|
-
return { windowId: focused.windowId };
|
|
79
|
-
}
|
|
80
|
-
return { error: { message: "Active window not found" } };
|
|
81
|
-
}
|
|
82
|
-
if (normalized === "last-focused") {
|
|
83
|
-
const lastFocused = getMostRecentFocusedWindowId(windows);
|
|
84
|
-
if (lastFocused != null) {
|
|
85
|
-
return { windowId: lastFocused };
|
|
86
|
-
}
|
|
87
|
-
return { error: { message: "Last focused window not found" } };
|
|
88
|
-
}
|
|
89
|
-
if (normalized === "new") {
|
|
90
|
-
return { error: { message: "--window new is only supported by open" } };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const windowId = Number(params.windowId);
|
|
94
|
-
const found = windows.find((win) => win.windowId === windowId);
|
|
95
|
-
if (!found) {
|
|
96
|
-
return { error: { message: "Window not found" } };
|
|
97
|
-
}
|
|
98
|
-
return { windowId };
|
|
99
|
-
}
|
|
100
|
-
if (params.windowTabId != null) {
|
|
101
|
-
const tabId = Number(params.windowTabId);
|
|
102
|
-
const found = windows.find((win) => win.tabs.some((tab) => tab.tabId === tabId));
|
|
103
|
-
if (!found) {
|
|
104
|
-
return { error: { message: "Window not found for tab" } };
|
|
105
|
-
}
|
|
106
|
-
return { windowId: found.windowId };
|
|
107
|
-
}
|
|
108
|
-
let candidates = [...windows];
|
|
109
|
-
let filtered = false;
|
|
110
|
-
if (typeof params.afterGroupTitle === "string" && params.afterGroupTitle.trim()) {
|
|
111
|
-
const groupTitle = params.afterGroupTitle.trim();
|
|
112
|
-
candidates = candidates.filter((win) => win.groups.some((group) => group.title === groupTitle));
|
|
113
|
-
filtered = true;
|
|
114
|
-
}
|
|
115
|
-
if (typeof params.windowGroupTitle === "string" && params.windowGroupTitle.trim()) {
|
|
116
|
-
const groupTitle = params.windowGroupTitle.trim();
|
|
117
|
-
candidates = candidates.filter((win) => win.groups.some((group) => group.title === groupTitle));
|
|
118
|
-
filtered = true;
|
|
119
|
-
}
|
|
120
|
-
if (typeof params.windowUrl === "string" && params.windowUrl.trim()) {
|
|
121
|
-
const needle = params.windowUrl.trim().toLowerCase();
|
|
122
|
-
candidates = candidates.filter((win) => win.tabs.some((tab) => matchIncludes(tab.url, needle)));
|
|
123
|
-
filtered = true;
|
|
124
|
-
}
|
|
125
|
-
if (filtered) {
|
|
126
|
-
if (candidates.length === 1) {
|
|
127
|
-
return { windowId: candidates[0].windowId };
|
|
128
|
-
}
|
|
129
|
-
if (candidates.length === 0) {
|
|
130
|
-
return { error: { message: "No matching window found" } };
|
|
131
|
-
}
|
|
132
|
-
return { error: { message: "Multiple windows match selection. Provide --window to disambiguate." } };
|
|
133
|
-
}
|
|
134
|
-
const focused = windows.find((win) => win.focused);
|
|
135
|
-
if (focused) {
|
|
136
|
-
return { windowId: focused.windowId };
|
|
137
|
-
}
|
|
138
|
-
if (windows.length === 1) {
|
|
139
|
-
return { windowId: windows[0].windowId };
|
|
140
|
-
}
|
|
141
|
-
const lastFocused = getMostRecentFocusedWindowId(windows);
|
|
142
|
-
if (lastFocused != null) {
|
|
143
|
-
return { windowId: lastFocused };
|
|
144
|
-
}
|
|
145
|
-
return { error: { message: "Multiple windows available. Provide --window to target one." } };
|
|
146
|
-
}
|
|
147
|
-
async function focusTab(params) {
|
|
148
|
-
const tabIds = Array.isArray(params.tabIds) ? params.tabIds.map(Number) : [];
|
|
149
|
-
const tabId = Number.isFinite(params.tabId)
|
|
150
|
-
? Number(params.tabId)
|
|
151
|
-
: tabIds.length
|
|
152
|
-
? Number(tabIds[0])
|
|
153
|
-
: null;
|
|
154
|
-
if (!tabId) {
|
|
155
|
-
throw new Error("Missing tabId");
|
|
156
|
-
}
|
|
157
|
-
const tab = await chrome.tabs.get(tabId);
|
|
158
|
-
await chrome.windows.update(tab.windowId, { focused: true });
|
|
159
|
-
await chrome.tabs.update(tabId, { active: true });
|
|
160
|
-
return {
|
|
161
|
-
tabId,
|
|
162
|
-
windowId: tab.windowId,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
async function refreshTabs(params) {
|
|
166
|
-
const tabId = Number.isFinite(params.tabId)
|
|
167
|
-
? Number(params.tabId)
|
|
168
|
-
: null;
|
|
169
|
-
if (!tabId) {
|
|
170
|
-
throw new Error("Missing tabId");
|
|
171
|
-
}
|
|
172
|
-
await chrome.tabs.reload(tabId);
|
|
173
|
-
return {
|
|
174
|
-
tabId,
|
|
175
|
-
summary: { refreshedTabs: 1 },
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
async function openTabs(params, deps) {
|
|
179
|
-
const urls = Array.isArray(params.urls)
|
|
180
|
-
? params.urls.map((url) => (typeof url === "string" ? url.trim() : "")).filter(Boolean)
|
|
181
|
-
: [];
|
|
182
|
-
const groupTitle = typeof params.groupTitle === "string" ? params.groupTitle.trim() : "";
|
|
183
|
-
const groupColor = typeof params.color === "string" ? params.color.trim() : "";
|
|
184
|
-
const afterGroupTitle = typeof params.afterGroupTitle === "string" ? params.afterGroupTitle.trim() : "";
|
|
185
|
-
const beforeTabId = Number.isFinite(params.beforeTabId) ? Number(params.beforeTabId) : null;
|
|
186
|
-
const afterTabId = Number.isFinite(params.afterTabId) ? Number(params.afterTabId) : null;
|
|
187
|
-
if (beforeTabId != null && afterTabId != null) {
|
|
188
|
-
throw new Error("Only one target position is allowed");
|
|
189
|
-
}
|
|
190
|
-
const newWindow = params.newWindow === true;
|
|
191
|
-
const forceNewGroup = params.newGroup === true;
|
|
192
|
-
const allowDuplicates = params.allowDuplicates === true;
|
|
193
|
-
if (!urls.length && !newWindow) {
|
|
194
|
-
throw new Error("No URLs provided");
|
|
195
|
-
}
|
|
196
|
-
if (newWindow) {
|
|
197
|
-
if (afterGroupTitle || beforeTabId || afterTabId) {
|
|
198
|
-
throw new Error("Cannot use --before/--after with --new-window");
|
|
199
|
-
}
|
|
200
|
-
if (params.windowId != null || params.windowGroupTitle || params.windowTabId != null || params.windowUrl) {
|
|
201
|
-
throw new Error("Cannot combine --new-window with window selectors");
|
|
202
|
-
}
|
|
203
|
-
const created = [];
|
|
204
|
-
const skipped = [];
|
|
205
|
-
const createdWindow = await chrome.windows.create({ focused: false });
|
|
206
|
-
const windowId = createdWindow.id;
|
|
207
|
-
let seedTabs = createdWindow.tabs;
|
|
208
|
-
if (!seedTabs) {
|
|
209
|
-
seedTabs = await chrome.tabs.query({ windowId });
|
|
210
|
-
}
|
|
211
|
-
const seedTabId = seedTabs.find((tab) => typeof tab.id === "number")?.id ?? null;
|
|
212
|
-
for (const url of urls) {
|
|
213
|
-
try {
|
|
214
|
-
const tab = await chrome.tabs.create({ windowId, url, active: false });
|
|
215
|
-
created.push({
|
|
216
|
-
tabId: tab.id,
|
|
217
|
-
windowId: tab.windowId,
|
|
218
|
-
index: tab.index,
|
|
219
|
-
url: tab.url,
|
|
220
|
-
title: tab.title,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
skipped.push({ url, reason: "create_failed" });
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (!urls.length && seedTabs.length) {
|
|
228
|
-
const tab = seedTabs[0];
|
|
229
|
-
created.push({
|
|
230
|
-
tabId: tab.id,
|
|
231
|
-
windowId: tab.windowId,
|
|
232
|
-
index: tab.index,
|
|
233
|
-
url: tab.url,
|
|
234
|
-
title: tab.title,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
if (seedTabId && created.length > 0 && urls.length > 0) {
|
|
238
|
-
try {
|
|
239
|
-
await chrome.tabs.remove(seedTabId);
|
|
240
|
-
}
|
|
241
|
-
catch (error) {
|
|
242
|
-
deps.log("Failed to remove seed tab", error);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
let groupId = null;
|
|
246
|
-
if (groupTitle && created.length > 0) {
|
|
247
|
-
try {
|
|
248
|
-
const tabIds = created.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
249
|
-
if (tabIds.length > 0) {
|
|
250
|
-
groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
|
|
251
|
-
const update = { title: groupTitle };
|
|
252
|
-
if (groupColor) {
|
|
253
|
-
update.color = groupColor;
|
|
254
|
-
}
|
|
255
|
-
await chrome.tabGroups.update(groupId, update);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
deps.log("Failed to create group", error);
|
|
260
|
-
groupId = null;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
return {
|
|
264
|
-
windowId,
|
|
265
|
-
groupId,
|
|
266
|
-
groupTitle: groupTitle || null,
|
|
267
|
-
afterGroupTitle: null,
|
|
268
|
-
insertIndex: null,
|
|
269
|
-
created,
|
|
270
|
-
skipped,
|
|
271
|
-
summary: {
|
|
272
|
-
createdTabs: created.length,
|
|
273
|
-
skippedUrls: skipped.length,
|
|
274
|
-
grouped: Boolean(groupId),
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
const snapshot = await deps.getTabSnapshot();
|
|
279
|
-
let openParams = params;
|
|
280
|
-
// Auto-resolve window by group name when no explicit window selector is provided
|
|
281
|
-
if (groupTitle && !forceNewGroup && openParams.windowId == null && !openParams.windowGroupTitle && !openParams.windowTabId && !openParams.windowUrl) {
|
|
282
|
-
const groupWindows = snapshot.windows.filter((win) => win.groups.some((g) => g.title === groupTitle));
|
|
283
|
-
if (groupWindows.length === 1) {
|
|
284
|
-
openParams = { ...openParams, windowId: groupWindows[0].windowId };
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (params.windowId == null && (beforeTabId != null || afterTabId != null)) {
|
|
288
|
-
const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
|
|
289
|
-
const anchorWindow = snapshot.windows
|
|
290
|
-
.find((win) => win.tabs.some((tab) => tab.tabId === anchorId));
|
|
291
|
-
if (anchorWindow) {
|
|
292
|
-
openParams = { ...params, windowId: anchorWindow.windowId };
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
const selection = resolveOpenWindow(snapshot, openParams);
|
|
296
|
-
if (selection.error) {
|
|
297
|
-
throw selection.error;
|
|
298
|
-
}
|
|
299
|
-
const windowId = selection.windowId;
|
|
300
|
-
const windowSnapshot = snapshot.windows.find((win) => win.windowId === windowId);
|
|
301
|
-
if (!windowSnapshot) {
|
|
302
|
-
throw new Error("Window snapshot unavailable");
|
|
303
|
-
}
|
|
304
|
-
// Resolve existing group for reuse
|
|
305
|
-
let existingGroupId = null;
|
|
306
|
-
const existingUrlSet = new Set();
|
|
307
|
-
if (groupTitle && !forceNewGroup) {
|
|
308
|
-
const matchingGroups = windowSnapshot.groups.filter((g) => g.title === groupTitle);
|
|
309
|
-
if (matchingGroups.length > 1) {
|
|
310
|
-
throw new Error(`Ambiguous group title "${groupTitle}": found ${matchingGroups.length} groups with the same name. Use --new-group to force a new group, group-gather to merge, or --group-id to target by ID.`);
|
|
311
|
-
}
|
|
312
|
-
if (matchingGroups.length === 1) {
|
|
313
|
-
existingGroupId = matchingGroups[0].groupId;
|
|
314
|
-
const existingTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === existingGroupId);
|
|
315
|
-
for (const tab of existingTabs) {
|
|
316
|
-
const norm = normalizeUrl(tab.url);
|
|
317
|
-
if (norm) {
|
|
318
|
-
existingUrlSet.add(norm);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
const created = [];
|
|
324
|
-
const skipped = [];
|
|
325
|
-
let insertIndex = null;
|
|
326
|
-
if (afterGroupTitle) {
|
|
327
|
-
const targetGroup = windowSnapshot.groups.find((group) => group.title === afterGroupTitle);
|
|
328
|
-
if (!targetGroup) {
|
|
329
|
-
throw new Error("Group not found in target window");
|
|
330
|
-
}
|
|
331
|
-
const groupTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === targetGroup.groupId);
|
|
332
|
-
if (!groupTabs.length) {
|
|
333
|
-
throw new Error("Group has no tabs to anchor insertion");
|
|
334
|
-
}
|
|
335
|
-
const indices = groupTabs
|
|
336
|
-
.map((tab) => normalizeTabIndex(tab.index))
|
|
337
|
-
.filter((value) => value != null);
|
|
338
|
-
if (!indices.length) {
|
|
339
|
-
throw new Error("Group tabs missing indices");
|
|
340
|
-
}
|
|
341
|
-
insertIndex = Math.max(...indices) + 1;
|
|
342
|
-
}
|
|
343
|
-
if (beforeTabId != null || afterTabId != null) {
|
|
344
|
-
if (afterGroupTitle) {
|
|
345
|
-
throw new Error("Only one target position is allowed");
|
|
346
|
-
}
|
|
347
|
-
const anchorId = beforeTabId != null ? beforeTabId : afterTabId;
|
|
348
|
-
const anchorTab = windowSnapshot.tabs.find((tab) => tab.tabId === anchorId);
|
|
349
|
-
if (!anchorTab) {
|
|
350
|
-
throw new Error("Anchor tab not found in target window");
|
|
351
|
-
}
|
|
352
|
-
const anchorIndex = normalizeTabIndex(anchorTab.index);
|
|
353
|
-
if (!Number.isFinite(anchorIndex)) {
|
|
354
|
-
throw new Error("Anchor tab index unavailable");
|
|
355
|
-
}
|
|
356
|
-
insertIndex = beforeTabId != null ? anchorIndex : anchorIndex + 1;
|
|
357
|
-
}
|
|
358
|
-
// Default insert position: append after existing group tabs
|
|
359
|
-
if (existingGroupId != null && insertIndex == null && beforeTabId == null && afterTabId == null) {
|
|
360
|
-
const groupTabs = windowSnapshot.tabs.filter((tab) => tab.groupId === existingGroupId);
|
|
361
|
-
const indices = groupTabs
|
|
362
|
-
.map((tab) => normalizeTabIndex(tab.index))
|
|
363
|
-
.filter((value) => value != null);
|
|
364
|
-
if (indices.length) {
|
|
365
|
-
insertIndex = Math.max(...indices) + 1;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
let nextIndex = insertIndex;
|
|
369
|
-
for (const url of urls) {
|
|
370
|
-
if (!allowDuplicates && existingGroupId != null) {
|
|
371
|
-
const norm = normalizeUrl(url);
|
|
372
|
-
if (norm && existingUrlSet.has(norm)) {
|
|
373
|
-
skipped.push({ url, reason: "duplicate" });
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
try {
|
|
378
|
-
const createOptions = { windowId, url, active: false };
|
|
379
|
-
if (nextIndex != null) {
|
|
380
|
-
createOptions.index = nextIndex;
|
|
381
|
-
nextIndex += 1;
|
|
382
|
-
}
|
|
383
|
-
const tab = await chrome.tabs.create(createOptions);
|
|
384
|
-
created.push({
|
|
385
|
-
tabId: tab.id,
|
|
386
|
-
windowId: tab.windowId,
|
|
387
|
-
index: tab.index,
|
|
388
|
-
url: tab.url,
|
|
389
|
-
title: tab.title,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
catch (error) {
|
|
393
|
-
skipped.push({ url, reason: "create_failed" });
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
let groupId = null;
|
|
397
|
-
if (groupTitle && created.length > 0) {
|
|
398
|
-
try {
|
|
399
|
-
const tabIds = created.map((tab) => tab.tabId).filter((id) => typeof id === "number");
|
|
400
|
-
if (tabIds.length > 0) {
|
|
401
|
-
if (existingGroupId != null) {
|
|
402
|
-
// Reuse existing group
|
|
403
|
-
groupId = await chrome.tabs.group({ groupId: existingGroupId, tabIds });
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
groupId = await chrome.tabs.group({ tabIds, createProperties: { windowId } });
|
|
407
|
-
const update = { title: groupTitle };
|
|
408
|
-
if (groupColor) {
|
|
409
|
-
update.color = groupColor;
|
|
410
|
-
}
|
|
411
|
-
await chrome.tabGroups.update(groupId, update);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
catch (error) {
|
|
416
|
-
deps.log("Failed to create group", error);
|
|
417
|
-
groupId = null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// All-dupes case: report existing group even when no new tabs were created
|
|
421
|
-
if (groupTitle && created.length === 0 && existingGroupId != null) {
|
|
422
|
-
groupId = existingGroupId;
|
|
423
|
-
}
|
|
424
|
-
// Reorder: groups before ungrouped tabs
|
|
425
|
-
try {
|
|
426
|
-
const freshTabs = await chrome.tabs.query({ windowId });
|
|
427
|
-
freshTabs.sort((a, b) => a.index - b.index);
|
|
428
|
-
const firstUngroupedIndex = freshTabs.findIndex(t => (t.groupId ?? -1) === -1);
|
|
429
|
-
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
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
catch (err) {
|
|
440
|
-
deps.log("Failed to reorder groups before ungrouped tabs", err);
|
|
441
|
-
}
|
|
442
|
-
return {
|
|
443
|
-
windowId,
|
|
444
|
-
groupId,
|
|
445
|
-
groupTitle: groupTitle || null,
|
|
446
|
-
afterGroupTitle: afterGroupTitle || null,
|
|
447
|
-
insertIndex,
|
|
448
|
-
created,
|
|
449
|
-
skipped,
|
|
450
|
-
summary: {
|
|
451
|
-
createdTabs: created.length,
|
|
452
|
-
skippedUrls: skipped.length,
|
|
453
|
-
grouped: Boolean(groupId),
|
|
454
|
-
},
|
|
455
|
-
};
|
|
456
|
-
}
|