@flrande/bak-extension 0.6.0 → 0.6.2

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,10 +1,13 @@
1
- export const DEFAULT_WORKSPACE_LABEL = 'bak agent';
2
- export const DEFAULT_WORKSPACE_COLOR = 'blue';
3
- export const DEFAULT_WORKSPACE_URL = 'about:blank';
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;
4
7
 
5
- export type WorkspaceColor = 'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange';
8
+ export type SessionBindingColor = 'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange';
6
9
 
7
- export interface WorkspaceTab {
10
+ export interface SessionBindingTab {
8
11
  id: number;
9
12
  title: string;
10
13
  url: string;
@@ -13,23 +16,23 @@ export interface WorkspaceTab {
13
16
  groupId: number | null;
14
17
  }
15
18
 
16
- export interface WorkspaceWindow {
19
+ export interface SessionBindingWindow {
17
20
  id: number;
18
21
  focused: boolean;
19
22
  }
20
23
 
21
- export interface WorkspaceGroup {
24
+ export interface SessionBindingGroup {
22
25
  id: number;
23
26
  windowId: number;
24
27
  title: string;
25
- color: WorkspaceColor;
28
+ color: SessionBindingColor;
26
29
  collapsed: boolean;
27
30
  }
28
31
 
29
- export interface WorkspaceRecord {
32
+ export interface SessionBindingRecord {
30
33
  id: string;
31
34
  label: string;
32
- color: WorkspaceColor;
35
+ color: SessionBindingColor;
33
36
  windowId: number | null;
34
37
  groupId: number | null;
35
38
  tabIds: number[];
@@ -37,99 +40,99 @@ export interface WorkspaceRecord {
37
40
  primaryTabId: number | null;
38
41
  }
39
42
 
40
- export interface WorkspaceInfo extends WorkspaceRecord {
41
- tabs: WorkspaceTab[];
43
+ export interface SessionBindingInfo extends SessionBindingRecord {
44
+ tabs: SessionBindingTab[];
42
45
  }
43
46
 
44
- export interface WorkspaceEnsureResult {
45
- workspace: WorkspaceInfo;
47
+ export interface SessionBindingEnsureResult {
48
+ binding: SessionBindingInfo;
46
49
  created: boolean;
47
50
  repaired: boolean;
48
51
  repairActions: string[];
49
52
  }
50
53
 
51
- export interface WorkspaceTargetResolution {
52
- tab: WorkspaceTab;
53
- workspace: WorkspaceInfo | null;
54
- resolution: 'explicit-tab' | 'explicit-workspace' | 'default-workspace' | 'browser-active';
55
- createdWorkspace: boolean;
54
+ export interface SessionBindingTargetResolution {
55
+ tab: SessionBindingTab;
56
+ binding: SessionBindingInfo | null;
57
+ resolution: 'explicit-tab' | 'explicit-binding' | 'default-binding' | 'browser-active';
58
+ createdBinding: boolean;
56
59
  repaired: boolean;
57
60
  repairActions: string[];
58
61
  }
59
62
 
60
- export interface WorkspaceStorage {
61
- load(workspaceId: string): Promise<WorkspaceRecord | null>;
62
- save(state: WorkspaceRecord): Promise<void>;
63
- delete(workspaceId: string): Promise<void>;
64
- list(): Promise<WorkspaceRecord[]>;
63
+ export interface SessionBindingStorage {
64
+ load(bindingId: string): Promise<SessionBindingRecord | null>;
65
+ save(state: SessionBindingRecord): Promise<void>;
66
+ delete(bindingId: string): Promise<void>;
67
+ list(): Promise<SessionBindingRecord[]>;
65
68
  }
66
69
 
67
- export interface WorkspaceBrowser {
68
- getTab(tabId: number): Promise<WorkspaceTab | null>;
69
- getActiveTab(): Promise<WorkspaceTab | null>;
70
- listTabs(filter?: { windowId?: number }): Promise<WorkspaceTab[]>;
71
- createTab(options: { windowId?: number; url?: string; active?: boolean }): Promise<WorkspaceTab>;
72
- updateTab(tabId: number, options: { active?: boolean; url?: string }): Promise<WorkspaceTab>;
70
+ export interface SessionBindingBrowser {
71
+ getTab(tabId: number): Promise<SessionBindingTab | null>;
72
+ getActiveTab(): Promise<SessionBindingTab | null>;
73
+ listTabs(filter?: { windowId?: number }): Promise<SessionBindingTab[]>;
74
+ createTab(options: { windowId?: number; url?: string; active?: boolean }): Promise<SessionBindingTab>;
75
+ updateTab(tabId: number, options: { active?: boolean; url?: string }): Promise<SessionBindingTab>;
73
76
  closeTab(tabId: number): Promise<void>;
74
- getWindow(windowId: number): Promise<WorkspaceWindow | null>;
75
- createWindow(options: { url?: string; focused?: boolean }): Promise<WorkspaceWindow>;
76
- updateWindow(windowId: number, options: { focused?: boolean }): Promise<WorkspaceWindow>;
77
+ getWindow(windowId: number): Promise<SessionBindingWindow | null>;
78
+ createWindow(options: { url?: string; focused?: boolean }): Promise<SessionBindingWindow>;
79
+ updateWindow(windowId: number, options: { focused?: boolean }): Promise<SessionBindingWindow>;
77
80
  closeWindow(windowId: number): Promise<void>;
78
- getGroup(groupId: number): Promise<WorkspaceGroup | null>;
81
+ getGroup(groupId: number): Promise<SessionBindingGroup | null>;
79
82
  groupTabs(tabIds: number[], groupId?: number): Promise<number>;
80
- updateGroup(groupId: number, options: { title?: string; color?: WorkspaceColor; collapsed?: boolean }): Promise<WorkspaceGroup>;
83
+ updateGroup(groupId: number, options: { title?: string; color?: SessionBindingColor; collapsed?: boolean }): Promise<SessionBindingGroup>;
81
84
  }
82
85
 
83
- interface WorkspaceWindowOwnership {
84
- workspaceTabs: WorkspaceTab[];
85
- foreignTabs: WorkspaceTab[];
86
+ interface SessionBindingWindowOwnership {
87
+ bindingTabs: SessionBindingTab[];
88
+ foreignTabs: SessionBindingTab[];
86
89
  }
87
90
 
88
- export interface WorkspaceEnsureOptions {
89
- workspaceId?: string;
91
+ export interface SessionBindingEnsureOptions {
92
+ bindingId?: string;
90
93
  focus?: boolean;
91
94
  initialUrl?: string;
92
95
  }
93
96
 
94
- export interface WorkspaceOpenTabOptions {
95
- workspaceId?: string;
97
+ export interface SessionBindingOpenTabOptions {
98
+ bindingId?: string;
96
99
  url?: string;
97
100
  active?: boolean;
98
101
  focus?: boolean;
99
102
  }
100
103
 
101
- export interface WorkspaceResolveTargetOptions {
104
+ export interface SessionBindingResolveTargetOptions {
102
105
  tabId?: number;
103
- workspaceId?: string;
106
+ bindingId?: string;
104
107
  createIfMissing?: boolean;
105
108
  }
106
109
 
107
110
  class SessionBindingManager {
108
- private readonly storage: WorkspaceStorage;
109
- private readonly browser: WorkspaceBrowser;
111
+ private readonly storage: SessionBindingStorage;
112
+ private readonly browser: SessionBindingBrowser;
110
113
 
111
- constructor(storage: WorkspaceStorage, browser: WorkspaceBrowser) {
114
+ constructor(storage: SessionBindingStorage, browser: SessionBindingBrowser) {
112
115
  this.storage = storage;
113
116
  this.browser = browser;
114
117
  }
115
118
 
116
- async getWorkspaceInfo(workspaceId: string): Promise<WorkspaceInfo | null> {
117
- return this.inspectWorkspace(workspaceId);
119
+ async getBindingInfo(bindingId: string): Promise<SessionBindingInfo | null> {
120
+ return this.inspectBinding(bindingId);
118
121
  }
119
122
 
120
- async ensureWorkspace(options: WorkspaceEnsureOptions = {}): Promise<WorkspaceEnsureResult> {
121
- const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
123
+ async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
124
+ const bindingId = this.normalizeBindingId(options.bindingId);
122
125
  const repairActions: string[] = [];
123
- const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
124
- const persisted = await this.storage.load(workspaceId);
126
+ const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
127
+ const persisted = await this.storage.load(bindingId);
125
128
  const created = !persisted;
126
- let state = this.normalizeState(persisted, workspaceId);
129
+ let state = this.normalizeState(persisted, bindingId);
127
130
 
128
131
  const originalWindowId = state.windowId;
129
132
  let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
130
- let tabs: WorkspaceTab[] = [];
133
+ let tabs: SessionBindingTab[] = [];
131
134
  if (!window) {
132
- const rebound = await this.rebindWorkspaceWindow(state);
135
+ const rebound = await this.rebindBindingWindow(state);
133
136
  if (rebound) {
134
137
  window = rebound.window;
135
138
  tabs = rebound.tabs;
@@ -161,7 +164,7 @@ class SessionBindingManager {
161
164
  }
162
165
 
163
166
  tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
164
- const recoveredTabs = await this.recoverWorkspaceTabs(state, tabs);
167
+ const recoveredTabs = await this.recoverBindingTabs(state, tabs);
165
168
  if (recoveredTabs.length > tabs.length) {
166
169
  tabs = recoveredTabs;
167
170
  repairActions.push('recovered-tracked-tabs');
@@ -172,9 +175,9 @@ class SessionBindingManager {
172
175
  state.tabIds = tabs.map((tab) => tab.id);
173
176
 
174
177
  if (state.windowId !== null) {
175
- const ownership = await this.inspectWorkspaceWindowOwnership(state, state.windowId);
178
+ const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
176
179
  if (ownership.foreignTabs.length > 0) {
177
- const migrated = await this.moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl);
180
+ const migrated = await this.moveBindingIntoDedicatedWindow(state, ownership, initialUrl);
178
181
  window = migrated.window;
179
182
  tabs = migrated.tabs;
180
183
  state.tabIds = tabs.map((tab) => tab.id);
@@ -183,7 +186,7 @@ class SessionBindingManager {
183
186
  }
184
187
 
185
188
  if (tabs.length === 0) {
186
- const primary = await this.createWorkspaceTab({
189
+ const primary = await this.createBindingTab({
187
190
  windowId: state.windowId,
188
191
  url: initialUrl,
189
192
  active: true
@@ -205,7 +208,7 @@ class SessionBindingManager {
205
208
  repairActions.push('reassigned-active-tab');
206
209
  }
207
210
 
208
- let group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
211
+ let group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
209
212
  if (!group || group.windowId !== state.windowId) {
210
213
  const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
211
214
  group = await this.browser.updateGroup(groupId, {
@@ -230,7 +233,7 @@ class SessionBindingManager {
230
233
  }
231
234
 
232
235
  tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
233
- tabs = await this.recoverWorkspaceTabs(state, tabs);
236
+ tabs = await this.recoverBindingTabs(state, tabs);
234
237
  const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
235
238
  if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
236
239
  tabs = [...tabs, activeTab];
@@ -253,7 +256,7 @@ class SessionBindingManager {
253
256
  await this.storage.save(state);
254
257
 
255
258
  return {
256
- workspace: {
259
+ binding: {
257
260
  ...state,
258
261
  tabs
259
262
  },
@@ -263,17 +266,17 @@ class SessionBindingManager {
263
266
  };
264
267
  }
265
268
 
266
- async openTab(options: WorkspaceOpenTabOptions = {}): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab }> {
267
- const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
268
- const hadWorkspace = (await this.loadWorkspaceRecord(workspaceId)) !== null;
269
- const ensured = await this.ensureWorkspace({
270
- workspaceId,
269
+ async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
270
+ const bindingId = this.normalizeBindingId(options.bindingId);
271
+ const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
272
+ const ensured = await this.ensureBinding({
273
+ bindingId,
271
274
  focus: false,
272
- initialUrl: hadWorkspace ? options.url ?? DEFAULT_WORKSPACE_URL : DEFAULT_WORKSPACE_URL
275
+ initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL
273
276
  });
274
- let state = { ...ensured.workspace, tabIds: [...ensured.workspace.tabIds], tabs: [...ensured.workspace.tabs] };
277
+ let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
275
278
  if (state.windowId !== null && state.tabs.length === 0) {
276
- const rebound = await this.rebindWorkspaceWindow(state);
279
+ const rebound = await this.rebindBindingWindow(state);
277
280
  if (rebound) {
278
281
  state.windowId = rebound.window.id;
279
282
  state.tabs = rebound.tabs;
@@ -281,7 +284,7 @@ class SessionBindingManager {
281
284
  }
282
285
  }
283
286
  const active = options.active === true;
284
- const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
287
+ const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
285
288
  let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
286
289
  state,
287
290
  ensured.created ||
@@ -290,14 +293,14 @@ class SessionBindingManager {
290
293
  ensured.repairActions.includes('migrated-dirty-window')
291
294
  );
292
295
 
293
- let createdTab: WorkspaceTab;
296
+ let createdTab: SessionBindingTab;
294
297
  try {
295
298
  createdTab = reusablePrimaryTab
296
299
  ? await this.browser.updateTab(reusablePrimaryTab.id, {
297
300
  url: desiredUrl,
298
301
  active
299
302
  })
300
- : await this.createWorkspaceTab({
303
+ : await this.createBindingTab({
301
304
  windowId: state.windowId,
302
305
  url: desiredUrl,
303
306
  active
@@ -306,19 +309,19 @@ class SessionBindingManager {
306
309
  if (!this.isMissingWindowError(error)) {
307
310
  throw error;
308
311
  }
309
- const repaired = await this.ensureWorkspace({
310
- workspaceId,
312
+ const repaired = await this.ensureBinding({
313
+ bindingId,
311
314
  focus: false,
312
315
  initialUrl: desiredUrl
313
316
  });
314
- state = { ...repaired.workspace };
317
+ state = { ...repaired.binding };
315
318
  reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
316
319
  createdTab = reusablePrimaryTab
317
320
  ? await this.browser.updateTab(reusablePrimaryTab.id, {
318
321
  url: desiredUrl,
319
322
  active
320
323
  })
321
- : await this.createWorkspaceTab({
324
+ : await this.createBindingTab({
322
325
  windowId: state.windowId,
323
326
  url: desiredUrl,
324
327
  active
@@ -331,14 +334,14 @@ class SessionBindingManager {
331
334
  color: state.color,
332
335
  collapsed: false
333
336
  });
334
- const nextState: WorkspaceRecord = {
337
+ const nextState: SessionBindingRecord = {
335
338
  id: state.id,
336
339
  label: state.label,
337
340
  color: state.color,
338
341
  windowId: state.windowId,
339
342
  groupId,
340
343
  tabIds: nextTabIds,
341
- activeTabId: createdTab.id,
344
+ activeTabId: active || options.focus === true ? createdTab.id : state.activeTabId ?? state.primaryTabId ?? createdTab.id,
342
345
  primaryTabId: state.primaryTabId ?? createdTab.id
343
346
  };
344
347
 
@@ -351,7 +354,7 @@ class SessionBindingManager {
351
354
  const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
352
355
  const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
353
356
  return {
354
- workspace: {
357
+ binding: {
355
358
  ...nextState,
356
359
  tabs
357
360
  },
@@ -359,58 +362,58 @@ class SessionBindingManager {
359
362
  };
360
363
  }
361
364
 
362
- async listTabs(workspaceId: string): Promise<{ workspace: WorkspaceInfo; tabs: WorkspaceTab[] }> {
363
- const ensured = await this.inspectWorkspace(workspaceId);
365
+ async listTabs(bindingId: string): Promise<{ binding: SessionBindingInfo; tabs: SessionBindingTab[] }> {
366
+ const ensured = await this.inspectBinding(bindingId);
364
367
  if (!ensured) {
365
- throw new Error(`Workspace ${workspaceId} does not exist`);
368
+ throw new Error(`Binding ${bindingId} does not exist`);
366
369
  }
367
370
  return {
368
- workspace: ensured,
371
+ binding: ensured,
369
372
  tabs: ensured.tabs
370
373
  };
371
374
  }
372
375
 
373
- async getActiveTab(workspaceId: string): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab | null }> {
374
- const ensured = await this.inspectWorkspace(workspaceId);
376
+ async getActiveTab(bindingId: string): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab | null }> {
377
+ const ensured = await this.inspectBinding(bindingId);
375
378
  if (!ensured) {
376
- const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
379
+ const normalizedBindingId = this.normalizeBindingId(bindingId);
377
380
  return {
378
- workspace: {
379
- ...this.normalizeState(null, normalizedWorkspaceId),
381
+ binding: {
382
+ ...this.normalizeState(null, normalizedBindingId),
380
383
  tabs: []
381
384
  },
382
385
  tab: null
383
386
  };
384
387
  }
385
388
  return {
386
- workspace: ensured,
389
+ binding: ensured,
387
390
  tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
388
391
  };
389
392
  }
390
393
 
391
- async setActiveTab(tabId: number, workspaceId: string): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab }> {
392
- const ensured = await this.ensureWorkspace({ workspaceId });
393
- if (!ensured.workspace.tabIds.includes(tabId)) {
394
- throw new Error(`Tab ${tabId} does not belong to workspace ${workspaceId}`);
394
+ async setActiveTab(tabId: number, bindingId: string): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
395
+ const ensured = await this.ensureBinding({ bindingId });
396
+ if (!ensured.binding.tabIds.includes(tabId)) {
397
+ throw new Error(`Tab ${tabId} does not belong to binding ${bindingId}`);
395
398
  }
396
- const nextState: WorkspaceRecord = {
397
- id: ensured.workspace.id,
398
- label: ensured.workspace.label,
399
- color: ensured.workspace.color,
400
- windowId: ensured.workspace.windowId,
401
- groupId: ensured.workspace.groupId,
402
- tabIds: [...ensured.workspace.tabIds],
399
+ const nextState: SessionBindingRecord = {
400
+ id: ensured.binding.id,
401
+ label: ensured.binding.label,
402
+ color: ensured.binding.color,
403
+ windowId: ensured.binding.windowId,
404
+ groupId: ensured.binding.groupId,
405
+ tabIds: [...ensured.binding.tabIds],
403
406
  activeTabId: tabId,
404
- primaryTabId: ensured.workspace.primaryTabId ?? tabId
407
+ primaryTabId: ensured.binding.primaryTabId ?? tabId
405
408
  };
406
409
  await this.storage.save(nextState);
407
410
  const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
408
411
  const tab = tabs.find((item) => item.id === tabId);
409
412
  if (!tab) {
410
- throw new Error(`Tab ${tabId} is missing from workspace ${workspaceId}`);
413
+ throw new Error(`Tab ${tabId} is missing from binding ${bindingId}`);
411
414
  }
412
415
  return {
413
- workspace: {
416
+ binding: {
414
417
  ...nextState,
415
418
  tabs
416
419
  },
@@ -418,36 +421,36 @@ class SessionBindingManager {
418
421
  };
419
422
  }
420
423
 
421
- async focus(workspaceId: string): Promise<{ ok: true; workspace: WorkspaceInfo }> {
422
- const ensured = await this.ensureWorkspace({ workspaceId, focus: false });
423
- if (ensured.workspace.activeTabId !== null) {
424
- await this.browser.updateTab(ensured.workspace.activeTabId, { active: true });
424
+ async focus(bindingId: string): Promise<{ ok: true; binding: SessionBindingInfo }> {
425
+ const ensured = await this.ensureBinding({ bindingId, focus: false });
426
+ if (ensured.binding.activeTabId !== null) {
427
+ await this.browser.updateTab(ensured.binding.activeTabId, { active: true });
425
428
  }
426
- if (ensured.workspace.windowId !== null) {
427
- await this.browser.updateWindow(ensured.workspace.windowId, { focused: true });
429
+ if (ensured.binding.windowId !== null) {
430
+ await this.browser.updateWindow(ensured.binding.windowId, { focused: true });
428
431
  }
429
- const refreshed = await this.ensureWorkspace({ workspaceId, focus: false });
430
- return { ok: true, workspace: refreshed.workspace };
432
+ const refreshed = await this.ensureBinding({ bindingId, focus: false });
433
+ return { ok: true, binding: refreshed.binding };
431
434
  }
432
435
 
433
- async reset(options: WorkspaceEnsureOptions = {}): Promise<WorkspaceEnsureResult> {
434
- const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
435
- await this.close(workspaceId);
436
- return this.ensureWorkspace({
436
+ async reset(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
437
+ const bindingId = this.normalizeBindingId(options.bindingId);
438
+ await this.close(bindingId);
439
+ return this.ensureBinding({
437
440
  ...options,
438
- workspaceId
441
+ bindingId
439
442
  });
440
443
  }
441
444
 
442
- async close(workspaceId: string): Promise<{ ok: true }> {
443
- const state = await this.loadWorkspaceRecord(workspaceId);
445
+ async close(bindingId: string): Promise<{ ok: true }> {
446
+ const state = await this.loadBindingRecord(bindingId);
444
447
  if (!state) {
445
- await this.storage.delete(workspaceId);
448
+ await this.storage.delete(bindingId);
446
449
  return { ok: true };
447
450
  }
448
451
  // Clear persisted state before closing the window so tab/window removal
449
- // listeners cannot race and resurrect an empty workspace record.
450
- await this.storage.delete(workspaceId);
452
+ // listeners cannot race and resurrect an empty binding record.
453
+ await this.storage.delete(bindingId);
451
454
  if (state.windowId !== null) {
452
455
  const existingWindow = await this.browser.getWindow(state.windowId);
453
456
  if (existingWindow) {
@@ -457,7 +460,7 @@ class SessionBindingManager {
457
460
  return { ok: true };
458
461
  }
459
462
 
460
- async resolveTarget(options: WorkspaceResolveTargetOptions = {}): Promise<WorkspaceTargetResolution> {
463
+ async resolveTarget(options: SessionBindingResolveTargetOptions = {}): Promise<SessionBindingTargetResolution> {
461
464
  if (typeof options.tabId === 'number') {
462
465
  const explicitTab = await this.browser.getTab(options.tabId);
463
466
  if (!explicitTab) {
@@ -465,21 +468,21 @@ class SessionBindingManager {
465
468
  }
466
469
  return {
467
470
  tab: explicitTab,
468
- workspace: null,
471
+ binding: null,
469
472
  resolution: 'explicit-tab',
470
- createdWorkspace: false,
473
+ createdBinding: false,
471
474
  repaired: false,
472
475
  repairActions: []
473
476
  };
474
477
  }
475
478
 
476
- const explicitWorkspaceId = typeof options.workspaceId === 'string' ? this.normalizeWorkspaceId(options.workspaceId) : undefined;
477
- if (explicitWorkspaceId) {
478
- const ensured = await this.ensureWorkspace({
479
- workspaceId: explicitWorkspaceId,
479
+ const explicitBindingId = typeof options.bindingId === 'string' ? this.normalizeBindingId(options.bindingId) : undefined;
480
+ if (explicitBindingId) {
481
+ const ensured = await this.ensureBinding({
482
+ bindingId: explicitBindingId,
480
483
  focus: false
481
484
  });
482
- return this.buildWorkspaceResolution(ensured, 'explicit-workspace');
485
+ return this.buildBindingResolution(ensured, 'explicit-binding');
483
486
  }
484
487
 
485
488
  if (options.createIfMissing !== true) {
@@ -489,30 +492,30 @@ class SessionBindingManager {
489
492
  }
490
493
  return {
491
494
  tab: activeTab,
492
- workspace: null,
495
+ binding: null,
493
496
  resolution: 'browser-active',
494
- createdWorkspace: false,
497
+ createdBinding: false,
495
498
  repaired: false,
496
499
  repairActions: []
497
500
  };
498
501
  }
499
502
 
500
- throw new Error('workspaceId is required when createIfMissing is true');
503
+ throw new Error('bindingId is required when createIfMissing is true');
501
504
  }
502
505
 
503
- private normalizeWorkspaceId(workspaceId?: string): string {
504
- const candidate = workspaceId?.trim();
506
+ private normalizeBindingId(bindingId?: string): string {
507
+ const candidate = bindingId?.trim();
505
508
  if (!candidate) {
506
- throw new Error('workspaceId is required');
509
+ throw new Error('bindingId is required');
507
510
  }
508
511
  return candidate;
509
512
  }
510
513
 
511
- private normalizeState(state: WorkspaceRecord | null, workspaceId: string): WorkspaceRecord {
514
+ private normalizeState(state: SessionBindingRecord | null, bindingId: string): SessionBindingRecord {
512
515
  return {
513
- id: workspaceId,
514
- label: state?.label ?? DEFAULT_WORKSPACE_LABEL,
515
- color: state?.color ?? DEFAULT_WORKSPACE_COLOR,
516
+ id: bindingId,
517
+ label: state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
518
+ color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
516
519
  windowId: state?.windowId ?? null,
517
520
  groupId: state?.groupId ?? null,
518
521
  tabIds: state?.tabIds ?? [],
@@ -521,43 +524,43 @@ class SessionBindingManager {
521
524
  };
522
525
  }
523
526
 
524
- async listWorkspaceRecords(): Promise<WorkspaceRecord[]> {
527
+ async listBindingRecords(): Promise<SessionBindingRecord[]> {
525
528
  return await this.storage.list();
526
529
  }
527
530
 
528
- private async loadWorkspaceRecord(workspaceId: string): Promise<WorkspaceRecord | null> {
529
- const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
530
- const state = await this.storage.load(normalizedWorkspaceId);
531
- if (!state || state.id !== normalizedWorkspaceId) {
531
+ private async loadBindingRecord(bindingId: string): Promise<SessionBindingRecord | null> {
532
+ const normalizedBindingId = this.normalizeBindingId(bindingId);
533
+ const state = await this.storage.load(normalizedBindingId);
534
+ if (!state || state.id !== normalizedBindingId) {
532
535
  return null;
533
536
  }
534
- return this.normalizeState(state, normalizedWorkspaceId);
537
+ return this.normalizeState(state, normalizedBindingId);
535
538
  }
536
539
 
537
- private async buildWorkspaceResolution(
538
- ensured: WorkspaceEnsureResult,
539
- resolution: 'explicit-workspace' | 'default-workspace'
540
- ): Promise<WorkspaceTargetResolution> {
541
- const tab = ensured.workspace.tabs.find((item) => item.id === ensured.workspace.activeTabId) ?? ensured.workspace.tabs[0] ?? null;
540
+ private async buildBindingResolution(
541
+ ensured: SessionBindingEnsureResult,
542
+ resolution: 'explicit-binding' | 'default-binding'
543
+ ): Promise<SessionBindingTargetResolution> {
544
+ const tab = ensured.binding.tabs.find((item) => item.id === ensured.binding.activeTabId) ?? ensured.binding.tabs[0] ?? null;
542
545
  if (tab) {
543
546
  return {
544
547
  tab,
545
- workspace: ensured.workspace,
548
+ binding: ensured.binding,
546
549
  resolution,
547
- createdWorkspace: ensured.created,
550
+ createdBinding: ensured.created,
548
551
  repaired: ensured.repaired,
549
552
  repairActions: ensured.repairActions
550
553
  };
551
554
  }
552
555
 
553
- if (ensured.workspace.activeTabId !== null) {
554
- const activeWorkspaceTab = await this.waitForTrackedTab(ensured.workspace.activeTabId, ensured.workspace.windowId);
555
- if (activeWorkspaceTab) {
556
+ if (ensured.binding.activeTabId !== null) {
557
+ const activeBindingTab = await this.waitForTrackedTab(ensured.binding.activeTabId, ensured.binding.windowId);
558
+ if (activeBindingTab) {
556
559
  return {
557
- tab: activeWorkspaceTab,
558
- workspace: ensured.workspace,
560
+ tab: activeBindingTab,
561
+ binding: ensured.binding,
559
562
  resolution,
560
- createdWorkspace: ensured.created,
563
+ createdBinding: ensured.created,
561
564
  repaired: ensured.repaired,
562
565
  repairActions: ensured.repairActions
563
566
  };
@@ -570,15 +573,15 @@ class SessionBindingManager {
570
573
  }
571
574
  return {
572
575
  tab: activeTab,
573
- workspace: null,
576
+ binding: null,
574
577
  resolution: 'browser-active',
575
- createdWorkspace: ensured.created,
578
+ createdBinding: ensured.created,
576
579
  repaired: ensured.repaired,
577
580
  repairActions: ensured.repairActions
578
581
  };
579
582
  }
580
583
 
581
- private async readTrackedTabs(tabIds: number[], windowId: number | null): Promise<WorkspaceTab[]> {
584
+ private async readTrackedTabs(tabIds: number[], windowId: number | null): Promise<SessionBindingTab[]> {
582
585
  const tabs = (
583
586
  await Promise.all(
584
587
  tabIds.map(async (tabId) => {
@@ -592,26 +595,26 @@ class SessionBindingManager {
592
595
  return tab;
593
596
  })
594
597
  )
595
- ).filter((tab): tab is WorkspaceTab => tab !== null);
598
+ ).filter((tab): tab is SessionBindingTab => tab !== null);
596
599
  return tabs;
597
600
  }
598
601
 
599
- private async readLooseTrackedTabs(tabIds: number[]): Promise<WorkspaceTab[]> {
602
+ private async readLooseTrackedTabs(tabIds: number[]): Promise<SessionBindingTab[]> {
600
603
  const tabs = (
601
604
  await Promise.all(
602
605
  tabIds.map(async (tabId) => {
603
606
  return await this.browser.getTab(tabId);
604
607
  })
605
608
  )
606
- ).filter((tab): tab is WorkspaceTab => tab !== null);
609
+ ).filter((tab): tab is SessionBindingTab => tab !== null);
607
610
  return tabs;
608
611
  }
609
612
 
610
- private collectCandidateTabIds(state: WorkspaceRecord): number[] {
613
+ private collectCandidateTabIds(state: SessionBindingRecord): number[] {
611
614
  return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))];
612
615
  }
613
616
 
614
- private async rebindWorkspaceWindow(state: WorkspaceRecord): Promise<{ window: WorkspaceWindow; tabs: WorkspaceTab[] } | null> {
617
+ private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
615
618
  const candidateWindowIds: number[] = [];
616
619
  const pushWindowId = (windowId: number | null | undefined): void => {
617
620
  if (typeof windowId !== 'number') {
@@ -622,7 +625,7 @@ class SessionBindingManager {
622
625
  }
623
626
  };
624
627
 
625
- const group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
628
+ const group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
626
629
  pushWindowId(group?.windowId);
627
630
 
628
631
  const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
@@ -631,13 +634,13 @@ class SessionBindingManager {
631
634
  }
632
635
 
633
636
  for (const candidateWindowId of candidateWindowIds) {
634
- const window = await this.waitForWindow(candidateWindowId);
637
+ const window = await this.waitForWindow(candidateWindowId);
635
638
  if (!window) {
636
639
  continue;
637
640
  }
638
641
  let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
639
642
  if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
640
- const windowTabs = await this.waitForWindowTabs(candidateWindowId, 750);
643
+ const windowTabs = await this.waitForWindowTabs(candidateWindowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
641
644
  tabs = windowTabs.filter((tab) => tab.groupId === group.id);
642
645
  }
643
646
  if (tabs.length === 0) {
@@ -659,27 +662,27 @@ class SessionBindingManager {
659
662
  return null;
660
663
  }
661
664
 
662
- private async inspectWorkspaceWindowOwnership(state: WorkspaceRecord, windowId: number): Promise<WorkspaceWindowOwnership> {
665
+ private async inspectBindingWindowOwnership(state: SessionBindingRecord, windowId: number): Promise<SessionBindingWindowOwnership> {
663
666
  const windowTabs = await this.waitForWindowTabs(windowId, 500);
664
667
  const trackedIds = new Set(this.collectCandidateTabIds(state));
665
668
  return {
666
- workspaceTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || (state.groupId !== null && tab.groupId === state.groupId)),
669
+ bindingTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || (state.groupId !== null && tab.groupId === state.groupId)),
667
670
  foreignTabs: windowTabs.filter((tab) => !trackedIds.has(tab.id) && (state.groupId === null || tab.groupId !== state.groupId))
668
671
  };
669
672
  }
670
673
 
671
- private async moveWorkspaceIntoDedicatedWindow(
672
- state: WorkspaceRecord,
673
- ownership: WorkspaceWindowOwnership,
674
+ private async moveBindingIntoDedicatedWindow(
675
+ state: SessionBindingRecord,
676
+ ownership: SessionBindingWindowOwnership,
674
677
  initialUrl: string
675
- ): Promise<{ window: WorkspaceWindow; tabs: WorkspaceTab[] }> {
676
- const sourceTabs = this.orderWorkspaceTabsForMigration(state, ownership.workspaceTabs);
678
+ ): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
679
+ const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
677
680
  const seedUrl = sourceTabs[0]?.url ?? initialUrl;
678
681
  const window = await this.browser.createWindow({
679
- url: seedUrl || DEFAULT_WORKSPACE_URL,
682
+ url: seedUrl || DEFAULT_SESSION_BINDING_URL,
680
683
  focused: false
681
684
  });
682
- const recreatedTabs = await this.waitForWindowTabs(window.id);
685
+ const recreatedTabs = await this.waitForWindowTabs(window.id);
683
686
  const firstTab = recreatedTabs[0] ?? null;
684
687
  const tabIdMap = new Map<number, number>();
685
688
  if (sourceTabs[0] && firstTab) {
@@ -687,7 +690,7 @@ class SessionBindingManager {
687
690
  }
688
691
 
689
692
  for (const sourceTab of sourceTabs.slice(1)) {
690
- const recreated = await this.createWorkspaceTab({
693
+ const recreated = await this.createBindingTab({
691
694
  windowId: window.id,
692
695
  url: sourceTab.url,
693
696
  active: false
@@ -713,9 +716,9 @@ class SessionBindingManager {
713
716
  state.primaryTabId = nextPrimaryTabId;
714
717
  state.activeTabId = nextActiveTabId;
715
718
 
716
- for (const workspaceTab of ownership.workspaceTabs) {
717
- await this.browser.closeTab(workspaceTab.id);
718
- }
719
+ for (const bindingTab of ownership.bindingTabs) {
720
+ await this.browser.closeTab(bindingTab.id);
721
+ }
719
722
 
720
723
  return {
721
724
  window,
@@ -723,8 +726,8 @@ class SessionBindingManager {
723
726
  };
724
727
  }
725
728
 
726
- private orderWorkspaceTabsForMigration(state: WorkspaceRecord, tabs: WorkspaceTab[]): WorkspaceTab[] {
727
- const ordered: WorkspaceTab[] = [];
729
+ private orderSessionBindingTabsForMigration(state: SessionBindingRecord, tabs: SessionBindingTab[]): SessionBindingTab[] {
730
+ const ordered: SessionBindingTab[] = [];
728
731
  const seen = new Set<number>();
729
732
  const pushById = (tabId: number | null): void => {
730
733
  if (typeof tabId !== 'number') {
@@ -750,7 +753,7 @@ class SessionBindingManager {
750
753
  return ordered;
751
754
  }
752
755
 
753
- private async recoverWorkspaceTabs(state: WorkspaceRecord, existingTabs: WorkspaceTab[]): Promise<WorkspaceTab[]> {
756
+ private async recoverBindingTabs(state: SessionBindingRecord, existingTabs: SessionBindingTab[]): Promise<SessionBindingTab[]> {
754
757
  if (state.windowId === null) {
755
758
  return existingTabs;
756
759
  }
@@ -782,9 +785,9 @@ class SessionBindingManager {
782
785
  return existingTabs;
783
786
  }
784
787
 
785
- private async createWorkspaceTab(options: { windowId: number | null; url: string; active: boolean }): Promise<WorkspaceTab> {
788
+ private async createBindingTab(options: { windowId: number | null; url: string; active: boolean }): Promise<SessionBindingTab> {
786
789
  if (options.windowId === null) {
787
- throw new Error('Workspace window is unavailable');
790
+ throw new Error('Binding window is unavailable');
788
791
  }
789
792
 
790
793
  const deadline = Date.now() + 1_500;
@@ -809,8 +812,8 @@ class SessionBindingManager {
809
812
  throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
810
813
  }
811
814
 
812
- private async inspectWorkspace(workspaceId: string): Promise<WorkspaceInfo | null> {
813
- const state = await this.loadWorkspaceRecord(workspaceId);
815
+ private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
816
+ const state = await this.loadBindingRecord(bindingId);
814
817
  if (!state) {
815
818
  return null;
816
819
  }
@@ -834,48 +837,60 @@ class SessionBindingManager {
834
837
  };
835
838
  }
836
839
 
837
- private async resolveReusablePrimaryTab(workspace: WorkspaceInfo, allowReuse: boolean): Promise<WorkspaceTab | null> {
838
- if (workspace.windowId === null) {
840
+ private async resolveReusablePrimaryTab(binding: SessionBindingInfo, allowReuse: boolean): Promise<SessionBindingTab | null> {
841
+ if (binding.windowId === null) {
839
842
  return null;
840
843
  }
841
- if (workspace.primaryTabId !== null) {
842
- const trackedPrimary = workspace.tabs.find((tab) => tab.id === workspace.primaryTabId) ?? (await this.waitForTrackedTab(workspace.primaryTabId, workspace.windowId));
843
- if (trackedPrimary && (allowReuse || this.isReusableBlankWorkspaceTab(trackedPrimary, workspace))) {
844
+ if (binding.primaryTabId !== null) {
845
+ const trackedPrimary = binding.tabs.find((tab) => tab.id === binding.primaryTabId) ?? (await this.waitForTrackedTab(binding.primaryTabId, binding.windowId));
846
+ if (trackedPrimary && (allowReuse || this.isReusableBlankSessionBindingTab(trackedPrimary, binding))) {
844
847
  return trackedPrimary;
845
848
  }
846
849
  }
847
- const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
850
+ const windowTabs = await this.waitForWindowTabs(binding.windowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
848
851
  if (windowTabs.length !== 1) {
849
852
  return null;
850
853
  }
851
854
  const candidate = windowTabs[0]!;
852
- if (allowReuse || this.isReusableBlankWorkspaceTab(candidate, workspace)) {
855
+ if (allowReuse || this.isReusableBlankSessionBindingTab(candidate, binding)) {
853
856
  return candidate;
854
857
  }
855
858
  return null;
856
859
  }
857
860
 
858
- private isReusableBlankWorkspaceTab(tab: WorkspaceTab, workspace: WorkspaceInfo): boolean {
859
- if (workspace.tabIds.length > 1) {
861
+ private isReusableBlankSessionBindingTab(tab: SessionBindingTab, binding: SessionBindingInfo): boolean {
862
+ if (binding.tabIds.length > 1) {
860
863
  return false;
861
864
  }
862
865
  const normalizedUrl = tab.url.trim().toLowerCase();
863
- return normalizedUrl === '' || normalizedUrl === DEFAULT_WORKSPACE_URL;
866
+ return normalizedUrl === '' || normalizedUrl === DEFAULT_SESSION_BINDING_URL;
864
867
  }
865
868
 
866
- private async waitForWindow(windowId: number, timeoutMs = 750): Promise<WorkspaceWindow | null> {
867
- const deadline = Date.now() + timeoutMs;
868
- while (Date.now() < deadline) {
869
- const window = await this.browser.getWindow(windowId);
870
- if (window) {
871
- return window;
869
+ private async waitForWindow(windowId: number, timeoutMs = WINDOW_LOOKUP_TIMEOUT_MS): Promise<SessionBindingWindow | null> {
870
+ const deadline = Date.now() + timeoutMs;
871
+ while (Date.now() < deadline) {
872
+ const window = await this.browser.getWindow(windowId);
873
+ if (window) {
874
+ return window;
872
875
  }
873
876
  await this.delay(50);
874
877
  }
875
- return null;
876
- }
877
-
878
- private async waitForTrackedTab(tabId: number, windowId: number | null, timeoutMs = 1_000): Promise<WorkspaceTab | null> {
878
+ return null;
879
+ }
880
+
881
+ private async waitForGroup(groupId: number, timeoutMs = GROUP_LOOKUP_TIMEOUT_MS): Promise<SessionBindingGroup | null> {
882
+ const deadline = Date.now() + timeoutMs;
883
+ while (Date.now() < deadline) {
884
+ const group = await this.browser.getGroup(groupId);
885
+ if (group) {
886
+ return group;
887
+ }
888
+ await this.delay(50);
889
+ }
890
+ return null;
891
+ }
892
+
893
+ private async waitForTrackedTab(tabId: number, windowId: number | null, timeoutMs = 1_000): Promise<SessionBindingTab | null> {
879
894
  const deadline = Date.now() + timeoutMs;
880
895
  while (Date.now() < deadline) {
881
896
  const tab = await this.browser.getTab(tabId);
@@ -887,7 +902,7 @@ class SessionBindingManager {
887
902
  return null;
888
903
  }
889
904
 
890
- private async waitForWindowTabs(windowId: number, timeoutMs = 1_000): Promise<WorkspaceTab[]> {
905
+ private async waitForWindowTabs(windowId: number, timeoutMs = WINDOW_TABS_LOOKUP_TIMEOUT_MS): Promise<SessionBindingTab[]> {
891
906
  const deadline = Date.now() + timeoutMs;
892
907
  while (Date.now() < deadline) {
893
908
  const tabs = await this.browser.listTabs({ windowId });
@@ -909,4 +924,4 @@ class SessionBindingManager {
909
924
  }
910
925
  }
911
926
 
912
- export { SessionBindingManager, SessionBindingManager as WorkspaceManager };
927
+ export { SessionBindingManager };