@flrande/bak-extension 0.6.6 → 0.6.8

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.
@@ -68,8 +68,8 @@ export interface SessionBindingStorage {
68
68
  list(): Promise<SessionBindingRecord[]>;
69
69
  }
70
70
 
71
- export interface SessionBindingBrowser {
72
- getTab(tabId: number): Promise<SessionBindingTab | null>;
71
+ export interface SessionBindingBrowser {
72
+ getTab(tabId: number): Promise<SessionBindingTab | null>;
73
73
  getActiveTab(): Promise<SessionBindingTab | null>;
74
74
  listTabs(filter?: { windowId?: number }): Promise<SessionBindingTab[]>;
75
75
  createTab(options: { windowId?: number; url?: string; active?: boolean }): Promise<SessionBindingTab>;
@@ -81,14 +81,15 @@ export interface SessionBindingBrowser {
81
81
  closeWindow(windowId: number): Promise<void>;
82
82
  getGroup(groupId: number): Promise<SessionBindingGroup | null>;
83
83
  groupTabs(tabIds: number[], groupId?: number): Promise<number>;
84
- updateGroup(groupId: number, options: { title?: string; color?: SessionBindingColor; collapsed?: boolean }): Promise<SessionBindingGroup>;
85
- }
86
-
87
- interface SessionBindingWindowOwnership {
88
- bindingTabs: SessionBindingTab[];
89
- foreignTabs: SessionBindingTab[];
90
- }
91
-
84
+ updateGroup(groupId: number, options: { title?: string; color?: SessionBindingColor; collapsed?: boolean }): Promise<SessionBindingGroup>;
85
+ }
86
+
87
+ interface SessionBindingWindowOwnership {
88
+ bindingTabs: SessionBindingTab[];
89
+ sharedBindingTabs: SessionBindingTab[];
90
+ foreignTabs: SessionBindingTab[];
91
+ }
92
+
92
93
  export interface SessionBindingEnsureOptions {
93
94
  bindingId?: string;
94
95
  focus?: boolean;
@@ -145,54 +146,65 @@ class SessionBindingManager {
145
146
  }
146
147
  }
147
148
  if (!window) {
148
- const createdWindow = await this.browser.createWindow({
149
- url: initialUrl,
150
- focused: options.focus === true
151
- });
152
- state.windowId = createdWindow.id;
153
- state.groupId = null;
154
- state.tabIds = [];
155
- state.activeTabId = null;
156
- state.primaryTabId = null;
157
- window = createdWindow;
158
- const initialTab =
159
- typeof createdWindow.initialTabId === 'number'
160
- ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id)
161
- : null;
162
- tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
163
- state.tabIds = tabs.map((tab) => tab.id);
164
- if (state.primaryTabId === null) {
165
- state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
166
- }
167
- if (state.activeTabId === null) {
168
- state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
149
+ const sharedWindow = await this.findSharedBindingWindow(bindingId);
150
+ if (sharedWindow) {
151
+ state.windowId = sharedWindow.id;
152
+ state.groupId = null;
153
+ state.tabIds = [];
154
+ state.activeTabId = null;
155
+ state.primaryTabId = null;
156
+ window = sharedWindow;
157
+ repairActions.push('attached-shared-window');
158
+ } else {
159
+ const createdWindow = await this.browser.createWindow({
160
+ url: initialUrl,
161
+ focused: options.focus === true
162
+ });
163
+ state.windowId = createdWindow.id;
164
+ state.groupId = null;
165
+ state.tabIds = [];
166
+ state.activeTabId = null;
167
+ state.primaryTabId = null;
168
+ window = createdWindow;
169
+ const initialTab =
170
+ typeof createdWindow.initialTabId === 'number'
171
+ ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id)
172
+ : null;
173
+ tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
174
+ state.tabIds = tabs.map((tab) => tab.id);
175
+ if (state.primaryTabId === null) {
176
+ state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
177
+ }
178
+ if (state.activeTabId === null) {
179
+ state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
180
+ }
181
+ repairActions.push(created ? 'created-window' : 'recreated-window');
169
182
  }
170
- repairActions.push(created ? 'created-window' : 'recreated-window');
171
183
  }
172
-
173
- tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
174
- const recoveredTabs = await this.recoverBindingTabs(state, tabs);
175
- if (recoveredTabs.length > tabs.length) {
184
+
185
+ tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
186
+ const recoveredTabs = await this.recoverBindingTabs(state, tabs);
187
+ if (recoveredTabs.length > tabs.length) {
176
188
  tabs = recoveredTabs;
177
189
  repairActions.push('recovered-tracked-tabs');
178
190
  }
179
191
  if (tabs.length !== state.tabIds.length) {
180
192
  repairActions.push('pruned-missing-tabs');
181
- }
182
- state.tabIds = tabs.map((tab) => tab.id);
183
-
184
- if (state.windowId !== null) {
185
- const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
186
- if (ownership.foreignTabs.length > 0) {
187
- const migrated = await this.moveBindingIntoDedicatedWindow(state, ownership, initialUrl);
188
- window = migrated.window;
189
- tabs = migrated.tabs;
190
- state.tabIds = tabs.map((tab) => tab.id);
191
- repairActions.push('migrated-dirty-window');
192
- }
193
- }
194
-
195
- if (tabs.length === 0) {
193
+ }
194
+ state.tabIds = tabs.map((tab) => tab.id);
195
+
196
+ if (state.windowId !== null) {
197
+ const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
198
+ if (ownership.foreignTabs.length > 0 && ownership.bindingTabs.length > 0) {
199
+ const migrated = await this.moveBindingIntoBakWindow(state, ownership, initialUrl);
200
+ window = migrated.window;
201
+ tabs = migrated.tabs;
202
+ state.tabIds = tabs.map((tab) => tab.id);
203
+ repairActions.push('evacuated-foreign-window');
204
+ }
205
+ }
206
+
207
+ if (tabs.length === 0) {
196
208
  const primary = await this.createBindingTab({
197
209
  windowId: state.windowId,
198
210
  url: initialUrl,
@@ -293,13 +305,12 @@ class SessionBindingManager {
293
305
  }
294
306
  const active = options.active === true;
295
307
  const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
296
- let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
297
- state,
298
- ensured.created ||
299
- ensured.repairActions.includes('recreated-window') ||
300
- ensured.repairActions.includes('created-primary-tab') ||
301
- ensured.repairActions.includes('migrated-dirty-window')
302
- );
308
+ let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
309
+ state,
310
+ ensured.created ||
311
+ ensured.repairActions.includes('recreated-window') ||
312
+ ensured.repairActions.includes('created-primary-tab')
313
+ );
303
314
 
304
315
  let createdTab: SessionBindingTab;
305
316
  try {
@@ -443,20 +454,23 @@ class SessionBindingManager {
443
454
  }
444
455
 
445
456
  async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
446
- const ensured = await this.ensureBinding({ bindingId, focus: false });
457
+ const binding = await this.inspectBinding(bindingId);
458
+ if (!binding) {
459
+ throw new Error(`Binding ${bindingId} does not exist`);
460
+ }
447
461
  const resolvedTabId =
448
462
  typeof tabId === 'number'
449
463
  ? tabId
450
- : ensured.binding.activeTabId ?? ensured.binding.primaryTabId ?? ensured.binding.tabs[0]?.id;
451
- if (typeof resolvedTabId !== 'number' || !ensured.binding.tabIds.includes(resolvedTabId)) {
464
+ : binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
465
+ if (typeof resolvedTabId !== 'number' || !binding.tabIds.includes(resolvedTabId)) {
452
466
  throw new Error(`Tab ${tabId ?? 'active'} does not belong to binding ${bindingId}`);
453
467
  }
454
468
 
455
469
  await this.browser.closeTab(resolvedTabId);
456
- const remainingTabIds = ensured.binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
470
+ const remainingTabIds = binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
457
471
 
458
472
  if (remainingTabIds.length === 0) {
459
- await this.storage.delete(ensured.binding.id);
473
+ await this.storage.delete(binding.id);
460
474
  return {
461
475
  binding: null,
462
476
  closedTabId: resolvedTabId
@@ -465,17 +479,17 @@ class SessionBindingManager {
465
479
 
466
480
  const tabs = await this.readLooseTrackedTabs(remainingTabIds);
467
481
  const nextPrimaryTabId =
468
- ensured.binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : ensured.binding.primaryTabId;
482
+ binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
469
483
  const nextActiveTabId =
470
- ensured.binding.activeTabId === resolvedTabId
484
+ binding.activeTabId === resolvedTabId
471
485
  ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
472
- : ensured.binding.activeTabId;
486
+ : binding.activeTabId;
473
487
  const nextState: SessionBindingRecord = {
474
- id: ensured.binding.id,
475
- label: ensured.binding.label,
476
- color: ensured.binding.color,
477
- windowId: tabs[0]?.windowId ?? ensured.binding.windowId,
478
- groupId: tabs[0]?.groupId ?? ensured.binding.groupId,
488
+ id: binding.id,
489
+ label: binding.label,
490
+ color: binding.color,
491
+ windowId: tabs[0]?.windowId ?? binding.windowId,
492
+ groupId: tabs[0]?.groupId ?? binding.groupId,
479
493
  tabIds: tabs.map((candidate) => candidate.id),
480
494
  activeTabId: nextActiveTabId,
481
495
  primaryTabId: nextPrimaryTabId
@@ -508,7 +522,7 @@ class SessionBindingManager {
508
522
  // Clear persisted state before closing the window so tab/window removal
509
523
  // listeners cannot race and resurrect an empty binding record.
510
524
  await this.storage.delete(bindingId);
511
- const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
525
+ const trackedTabs = await this.collectBindingTabsForClose(state);
512
526
  for (const trackedTab of trackedTabs) {
513
527
  try {
514
528
  await this.browser.closeTab(trackedTab.id);
@@ -673,7 +687,7 @@ class SessionBindingManager {
673
687
  return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))];
674
688
  }
675
689
 
676
- private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
690
+ private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
677
691
  const candidateWindowIds: number[] = [];
678
692
  const pushWindowId = (windowId: number | null | undefined): void => {
679
693
  if (typeof windowId !== 'number') {
@@ -718,59 +732,100 @@ class SessionBindingManager {
718
732
  return { window, tabs };
719
733
  }
720
734
 
721
- return null;
722
- }
723
-
724
- private async inspectBindingWindowOwnership(state: SessionBindingRecord, windowId: number): Promise<SessionBindingWindowOwnership> {
725
- const windowTabs = await this.waitForWindowTabs(windowId, 500);
726
- const trackedIds = new Set(this.collectCandidateTabIds(state));
727
- return {
728
- bindingTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || (state.groupId !== null && tab.groupId === state.groupId)),
729
- foreignTabs: windowTabs.filter((tab) => !trackedIds.has(tab.id) && (state.groupId === null || tab.groupId !== state.groupId))
730
- };
731
- }
732
-
733
- private async moveBindingIntoDedicatedWindow(
735
+ return null;
736
+ }
737
+
738
+ private async inspectBindingWindowOwnership(state: SessionBindingRecord, windowId: number): Promise<SessionBindingWindowOwnership> {
739
+ const windowTabs = await this.waitForWindowTabs(windowId, 500);
740
+ const bindingTabIds = new Set(this.collectCandidateTabIds(state));
741
+ const peerBindings = (await this.storage.list()).filter((candidate) => candidate.id !== state.id);
742
+ const peerTabIds = new Set<number>();
743
+ const peerGroupIds = new Set<number>();
744
+ for (const peer of peerBindings) {
745
+ for (const tabId of this.collectCandidateTabIds(peer)) {
746
+ peerTabIds.add(tabId);
747
+ }
748
+ if (peer.groupId !== null) {
749
+ peerGroupIds.add(peer.groupId);
750
+ }
751
+ }
752
+
753
+ const bindingTabs: SessionBindingTab[] = [];
754
+ const sharedBindingTabs: SessionBindingTab[] = [];
755
+ const foreignTabs: SessionBindingTab[] = [];
756
+ for (const tab of windowTabs) {
757
+ if (bindingTabIds.has(tab.id) || (state.groupId !== null && tab.groupId === state.groupId)) {
758
+ bindingTabs.push(tab);
759
+ continue;
760
+ }
761
+ if (peerTabIds.has(tab.id) || (tab.groupId !== null && peerGroupIds.has(tab.groupId))) {
762
+ sharedBindingTabs.push(tab);
763
+ continue;
764
+ }
765
+ foreignTabs.push(tab);
766
+ }
767
+
768
+ return {
769
+ bindingTabs,
770
+ sharedBindingTabs,
771
+ foreignTabs
772
+ };
773
+ }
774
+
775
+ private async moveBindingIntoBakWindow(
734
776
  state: SessionBindingRecord,
735
777
  ownership: SessionBindingWindowOwnership,
736
778
  initialUrl: string
737
779
  ): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
738
- const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
739
- const seedUrl = sourceTabs[0]?.url ?? initialUrl;
740
- const window = await this.browser.createWindow({
741
- url: seedUrl || DEFAULT_SESSION_BINDING_URL,
742
- focused: false
743
- });
780
+ const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
781
+ const seedUrl = sourceTabs[0]?.url ?? initialUrl;
782
+ const sharedWindow = await this.findSharedBindingWindow(state.id, state.windowId === null ? [] : [state.windowId]);
783
+ const window =
784
+ sharedWindow ??
785
+ (await this.browser.createWindow({
786
+ url: seedUrl || DEFAULT_SESSION_BINDING_URL,
787
+ focused: false
788
+ }));
744
789
  const initialTab =
745
- typeof window.initialTabId === 'number' ? await this.waitForTrackedTab(window.initialTabId, window.id) : null;
746
- const recreatedTabs = initialTab ? [initialTab] : await this.waitForWindowTabs(window.id);
747
- const firstTab = recreatedTabs[0] ?? null;
748
- const tabIdMap = new Map<number, number>();
749
- if (sourceTabs[0] && firstTab) {
750
- tabIdMap.set(sourceTabs[0].id, firstTab.id);
751
- }
752
-
753
- for (const sourceTab of sourceTabs.slice(1)) {
754
- const recreated = await this.createBindingTab({
755
- windowId: window.id,
756
- url: sourceTab.url,
757
- active: false
758
- });
759
- recreatedTabs.push(recreated);
760
- tabIdMap.set(sourceTab.id, recreated.id);
761
- }
762
-
763
- const nextPrimaryTabId =
764
- (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : undefined) ??
765
- firstTab?.id ??
766
- recreatedTabs[0]?.id ??
767
- null;
768
- const nextActiveTabId =
769
- (state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : undefined) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
770
- if (nextActiveTabId !== null) {
771
- await this.browser.updateTab(nextActiveTabId, { active: true });
772
- }
773
-
790
+ sharedWindow || typeof window.initialTabId !== 'number' ? null : await this.waitForTrackedTab(window.initialTabId, window.id);
791
+ const recreatedTabs: SessionBindingTab[] = [];
792
+ const tabIdMap = new Map<number, number>();
793
+
794
+ if (sourceTabs[0]) {
795
+ const firstTab = initialTab
796
+ ? await this.browser.updateTab(initialTab.id, {
797
+ url: sourceTabs[0].url,
798
+ active: false
799
+ })
800
+ : await this.createBindingTab({
801
+ windowId: window.id,
802
+ url: sourceTabs[0].url,
803
+ active: false
804
+ });
805
+ recreatedTabs.push(firstTab);
806
+ tabIdMap.set(sourceTabs[0].id, firstTab.id);
807
+ }
808
+
809
+ for (const sourceTab of sourceTabs.slice(1)) {
810
+ const recreated = await this.createBindingTab({
811
+ windowId: window.id,
812
+ url: sourceTab.url,
813
+ active: false
814
+ });
815
+ recreatedTabs.push(recreated);
816
+ tabIdMap.set(sourceTab.id, recreated.id);
817
+ }
818
+
819
+ const nextPrimaryTabId =
820
+ (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : undefined) ??
821
+ recreatedTabs[0]?.id ??
822
+ null;
823
+ const nextActiveTabId =
824
+ (state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : undefined) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
825
+ if (nextActiveTabId !== null) {
826
+ await this.browser.updateTab(nextActiveTabId, { active: true });
827
+ }
828
+
774
829
  state.windowId = window.id;
775
830
  state.groupId = null;
776
831
  state.tabIds = recreatedTabs.map((tab) => tab.id);
@@ -782,43 +837,102 @@ class SessionBindingManager {
782
837
  });
783
838
 
784
839
  for (const bindingTab of ownership.bindingTabs) {
785
- await this.browser.closeTab(bindingTab.id);
840
+ try {
841
+ await this.browser.closeTab(bindingTab.id);
842
+ } catch {
843
+ // Ignore tabs that were already removed while evacuating the binding.
844
+ }
786
845
  }
846
+
847
+ return {
848
+ window,
849
+ tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
850
+ };
851
+ }
852
+
853
+ private orderSessionBindingTabsForMigration(state: SessionBindingRecord, tabs: SessionBindingTab[]): SessionBindingTab[] {
854
+ const ordered: SessionBindingTab[] = [];
855
+ const seen = new Set<number>();
856
+ const pushById = (tabId: number | null): void => {
857
+ if (typeof tabId !== 'number') {
858
+ return;
859
+ }
860
+ const tab = tabs.find((candidate) => candidate.id === tabId);
861
+ if (!tab || seen.has(tab.id)) {
862
+ return;
863
+ }
864
+ ordered.push(tab);
865
+ seen.add(tab.id);
866
+ };
867
+
868
+ pushById(state.primaryTabId);
869
+ pushById(state.activeTabId);
870
+ for (const tab of tabs) {
871
+ if (seen.has(tab.id)) {
872
+ continue;
873
+ }
874
+ ordered.push(tab);
875
+ seen.add(tab.id);
876
+ }
877
+ return ordered;
878
+ }
879
+
880
+ private async findSharedBindingWindow(bindingId: string, excludedWindowIds: number[] = []): Promise<SessionBindingWindow | null> {
881
+ const peers = (await this.storage.list()).filter((candidate) => candidate.id !== bindingId);
882
+ const candidateWindowIds: number[] = [];
883
+ const peerTabIds = new Set<number>();
884
+ const peerGroupIds = new Set<number>();
885
+ const pushWindowId = (windowId: number | null | undefined): void => {
886
+ if (typeof windowId !== 'number') {
887
+ return;
888
+ }
889
+ if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
890
+ return;
891
+ }
892
+ candidateWindowIds.push(windowId);
893
+ };
894
+
895
+ for (const peer of peers) {
896
+ pushWindowId(peer.windowId);
897
+ if (peer.groupId !== null) {
898
+ peerGroupIds.add(peer.groupId);
899
+ const group = await this.waitForGroup(peer.groupId, 300);
900
+ pushWindowId(group?.windowId);
901
+ }
902
+ const trackedTabIds = this.collectCandidateTabIds(peer);
903
+ for (const trackedTabId of trackedTabIds) {
904
+ peerTabIds.add(trackedTabId);
905
+ }
906
+ const trackedTabs = await this.readLooseTrackedTabs(trackedTabIds);
907
+ for (const tab of trackedTabs) {
908
+ pushWindowId(tab.windowId);
909
+ }
910
+ }
911
+
912
+ for (const windowId of candidateWindowIds) {
913
+ const window = await this.waitForWindow(windowId, 300);
914
+ if (window) {
915
+ const windowTabs = await this.waitForWindowTabs(window.id, 300);
916
+ const ownedTabs = windowTabs.filter(
917
+ (tab) => peerTabIds.has(tab.id) || (tab.groupId !== null && peerGroupIds.has(tab.groupId))
918
+ );
919
+ if (ownedTabs.length === 0) {
920
+ continue;
921
+ }
922
+ const foreignTabs = windowTabs.filter(
923
+ (tab) => !peerTabIds.has(tab.id) && (tab.groupId === null || !peerGroupIds.has(tab.groupId))
924
+ );
925
+ if (foreignTabs.length > 0) {
926
+ continue;
927
+ }
928
+ return window;
929
+ }
930
+ }
931
+
932
+ return null;
933
+ }
787
934
 
788
- return {
789
- window,
790
- tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
791
- };
792
- }
793
-
794
- private orderSessionBindingTabsForMigration(state: SessionBindingRecord, tabs: SessionBindingTab[]): SessionBindingTab[] {
795
- const ordered: SessionBindingTab[] = [];
796
- const seen = new Set<number>();
797
- const pushById = (tabId: number | null): void => {
798
- if (typeof tabId !== 'number') {
799
- return;
800
- }
801
- const tab = tabs.find((candidate) => candidate.id === tabId);
802
- if (!tab || seen.has(tab.id)) {
803
- return;
804
- }
805
- ordered.push(tab);
806
- seen.add(tab.id);
807
- };
808
-
809
- pushById(state.primaryTabId);
810
- pushById(state.activeTabId);
811
- for (const tab of tabs) {
812
- if (seen.has(tab.id)) {
813
- continue;
814
- }
815
- ordered.push(tab);
816
- seen.add(tab.id);
817
- }
818
- return ordered;
819
- }
820
-
821
- private async recoverBindingTabs(state: SessionBindingRecord, existingTabs: SessionBindingTab[]): Promise<SessionBindingTab[]> {
935
+ private async recoverBindingTabs(state: SessionBindingRecord, existingTabs: SessionBindingTab[]): Promise<SessionBindingTab[]> {
822
936
  if (state.windowId === null) {
823
937
  return existingTabs;
824
938
  }
@@ -847,8 +961,22 @@ class SessionBindingManager {
847
961
  return preferredTabs;
848
962
  }
849
963
 
850
- return existingTabs;
851
- }
964
+ return existingTabs;
965
+ }
966
+
967
+ private async collectBindingTabsForClose(state: SessionBindingRecord): Promise<SessionBindingTab[]> {
968
+ const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
969
+ if (state.windowId === null || state.groupId === null) {
970
+ return trackedTabs;
971
+ }
972
+ const windowTabs = await this.waitForWindowTabs(state.windowId, 300);
973
+ const groupedTabs = windowTabs.filter((tab) => tab.groupId === state.groupId);
974
+ const merged = new Map<number, SessionBindingTab>();
975
+ for (const tab of [...trackedTabs, ...groupedTabs]) {
976
+ merged.set(tab.id, tab);
977
+ }
978
+ return [...merged.values()];
979
+ }
852
980
 
853
981
  private async createBindingTab(options: { windowId: number | null; url: string; active: boolean }): Promise<SessionBindingTab> {
854
982
  if (options.windowId === null) {
@@ -877,17 +1005,18 @@ class SessionBindingManager {
877
1005
  throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
878
1006
  }
879
1007
 
880
- private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
881
- const state = await this.loadBindingRecord(bindingId);
882
- if (!state) {
883
- return null;
884
- }
885
-
886
- let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
887
- const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
888
- if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
889
- tabs = [...tabs, activeTab];
890
- }
1008
+ private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
1009
+ const state = await this.loadBindingRecord(bindingId);
1010
+ if (!state) {
1011
+ return null;
1012
+ }
1013
+
1014
+ let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
1015
+ tabs = await this.recoverBindingTabs(state, tabs);
1016
+ const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
1017
+ if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
1018
+ tabs = [...tabs, activeTab];
1019
+ }
891
1020
  if (tabs.length === 0 && state.primaryTabId !== null) {
892
1021
  const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
893
1022
  if (primaryTab) {