chromium-tabs 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,761 @@
1
+ /**
2
+ * Port of ui/base/models/list_selection_model.{h,cc}.
3
+ *
4
+ * Selection model represented as a list of indexes. In addition to the set of
5
+ * selected indices it maintains:
6
+ *
7
+ * - active: the index of the currently visible item, or null if nothing is
8
+ * selected.
9
+ * - anchor: the index of the last item the user clicked on. Extending the
10
+ * selection extends it from this index. Null if nothing is selected.
11
+ *
12
+ * Typically there is one selected item, in which case anchor and active are
13
+ * the same.
14
+ */
15
+ declare class ListSelectionModel {
16
+ private selectedIndices_;
17
+ private active_;
18
+ private anchor_;
19
+ get anchor(): number | null;
20
+ setAnchor(anchor: number | null): void;
21
+ get active(): number | null;
22
+ setActive(active: number | null): void;
23
+ /** True if nothing is selected. */
24
+ get empty(): boolean;
25
+ get size(): number;
26
+ /** Selected indices in ascending order. */
27
+ selectedIndices(): number[];
28
+ /**
29
+ * Increments all indices >= index. Used when a new item is inserted.
30
+ * list_selection_model.cc:112
31
+ */
32
+ incrementFrom(index: number): void;
33
+ /**
34
+ * Shifts all indices > index down by 1; index itself is removed from the
35
+ * selection. Used when an item is removed. list_selection_model.cc:122
36
+ */
37
+ decrementFrom(index: number): void;
38
+ /** Sets the anchor, active and selection to index. */
39
+ setSelectedIndex(index: number | null): void;
40
+ isSelected(index: number): boolean;
41
+ /** Adds index to the selection without changing active or anchor. */
42
+ addIndexToSelection(index: number): void;
43
+ /** Adds [indexStart, indexEnd] inclusive without changing active or anchor. */
44
+ addIndexRangeToSelection(indexStart: number, indexEnd: number): void;
45
+ /** Removes index from the selection without changing active or anchor. */
46
+ removeIndexFromSelection(index: number): void;
47
+ /**
48
+ * Sets the selection to the range anchor..index. If there is no anchor,
49
+ * behaves like setSelectedIndex. list_selection_model.cc:171
50
+ */
51
+ setSelectionFromAnchorTo(index: number): void;
52
+ /**
53
+ * Makes sure anchor..index are selected, adding to the existing selection.
54
+ * list_selection_model.cc:186
55
+ */
56
+ addSelectionFromAnchorTo(index: number): void;
57
+ /**
58
+ * Invoked when `length` items move from oldIndex to newIndex. If moving to
59
+ * a greater index, newIndex is the index *after* removing the moved range.
60
+ * list_selection_model.cc:199
61
+ */
62
+ move(oldIndex: number, newIndex: number, length: number): void;
63
+ /** Clears the selection, anchor and active. */
64
+ clear(): void;
65
+ clone(): ListSelectionModel;
66
+ equals(other: ListSelectionModel): boolean;
67
+ /** 'active=X anchor=X selection=X X X...' — matches the C++ ToString. */
68
+ toString(): string;
69
+ }
70
+
71
+ /**
72
+ * Core types. Ported from chromium-reference/chrome/browser/ui/tabs/tab_enums.h
73
+ * and the supporting types in tab_strip_model.h.
74
+ *
75
+ * Chrome's tabs carry a WebContents. This library carries a generic `data: T`
76
+ * payload instead so it works for any application.
77
+ */
78
+ /** Sentinel for "no tab" — mirrors TabStripModel::kNoTab. */
79
+ declare const NO_TAB = -1;
80
+ type TabId = string;
81
+ type TabGroupId = string;
82
+ /**
83
+ * How a tab is being opened. Trimmed from ui::PageTransition to the two
84
+ * qualities the TabStripModel actually branches on (tab_strip_model.cc:1731,
85
+ * 1786): link-like openings (inherit opener, insert adjacent) and typed/manual
86
+ * openings (append at end, transient opener at end-of-strip).
87
+ */
88
+ type TabOpenCause = 'link' | 'typed' | 'other';
89
+ /** Bitmask flags used when adding tabs. Mirrors AddTabTypes (tab_enums.h:42). */
90
+ declare const AddTabFlags: {
91
+ readonly NONE: 0;
92
+ /** The tab should become the active tab. */
93
+ readonly ACTIVE: number;
94
+ /** The tab should be pinned. */
95
+ readonly PINNED: number;
96
+ /**
97
+ * Use the caller-supplied index rather than letting the model determine
98
+ * the position from the open cause and opener relationships.
99
+ */
100
+ readonly FORCE_INDEX: number;
101
+ /** Set the new tab's opener to the currently active tab. */
102
+ readonly INHERIT_OPENER: number;
103
+ };
104
+ /** Bitmask flags used when closing tabs. Mirrors TabCloseTypes (tab_enums.h:26). */
105
+ declare const CloseTabFlags: {
106
+ readonly NONE: 0;
107
+ /** The close was triggered directly by a user gesture. */
108
+ readonly USER_GESTURE: number;
109
+ };
110
+ /**
111
+ * Visual data attached to a tab group. Mirrors tab_groups::TabGroupVisualData.
112
+ * Chrome cycles through 9 named colors; we keep the names so UIs can map them
113
+ * to whatever palette they like.
114
+ */
115
+ type TabGroupColor = 'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange';
116
+ declare const TAB_GROUP_COLORS: readonly TabGroupColor[];
117
+ interface TabGroupVisualData {
118
+ title: string;
119
+ color: TabGroupColor;
120
+ isCollapsed: boolean;
121
+ }
122
+ /**
123
+ * A tab in the strip. Identity is the object (like Chrome's TabModel pointer);
124
+ * `id` exists for React keys and serialization.
125
+ */
126
+ interface Tab<T = unknown> {
127
+ readonly id: TabId;
128
+ /** Application payload (your "WebContents"). */
129
+ data: T;
130
+ /** The tab that opened this tab, if tracked. Mirrors TabModel::opener(). */
131
+ opener: Tab<T> | null;
132
+ /**
133
+ * When true, the opener relationship is cleared the next time the active
134
+ * tab changes. Set for typed new-tabs at end of strip ("quick look-up"
135
+ * pattern, tab_strip_model.cc:1804-1810).
136
+ */
137
+ resetOpenerOnActiveTabChange: boolean;
138
+ /** Pinned tabs are locked to the left side of the strip. */
139
+ pinned: boolean;
140
+ /** Group membership, if any. Pinned tabs are never grouped. */
141
+ group: TabGroupId | null;
142
+ /** Blocked by a modal — selectable but flagged. Mirrors TabModel::blocked. */
143
+ blocked: boolean;
144
+ /**
145
+ * True when the tab's content has been dropped to save memory. The tab
146
+ * stays in the strip (title intact via `data`); content remounts fresh
147
+ * when the tab is next activated. Mirrors TabLifecycleUnit::is_discarded_
148
+ * (tab_lifecycle_unit.cc:312) and WebContents::WasDiscarded.
149
+ */
150
+ discarded: boolean;
151
+ /**
152
+ * Wall-clock ms of the last time this tab stopped being active, or
153
+ * Infinity while it is active. Used for least-recently-used discard
154
+ * ordering. Mirrors last_focused_time_ (tab_lifecycle_unit.cc:142, which
155
+ * uses Time::Max() while focused).
156
+ */
157
+ lastActiveAt: number;
158
+ /**
159
+ * Per-tab opt-out from automatic discarding. Mirrors
160
+ * TabLifecycleUnit::auto_discardable_ (the extensions setAutoDiscardable
161
+ * API surface).
162
+ */
163
+ autoDiscardable: boolean;
164
+ }
165
+ interface TabGroup {
166
+ readonly id: TabGroupId;
167
+ visualData: TabGroupVisualData;
168
+ }
169
+ /** Options for TabStripModel.addTab / insertTabAt. */
170
+ interface AddTabOptions {
171
+ /** Target index. Omit (or pass NO_TAB) to let the model decide. */
172
+ index?: number;
173
+ /** What kind of action opened this tab. Default 'other'. */
174
+ cause?: TabOpenCause;
175
+ /** Bitmask of AddTabFlags. */
176
+ flags?: number;
177
+ /** Insert directly into an existing group. */
178
+ group?: TabGroupId;
179
+ /** Stable id; generated if omitted. */
180
+ id?: TabId;
181
+ }
182
+ /** Desired state of one tab, for TabStripModel.reconcile. */
183
+ interface ReconcileTab<T> {
184
+ id: TabId;
185
+ data: T;
186
+ /** Defaults to false. The list should be pinned-first-consistent. */
187
+ pinned?: boolean;
188
+ }
189
+ interface ReconcileOptions<T> {
190
+ /** Tab to end up active. Omit to leave activation to the model. */
191
+ activeId?: TabId | null;
192
+ /** Equality for data payloads; defaults to Object.is. */
193
+ dataEquals?: (a: T, b: T) => boolean;
194
+ }
195
+ interface TabStripModelOptions<T> {
196
+ /**
197
+ * Veto tab closes (Chrome: IsTabClosable / policy). Return false to keep
198
+ * the tab open; observers get a tabCloseCancelled event.
199
+ */
200
+ canCloseTab?: (tab: Tab<T>) => boolean;
201
+ /** Disable the group feature entirely (Chrome: null TabGroupModelFactory). */
202
+ supportsGroups?: boolean;
203
+ /** Custom id generator for tabs and groups. */
204
+ generateId?: () => string;
205
+ }
206
+
207
+ /**
208
+ * Observer event types. Ported from
209
+ * chromium-reference/chrome/browser/ui/tabs/tab_strip_model_observer.h.
210
+ *
211
+ * Changes to (1) the selection model, (2) the active tab, and (3) the set of
212
+ * tabs are bundled into a single onTabStripModelChanged call because the
213
+ * first two consist of indices into the list determined by the third.
214
+ */
215
+
216
+ /** Mirrors TabStripModelChange::Type. */
217
+ type TabStripModelChange<T> = {
218
+ type: 'selectionOnly';
219
+ } | {
220
+ /**
221
+ * Tabs were inserted at the given indices (index is the position at the
222
+ * time of that insertion, see tab_strip_model_observer.h:88).
223
+ */
224
+ type: 'inserted';
225
+ contents: Array<{
226
+ tab: Tab<T>;
227
+ index: number;
228
+ }>;
229
+ } | {
230
+ /**
231
+ * Tabs were removed; index is the position at the time of that removal
232
+ * (tab_strip_model_observer.h:123).
233
+ */
234
+ type: 'removed';
235
+ contents: Array<{
236
+ tab: Tab<T>;
237
+ index: number;
238
+ }>;
239
+ } | {
240
+ type: 'moved';
241
+ tab: Tab<T>;
242
+ fromIndex: number;
243
+ toIndex: number;
244
+ } | {
245
+ type: 'replaced';
246
+ tab: Tab<T>;
247
+ oldData: T;
248
+ newData: T;
249
+ index: number;
250
+ };
251
+ /** Mirrors TabStripSelectionChange. */
252
+ interface TabStripSelectionChange<T> {
253
+ oldTab: Tab<T> | null;
254
+ newTab: Tab<T> | null;
255
+ oldModel: ListSelectionModel;
256
+ newModel: ListSelectionModel;
257
+ /** Mirrors TabStripModelObserver::CHANGE_REASON_USER_GESTURE. */
258
+ reason: 'none' | 'userGesture';
259
+ get activeTabChanged(): boolean;
260
+ get selectionChanged(): boolean;
261
+ }
262
+ /** Mirrors TabGroupChange::Type (minus editor-UI concerns). */
263
+ type TabGroupChange = {
264
+ type: 'created';
265
+ groupId: TabGroupId;
266
+ } | {
267
+ type: 'visualsChanged';
268
+ groupId: TabGroupId;
269
+ oldVisuals: TabGroupVisualData;
270
+ newVisuals: TabGroupVisualData;
271
+ } | {
272
+ type: 'moved';
273
+ groupId: TabGroupId;
274
+ } | {
275
+ type: 'closed';
276
+ groupId: TabGroupId;
277
+ };
278
+ type CloseAllStoppedReason = 'completed' | 'canceled';
279
+ /**
280
+ * Observer interface. All methods optional — implement what you need.
281
+ * Mirrors TabStripModelObserver.
282
+ */
283
+ interface TabStripModelObserver<T> {
284
+ onTabStripModelChanged?(change: TabStripModelChange<T>, selection: TabStripSelectionChange<T>): void;
285
+ /** A tab's pinned state changed. Fired after any accompanying move. */
286
+ onTabPinnedStateChanged?(tab: Tab<T>, index: number): void;
287
+ /** A tab entered or left a group. */
288
+ onTabGroupedStateChanged?(oldGroup: TabGroupId | null, newGroup: TabGroupId | null, tab: Tab<T>, index: number): void;
289
+ /** Group lifecycle and visual changes. */
290
+ onTabGroupChanged?(change: TabGroupChange): void;
291
+ /** A tab's data payload or blocked state changed in place. */
292
+ onTabChanged?(tab: Tab<T>, index: number): void;
293
+ /**
294
+ * A tab was discarded (content dropped to save memory) or restored.
295
+ * Mirrors TabLifecycleObserver::OnDiscardedStateChange.
296
+ */
297
+ onTabDiscardedStateChanged?(tab: Tab<T>, index: number, discarded: boolean): void;
298
+ /** A close was vetoed by canCloseTab. */
299
+ onTabCloseCancelled?(tab: Tab<T>): void;
300
+ /** CloseAllTabs is starting. */
301
+ willCloseAllTabs?(): void;
302
+ /** CloseAllTabs finished or was canceled by a veto. */
303
+ closeAllTabsStopped?(reason: CloseAllStoppedReason): void;
304
+ }
305
+
306
+ /**
307
+ * Port of chrome/browser/ui/tabs/tab_strip_model.{h,cc} (Chromium main
308
+ * @ 3dbd2135). Line references in comments point into chromium-reference/.
309
+ *
310
+ * Differences from Chrome are listed in PORTING_NOTES.md. The big ones:
311
+ * tabs carry generic `data: T` instead of WebContents, split tabs are not
312
+ * ported, and there is no async unload-handler dance — closes are
313
+ * synchronous, vetoable via `canCloseTab`.
314
+ *
315
+ * Like Chrome, the model maintains these invariants:
316
+ * - all pinned tabs occur before all non-pinned tabs
317
+ * - tabs of a group are contiguous, and never pinned
318
+ * - the active tab is always valid while the strip is non-empty
319
+ */
320
+
321
+ /** [start, end) index range, mirroring gfx::Range usage for group spans. */
322
+ interface IndexRange {
323
+ start: number;
324
+ end: number;
325
+ }
326
+ interface ActivateOptions {
327
+ /**
328
+ * Marks the activation as a direct user gesture. Mirrors
329
+ * TabStripUserGestureDetails; gates the opener-forgetting heuristic
330
+ * (tab_strip_model.cc:5301).
331
+ */
332
+ userGesture?: boolean;
333
+ }
334
+ declare class TabStripModel<T = unknown> {
335
+ private tabs_;
336
+ private groups_;
337
+ private selectedTabs_;
338
+ private activeTab_;
339
+ private anchorTab_;
340
+ private observers_;
341
+ private closingAll_;
342
+ private reentrancyGuard_;
343
+ private readonly canCloseTab_;
344
+ private readonly supportsGroups_;
345
+ private readonly generateId_;
346
+ constructor(options?: TabStripModelOptions<T>);
347
+ get count(): number;
348
+ get empty(): boolean;
349
+ /** True while closeAllTabs is in progress. */
350
+ get closingAll(): boolean;
351
+ containsIndex(index: number): boolean;
352
+ getTabAt(index: number): Tab<T>;
353
+ indexOfTab(tab: Tab<T> | null): number;
354
+ getTabById(id: TabId): Tab<T> | null;
355
+ /** Snapshot of the tabs in strip order. */
356
+ getTabs(): ReadonlyArray<Tab<T>>;
357
+ get activeTab(): Tab<T> | null;
358
+ get activeIndex(): number;
359
+ /** Index of the first non-pinned tab; count if all pinned. (cc:1418 area) */
360
+ indexOfFirstNonPinnedTab(): number;
361
+ isTabPinned(index: number): boolean;
362
+ isTabBlocked(index: number): boolean;
363
+ isTabSelected(index: number): boolean;
364
+ /** Derived index-based view of the selection (Chrome: GetListSelectionModel). */
365
+ selectionModel(): ListSelectionModel;
366
+ get supportsTabGroups(): boolean;
367
+ getTabGroupForTab(index: number): TabGroupId | null;
368
+ getGroups(): TabGroup[];
369
+ getGroupVisualData(group: TabGroupId): TabGroupVisualData | null;
370
+ containsGroup(group: TabGroupId): boolean;
371
+ /** [start, end) range of the group's tabs. Mirrors TabGroup::ListTabs. */
372
+ listTabsInGroup(group: TabGroupId): IndexRange;
373
+ isGroupCollapsed(group: TabGroupId): boolean;
374
+ /** True if the tab is inside a collapsed group. (cc:1423) */
375
+ isTabCollapsed(index: number): boolean;
376
+ /**
377
+ * If a tab inserted at index would land inside a group, returns that group.
378
+ * Returns null at the first index of a group (a tab there sits between
379
+ * groups, not inside one). Mirrors GetSurroundingTabGroup.
380
+ */
381
+ getSurroundingTabGroup(index: number): TabGroupId | null;
382
+ addObserver(observer: TabStripModelObserver<T>): () => void;
383
+ removeObserver(observer: TabStripModelObserver<T>): void;
384
+ /**
385
+ * Command-level add: picks the position from the open cause and opener
386
+ * relationships, then inserts. Port of AddTab (cc:1715-1828).
387
+ */
388
+ addTab(data: T, options?: AddTabOptions): Tab<T>;
389
+ /** Adds a tab at the end of the strip. Port of AppendTab. */
390
+ appendTab(data: T, foreground?: boolean): Tab<T>;
391
+ /**
392
+ * Inserts at the given index, only adjusting it to keep pinned tabs at the
393
+ * front. Does NOT consult the order controller. Port of InsertWebContentsAt.
394
+ * Returns the index actually used.
395
+ */
396
+ insertTabAt(index: number, data: T, options?: Omit<AddTabOptions, 'index' | 'cause'>): Tab<T>;
397
+ /** Makes the tab at index active. Port of ActivateTabAt (cc:1022). */
398
+ activateTabAt(index: number, options?: ActivateOptions): void;
399
+ /** Extends the selection from the anchor to index. Port of cc:1514. */
400
+ extendSelectionTo(index: number): void;
401
+ /** Adds the tab at index to the selection and makes it active+anchor. (cc:1545) */
402
+ selectTabAt(index: number): void;
403
+ /** Removes the tab at index from the selection. No-op if it's the last one. (cc:1570) */
404
+ deselectTabAt(index: number): void;
405
+ /** Selects anchor..index, adding to the current selection. (cc:1616) */
406
+ addSelectionFromAnchorTo(index: number): void;
407
+ /** Replaces the selection with the given index-based model. */
408
+ setSelectionFromModel(source: ListSelectionModel): void;
409
+ /**
410
+ * Activates the next/previous tab, wrapping around and skipping collapsed
411
+ * groups. Port of SelectRelativeTab (cc:3951).
412
+ */
413
+ selectNextTab(options?: ActivateOptions): void;
414
+ selectPreviousTab(options?: ActivateOptions): void;
415
+ selectLastTab(options?: ActivateOptions): void;
416
+ private selectRelativeTab_;
417
+ /**
418
+ * Moves the tab at index to toPosition (clamped so pinned tabs stay
419
+ * together). Group membership adjusts to keep groups contiguous. Port of
420
+ * MoveWebContentsAt (cc:1053). Returns the final index.
421
+ */
422
+ moveTabTo(index: number, toPosition: number, selectAfterMove?: boolean): number;
423
+ /**
424
+ * Moves the selected tabs to index, pinned tabs first as a chunk, then
425
+ * unpinned. `index` is interpreted as if the strip did not contain the
426
+ * selected tabs. Port of MoveSelectedTabsTo (cc:1089).
427
+ */
428
+ moveSelectedTabsTo(index: number, group?: TabGroupId | null): void;
429
+ /** Moves all tabs of a group to toIndex. Port of MoveGroupTo (cc:1117). */
430
+ moveGroupTo(group: TabGroupId, toIndex: number): void;
431
+ /**
432
+ * Moves the active tab one slot right/left. At a group boundary the tab
433
+ * first changes group membership without moving; collapsed neighbor groups
434
+ * are hopped over entirely. Port of MoveTabRelative (cc:3976).
435
+ */
436
+ moveTabNext(): void;
437
+ moveTabPrevious(): void;
438
+ private moveTabRelative_;
439
+ /**
440
+ * Pins or unpins the tab, moving it to the pinned/unpinned boundary.
441
+ * Pinning removes the tab from its group. Returns the final index.
442
+ * Port of SetTabPinned (cc:1407) + SetTabPinnedImpl (cc:5052).
443
+ */
444
+ setTabPinned(index: number, pinned: boolean): number;
445
+ /** Closes the tab at index. Returns true if it closed (not vetoed). */
446
+ closeTabAt(index: number): boolean;
447
+ /** Closes the tabs at the given indices. */
448
+ closeTabsAt(indices: number[]): boolean;
449
+ /** Port of CloseSelectedTabs. */
450
+ closeSelectedTabs(): boolean;
451
+ /** Port of CloseAllTabs (cc:455). */
452
+ closeAllTabs(): boolean;
453
+ /** Context-menu style helper: close every tab except the one at index. */
454
+ closeOtherTabs(index: number): boolean;
455
+ /** Context-menu style helper: close unpinned tabs to the right of index. */
456
+ closeTabsToRight(index: number): boolean;
457
+ /** Closes all tabs in a group. Port of CloseAllTabsInGroup. */
458
+ closeAllTabsInGroup(group: TabGroupId): boolean;
459
+ private closeTabs_;
460
+ /**
461
+ * Removes the tab and fixes up the selection. Port of
462
+ * RemoveTabFromIndexImpl (cc:4551). Caller batches notifications.
463
+ */
464
+ private removeTabFromIndexImpl_;
465
+ /**
466
+ * Converges the strip to an external source-of-truth list with minimal
467
+ * mutations (no remove-all/re-add), so an app whose canonical tab state
468
+ * lives elsewhere (a router, a store, another window) can mirror it into
469
+ * the model without losing tab identity, content state, or discard status.
470
+ *
471
+ * - tabs absent from `desired` are removed, bypassing `canCloseTab` (the
472
+ * external state has already decided)
473
+ * - missing tabs are inserted at their position under the given id
474
+ * - `data` is swapped via setTabData where `dataEquals` reports a change
475
+ * - pinned state and order converge to the desired list; pass a
476
+ * pinned-first-consistent list or Chrome's clamping rules win
477
+ * - `activeId`, when provided and present, is activated last
478
+ *
479
+ * Observers fire for each underlying mutation as usual; reconcile-driven
480
+ * activations carry reason 'none', so a consumer that also writes model
481
+ * changes back to the external store can tell them from user gestures.
482
+ * Groups are not reconciled. No Chrome equivalent: this is integration
483
+ * surface for embedding apps.
484
+ */
485
+ reconcile(desired: ReadonlyArray<ReconcileTab<T>>, options?: ReconcileOptions<T>): void;
486
+ getOpenerOfTabAt(index: number): Tab<T> | null;
487
+ setOpenerOfTabAt(index: number, opener: Tab<T> | null): void;
488
+ /** Port of ForgetAllOpeners (cc:3376). */
489
+ forgetAllOpeners(): void;
490
+ forgetOpener(tab: Tab<T>): void;
491
+ /**
492
+ * Call when the user navigates a tab. Typed-style navigations reset all
493
+ * opener relationships (the user started a new task), except in a fresh
494
+ * end-of-strip tab. Port of TabNavigating (cc:1378).
495
+ */
496
+ tabNavigating(tab: Tab<T>, cause: TabOpenCause): void;
497
+ /**
498
+ * Index of the last tab opened (transitively) by the tab at startIndex,
499
+ * scanning right, skipping pinned tabs, stopping at the first unrelated
500
+ * unpinned tab. Port of GetIndexOfLastWebContentsOpenedBy (cc:1351).
501
+ */
502
+ getIndexOfLastTabOpenedBy(opener: Tab<T>, startIndex: number): number;
503
+ /**
504
+ * Creates a group containing the tabs at indices (ascending). Tabs are
505
+ * unpinned and made contiguous without splitting other groups. Returns the
506
+ * group id. Port of AddToNewGroup (cc:671) + AddToNewGroupImpl (cc:4344).
507
+ */
508
+ addToNewGroup(indices: number[], visualData?: Partial<TabGroupVisualData>): TabGroupId;
509
+ /**
510
+ * Adds the tabs at indices (ascending) to an existing group. Tabs left of
511
+ * the group move to its start, tabs right of it to its end; addToEnd sends
512
+ * everything to the end. Port of AddToExistingGroup (cc:4415).
513
+ */
514
+ addToExistingGroup(indices: number[], group: TabGroupId, addToEnd?: boolean): void;
515
+ /**
516
+ * Removes the tabs at indices (ascending) from their groups. Tabs in the
517
+ * first half of a group exit left of it, the rest exit right. Port of
518
+ * RemoveFromGroup (cc:4253 area) + SeparateTabsByVisualPosition.
519
+ */
520
+ removeFromGroup(indices: number[]): void;
521
+ /** Updates a group's title/color/collapsed state. Port of ChangeTabGroupVisuals. */
522
+ updateGroupVisuals(group: TabGroupId, visuals: Partial<TabGroupVisualData>): void;
523
+ setGroupCollapsed(group: TabGroupId, collapsed: boolean): void;
524
+ /** Chrome's TabGroupModel::GetNextColor: least-used color, in palette order. */
525
+ private nextGroupColor_;
526
+ /** Swaps the tab's data payload. Emits a 'replaced' change (Chrome: Replace). */
527
+ setTabData(index: number, data: T): void;
528
+ /** Notify observers the tab changed in place (after mutating tab.data). */
529
+ notifyTabChanged(index: number): void;
530
+ /** Port of SetTabBlocked (cc:1397). */
531
+ setTabBlocked(index: number, blocked: boolean): void;
532
+ /**
533
+ * Drops the tab's content to save memory while keeping the tab in the
534
+ * strip. The active tab cannot be discarded (it's visible). Content
535
+ * remounts fresh on the next activation, like Chrome's reload-on-focus.
536
+ * Port of TabLifecycleUnit::Discard (tab_lifecycle_unit.cc:346) +
537
+ * TabStripModel::DiscardWebContentsAt semantics. Returns true on success.
538
+ */
539
+ discardTabAt(index: number): boolean;
540
+ /**
541
+ * Restores a discarded tab without activating it (Chrome: reloading a
542
+ * background discarded tab, DidStartLoading path).
543
+ */
544
+ restoreTabAt(index: number): boolean;
545
+ /** Per-tab opt-out from automatic discarding (extensions setAutoDiscardable). */
546
+ setTabAutoDiscardable(index: number, autoDiscardable: boolean): void;
547
+ isTabDiscarded(index: number): boolean;
548
+ /** Number of tabs whose content is currently live (not discarded). */
549
+ get loadedTabCount(): number;
550
+ private restoreTab_;
551
+ /**
552
+ * Where to place a newly opened tab. Port of DetermineInsertionIndex
553
+ * (cc:5329).
554
+ */
555
+ determineInsertionIndex(cause: TabOpenCause, foreground: boolean): number;
556
+ /**
557
+ * Which tab should become active after the tab at `index` closes.
558
+ * Port of DetermineNewSelectedIndex (cc:5377), single-tab block, with the
559
+ * "parent collection" preference specialized to groups. Returns the index
560
+ * in post-close coordinates, or null if this is the last tab.
561
+ */
562
+ private determineNewSelectedIndex_;
563
+ /** Port of GetIndexOfNextWebContentsOpenedBy (cc:3286). */
564
+ private getIndexOfNextTabOpenedBy_;
565
+ /** Port of GetIndexOfNextWebContentsOpenedByOpenerOf (cc:3312). */
566
+ private getIndexOfNextTabOpenedByOpenerOf_;
567
+ /** Port of GetNextExpandedActiveTab (cc:3346): right of block, then left. */
568
+ private getNextExpandedActiveTab_;
569
+ private createTab_;
570
+ /** Port of ConstrainInsertionIndex (cc:3408). */
571
+ private constrainInsertionIndex_;
572
+ /** Port of ConstrainMoveIndex (cc:3413). */
573
+ private constrainMoveIndex_;
574
+ /** Port of InsertTabAtImpl (cc:3575). Returns the index actually used. */
575
+ private insertTabAtImpl_;
576
+ /**
577
+ * Single-tab move with explicit final group/pin state. Port of
578
+ * MoveTabToIndexImpl (cc:4617).
579
+ */
580
+ private moveTabToIndexImpl_;
581
+ /**
582
+ * Block move: removes the tabs at `indices`, reinserts them contiguously at
583
+ * `destination` (post-removal coordinates), assigning the given group and
584
+ * the pinned state of the first moving tab. Port of MoveTabsToIndexImpl
585
+ * (cc:4698) + MoveTabsWithNotifications (cc:5081).
586
+ */
587
+ private moveTabsToIndexImpl_;
588
+ /**
589
+ * Port of MoveTabsAndSetPropertiesImpl (cc:4469): destination is given in
590
+ * pre-removal coordinates and adjusted here.
591
+ */
592
+ private moveTabsAndSetProperties_;
593
+ /**
594
+ * Group to assign when a tab moves from index to toPosition so groups stay
595
+ * contiguous. Port of GetGroupToAssign (cc:5195).
596
+ */
597
+ private getGroupToAssign_;
598
+ /**
599
+ * Re-points the openers of any tab that referenced the tab at index at that
600
+ * tab's own opener. Port of FixOpeners (cc:5171).
601
+ */
602
+ private fixOpeners_;
603
+ /** Selection helpers (Chrome: TabStripModelSelectionState). */
604
+ private setSelectedTab_;
605
+ private firstSelectedTab_;
606
+ private selectedIndices_;
607
+ /**
608
+ * Wraps a selection mutation in change tracking + notification. Mirrors
609
+ * SetSelection (cc:1178).
610
+ */
611
+ private setSelection_;
612
+ private buildSelectionChange_;
613
+ /**
614
+ * Opener and lifecycle bookkeeping when the active tab changes. Port of
615
+ * OnActiveTabChanged (cc:5255) plus TabLifecycleUnit::SetFocused
616
+ * (tab_lifecycle_unit.cc:135).
617
+ */
618
+ private handleActiveTabChanged_;
619
+ private emitGroupStateChange_;
620
+ private notifyAll_;
621
+ private requireGroups_;
622
+ private checkReentrancy_;
623
+ /**
624
+ * Invariant validation. Port of CompleteModelUpdateTransaction; Chrome
625
+ * CHECKs, we throw.
626
+ */
627
+ private validate_;
628
+ }
629
+
630
+ /**
631
+ * Automatic tab discarding. Port of Chrome's tab lifecycle stack:
632
+ *
633
+ * - chrome/browser/resource_coordinator/tab_manager.cc (orchestration)
634
+ * - chrome/browser/performance_manager/policies/discard_eligibility_policy.h
635
+ * (CanDiscardResult tri-state, CannotDiscardReason, the importance sort in
636
+ * PageNodeSortProxy::operator<, kNonVisiblePagesUrgentProtectionTime)
637
+ * - chrome/browser/resource_coordinator/tab_lifecycle_unit.cc (discard
638
+ * mechanics live on the model as discardTabAt/restore-on-focus)
639
+ *
640
+ * Chrome triggers discards on OS memory pressure, which the web platform
641
+ * does not expose reliably. The deterministic equivalent is a budget on the
642
+ * number of loaded (mounted) tabs: when `maxLoadedTabs` is exceeded, the
643
+ * least important tabs are discarded, in exactly Chrome's importance order.
644
+ */
645
+
646
+ /** Mirrors LifecycleUnitDiscardReason (minus Chrome-internal variants). */
647
+ type DiscardReason = 'proactive' | 'urgent' | 'external';
648
+ /** Trimmed CannotDiscardReason (cannot_discard_reason.h). */
649
+ type CannotDiscardReason = 'activeTab' | 'alreadyDiscarded' | 'optedOut' | 'appVeto' | 'pinnedTab' | 'recentlyActive';
650
+ /** Mirrors CanDiscardResult (discard_eligibility_policy.h:56). */
651
+ type CanDiscardResult = 'eligible' | 'protected' | 'disallowed';
652
+ interface CanDiscardDecision {
653
+ result: CanDiscardResult;
654
+ reasons: CannotDiscardReason[];
655
+ }
656
+ interface TabLifecycleOptions<T> {
657
+ /**
658
+ * Maximum number of tabs whose content stays loaded at once. Exceeding it
659
+ * triggers automatic discarding of the least important tabs. Pass null to
660
+ * disable automatic discarding (manual discardLeastImportant still works).
661
+ * Chrome's equivalent trigger is OS memory pressure.
662
+ */
663
+ maxLoadedTabs?: number | null;
664
+ /**
665
+ * Tabs active within this window are protected (not disallowed) from
666
+ * discarding. Mirrors kNonVisiblePagesUrgentProtectionTime, 10 minutes on
667
+ * desktop (discard_eligibility_policy.h:38).
668
+ */
669
+ recentlyActiveProtectionMs?: number;
670
+ /**
671
+ * Protect pinned tabs (CannotDiscardReason::kPinnedTab). Protected tabs
672
+ * are only discarded when eligible tabs alone can't satisfy the budget.
673
+ */
674
+ protectPinnedTabs?: boolean;
675
+ /**
676
+ * App-level veto, the equivalent of the protections Chrome detects in the
677
+ * renderer (playing audio, form input, user edits, capturing, PiP...).
678
+ * Return false to disallow discarding a tab.
679
+ */
680
+ canDiscardTab?: (tab: Tab<T>) => boolean;
681
+ /**
682
+ * Called just before a tab's content is dropped, the equivalent of
683
+ * WebContents::AboutToBeDiscarded. Last chance to snapshot restorable
684
+ * state (scroll position, draft text) into tab.data.
685
+ */
686
+ onBeforeDiscard?: (tab: Tab<T>) => void;
687
+ /**
688
+ * Apps whose tab content shares global state per content type (a
689
+ * singleton store per route/scene) can return a key here: at most one
690
+ * non-discarded tab per distinct key stays loaded. Whenever two loaded
691
+ * tabs share a key, every one except the active (else the most recently
692
+ * active) is discarded immediately, and reload-on-focus re-derives each
693
+ * tab's state from its own `data` when it is next activated — shared
694
+ * state can then never bleed between two visible-at-once duplicates.
695
+ * Return null to exempt a tab (e.g. content that isolates correctly).
696
+ *
697
+ * This is a correctness policy, not a memory policy: it overrides the
698
+ * pinned/recently-active protections and the `canDiscardTab` veto (the
699
+ * active tab still always keeps its content). No Chrome equivalent —
700
+ * Chrome tabs never share renderer state.
701
+ */
702
+ exclusiveContentKey?: (tab: Tab<T>) => string | null;
703
+ }
704
+ declare class TabLifecycleManager<T = unknown> {
705
+ private readonly model_;
706
+ private readonly maxLoadedTabs_;
707
+ private readonly recentlyActiveProtectionMs_;
708
+ private readonly protectPinnedTabs_;
709
+ private readonly canDiscardTab_;
710
+ private readonly onBeforeDiscard_;
711
+ private readonly exclusiveContentKey_;
712
+ private detach_;
713
+ private enforcePending_;
714
+ constructor(model: TabStripModel<T>, options?: TabLifecycleOptions<T>);
715
+ /**
716
+ * Starts observing the model and enforcing the loaded-tab budget. Returns
717
+ * a stop function. Discards run on a microtask after model changes, never
718
+ * re-entrantly (Chrome posts discard tasks for the same reason).
719
+ */
720
+ start(): () => void;
721
+ stop(): void;
722
+ /**
723
+ * Tri-state discard eligibility for one tab. Port of
724
+ * DiscardEligibilityPolicy::CanDiscard.
725
+ */
726
+ canDiscard(tab: Tab<T>): CanDiscardDecision;
727
+ /**
728
+ * Discard candidates in Chrome's importance order, least important first:
729
+ * eligible before protected, each group least-recently-active first,
730
+ * disallowed never. Port of PageNodeSortProxy::operator<
731
+ * (discard_eligibility_policy.h:95) with focused/visible folded into
732
+ * 'activeTab' (a single-window strip has one visible tab).
733
+ */
734
+ getDiscardCandidates(includeProtected: boolean): Array<Tab<T>>;
735
+ /**
736
+ * Discards the least important discardable tab. 'urgent' may take
737
+ * protected tabs (Chrome: urgent discarding under memory pressure);
738
+ * 'proactive' and 'external' only take eligible ones. Port of
739
+ * PageDiscardingHelper::DiscardAPage. Returns the discarded tab or null.
740
+ */
741
+ discardLeastImportant(reason?: DiscardReason): Tab<T> | null;
742
+ /**
743
+ * Discards tabs until the loaded count fits the budget. Eligible tabs go
744
+ * first; protected tabs are taken only if the budget still isn't met
745
+ * (matching the sort order Chrome walks when reclaiming memory).
746
+ * Disallowed tabs are never discarded, so the budget can be exceeded when
747
+ * everything left is active/opted-out/vetoed.
748
+ */
749
+ enforceBudget(): number;
750
+ /**
751
+ * Enforces `exclusiveContentKey`: for every key shared by more than one
752
+ * loaded tab, keep the active tab (else the most recently active) and
753
+ * discard the rest. Returns the number of tabs discarded. Runs before the
754
+ * budget pass, so duplicates count toward freed budget first.
755
+ */
756
+ enforceExclusiveContent(): number;
757
+ private discardTab_;
758
+ private scheduleEnforce_;
759
+ }
760
+
761
+ export { AddTabFlags, type AddTabOptions, type CanDiscardDecision, type CanDiscardResult, type CannotDiscardReason, type CloseAllStoppedReason, CloseTabFlags, type DiscardReason, type IndexRange, ListSelectionModel, NO_TAB, type ReconcileOptions, type ReconcileTab, TAB_GROUP_COLORS, type Tab, type TabGroup, type TabGroupChange, type TabGroupColor, type TabGroupId, type TabGroupVisualData, type TabId, TabLifecycleManager, type TabLifecycleOptions, type TabOpenCause, TabStripModel, type TabStripModelChange, type TabStripModelObserver, type TabStripModelOptions, type TabStripSelectionChange };