@flrande/bak-extension 0.3.0 → 0.3.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 +1 @@
1
- 2026-03-09T03:04:15.266Z
1
+ 2026-03-09T04:50:59.948Z
@@ -43,12 +43,7 @@
43
43
  this.browser = browser;
44
44
  }
45
45
  async getWorkspaceInfo(workspaceId = DEFAULT_WORKSPACE_ID) {
46
- const state = await this.loadWorkspaceRecord(workspaceId);
47
- if (!state) {
48
- return null;
49
- }
50
- const repaired = await this.ensureWorkspace({ workspaceId, focus: false, initialUrl: DEFAULT_WORKSPACE_URL });
51
- return repaired.workspace;
46
+ return this.inspectWorkspace(workspaceId);
52
47
  }
53
48
  async ensureWorkspace(options = {}) {
54
49
  const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
@@ -57,8 +52,19 @@
57
52
  const persisted = await this.storage.load();
58
53
  const created = !persisted;
59
54
  let state = this.normalizeState(persisted, workspaceId);
60
- let window = state.windowId !== null ? await this.browser.getWindow(state.windowId) : null;
55
+ const originalWindowId = state.windowId;
56
+ let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
61
57
  let tabs = [];
58
+ if (!window) {
59
+ const rebound = await this.rebindWorkspaceWindow(state);
60
+ if (rebound) {
61
+ window = rebound.window;
62
+ tabs = rebound.tabs;
63
+ if (originalWindowId !== rebound.window.id) {
64
+ repairActions.push("rebound-window");
65
+ }
66
+ }
67
+ }
62
68
  if (!window) {
63
69
  const createdWindow = await this.browser.createWindow({
64
70
  url: initialUrl,
@@ -138,6 +144,12 @@
138
144
  if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
139
145
  tabs = [...tabs, activeTab];
140
146
  }
147
+ if (tabs.length === 0 && state.primaryTabId !== null) {
148
+ const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId);
149
+ if (primaryTab) {
150
+ tabs = [primaryTab];
151
+ }
152
+ }
141
153
  state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
142
154
  if (options.focus === true && state.activeTabId !== null) {
143
155
  await this.browser.updateTab(state.activeTabId, { active: true });
@@ -162,18 +174,51 @@
162
174
  focus: false,
163
175
  initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
164
176
  });
165
- const state = { ...ensured.workspace };
177
+ let state = { ...ensured.workspace, tabIds: [...ensured.workspace.tabIds], tabs: [...ensured.workspace.tabs] };
178
+ if (state.windowId !== null && state.tabs.length === 0) {
179
+ const rebound = await this.rebindWorkspaceWindow(state);
180
+ if (rebound) {
181
+ state.windowId = rebound.window.id;
182
+ state.tabs = rebound.tabs;
183
+ state.tabIds = [...new Set(rebound.tabs.map((tab2) => tab2.id))];
184
+ }
185
+ }
166
186
  const active = options.active === true;
167
187
  const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
168
- const reusablePrimaryTab = (ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")) && state.tabs.length === 1 && state.primaryTabId !== null ? state.tabs.find((tab2) => tab2.id === state.primaryTabId) ?? null : null;
169
- const createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
170
- url: desiredUrl,
171
- active
172
- }) : await this.createWorkspaceTab({
173
- windowId: state.windowId,
174
- url: desiredUrl,
175
- active
176
- });
188
+ let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
189
+ state,
190
+ ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
191
+ );
192
+ let createdTab;
193
+ try {
194
+ createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
195
+ url: desiredUrl,
196
+ active
197
+ }) : await this.createWorkspaceTab({
198
+ windowId: state.windowId,
199
+ url: desiredUrl,
200
+ active
201
+ });
202
+ } catch (error) {
203
+ if (!this.isMissingWindowError(error)) {
204
+ throw error;
205
+ }
206
+ const repaired = await this.ensureWorkspace({
207
+ workspaceId: options.workspaceId,
208
+ focus: false,
209
+ initialUrl: desiredUrl
210
+ });
211
+ state = { ...repaired.workspace };
212
+ reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
213
+ createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
214
+ url: desiredUrl,
215
+ active
216
+ }) : await this.createWorkspaceTab({
217
+ windowId: state.windowId,
218
+ url: desiredUrl,
219
+ active
220
+ });
221
+ }
177
222
  const nextTabIds = [.../* @__PURE__ */ new Set([...state.tabIds, createdTab.id])];
178
223
  const groupId = await this.browser.groupTabs([createdTab.id], state.groupId ?? void 0);
179
224
  await this.browser.updateGroup(groupId, {
@@ -207,17 +252,30 @@
207
252
  };
208
253
  }
209
254
  async listTabs(workspaceId = DEFAULT_WORKSPACE_ID) {
210
- const ensured = await this.ensureWorkspace({ workspaceId });
255
+ const ensured = await this.inspectWorkspace(workspaceId);
256
+ if (!ensured) {
257
+ throw new Error(`Workspace ${workspaceId} does not exist`);
258
+ }
211
259
  return {
212
- workspace: ensured.workspace,
213
- tabs: ensured.workspace.tabs
260
+ workspace: ensured,
261
+ tabs: ensured.tabs
214
262
  };
215
263
  }
216
264
  async getActiveTab(workspaceId = DEFAULT_WORKSPACE_ID) {
217
- const ensured = await this.ensureWorkspace({ workspaceId });
265
+ const ensured = await this.inspectWorkspace(workspaceId);
266
+ if (!ensured) {
267
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
268
+ return {
269
+ workspace: {
270
+ ...this.normalizeState(null, normalizedWorkspaceId),
271
+ tabs: []
272
+ },
273
+ tab: null
274
+ };
275
+ }
218
276
  return {
219
- workspace: ensured.workspace,
220
- tab: ensured.workspace.tabs.find((tab) => tab.id === ensured.workspace.activeTabId) ?? null
277
+ workspace: ensured,
278
+ tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
221
279
  };
222
280
  }
223
281
  async setActiveTab(tabId, workspaceId = DEFAULT_WORKSPACE_ID) {
@@ -413,6 +471,60 @@
413
471
  )).filter((tab) => tab !== null);
414
472
  return tabs;
415
473
  }
474
+ async readLooseTrackedTabs(tabIds) {
475
+ const tabs = (await Promise.all(
476
+ tabIds.map(async (tabId) => {
477
+ return await this.browser.getTab(tabId);
478
+ })
479
+ )).filter((tab) => tab !== null);
480
+ return tabs;
481
+ }
482
+ collectCandidateTabIds(state) {
483
+ return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number")))];
484
+ }
485
+ async rebindWorkspaceWindow(state) {
486
+ const candidateWindowIds = [];
487
+ const pushWindowId = (windowId) => {
488
+ if (typeof windowId !== "number") {
489
+ return;
490
+ }
491
+ if (!candidateWindowIds.includes(windowId)) {
492
+ candidateWindowIds.push(windowId);
493
+ }
494
+ };
495
+ const group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
496
+ pushWindowId(group?.windowId);
497
+ const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
498
+ for (const tab of trackedTabs) {
499
+ pushWindowId(tab.windowId);
500
+ }
501
+ for (const candidateWindowId of candidateWindowIds) {
502
+ const window = await this.waitForWindow(candidateWindowId);
503
+ if (!window) {
504
+ continue;
505
+ }
506
+ let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
507
+ if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
508
+ const windowTabs = await this.waitForWindowTabs(candidateWindowId, 750);
509
+ tabs = windowTabs.filter((tab) => tab.groupId === group.id);
510
+ }
511
+ if (tabs.length === 0) {
512
+ tabs = trackedTabs.filter((tab) => tab.windowId === candidateWindowId);
513
+ }
514
+ state.windowId = candidateWindowId;
515
+ if (tabs.length > 0) {
516
+ state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
517
+ if (state.primaryTabId === null || !state.tabIds.includes(state.primaryTabId)) {
518
+ state.primaryTabId = tabs[0]?.id ?? null;
519
+ }
520
+ if (state.activeTabId === null || !state.tabIds.includes(state.activeTabId)) {
521
+ state.activeTabId = tabs.find((tab) => tab.active)?.id ?? state.primaryTabId;
522
+ }
523
+ }
524
+ return { window, tabs };
525
+ }
526
+ return null;
527
+ }
416
528
  async recoverWorkspaceTabs(state, existingTabs) {
417
529
  if (state.windowId === null) {
418
530
  return existingTabs;
@@ -446,12 +558,6 @@
446
558
  const deadline = Date.now() + 1500;
447
559
  let lastError2 = null;
448
560
  while (Date.now() < deadline) {
449
- const window = await this.browser.getWindow(options.windowId);
450
- if (!window) {
451
- lastError2 = new Error(`No window with id: ${options.windowId}.`);
452
- await this.delay(50);
453
- continue;
454
- }
455
561
  try {
456
562
  return await this.browser.createTab({
457
563
  windowId: options.windowId,
@@ -468,6 +574,52 @@
468
574
  }
469
575
  throw lastError2 ?? new Error(`No window with id: ${options.windowId}.`);
470
576
  }
577
+ async inspectWorkspace(workspaceId = DEFAULT_WORKSPACE_ID) {
578
+ const state = await this.loadWorkspaceRecord(workspaceId);
579
+ if (!state) {
580
+ return null;
581
+ }
582
+ let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
583
+ const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
584
+ if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
585
+ tabs = [...tabs, activeTab];
586
+ }
587
+ if (tabs.length === 0 && state.primaryTabId !== null) {
588
+ const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
589
+ if (primaryTab) {
590
+ tabs = [primaryTab];
591
+ }
592
+ }
593
+ return {
594
+ ...state,
595
+ tabIds: [...new Set(state.tabIds.concat(tabs.map((tab) => tab.id)))],
596
+ tabs
597
+ };
598
+ }
599
+ async resolveReusablePrimaryTab(workspace, allowReuse) {
600
+ if (!allowReuse || workspace.windowId === null) {
601
+ return null;
602
+ }
603
+ if (workspace.primaryTabId !== null) {
604
+ const trackedPrimary = workspace.tabs.find((tab) => tab.id === workspace.primaryTabId) ?? await this.waitForTrackedTab(workspace.primaryTabId, workspace.windowId);
605
+ if (trackedPrimary) {
606
+ return trackedPrimary;
607
+ }
608
+ }
609
+ const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
610
+ return windowTabs.length === 1 ? windowTabs[0] : null;
611
+ }
612
+ async waitForWindow(windowId, timeoutMs = 750) {
613
+ const deadline = Date.now() + timeoutMs;
614
+ while (Date.now() < deadline) {
615
+ const window = await this.browser.getWindow(windowId);
616
+ if (window) {
617
+ return window;
618
+ }
619
+ await this.delay(50);
620
+ }
621
+ return null;
622
+ }
471
623
  async waitForTrackedTab(tabId, windowId, timeoutMs = 1e3) {
472
624
  const deadline = Date.now() + timeoutMs;
473
625
  while (Date.now() < deadline) {
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@flrande/bak-extension",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@flrande/bak-protocol": "0.3.0"
6
+ "@flrande/bak-protocol": "0.3.2"
7
7
  },
8
8
  "devDependencies": {
9
9
  "@types/chrome": "^0.1.14",
package/src/workspace.ts CHANGED
@@ -108,12 +108,7 @@ export class WorkspaceManager {
108
108
  }
109
109
 
110
110
  async getWorkspaceInfo(workspaceId = DEFAULT_WORKSPACE_ID): Promise<WorkspaceInfo | null> {
111
- const state = await this.loadWorkspaceRecord(workspaceId);
112
- if (!state) {
113
- return null;
114
- }
115
- const repaired = await this.ensureWorkspace({ workspaceId, focus: false, initialUrl: DEFAULT_WORKSPACE_URL });
116
- return repaired.workspace;
111
+ return this.inspectWorkspace(workspaceId);
117
112
  }
118
113
 
119
114
  async ensureWorkspace(options: WorkspaceEnsureOptions = {}): Promise<WorkspaceEnsureResult> {
@@ -124,8 +119,19 @@ export class WorkspaceManager {
124
119
  const created = !persisted;
125
120
  let state = this.normalizeState(persisted, workspaceId);
126
121
 
127
- let window = state.windowId !== null ? await this.browser.getWindow(state.windowId) : null;
122
+ const originalWindowId = state.windowId;
123
+ let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
128
124
  let tabs: WorkspaceTab[] = [];
125
+ if (!window) {
126
+ const rebound = await this.rebindWorkspaceWindow(state);
127
+ if (rebound) {
128
+ window = rebound.window;
129
+ tabs = rebound.tabs;
130
+ if (originalWindowId !== rebound.window.id) {
131
+ repairActions.push('rebound-window');
132
+ }
133
+ }
134
+ }
129
135
  if (!window) {
130
136
  const createdWindow = await this.browser.createWindow({
131
137
  url: initialUrl,
@@ -212,6 +218,12 @@ export class WorkspaceManager {
212
218
  if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
213
219
  tabs = [...tabs, activeTab];
214
220
  }
221
+ if (tabs.length === 0 && state.primaryTabId !== null) {
222
+ const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId);
223
+ if (primaryTab) {
224
+ tabs = [primaryTab];
225
+ }
226
+ }
215
227
  state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
216
228
 
217
229
  if (options.focus === true && state.activeTabId !== null) {
@@ -240,26 +252,56 @@ export class WorkspaceManager {
240
252
  focus: false,
241
253
  initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
242
254
  });
243
- const state = { ...ensured.workspace };
255
+ let state = { ...ensured.workspace, tabIds: [...ensured.workspace.tabIds], tabs: [...ensured.workspace.tabs] };
256
+ if (state.windowId !== null && state.tabs.length === 0) {
257
+ const rebound = await this.rebindWorkspaceWindow(state);
258
+ if (rebound) {
259
+ state.windowId = rebound.window.id;
260
+ state.tabs = rebound.tabs;
261
+ state.tabIds = [...new Set(rebound.tabs.map((tab) => tab.id))];
262
+ }
263
+ }
244
264
  const active = options.active === true;
245
265
  const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
246
- const reusablePrimaryTab =
247
- (ensured.created || ensured.repairActions.includes('recreated-window') || ensured.repairActions.includes('created-primary-tab')) &&
248
- state.tabs.length === 1 &&
249
- state.primaryTabId !== null
250
- ? state.tabs.find((tab) => tab.id === state.primaryTabId) ?? null
251
- : null;
252
-
253
- const createdTab = reusablePrimaryTab
254
- ? await this.browser.updateTab(reusablePrimaryTab.id, {
255
- url: desiredUrl,
256
- active
257
- })
258
- : await this.createWorkspaceTab({
259
- windowId: state.windowId,
260
- url: desiredUrl,
261
- active
262
- });
266
+ let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
267
+ state,
268
+ ensured.created || ensured.repairActions.includes('recreated-window') || ensured.repairActions.includes('created-primary-tab')
269
+ );
270
+
271
+ let createdTab: WorkspaceTab;
272
+ try {
273
+ createdTab = reusablePrimaryTab
274
+ ? await this.browser.updateTab(reusablePrimaryTab.id, {
275
+ url: desiredUrl,
276
+ active
277
+ })
278
+ : await this.createWorkspaceTab({
279
+ windowId: state.windowId,
280
+ url: desiredUrl,
281
+ active
282
+ });
283
+ } catch (error) {
284
+ if (!this.isMissingWindowError(error)) {
285
+ throw error;
286
+ }
287
+ const repaired = await this.ensureWorkspace({
288
+ workspaceId: options.workspaceId,
289
+ focus: false,
290
+ initialUrl: desiredUrl
291
+ });
292
+ state = { ...repaired.workspace };
293
+ reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
294
+ createdTab = reusablePrimaryTab
295
+ ? await this.browser.updateTab(reusablePrimaryTab.id, {
296
+ url: desiredUrl,
297
+ active
298
+ })
299
+ : await this.createWorkspaceTab({
300
+ windowId: state.windowId,
301
+ url: desiredUrl,
302
+ active
303
+ });
304
+ }
263
305
  const nextTabIds = [...new Set([...state.tabIds, createdTab.id])];
264
306
  const groupId = await this.browser.groupTabs([createdTab.id], state.groupId ?? undefined);
265
307
  await this.browser.updateGroup(groupId, {
@@ -296,18 +338,31 @@ export class WorkspaceManager {
296
338
  }
297
339
 
298
340
  async listTabs(workspaceId = DEFAULT_WORKSPACE_ID): Promise<{ workspace: WorkspaceInfo; tabs: WorkspaceTab[] }> {
299
- const ensured = await this.ensureWorkspace({ workspaceId });
341
+ const ensured = await this.inspectWorkspace(workspaceId);
342
+ if (!ensured) {
343
+ throw new Error(`Workspace ${workspaceId} does not exist`);
344
+ }
300
345
  return {
301
- workspace: ensured.workspace,
302
- tabs: ensured.workspace.tabs
346
+ workspace: ensured,
347
+ tabs: ensured.tabs
303
348
  };
304
349
  }
305
350
 
306
351
  async getActiveTab(workspaceId = DEFAULT_WORKSPACE_ID): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab | null }> {
307
- const ensured = await this.ensureWorkspace({ workspaceId });
352
+ const ensured = await this.inspectWorkspace(workspaceId);
353
+ if (!ensured) {
354
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
355
+ return {
356
+ workspace: {
357
+ ...this.normalizeState(null, normalizedWorkspaceId),
358
+ tabs: []
359
+ },
360
+ tab: null
361
+ };
362
+ }
308
363
  return {
309
- workspace: ensured.workspace,
310
- tab: ensured.workspace.tabs.find((tab) => tab.id === ensured.workspace.activeTabId) ?? null
364
+ workspace: ensured,
365
+ tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
311
366
  };
312
367
  }
313
368
 
@@ -525,6 +580,69 @@ export class WorkspaceManager {
525
580
  return tabs;
526
581
  }
527
582
 
583
+ private async readLooseTrackedTabs(tabIds: number[]): Promise<WorkspaceTab[]> {
584
+ const tabs = (
585
+ await Promise.all(
586
+ tabIds.map(async (tabId) => {
587
+ return await this.browser.getTab(tabId);
588
+ })
589
+ )
590
+ ).filter((tab): tab is WorkspaceTab => tab !== null);
591
+ return tabs;
592
+ }
593
+
594
+ private collectCandidateTabIds(state: WorkspaceRecord): number[] {
595
+ return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))];
596
+ }
597
+
598
+ private async rebindWorkspaceWindow(state: WorkspaceRecord): Promise<{ window: WorkspaceWindow; tabs: WorkspaceTab[] } | null> {
599
+ const candidateWindowIds: number[] = [];
600
+ const pushWindowId = (windowId: number | null | undefined): void => {
601
+ if (typeof windowId !== 'number') {
602
+ return;
603
+ }
604
+ if (!candidateWindowIds.includes(windowId)) {
605
+ candidateWindowIds.push(windowId);
606
+ }
607
+ };
608
+
609
+ const group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
610
+ pushWindowId(group?.windowId);
611
+
612
+ const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
613
+ for (const tab of trackedTabs) {
614
+ pushWindowId(tab.windowId);
615
+ }
616
+
617
+ for (const candidateWindowId of candidateWindowIds) {
618
+ const window = await this.waitForWindow(candidateWindowId);
619
+ if (!window) {
620
+ continue;
621
+ }
622
+ let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
623
+ if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
624
+ const windowTabs = await this.waitForWindowTabs(candidateWindowId, 750);
625
+ tabs = windowTabs.filter((tab) => tab.groupId === group.id);
626
+ }
627
+ if (tabs.length === 0) {
628
+ tabs = trackedTabs.filter((tab) => tab.windowId === candidateWindowId);
629
+ }
630
+ state.windowId = candidateWindowId;
631
+ if (tabs.length > 0) {
632
+ state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
633
+ if (state.primaryTabId === null || !state.tabIds.includes(state.primaryTabId)) {
634
+ state.primaryTabId = tabs[0]?.id ?? null;
635
+ }
636
+ if (state.activeTabId === null || !state.tabIds.includes(state.activeTabId)) {
637
+ state.activeTabId = tabs.find((tab) => tab.active)?.id ?? state.primaryTabId;
638
+ }
639
+ }
640
+ return { window, tabs };
641
+ }
642
+
643
+ return null;
644
+ }
645
+
528
646
  private async recoverWorkspaceTabs(state: WorkspaceRecord, existingTabs: WorkspaceTab[]): Promise<WorkspaceTab[]> {
529
647
  if (state.windowId === null) {
530
648
  return existingTabs;
@@ -566,13 +684,6 @@ export class WorkspaceManager {
566
684
  let lastError: Error | null = null;
567
685
 
568
686
  while (Date.now() < deadline) {
569
- const window = await this.browser.getWindow(options.windowId);
570
- if (!window) {
571
- lastError = new Error(`No window with id: ${options.windowId}.`);
572
- await this.delay(50);
573
- continue;
574
- }
575
-
576
687
  try {
577
688
  return await this.browser.createTab({
578
689
  windowId: options.windowId,
@@ -591,6 +702,57 @@ export class WorkspaceManager {
591
702
  throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
592
703
  }
593
704
 
705
+ private async inspectWorkspace(workspaceId = DEFAULT_WORKSPACE_ID): Promise<WorkspaceInfo | null> {
706
+ const state = await this.loadWorkspaceRecord(workspaceId);
707
+ if (!state) {
708
+ return null;
709
+ }
710
+
711
+ let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
712
+ const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
713
+ if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
714
+ tabs = [...tabs, activeTab];
715
+ }
716
+ if (tabs.length === 0 && state.primaryTabId !== null) {
717
+ const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
718
+ if (primaryTab) {
719
+ tabs = [primaryTab];
720
+ }
721
+ }
722
+
723
+ return {
724
+ ...state,
725
+ tabIds: [...new Set(state.tabIds.concat(tabs.map((tab) => tab.id)))],
726
+ tabs
727
+ };
728
+ }
729
+
730
+ private async resolveReusablePrimaryTab(workspace: WorkspaceInfo, allowReuse: boolean): Promise<WorkspaceTab | null> {
731
+ if (!allowReuse || workspace.windowId === null) {
732
+ return null;
733
+ }
734
+ if (workspace.primaryTabId !== null) {
735
+ const trackedPrimary = workspace.tabs.find((tab) => tab.id === workspace.primaryTabId) ?? (await this.waitForTrackedTab(workspace.primaryTabId, workspace.windowId));
736
+ if (trackedPrimary) {
737
+ return trackedPrimary;
738
+ }
739
+ }
740
+ const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
741
+ return windowTabs.length === 1 ? windowTabs[0]! : null;
742
+ }
743
+
744
+ private async waitForWindow(windowId: number, timeoutMs = 750): Promise<WorkspaceWindow | null> {
745
+ const deadline = Date.now() + timeoutMs;
746
+ while (Date.now() < deadline) {
747
+ const window = await this.browser.getWindow(windowId);
748
+ if (window) {
749
+ return window;
750
+ }
751
+ await this.delay(50);
752
+ }
753
+ return null;
754
+ }
755
+
594
756
  private async waitForTrackedTab(tabId: number, windowId: number | null, timeoutMs = 1_000): Promise<WorkspaceTab | null> {
595
757
  const deadline = Date.now() + timeoutMs;
596
758
  while (Date.now() < deadline) {