tabctl 0.1.0

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.
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseNumberOption = parseNumberOption;
4
+ exports.buildNextCommand = buildNextCommand;
5
+ exports.resolvePagination = resolvePagination;
6
+ const constants_1 = require("./constants");
7
+ const output_1 = require("./output");
8
+ function parseNumberOption(options, key) {
9
+ if (!Object.prototype.hasOwnProperty.call(options, key)) {
10
+ return null;
11
+ }
12
+ const value = Number(options[key]);
13
+ if (!Number.isFinite(value)) {
14
+ (0, output_1.errorOut)(`Invalid --${key} value`);
15
+ }
16
+ return value;
17
+ }
18
+ function buildNextCommand(command, scopeArgs, offset, limit) {
19
+ const parts = ["tabctl", command, ...scopeArgs, "--offset", String(offset), "--limit", String(limit)];
20
+ return parts.join(" ");
21
+ }
22
+ function resolvePagination(options, total, command, scopeArgs) {
23
+ const noPage = options["no-page"] === true;
24
+ if (noPage) {
25
+ return { offset: 0, limit: total, page: null };
26
+ }
27
+ const limitRaw = parseNumberOption(options, "limit");
28
+ const offsetRaw = parseNumberOption(options, "offset");
29
+ const limit = limitRaw != null ? Math.floor(limitRaw) : constants_1.DEFAULT_PAGE_LIMIT;
30
+ const offset = offsetRaw != null ? Math.floor(offsetRaw) : 0;
31
+ if (!Number.isFinite(limit) || limit <= 0) {
32
+ (0, output_1.errorOut)("--limit must be a positive number");
33
+ }
34
+ if (!Number.isFinite(offset) || offset < 0) {
35
+ (0, output_1.errorOut)("--offset must be a non-negative number");
36
+ }
37
+ const remaining = total - offset;
38
+ const returned = remaining > 0 ? Math.min(limit, remaining) : 0;
39
+ const hasMore = offset + limit < total;
40
+ const nextOffset = hasMore ? offset + limit : null;
41
+ const hint = hasMore ? `Partial results. Next: ${buildNextCommand(command, scopeArgs, nextOffset, limit)}` : null;
42
+ return {
43
+ offset,
44
+ limit,
45
+ page: {
46
+ offset,
47
+ limit,
48
+ returned,
49
+ total,
50
+ hasMore,
51
+ nextOffset,
52
+ hint,
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.defaultPolicyPath = defaultPolicyPath;
7
+ exports.defaultPolicyTemplate = defaultPolicyTemplate;
8
+ exports.loadPolicy = loadPolicy;
9
+ exports.getDomain = getDomain;
10
+ exports.evaluateTab = evaluateTab;
11
+ exports.annotateEntry = annotateEntry;
12
+ exports.summarizePolicy = summarizePolicy;
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const config_1 = require("../../shared/config");
15
+ function defaultPolicyPath() {
16
+ return (0, config_1.resolveConfig)().policyPath;
17
+ }
18
+ function defaultPolicyTemplate() {
19
+ return {
20
+ protect: {
21
+ pinned: true,
22
+ groupTitles: ["🔒"],
23
+ },
24
+ };
25
+ }
26
+ function loadPolicy() {
27
+ const resolvedPath = defaultPolicyPath();
28
+ if (!fs_1.default.existsSync(resolvedPath)) {
29
+ return { policy: null, path: resolvedPath };
30
+ }
31
+ const raw = fs_1.default.readFileSync(resolvedPath, "utf8");
32
+ const parsed = JSON.parse(raw);
33
+ return { policy: parsed, path: resolvedPath };
34
+ }
35
+ function getDomain(rawUrl) {
36
+ if (!rawUrl) {
37
+ return null;
38
+ }
39
+ try {
40
+ const url = new URL(rawUrl);
41
+ return url.hostname;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function evaluateTab(tab, policy) {
48
+ const reasons = [];
49
+ if (!policy?.protect) {
50
+ return { eligible: true, protectedReasons: reasons };
51
+ }
52
+ const protect = policy.protect;
53
+ if (protect.pinned && tab.pinned === true) {
54
+ reasons.push("pinned");
55
+ }
56
+ const groupTitle = typeof tab.groupTitle === "string" ? tab.groupTitle : null;
57
+ if (groupTitle && Array.isArray(protect.groupTitles) && protect.groupTitles.includes(groupTitle)) {
58
+ reasons.push("groupTitle");
59
+ }
60
+ const groupId = Number(tab.groupId);
61
+ if (Number.isFinite(groupId) && Array.isArray(protect.groupIds) && protect.groupIds.includes(groupId)) {
62
+ reasons.push("groupId");
63
+ }
64
+ const windowId = Number(tab.windowId);
65
+ if (Number.isFinite(windowId) && Array.isArray(protect.windowIds) && protect.windowIds.includes(windowId)) {
66
+ reasons.push("windowId");
67
+ }
68
+ const domain = getDomain(typeof tab.url === "string" ? tab.url : null);
69
+ if (domain && Array.isArray(protect.domains) && protect.domains.includes(domain)) {
70
+ reasons.push("domain");
71
+ }
72
+ return { eligible: reasons.length === 0, protectedReasons: reasons };
73
+ }
74
+ function annotateEntry(entry, policy) {
75
+ const { eligible, protectedReasons } = evaluateTab(entry, policy);
76
+ return {
77
+ ...entry,
78
+ eligible,
79
+ protectedReasons,
80
+ };
81
+ }
82
+ function summarizePolicy(policy, policyPath) {
83
+ if (!policy) {
84
+ return { enabled: false, path: policyPath };
85
+ }
86
+ return {
87
+ enabled: true,
88
+ path: policyPath,
89
+ protect: policy.protect || {},
90
+ };
91
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderCsv = renderCsv;
4
+ exports.renderMarkdown = renderMarkdown;
5
+ function renderCsv(entries) {
6
+ const header = ["windowLabel", "groupTitle", "title", "url", "description", "lastFocusedAt"];
7
+ const lines = [header.join(",")];
8
+ const csvEscape = (value) => {
9
+ if (value == null) {
10
+ return "";
11
+ }
12
+ const str = String(value);
13
+ if (/[",\n]/.test(str)) {
14
+ return `"${str.replace(/"/g, '""')}"`;
15
+ }
16
+ return str;
17
+ };
18
+ for (const entry of entries) {
19
+ lines.push([
20
+ csvEscape(entry.windowLabel),
21
+ csvEscape(entry.groupTitle || ""),
22
+ csvEscape(entry.title || ""),
23
+ csvEscape(entry.url || ""),
24
+ csvEscape(entry.description || ""),
25
+ csvEscape(entry.lastFocusedAt || ""),
26
+ ].join(","));
27
+ }
28
+ return lines.join("\n");
29
+ }
30
+ function renderMarkdown(entries, generatedAt) {
31
+ const lines = [];
32
+ const date = generatedAt ? new Date(generatedAt).toISOString() : new Date().toISOString();
33
+ lines.push(`# Tab Report (${date})`);
34
+ const grouped = new Map();
35
+ for (const entry of entries) {
36
+ const windowKey = entry.windowLabel || `W${entry.windowId}`;
37
+ const groupKey = entry.groupTitle || "Ungrouped";
38
+ if (!grouped.has(windowKey)) {
39
+ grouped.set(windowKey, new Map());
40
+ }
41
+ const windowMap = grouped.get(windowKey);
42
+ if (!windowMap.has(groupKey)) {
43
+ windowMap.set(groupKey, []);
44
+ }
45
+ windowMap.get(groupKey)?.push(entry);
46
+ }
47
+ for (const [windowKey, groupMap] of grouped.entries()) {
48
+ lines.push(`\n## ${windowKey}`);
49
+ for (const [groupKey, items] of groupMap.entries()) {
50
+ lines.push(`\n### ${groupKey}`);
51
+ for (const item of items) {
52
+ const title = item.title || item.url || "(untitled)";
53
+ const url = item.url;
54
+ const link = url ? `[${title}](${url})` : title;
55
+ const desc = item.description ? ` - ${item.description}` : "";
56
+ lines.push(`- ${link}${desc}`);
57
+ }
58
+ }
59
+ }
60
+ return lines.join("\n");
61
+ }
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCliArgValue = formatCliArgValue;
4
+ exports.buildScopeArgs = buildScopeArgs;
5
+ exports.resolveScopeFlags = resolveScopeFlags;
6
+ exports.extractScopeParams = extractScopeParams;
7
+ exports.selectTabsFromSnapshot = selectTabsFromSnapshot;
8
+ exports.resolveWindowIdFromSnapshot = resolveWindowIdFromSnapshot;
9
+ exports.filterGroupsByScope = filterGroupsByScope;
10
+ const output_1 = require("./output");
11
+ function formatCliArgValue(value) {
12
+ const raw = String(value);
13
+ if (!raw) {
14
+ return raw;
15
+ }
16
+ if (/[\s"]/g.test(raw)) {
17
+ const escaped = raw.replace(/"/g, '\\"');
18
+ return `"${escaped}"`;
19
+ }
20
+ return raw;
21
+ }
22
+ function buildScopeArgs(options, includeAll) {
23
+ const args = [];
24
+ if (includeAll) {
25
+ args.push("--all");
26
+ return args;
27
+ }
28
+ if (options.ungrouped === true) {
29
+ args.push("--ungrouped");
30
+ }
31
+ if (Array.isArray(options.tab)) {
32
+ for (const entry of options.tab) {
33
+ if (typeof entry === "string" && entry.trim()) {
34
+ args.push("--tab", formatCliArgValue(entry.trim()));
35
+ }
36
+ }
37
+ }
38
+ if (typeof options.group === "string" && options.group.trim()) {
39
+ args.push("--group", formatCliArgValue(options.group.trim()));
40
+ }
41
+ if (options["group-id"] != null && options.ungrouped !== true) {
42
+ args.push("--group-id", formatCliArgValue(options["group-id"]));
43
+ }
44
+ if (options.window != null) {
45
+ const windowValue = normalizeWindowScope(options.window);
46
+ args.push("--window", formatCliArgValue(windowValue));
47
+ }
48
+ return args;
49
+ }
50
+ function resolveScopeFlags(options) {
51
+ const tabIds = Array.isArray(options.tab)
52
+ ? options.tab.map(Number).filter(Number.isFinite)
53
+ : [];
54
+ const groupTitle = typeof options.group === "string" ? options.group.trim() : "";
55
+ const ungrouped = options.ungrouped === true;
56
+ const groupId = ungrouped ? -1 : (options["group-id"] != null ? Number(options["group-id"]) : null);
57
+ const normalizedWindow = options.window != null ? normalizeWindowScope(options.window) : null;
58
+ const windowId = normalizedWindow == null
59
+ ? null
60
+ : normalizedWindow;
61
+ if (options["group-id"] != null && !Number.isFinite(groupId)) {
62
+ (0, output_1.errorOut)("Invalid --group-id value");
63
+ }
64
+ if (ungrouped && options["group-id"] != null) {
65
+ (0, output_1.errorOut)("--ungrouped cannot be combined with --group-id");
66
+ }
67
+ if (options.window != null && typeof windowId === "number" && !Number.isFinite(windowId)) {
68
+ (0, output_1.errorOut)("Invalid --window value");
69
+ }
70
+ const hasScope = tabIds.length > 0
71
+ || Boolean(groupTitle)
72
+ || Number.isFinite(groupId)
73
+ || (typeof windowId === "number" && Number.isFinite(windowId))
74
+ || (typeof windowId === "string" && windowId.length > 0);
75
+ return { tabIds, groupTitle, groupId, windowId, hasScope, ungrouped };
76
+ }
77
+ function normalizeWindowScope(value) {
78
+ if (typeof value === "string") {
79
+ const trimmed = value.trim().toLowerCase();
80
+ if (trimmed === "active") {
81
+ return "active";
82
+ }
83
+ if (trimmed === "last-focused" || trimmed === "lastfocused") {
84
+ return "last-focused";
85
+ }
86
+ if (trimmed === "new") {
87
+ (0, output_1.errorOut)("--window new is only supported by open");
88
+ }
89
+ const numeric = Number(trimmed);
90
+ if (Number.isFinite(numeric)) {
91
+ return numeric;
92
+ }
93
+ (0, output_1.errorOut)("Invalid --window value");
94
+ }
95
+ return typeof value === "number" ? value : String(value);
96
+ }
97
+ function extractScopeParams(options) {
98
+ const scope = resolveScopeFlags(options);
99
+ return {
100
+ tabIds: scope.tabIds.length ? scope.tabIds : undefined,
101
+ groupTitle: scope.groupTitle || undefined,
102
+ groupId: scope.groupId != null ? scope.groupId : undefined,
103
+ windowId: scope.windowId != null ? scope.windowId : undefined,
104
+ all: options.all === true,
105
+ };
106
+ }
107
+ // Helper to build window label index (needed by selectTabsFromSnapshot)
108
+ function buildWindowLabelIndex(snapshot) {
109
+ const windowLabels = new Map();
110
+ const windows = snapshot.windows || [];
111
+ windows.forEach((win, index) => {
112
+ const windowId = win.windowId;
113
+ if (typeof windowId === "number") {
114
+ windowLabels.set(windowId, `W${index + 1}`);
115
+ }
116
+ });
117
+ return windowLabels;
118
+ }
119
+ // Helper to list group summaries (needed by selectTabsFromSnapshot)
120
+ function listGroupSummaries(snapshot, windowLabels) {
121
+ const windows = snapshot.windows || [];
122
+ const summaries = [];
123
+ for (const win of windows) {
124
+ const groups = win.groups || [];
125
+ for (const group of groups) {
126
+ summaries.push({
127
+ windowId: win.windowId,
128
+ windowLabel: windowLabels.get(win.windowId) ?? null,
129
+ groupId: group.groupId,
130
+ title: typeof group.title === "string" ? group.title : null,
131
+ });
132
+ }
133
+ }
134
+ return summaries;
135
+ }
136
+ function selectTabsFromSnapshot(snapshot, params) {
137
+ const windows = snapshot.windows || [];
138
+ const allTabs = windows.flatMap((win) => win.tabs || []);
139
+ if (params.tabIds && params.tabIds.length) {
140
+ const idSet = new Set(params.tabIds.map(Number));
141
+ return { tabs: allTabs.filter((tab) => idSet.has(tab.tabId)) };
142
+ }
143
+ if (params.groupId != null) {
144
+ const groupId = Number(params.groupId);
145
+ return { tabs: allTabs.filter((tab) => tab.groupId === groupId) };
146
+ }
147
+ if (params.groupTitle) {
148
+ const windowLabels = buildWindowLabelIndex(snapshot);
149
+ const matches = [];
150
+ for (const win of windows) {
151
+ const groups = win.groups || [];
152
+ for (const group of groups) {
153
+ if (group.title === params.groupTitle) {
154
+ matches.push({
155
+ windowId: win.windowId,
156
+ windowLabel: windowLabels.get(win.windowId) ?? null,
157
+ groupId: group.groupId,
158
+ });
159
+ }
160
+ }
161
+ }
162
+ const availableGroups = listGroupSummaries(snapshot, windowLabels);
163
+ if (matches.length === 0) {
164
+ return {
165
+ tabs: [],
166
+ error: {
167
+ message: "No matching group title found",
168
+ hint: "Use tabctl group-list to see existing groups.",
169
+ availableGroups,
170
+ },
171
+ };
172
+ }
173
+ if (matches.length > 1 && !params.windowId) {
174
+ return {
175
+ tabs: [],
176
+ error: {
177
+ message: "Group title is ambiguous. Provide a windowId.",
178
+ hint: "Use --window to disambiguate group titles.",
179
+ matches,
180
+ availableGroups,
181
+ },
182
+ };
183
+ }
184
+ const resolvedWindowId = params.windowId != null
185
+ ? resolveWindowIdFromSnapshot(snapshot, params.windowId)
186
+ : null;
187
+ const target = resolvedWindowId != null
188
+ ? matches.find((match) => match.windowId === resolvedWindowId)
189
+ : matches[0];
190
+ if (!target) {
191
+ return {
192
+ tabs: [],
193
+ error: {
194
+ message: "Group title not found in specified window",
195
+ hint: "Use tabctl group-list to see existing groups.",
196
+ matches,
197
+ availableGroups,
198
+ },
199
+ };
200
+ }
201
+ return { tabs: allTabs.filter((tab) => tab.groupId === target.groupId && tab.windowId === target.windowId) };
202
+ }
203
+ if (params.windowId != null) {
204
+ const windowId = resolveWindowIdFromSnapshot(snapshot, params.windowId);
205
+ if (!Number.isFinite(windowId)) {
206
+ return { tabs: [] };
207
+ }
208
+ return { tabs: allTabs.filter((tab) => tab.windowId === windowId) };
209
+ }
210
+ if (params.all) {
211
+ return { tabs: allTabs };
212
+ }
213
+ const focused = windows.find((win) => win.focused);
214
+ return { tabs: focused ? (focused.tabs || []) : [] };
215
+ }
216
+ function resolveWindowIdFromSnapshot(snapshot, value) {
217
+ if (typeof value === "number" && Number.isFinite(value)) {
218
+ return value;
219
+ }
220
+ const windows = snapshot.windows || [];
221
+ if (typeof value === "string") {
222
+ const normalized = value.trim().toLowerCase();
223
+ if (normalized === "active") {
224
+ const focused = windows.find((win) => win.focused === true);
225
+ return typeof focused?.windowId === "number" ? focused.windowId : null;
226
+ }
227
+ if (normalized === "last-focused") {
228
+ let bestWindowId = null;
229
+ let bestFocusedAt = -Infinity;
230
+ for (const win of windows) {
231
+ const tabs = win.tabs || [];
232
+ for (const tab of tabs) {
233
+ const focusedAt = Number(tab.lastFocusedAt);
234
+ if (!Number.isFinite(focusedAt)) {
235
+ continue;
236
+ }
237
+ if (focusedAt > bestFocusedAt) {
238
+ bestFocusedAt = focusedAt;
239
+ bestWindowId = typeof win.windowId === "number" ? win.windowId : null;
240
+ }
241
+ }
242
+ }
243
+ return bestWindowId;
244
+ }
245
+ }
246
+ return Number.isFinite(Number(value)) ? Number(value) : null;
247
+ }
248
+ function filterGroupsByScope(groups, scope, snapshot, buildTabIndex) {
249
+ let filtered = groups;
250
+ const allScope = !scope.hasScope;
251
+ if (!allScope) {
252
+ if (typeof scope.windowId === "number" && Number.isFinite(scope.windowId)) {
253
+ filtered = filtered.filter((group) => group.windowId === scope.windowId);
254
+ }
255
+ if (Number.isFinite(scope.groupId)) {
256
+ filtered = filtered.filter((group) => group.groupId === scope.groupId);
257
+ }
258
+ if (scope.groupTitle) {
259
+ filtered = filtered.filter((group) => group.title === scope.groupTitle);
260
+ }
261
+ if (scope.tabIds.length > 0 && snapshot) {
262
+ const tabIndex = buildTabIndex(snapshot);
263
+ const groupIds = new Set();
264
+ for (const tabId of scope.tabIds) {
265
+ const tab = tabIndex.get(tabId);
266
+ if (!tab) {
267
+ continue;
268
+ }
269
+ const groupId = tab.groupId;
270
+ if (Number.isFinite(groupId) && groupId !== -1) {
271
+ groupIds.add(groupId);
272
+ }
273
+ }
274
+ filtered = filtered.filter((group) => groupIds.has(group.groupId));
275
+ }
276
+ }
277
+ return filtered;
278
+ }
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTabIndex = buildTabIndex;
4
+ exports.buildWindowTitleIndex = buildWindowTitleIndex;
5
+ exports.buildWindowLabelIndex = buildWindowLabelIndex;
6
+ exports.buildGroupsFromSnapshot = buildGroupsFromSnapshot;
7
+ exports.listGroupSummaries = listGroupSummaries;
8
+ exports.compareTabIndex = compareTabIndex;
9
+ exports.orderTabs = orderTabs;
10
+ exports.buildPagedSnapshot = buildPagedSnapshot;
11
+ exports.filterSnapshotByPolicy = filterSnapshotByPolicy;
12
+ const policy_1 = require("./policy");
13
+ function buildTabIndex(snapshot) {
14
+ const tabIndex = new Map();
15
+ const windows = snapshot.windows || [];
16
+ for (const window of windows) {
17
+ const tabs = window.tabs || [];
18
+ for (const tab of tabs) {
19
+ if (typeof tab.tabId === "number") {
20
+ tabIndex.set(tab.tabId, tab);
21
+ }
22
+ }
23
+ }
24
+ return tabIndex;
25
+ }
26
+ function buildWindowTitleIndex(snapshot, policy) {
27
+ const windowTitleIndex = new Map();
28
+ const windows = snapshot.windows || [];
29
+ for (const window of windows) {
30
+ const windowId = window.windowId;
31
+ if (typeof windowId !== "number") {
32
+ continue;
33
+ }
34
+ const tabs = window.tabs || [];
35
+ const activeTab = tabs.find((tab) => tab.active === true);
36
+ if (!activeTab) {
37
+ windowTitleIndex.set(windowId, null);
38
+ continue;
39
+ }
40
+ const { eligible } = (0, policy_1.evaluateTab)(activeTab, policy);
41
+ if (!eligible) {
42
+ windowTitleIndex.set(windowId, null);
43
+ continue;
44
+ }
45
+ const title = typeof activeTab.title === "string" ? activeTab.title : null;
46
+ windowTitleIndex.set(windowId, title);
47
+ }
48
+ return windowTitleIndex;
49
+ }
50
+ function buildWindowLabelIndex(snapshot) {
51
+ const windowLabels = new Map();
52
+ const windows = snapshot.windows || [];
53
+ windows.forEach((win, index) => {
54
+ const windowId = win.windowId;
55
+ if (typeof windowId === "number") {
56
+ windowLabels.set(windowId, `W${index + 1}`);
57
+ }
58
+ });
59
+ return windowLabels;
60
+ }
61
+ function buildGroupsFromSnapshot(snapshot, windowId) {
62
+ const windows = snapshot.windows || [];
63
+ const windowLabels = buildWindowLabelIndex(snapshot);
64
+ const groups = [];
65
+ for (const win of windows) {
66
+ const winId = win.windowId;
67
+ if (!Number.isFinite(winId)) {
68
+ continue;
69
+ }
70
+ if (Number.isFinite(windowId) && winId !== windowId) {
71
+ continue;
72
+ }
73
+ const counts = new Map();
74
+ const tabs = win.tabs || [];
75
+ for (const tab of tabs) {
76
+ const groupId = tab.groupId;
77
+ if (typeof groupId === "number" && groupId !== -1) {
78
+ counts.set(groupId, (counts.get(groupId) || 0) + 1);
79
+ }
80
+ }
81
+ const windowGroups = win.groups || [];
82
+ for (const group of windowGroups) {
83
+ const groupId = group.groupId;
84
+ if (!Number.isFinite(groupId)) {
85
+ continue;
86
+ }
87
+ groups.push({
88
+ windowId: winId,
89
+ windowLabel: windowLabels.get(winId) ?? null,
90
+ groupId,
91
+ title: group.title ?? null,
92
+ color: group.color ?? null,
93
+ collapsed: group.collapsed ?? null,
94
+ tabCount: counts.get(groupId) || 0,
95
+ });
96
+ }
97
+ }
98
+ return groups;
99
+ }
100
+ function listGroupSummaries(snapshot, windowLabels) {
101
+ const windows = snapshot.windows || [];
102
+ const summaries = [];
103
+ for (const win of windows) {
104
+ const groups = win.groups || [];
105
+ for (const group of groups) {
106
+ summaries.push({
107
+ windowId: win.windowId,
108
+ windowLabel: windowLabels.get(win.windowId) ?? null,
109
+ groupId: group.groupId,
110
+ title: typeof group.title === "string" ? group.title : null,
111
+ });
112
+ }
113
+ }
114
+ return summaries;
115
+ }
116
+ function compareTabIndex(a, b) {
117
+ const aIndex = Number(a.index);
118
+ const bIndex = Number(b.index);
119
+ if (!Number.isFinite(aIndex) && !Number.isFinite(bIndex)) {
120
+ return 0;
121
+ }
122
+ if (!Number.isFinite(aIndex)) {
123
+ return 1;
124
+ }
125
+ if (!Number.isFinite(bIndex)) {
126
+ return -1;
127
+ }
128
+ if (aIndex === bIndex) {
129
+ const aId = Number(a.tabId);
130
+ const bId = Number(b.tabId);
131
+ if (Number.isFinite(aId) && Number.isFinite(bId)) {
132
+ return aId - bId;
133
+ }
134
+ return 0;
135
+ }
136
+ return aIndex - bIndex;
137
+ }
138
+ function orderTabs(snapshot, tabFilter) {
139
+ const windows = snapshot.windows || [];
140
+ const ordered = [];
141
+ for (const win of windows) {
142
+ const tabs = (win.tabs || []).slice().sort(compareTabIndex);
143
+ for (const tab of tabs) {
144
+ const tabId = tab.tabId;
145
+ if (!tabFilter || tabFilter.has(tabId)) {
146
+ ordered.push(tab);
147
+ }
148
+ }
149
+ }
150
+ return ordered;
151
+ }
152
+ function buildPagedSnapshot(snapshot, tabs) {
153
+ const tabsByWindow = new Map();
154
+ const groupsByWindow = new Map();
155
+ for (const tab of tabs) {
156
+ const windowId = tab.windowId;
157
+ if (!Number.isFinite(windowId)) {
158
+ continue;
159
+ }
160
+ if (!tabsByWindow.has(windowId)) {
161
+ tabsByWindow.set(windowId, []);
162
+ groupsByWindow.set(windowId, new Set());
163
+ }
164
+ tabsByWindow.get(windowId)?.push(tab);
165
+ const groupId = tab.groupId;
166
+ if (Number.isFinite(groupId) && groupId !== -1) {
167
+ groupsByWindow.get(windowId)?.add(groupId);
168
+ }
169
+ }
170
+ const windows = snapshot.windows || [];
171
+ const pagedWindows = [];
172
+ for (const win of windows) {
173
+ const windowId = win.windowId;
174
+ const windowTabs = tabsByWindow.get(windowId) || [];
175
+ if (windowTabs.length === 0) {
176
+ continue;
177
+ }
178
+ const allowedGroupIds = groupsByWindow.get(windowId) || new Set();
179
+ const groups = (win.groups || []).filter((group) => allowedGroupIds.has(group.groupId));
180
+ pagedWindows.push({
181
+ ...win,
182
+ tabs: windowTabs,
183
+ groups,
184
+ });
185
+ }
186
+ return {
187
+ ...snapshot,
188
+ windows: pagedWindows,
189
+ };
190
+ }
191
+ function filterSnapshotByPolicy(snapshot, policy) {
192
+ if (!policy) {
193
+ return snapshot;
194
+ }
195
+ const windows = snapshot.windows || [];
196
+ const filteredWindows = windows.map((win) => {
197
+ const tabs = win.tabs || [];
198
+ const eligibleTabs = tabs
199
+ .filter((tab) => (0, policy_1.evaluateTab)(tab, policy).eligible)
200
+ .map((tab) => (0, policy_1.annotateEntry)(tab, policy));
201
+ const eligibleGroupIds = new Set(eligibleTabs
202
+ .map((tab) => tab.groupId)
203
+ .filter((groupId) => typeof groupId === "number" && groupId !== -1));
204
+ const groups = win.groups || [];
205
+ const filteredGroups = groups.filter((group) => eligibleGroupIds.has(group.groupId));
206
+ return {
207
+ ...win,
208
+ tabs: eligibleTabs,
209
+ groups: filteredGroups,
210
+ };
211
+ }).filter((win) => win.tabs.length > 0);
212
+ return {
213
+ ...snapshot,
214
+ windows: filteredWindows,
215
+ };
216
+ }