@flrande/bak-extension 0.6.9 → 0.6.11

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.
@@ -1,9 +1,9 @@
1
- export const DEFAULT_SESSION_BINDING_LABEL = 'bak agent';
2
- export const DEFAULT_SESSION_BINDING_COLOR = 'blue';
3
- export const DEFAULT_SESSION_BINDING_URL = 'about:blank';
4
- const WINDOW_LOOKUP_TIMEOUT_MS = 1_500;
5
- const GROUP_LOOKUP_TIMEOUT_MS = 1_000;
6
- const WINDOW_TABS_LOOKUP_TIMEOUT_MS = 1_500;
1
+ export const DEFAULT_SESSION_BINDING_LABEL = 'bak agent';
2
+ export const DEFAULT_SESSION_BINDING_COLOR = 'blue';
3
+ export const DEFAULT_SESSION_BINDING_URL = 'about:blank';
4
+ const WINDOW_LOOKUP_TIMEOUT_MS = 1_500;
5
+ const GROUP_LOOKUP_TIMEOUT_MS = 1_000;
6
+ const WINDOW_TABS_LOOKUP_TIMEOUT_MS = 1_500;
7
7
 
8
8
  export type SessionBindingColor = 'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange';
9
9
 
@@ -16,11 +16,11 @@ export interface SessionBindingTab {
16
16
  groupId: number | null;
17
17
  }
18
18
 
19
- export interface SessionBindingWindow {
20
- id: number;
21
- focused: boolean;
22
- initialTabId?: number | null;
23
- }
19
+ export interface SessionBindingWindow {
20
+ id: number;
21
+ focused: boolean;
22
+ initialTabId?: number | null;
23
+ }
24
24
 
25
25
  export interface SessionBindingGroup {
26
26
  id: number;
@@ -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,29 +81,23 @@ 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
- sharedBindingTabs: SessionBindingTab[];
90
- foreignTabs: SessionBindingTab[];
91
- }
92
-
84
+ updateGroup(groupId: number, options: { title?: string; color?: SessionBindingColor; collapsed?: boolean }): Promise<SessionBindingGroup>;
85
+ }
86
+
93
87
  export interface SessionBindingEnsureOptions {
94
- bindingId?: string;
95
- focus?: boolean;
96
- initialUrl?: string;
97
- label?: string;
98
- }
99
-
100
- export interface SessionBindingOpenTabOptions {
101
- bindingId?: string;
102
- url?: string;
103
- active?: boolean;
104
- focus?: boolean;
105
- label?: string;
106
- }
88
+ bindingId?: string;
89
+ focus?: boolean;
90
+ initialUrl?: string;
91
+ label?: string;
92
+ }
93
+
94
+ export interface SessionBindingOpenTabOptions {
95
+ bindingId?: string;
96
+ url?: string;
97
+ active?: boolean;
98
+ focus?: boolean;
99
+ label?: string;
100
+ }
107
101
 
108
102
  export interface SessionBindingResolveTargetOptions {
109
103
  tabId?: number;
@@ -124,13 +118,13 @@ class SessionBindingManager {
124
118
  return this.inspectBinding(bindingId);
125
119
  }
126
120
 
127
- async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
128
- const bindingId = this.normalizeBindingId(options.bindingId);
129
- const repairActions: string[] = [];
130
- const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
131
- const persisted = await this.storage.load(bindingId);
132
- const created = !persisted;
133
- let state = this.normalizeState(persisted, bindingId, options.label);
121
+ async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
122
+ const bindingId = this.normalizeBindingId(options.bindingId);
123
+ const repairActions: string[] = [];
124
+ const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
125
+ const persisted = await this.storage.load(bindingId);
126
+ const created = !persisted;
127
+ let state = this.normalizeState(persisted, bindingId, options.label);
134
128
 
135
129
  const originalWindowId = state.windowId;
136
130
  let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
@@ -145,46 +139,53 @@ class SessionBindingManager {
145
139
  }
146
140
  }
147
141
  }
142
+ if (!window) {
143
+ const activeWindow = await this.attachBindingToActiveWindow(state);
144
+ if (activeWindow) {
145
+ window = activeWindow;
146
+ repairActions.push('attached-active-window');
147
+ }
148
+ }
148
149
  if (!window) {
149
150
  const sharedWindow = await this.findSharedBindingWindow(bindingId);
150
151
  if (sharedWindow) {
151
152
  state.windowId = sharedWindow.id;
152
153
  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');
182
- }
154
+ state.tabIds = [];
155
+ state.activeTabId = null;
156
+ state.primaryTabId = null;
157
+ window = sharedWindow;
158
+ repairActions.push('attached-shared-window');
159
+ } else {
160
+ const createdWindow = await this.browser.createWindow({
161
+ url: initialUrl,
162
+ focused: options.focus === true
163
+ });
164
+ state.windowId = createdWindow.id;
165
+ state.groupId = null;
166
+ state.tabIds = [];
167
+ state.activeTabId = null;
168
+ state.primaryTabId = null;
169
+ window = createdWindow;
170
+ const initialTab =
171
+ typeof createdWindow.initialTabId === 'number'
172
+ ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id)
173
+ : null;
174
+ tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
175
+ state.tabIds = tabs.map((tab) => tab.id);
176
+ if (state.primaryTabId === null) {
177
+ state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
178
+ }
179
+ if (state.activeTabId === null) {
180
+ state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
181
+ }
182
+ repairActions.push(created ? 'created-window' : 'recreated-window');
183
+ }
183
184
  }
184
185
 
185
186
  tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
186
187
  const recoveredTabs = await this.recoverBindingTabs(state, tabs);
187
- if (recoveredTabs.length > tabs.length) {
188
+ if (recoveredTabs.length > tabs.length) {
188
189
  tabs = recoveredTabs;
189
190
  repairActions.push('recovered-tracked-tabs');
190
191
  }
@@ -193,26 +194,15 @@ class SessionBindingManager {
193
194
  }
194
195
  state.tabIds = tabs.map((tab) => tab.id);
195
196
 
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
197
  if (tabs.length === 0) {
208
- const primary = await this.createBindingTab({
209
- windowId: state.windowId,
210
- url: initialUrl,
211
- active: true
212
- });
213
- tabs = [primary];
214
- state.tabIds = [primary.id];
215
- state.primaryTabId = primary.id;
198
+ const primary = await this.createBindingTab({
199
+ windowId: state.windowId,
200
+ url: initialUrl,
201
+ active: options.focus === true
202
+ });
203
+ tabs = [primary];
204
+ state.tabIds = [primary.id];
205
+ state.primaryTabId = primary.id;
216
206
  state.activeTabId = primary.id;
217
207
  repairActions.push('created-primary-tab');
218
208
  }
@@ -227,7 +217,7 @@ class SessionBindingManager {
227
217
  repairActions.push('reassigned-active-tab');
228
218
  }
229
219
 
230
- let group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
220
+ let group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
231
221
  if (!group || group.windowId !== state.windowId) {
232
222
  const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
233
223
  group = await this.browser.updateGroup(groupId, {
@@ -265,12 +255,12 @@ class SessionBindingManager {
265
255
  }
266
256
  state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
267
257
 
268
- if (options.focus === true && state.activeTabId !== null) {
269
- await this.browser.updateTab(state.activeTabId, { active: true });
270
- window = await this.browser.updateWindow(state.windowId!, { focused: true });
271
- void window;
272
- repairActions.push('focused-window');
273
- }
258
+ if (options.focus === true && state.windowId !== null && state.activeTabId !== null) {
259
+ await this.focusBindingWindow(state.windowId, state.activeTabId);
260
+ window = await this.waitForWindow(state.windowId, 300);
261
+ void window;
262
+ repairActions.push('focused-window');
263
+ }
274
264
 
275
265
  await this.storage.save(state);
276
266
 
@@ -285,15 +275,15 @@ class SessionBindingManager {
285
275
  };
286
276
  }
287
277
 
288
- async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
289
- const bindingId = this.normalizeBindingId(options.bindingId);
290
- const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
291
- const ensured = await this.ensureBinding({
292
- bindingId,
293
- focus: false,
294
- initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL,
295
- label: options.label
296
- });
278
+ async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
279
+ const bindingId = this.normalizeBindingId(options.bindingId);
280
+ const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
281
+ const ensured = await this.ensureBinding({
282
+ bindingId,
283
+ focus: false,
284
+ initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL,
285
+ label: options.label
286
+ });
297
287
  let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
298
288
  if (state.windowId !== null && state.tabs.length === 0) {
299
289
  const rebound = await this.rebindBindingWindow(state);
@@ -305,12 +295,12 @@ class SessionBindingManager {
305
295
  }
306
296
  const active = options.active === true;
307
297
  const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
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
- );
298
+ let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
299
+ state,
300
+ ensured.created ||
301
+ ensured.repairActions.includes('recreated-window') ||
302
+ ensured.repairActions.includes('created-primary-tab')
303
+ );
314
304
 
315
305
  let createdTab: SessionBindingTab;
316
306
  try {
@@ -328,12 +318,12 @@ class SessionBindingManager {
328
318
  if (!this.isMissingWindowError(error)) {
329
319
  throw error;
330
320
  }
331
- const repaired = await this.ensureBinding({
332
- bindingId,
333
- focus: false,
334
- initialUrl: desiredUrl,
335
- label: options.label
336
- });
321
+ const repaired = await this.ensureBinding({
322
+ bindingId,
323
+ focus: false,
324
+ initialUrl: desiredUrl,
325
+ label: options.label
326
+ });
337
327
  state = { ...repaired.binding };
338
328
  reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
339
329
  createdTab = reusablePrimaryTab
@@ -361,14 +351,13 @@ class SessionBindingManager {
361
351
  windowId: state.windowId,
362
352
  groupId,
363
353
  tabIds: nextTabIds,
364
- activeTabId: active || options.focus === true ? createdTab.id : state.activeTabId ?? state.primaryTabId ?? createdTab.id,
354
+ activeTabId: active || options.focus === true ? createdTab.id : state.activeTabId ?? state.primaryTabId ?? createdTab.id,
365
355
  primaryTabId: state.primaryTabId ?? createdTab.id
366
356
  };
367
357
 
368
- if (options.focus === true) {
369
- await this.browser.updateTab(createdTab.id, { active: true });
370
- await this.browser.updateWindow(state.windowId!, { focused: true });
371
- }
358
+ if (options.focus === true && state.windowId !== null) {
359
+ await this.focusBindingWindow(state.windowId, createdTab.id);
360
+ }
372
361
 
373
362
  await this.storage.save(nextState);
374
363
  const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
@@ -442,96 +431,136 @@ class SessionBindingManager {
442
431
  }
443
432
 
444
433
  async focus(bindingId: string): Promise<{ ok: true; binding: SessionBindingInfo }> {
445
- const ensured = await this.ensureBinding({ bindingId, focus: false });
446
- if (ensured.binding.activeTabId !== null) {
447
- await this.browser.updateTab(ensured.binding.activeTabId, { active: true });
448
- }
449
- if (ensured.binding.windowId !== null) {
450
- await this.browser.updateWindow(ensured.binding.windowId, { focused: true });
451
- }
452
- const refreshed = await this.ensureBinding({ bindingId, focus: false });
453
- return { ok: true, binding: refreshed.binding };
454
- }
434
+ const normalizedBindingId = this.normalizeBindingId(bindingId);
435
+ let binding =
436
+ (await this.inspectBinding(normalizedBindingId)) ??
437
+ (
438
+ await this.ensureBinding({
439
+ bindingId: normalizedBindingId,
440
+ focus: false
441
+ })
442
+ ).binding;
443
+ let targetTabId = this.resolveFocusTabId(binding);
455
444
 
456
- async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
457
- const binding = await this.inspectBinding(bindingId);
458
- if (!binding) {
459
- throw new Error(`Binding ${bindingId} does not exist`);
460
- }
461
- const resolvedTabId =
462
- typeof tabId === 'number'
463
- ? tabId
464
- : binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
465
- if (typeof resolvedTabId !== 'number' || !binding.tabIds.includes(resolvedTabId)) {
466
- throw new Error(`Tab ${tabId ?? 'active'} does not belong to binding ${bindingId}`);
445
+ if (binding.windowId === null || targetTabId === null) {
446
+ binding = (
447
+ await this.ensureBinding({
448
+ bindingId: normalizedBindingId,
449
+ focus: false
450
+ })
451
+ ).binding;
452
+ targetTabId = this.resolveFocusTabId(binding);
467
453
  }
468
454
 
469
- await this.browser.closeTab(resolvedTabId);
470
- const remainingTabIds = binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
471
-
472
- if (remainingTabIds.length === 0) {
473
- await this.storage.delete(binding.id);
474
- return {
475
- binding: null,
476
- closedTabId: resolvedTabId
477
- };
455
+ if (binding.windowId !== null && targetTabId !== null) {
456
+ try {
457
+ await this.focusBindingWindow(binding.windowId, targetTabId);
458
+ } catch (error) {
459
+ if (!this.isMissingWindowError(error)) {
460
+ throw error;
461
+ }
462
+ binding = (
463
+ await this.ensureBinding({
464
+ bindingId: normalizedBindingId,
465
+ focus: false
466
+ })
467
+ ).binding;
468
+ targetTabId = this.resolveFocusTabId(binding);
469
+ if (binding.windowId !== null && targetTabId !== null) {
470
+ await this.focusBindingWindow(binding.windowId, targetTabId);
471
+ }
472
+ }
478
473
  }
479
474
 
480
- const tabs = await this.readLooseTrackedTabs(remainingTabIds);
481
- const nextPrimaryTabId =
482
- binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
483
- const nextActiveTabId =
484
- binding.activeTabId === resolvedTabId
485
- ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
486
- : binding.activeTabId;
487
- const nextState: SessionBindingRecord = {
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,
493
- tabIds: tabs.map((candidate) => candidate.id),
494
- activeTabId: nextActiveTabId,
495
- primaryTabId: nextPrimaryTabId
496
- };
497
- await this.storage.save(nextState);
475
+ const refreshed = (await this.inspectBinding(normalizedBindingId)) ?? binding;
498
476
  return {
499
- binding: {
500
- ...nextState,
501
- tabs
502
- },
503
- closedTabId: resolvedTabId
477
+ ok: true,
478
+ binding:
479
+ refreshed.windowId !== null && targetTabId !== null
480
+ ? this.withFocusedTab(refreshed, targetTabId)
481
+ : refreshed
504
482
  };
505
483
  }
506
-
507
- async reset(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
508
- const bindingId = this.normalizeBindingId(options.bindingId);
509
- await this.close(bindingId);
484
+
485
+ async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
486
+ const binding = await this.inspectBinding(bindingId);
487
+ if (!binding) {
488
+ throw new Error(`Binding ${bindingId} does not exist`);
489
+ }
490
+ const resolvedTabId =
491
+ typeof tabId === 'number'
492
+ ? tabId
493
+ : binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
494
+ if (typeof resolvedTabId !== 'number' || !binding.tabIds.includes(resolvedTabId)) {
495
+ throw new Error(`Tab ${tabId ?? 'active'} does not belong to binding ${bindingId}`);
496
+ }
497
+
498
+ await this.browser.closeTab(resolvedTabId);
499
+ const remainingTabIds = binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
500
+
501
+ if (remainingTabIds.length === 0) {
502
+ await this.storage.delete(binding.id);
503
+ return {
504
+ binding: null,
505
+ closedTabId: resolvedTabId
506
+ };
507
+ }
508
+
509
+ const tabs = await this.readLooseTrackedTabs(remainingTabIds);
510
+ const nextPrimaryTabId =
511
+ binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
512
+ const nextActiveTabId =
513
+ binding.activeTabId === resolvedTabId
514
+ ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
515
+ : binding.activeTabId;
516
+ const nextState: SessionBindingRecord = {
517
+ id: binding.id,
518
+ label: binding.label,
519
+ color: binding.color,
520
+ windowId: tabs[0]?.windowId ?? binding.windowId,
521
+ groupId: tabs[0]?.groupId ?? binding.groupId,
522
+ tabIds: tabs.map((candidate) => candidate.id),
523
+ activeTabId: nextActiveTabId,
524
+ primaryTabId: nextPrimaryTabId
525
+ };
526
+ await this.storage.save(nextState);
527
+ return {
528
+ binding: {
529
+ ...nextState,
530
+ tabs
531
+ },
532
+ closedTabId: resolvedTabId
533
+ };
534
+ }
535
+
536
+ async reset(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
537
+ const bindingId = this.normalizeBindingId(options.bindingId);
538
+ await this.close(bindingId);
510
539
  return this.ensureBinding({
511
540
  ...options,
512
541
  bindingId
513
542
  });
514
543
  }
515
544
 
516
- async close(bindingId: string): Promise<{ ok: true }> {
517
- const state = await this.loadBindingRecord(bindingId);
518
- if (!state) {
519
- await this.storage.delete(bindingId);
520
- return { ok: true };
521
- }
522
- // Clear persisted state before closing the window so tab/window removal
523
- // listeners cannot race and resurrect an empty binding record.
524
- await this.storage.delete(bindingId);
525
- const trackedTabs = await this.collectBindingTabsForClose(state);
526
- for (const trackedTab of trackedTabs) {
527
- try {
528
- await this.browser.closeTab(trackedTab.id);
529
- } catch {
530
- // Ignore tabs that were already removed before explicit close.
531
- }
532
- }
533
- return { ok: true };
534
- }
545
+ async close(bindingId: string): Promise<{ ok: true }> {
546
+ const state = await this.loadBindingRecord(bindingId);
547
+ if (!state) {
548
+ await this.storage.delete(bindingId);
549
+ return { ok: true };
550
+ }
551
+ // Clear persisted state before closing the window so tab/window removal
552
+ // listeners cannot race and resurrect an empty binding record.
553
+ await this.storage.delete(bindingId);
554
+ const trackedTabs = await this.collectBindingTabsForClose(state);
555
+ for (const trackedTab of trackedTabs) {
556
+ try {
557
+ await this.browser.closeTab(trackedTab.id);
558
+ } catch {
559
+ // Ignore tabs that were already removed before explicit close.
560
+ }
561
+ }
562
+ return { ok: true };
563
+ }
535
564
 
536
565
  async resolveTarget(options: SessionBindingResolveTargetOptions = {}): Promise<SessionBindingTargetResolution> {
537
566
  if (typeof options.tabId === 'number') {
@@ -584,13 +613,13 @@ class SessionBindingManager {
584
613
  return candidate;
585
614
  }
586
615
 
587
- private normalizeState(state: SessionBindingRecord | null, bindingId: string, label?: string): SessionBindingRecord {
588
- return {
589
- id: bindingId,
590
- label: label?.trim() ? label.trim() : state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
591
- color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
592
- windowId: state?.windowId ?? null,
593
- groupId: state?.groupId ?? null,
616
+ private normalizeState(state: SessionBindingRecord | null, bindingId: string, label?: string): SessionBindingRecord {
617
+ return {
618
+ id: bindingId,
619
+ label: label?.trim() ? label.trim() : state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
620
+ color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
621
+ windowId: state?.windowId ?? null,
622
+ groupId: state?.groupId ?? null,
594
623
  tabIds: state?.tabIds ?? [],
595
624
  activeTabId: state?.activeTabId ?? null,
596
625
  primaryTabId: state?.primaryTabId ?? null
@@ -687,7 +716,7 @@ class SessionBindingManager {
687
716
  return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))];
688
717
  }
689
718
 
690
- private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
719
+ private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
691
720
  const candidateWindowIds: number[] = [];
692
721
  const pushWindowId = (windowId: number | null | undefined): void => {
693
722
  if (typeof windowId !== 'number') {
@@ -698,7 +727,7 @@ class SessionBindingManager {
698
727
  }
699
728
  };
700
729
 
701
- const group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
730
+ const group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
702
731
  pushWindowId(group?.windowId);
703
732
 
704
733
  const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
@@ -707,13 +736,13 @@ class SessionBindingManager {
707
736
  }
708
737
 
709
738
  for (const candidateWindowId of candidateWindowIds) {
710
- const window = await this.waitForWindow(candidateWindowId);
739
+ const window = await this.waitForWindow(candidateWindowId);
711
740
  if (!window) {
712
741
  continue;
713
742
  }
714
743
  let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
715
744
  if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
716
- const windowTabs = await this.waitForWindowTabs(candidateWindowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
745
+ const windowTabs = await this.waitForWindowTabs(candidateWindowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
717
746
  tabs = windowTabs.filter((tab) => tab.groupId === group.id);
718
747
  }
719
748
  if (tabs.length === 0) {
@@ -735,146 +764,21 @@ class SessionBindingManager {
735
764
  return null;
736
765
  }
737
766
 
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(
776
- state: SessionBindingRecord,
777
- ownership: SessionBindingWindowOwnership,
778
- initialUrl: string
779
- ): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
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
- }));
789
- const initialTab =
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);
767
+ private async attachBindingToActiveWindow(state: SessionBindingRecord): Promise<SessionBindingWindow | null> {
768
+ const activeTab = await this.browser.getActiveTab();
769
+ if (!activeTab) {
770
+ return null;
817
771
  }
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 });
772
+ const window = await this.waitForWindow(activeTab.windowId, 300);
773
+ if (!window) {
774
+ return null;
827
775
  }
828
-
829
776
  state.windowId = window.id;
830
777
  state.groupId = null;
831
- state.tabIds = recreatedTabs.map((tab) => tab.id);
832
- state.primaryTabId = nextPrimaryTabId;
833
- state.activeTabId = nextActiveTabId;
834
- await this.storage.save({
835
- ...state,
836
- tabIds: [...state.tabIds]
837
- });
838
-
839
- for (const bindingTab of ownership.bindingTabs) {
840
- try {
841
- await this.browser.closeTab(bindingTab.id);
842
- } catch {
843
- // Ignore tabs that were already removed while evacuating the binding.
844
- }
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;
778
+ state.tabIds = [];
779
+ state.activeTabId = null;
780
+ state.primaryTabId = null;
781
+ return window;
878
782
  }
879
783
 
880
784
  private async findSharedBindingWindow(bindingId: string, excludedWindowIds: number[] = []): Promise<SessionBindingWindow | null> {
@@ -882,68 +786,51 @@ class SessionBindingManager {
882
786
  const candidateWindowIds: number[] = [];
883
787
  const peerTabIds = new Set<number>();
884
788
  const peerGroupIds = new Set<number>();
885
- const reusablePeerIds = new Set<string>();
886
789
  const pushWindowId = (windowId: number | null | undefined): void => {
887
790
  if (typeof windowId !== 'number') {
888
791
  return;
889
792
  }
890
- if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
891
- return;
892
- }
893
- candidateWindowIds.push(windowId);
894
- };
895
-
896
- for (const peer of peers) {
897
- if (peer.groupId === null) {
898
- continue;
899
- }
900
- const group = await this.waitForGroup(peer.groupId, 300);
901
- if (!group) {
902
- continue;
903
- }
904
- reusablePeerIds.add(peer.id);
905
- peerGroupIds.add(group.id);
906
- pushWindowId(group.windowId);
907
- }
793
+ if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
794
+ return;
795
+ }
796
+ candidateWindowIds.push(windowId);
797
+ };
908
798
 
909
799
  for (const peer of peers) {
910
- if (!reusablePeerIds.has(peer.id)) {
911
- continue;
912
- }
913
800
  const trackedTabIds = this.collectCandidateTabIds(peer);
801
+ pushWindowId(peer.windowId);
914
802
  for (const trackedTabId of trackedTabIds) {
915
803
  peerTabIds.add(trackedTabId);
916
804
  }
805
+ if (peer.groupId !== null) {
806
+ const group = await this.waitForGroup(peer.groupId, 300);
807
+ if (group) {
808
+ peerGroupIds.add(group.id);
809
+ pushWindowId(group.windowId);
810
+ }
811
+ }
917
812
  const trackedTabs = await this.readLooseTrackedTabs(trackedTabIds);
918
813
  for (const tab of trackedTabs) {
919
- if (tab.groupId !== null && peerGroupIds.has(tab.groupId)) {
920
- pushWindowId(tab.windowId);
921
- }
814
+ pushWindowId(tab.windowId);
922
815
  }
923
816
  }
924
-
817
+
925
818
  for (const windowId of candidateWindowIds) {
926
819
  const window = await this.waitForWindow(windowId, 300);
927
820
  if (window) {
928
- const windowTabs = await this.waitForWindowTabs(window.id, 300);
821
+ const windowTabs = await this.readStableWindowTabs(window.id, WINDOW_TABS_LOOKUP_TIMEOUT_MS, 150);
929
822
  const ownedTabs = windowTabs.filter(
930
823
  (tab) => peerTabIds.has(tab.id) || (tab.groupId !== null && peerGroupIds.has(tab.groupId))
931
824
  );
932
825
  if (ownedTabs.length === 0) {
933
826
  continue;
934
827
  }
935
- const foreignTabs = windowTabs.filter(
936
- (tab) => !peerTabIds.has(tab.id) && (tab.groupId === null || !peerGroupIds.has(tab.groupId))
937
- );
938
- if (foreignTabs.length > 0) {
939
- continue;
940
- }
941
828
  return window;
942
829
  }
943
830
  }
944
-
945
- return null;
946
- }
831
+
832
+ return null;
833
+ }
947
834
 
948
835
  private async recoverBindingTabs(state: SessionBindingRecord, existingTabs: SessionBindingTab[]): Promise<SessionBindingTab[]> {
949
836
  if (state.windowId === null) {
@@ -977,19 +864,43 @@ class SessionBindingManager {
977
864
  return existingTabs;
978
865
  }
979
866
 
980
- private async collectBindingTabsForClose(state: SessionBindingRecord): Promise<SessionBindingTab[]> {
981
- const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
982
- if (state.windowId === null || state.groupId === null) {
983
- return trackedTabs;
984
- }
985
- const windowTabs = await this.waitForWindowTabs(state.windowId, 300);
986
- const groupedTabs = windowTabs.filter((tab) => tab.groupId === state.groupId);
987
- const merged = new Map<number, SessionBindingTab>();
988
- for (const tab of [...trackedTabs, ...groupedTabs]) {
989
- merged.set(tab.id, tab);
867
+ private resolveFocusTabId(binding: Pick<SessionBindingInfo, 'activeTabId' | 'primaryTabId' | 'tabs'>): number | null {
868
+ return binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id ?? null;
869
+ }
870
+
871
+ private withFocusedTab(binding: SessionBindingInfo, targetTabId: number): SessionBindingInfo {
872
+ return {
873
+ ...binding,
874
+ activeTabId: targetTabId,
875
+ tabs: binding.tabs.map((tab) => ({
876
+ ...tab,
877
+ active: tab.id === targetTabId
878
+ }))
879
+ };
880
+ }
881
+
882
+ private async focusBindingWindow(windowId: number, tabId: number): Promise<void> {
883
+ await this.browser.updateWindow(windowId, { focused: true });
884
+ await this.browser.updateTab(tabId, { active: true });
885
+ const focusedTab = await this.waitForTrackedTab(tabId, windowId, 500);
886
+ if (!focusedTab?.active) {
887
+ await this.browser.updateTab(tabId, { active: true });
990
888
  }
991
- return [...merged.values()];
992
889
  }
890
+
891
+ private async collectBindingTabsForClose(state: SessionBindingRecord): Promise<SessionBindingTab[]> {
892
+ const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
893
+ if (state.windowId === null || state.groupId === null) {
894
+ return trackedTabs;
895
+ }
896
+ const windowTabs = await this.waitForWindowTabs(state.windowId, 300);
897
+ const groupedTabs = windowTabs.filter((tab) => tab.groupId === state.groupId);
898
+ const merged = new Map<number, SessionBindingTab>();
899
+ for (const tab of [...trackedTabs, ...groupedTabs]) {
900
+ merged.set(tab.id, tab);
901
+ }
902
+ return [...merged.values()];
903
+ }
993
904
 
994
905
  private async createBindingTab(options: { windowId: number | null; url: string; active: boolean }): Promise<SessionBindingTab> {
995
906
  if (options.windowId === null) {
@@ -1018,18 +929,18 @@ class SessionBindingManager {
1018
929
  throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
1019
930
  }
1020
931
 
1021
- private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
1022
- const state = await this.loadBindingRecord(bindingId);
1023
- if (!state) {
1024
- return null;
1025
- }
1026
-
1027
- let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
1028
- tabs = await this.recoverBindingTabs(state, tabs);
1029
- const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
1030
- if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
1031
- tabs = [...tabs, activeTab];
1032
- }
932
+ private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
933
+ const state = await this.loadBindingRecord(bindingId);
934
+ if (!state) {
935
+ return null;
936
+ }
937
+
938
+ let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
939
+ tabs = await this.recoverBindingTabs(state, tabs);
940
+ const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
941
+ if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
942
+ tabs = [...tabs, activeTab];
943
+ }
1033
944
  if (tabs.length === 0 && state.primaryTabId !== null) {
1034
945
  const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
1035
946
  if (primaryTab) {
@@ -1049,12 +960,12 @@ class SessionBindingManager {
1049
960
  return null;
1050
961
  }
1051
962
  if (binding.primaryTabId !== null) {
1052
- const trackedPrimary = binding.tabs.find((tab) => tab.id === binding.primaryTabId) ?? (await this.waitForTrackedTab(binding.primaryTabId, binding.windowId));
963
+ const trackedPrimary = binding.tabs.find((tab) => tab.id === binding.primaryTabId) ?? (await this.waitForTrackedTab(binding.primaryTabId, binding.windowId));
1053
964
  if (trackedPrimary && (allowReuse || this.isReusableBlankSessionBindingTab(trackedPrimary, binding))) {
1054
965
  return trackedPrimary;
1055
966
  }
1056
967
  }
1057
- const windowTabs = await this.waitForWindowTabs(binding.windowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
968
+ const windowTabs = await this.waitForWindowTabs(binding.windowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
1058
969
  if (windowTabs.length !== 1) {
1059
970
  return null;
1060
971
  }
@@ -1073,29 +984,29 @@ class SessionBindingManager {
1073
984
  return normalizedUrl === '' || normalizedUrl === DEFAULT_SESSION_BINDING_URL;
1074
985
  }
1075
986
 
1076
- private async waitForWindow(windowId: number, timeoutMs = WINDOW_LOOKUP_TIMEOUT_MS): Promise<SessionBindingWindow | null> {
1077
- const deadline = Date.now() + timeoutMs;
1078
- while (Date.now() < deadline) {
1079
- const window = await this.browser.getWindow(windowId);
1080
- if (window) {
1081
- return window;
987
+ private async waitForWindow(windowId: number, timeoutMs = WINDOW_LOOKUP_TIMEOUT_MS): Promise<SessionBindingWindow | null> {
988
+ const deadline = Date.now() + timeoutMs;
989
+ while (Date.now() < deadline) {
990
+ const window = await this.browser.getWindow(windowId);
991
+ if (window) {
992
+ return window;
1082
993
  }
1083
994
  await this.delay(50);
1084
995
  }
1085
- return null;
1086
- }
1087
-
1088
- private async waitForGroup(groupId: number, timeoutMs = GROUP_LOOKUP_TIMEOUT_MS): Promise<SessionBindingGroup | null> {
1089
- const deadline = Date.now() + timeoutMs;
1090
- while (Date.now() < deadline) {
1091
- const group = await this.browser.getGroup(groupId);
1092
- if (group) {
1093
- return group;
1094
- }
1095
- await this.delay(50);
1096
- }
1097
- return null;
1098
- }
996
+ return null;
997
+ }
998
+
999
+ private async waitForGroup(groupId: number, timeoutMs = GROUP_LOOKUP_TIMEOUT_MS): Promise<SessionBindingGroup | null> {
1000
+ const deadline = Date.now() + timeoutMs;
1001
+ while (Date.now() < deadline) {
1002
+ const group = await this.browser.getGroup(groupId);
1003
+ if (group) {
1004
+ return group;
1005
+ }
1006
+ await this.delay(50);
1007
+ }
1008
+ return null;
1009
+ }
1099
1010
 
1100
1011
  private async waitForTrackedTab(tabId: number, windowId: number | null, timeoutMs = 1_000): Promise<SessionBindingTab | null> {
1101
1012
  const deadline = Date.now() + timeoutMs;
@@ -1110,16 +1021,55 @@ class SessionBindingManager {
1110
1021
  }
1111
1022
 
1112
1023
  private async waitForWindowTabs(windowId: number, timeoutMs = WINDOW_TABS_LOOKUP_TIMEOUT_MS): Promise<SessionBindingTab[]> {
1113
- const deadline = Date.now() + timeoutMs;
1114
- while (Date.now() < deadline) {
1115
- const tabs = await this.browser.listTabs({ windowId });
1116
- if (tabs.length > 0) {
1024
+ const deadline = Date.now() + timeoutMs;
1025
+ while (Date.now() < deadline) {
1026
+ const tabs = await this.browser.listTabs({ windowId });
1027
+ if (tabs.length > 0) {
1117
1028
  return tabs;
1118
1029
  }
1119
1030
  await this.delay(50);
1120
- }
1121
- return [];
1122
- }
1031
+ }
1032
+ return [];
1033
+ }
1034
+
1035
+ private async readStableWindowTabs(
1036
+ windowId: number,
1037
+ timeoutMs = WINDOW_TABS_LOOKUP_TIMEOUT_MS,
1038
+ settleMs = 150
1039
+ ): Promise<SessionBindingTab[]> {
1040
+ const deadline = Date.now() + timeoutMs;
1041
+ let bestTabs: SessionBindingTab[] = [];
1042
+ let bestSignature = '';
1043
+ let lastSignature = '';
1044
+ let stableSince = 0;
1045
+
1046
+ while (Date.now() < deadline) {
1047
+ const tabs = await this.browser.listTabs({ windowId });
1048
+ if (tabs.length > 0) {
1049
+ const signature = this.windowTabSnapshotSignature(tabs);
1050
+ if (tabs.length > bestTabs.length || (tabs.length === bestTabs.length && signature !== bestSignature)) {
1051
+ bestTabs = tabs;
1052
+ bestSignature = signature;
1053
+ }
1054
+ if (signature !== lastSignature) {
1055
+ lastSignature = signature;
1056
+ stableSince = Date.now();
1057
+ } else if (Date.now() - stableSince >= settleMs) {
1058
+ return bestTabs;
1059
+ }
1060
+ }
1061
+ await this.delay(50);
1062
+ }
1063
+
1064
+ return bestTabs;
1065
+ }
1066
+
1067
+ private windowTabSnapshotSignature(tabs: SessionBindingTab[]): string {
1068
+ return [...tabs]
1069
+ .map((tab) => `${tab.id}:${tab.groupId ?? 'ungrouped'}`)
1070
+ .sort()
1071
+ .join('|');
1072
+ }
1123
1073
 
1124
1074
  private async delay(ms: number): Promise<void> {
1125
1075
  await new Promise((resolve) => setTimeout(resolve, ms));
@@ -1131,4 +1081,4 @@ class SessionBindingManager {
1131
1081
  }
1132
1082
  }
1133
1083
 
1134
- export { SessionBindingManager };
1084
+ export { SessionBindingManager };