@flrande/bak-extension 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,626 @@
1
+ export const DEFAULT_WORKSPACE_ID = 'default';
2
+ export const DEFAULT_WORKSPACE_LABEL = 'bak agent';
3
+ export const DEFAULT_WORKSPACE_COLOR = 'blue';
4
+ export const DEFAULT_WORKSPACE_URL = 'about:blank';
5
+
6
+ export type WorkspaceColor = 'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange';
7
+
8
+ export interface WorkspaceTab {
9
+ id: number;
10
+ title: string;
11
+ url: string;
12
+ active: boolean;
13
+ windowId: number;
14
+ groupId: number | null;
15
+ }
16
+
17
+ export interface WorkspaceWindow {
18
+ id: number;
19
+ focused: boolean;
20
+ }
21
+
22
+ export interface WorkspaceGroup {
23
+ id: number;
24
+ windowId: number;
25
+ title: string;
26
+ color: WorkspaceColor;
27
+ collapsed: boolean;
28
+ }
29
+
30
+ export interface WorkspaceRecord {
31
+ id: string;
32
+ label: string;
33
+ color: WorkspaceColor;
34
+ windowId: number | null;
35
+ groupId: number | null;
36
+ tabIds: number[];
37
+ activeTabId: number | null;
38
+ primaryTabId: number | null;
39
+ }
40
+
41
+ export interface WorkspaceInfo extends WorkspaceRecord {
42
+ tabs: WorkspaceTab[];
43
+ }
44
+
45
+ export interface WorkspaceEnsureResult {
46
+ workspace: WorkspaceInfo;
47
+ created: boolean;
48
+ repaired: boolean;
49
+ repairActions: string[];
50
+ }
51
+
52
+ export interface WorkspaceTargetResolution {
53
+ tab: WorkspaceTab;
54
+ workspace: WorkspaceInfo | null;
55
+ resolution: 'explicit-tab' | 'explicit-workspace' | 'default-workspace' | 'browser-active';
56
+ createdWorkspace: boolean;
57
+ repaired: boolean;
58
+ repairActions: string[];
59
+ }
60
+
61
+ export interface WorkspaceStorage {
62
+ load(): Promise<WorkspaceRecord | null>;
63
+ save(state: WorkspaceRecord | null): Promise<void>;
64
+ }
65
+
66
+ export interface WorkspaceBrowser {
67
+ getTab(tabId: number): Promise<WorkspaceTab | null>;
68
+ getActiveTab(): Promise<WorkspaceTab | null>;
69
+ listTabs(filter?: { windowId?: number }): Promise<WorkspaceTab[]>;
70
+ createTab(options: { windowId?: number; url?: string; active?: boolean }): Promise<WorkspaceTab>;
71
+ updateTab(tabId: number, options: { active?: boolean; url?: string }): Promise<WorkspaceTab>;
72
+ closeTab(tabId: number): Promise<void>;
73
+ getWindow(windowId: number): Promise<WorkspaceWindow | null>;
74
+ createWindow(options: { url?: string; focused?: boolean }): Promise<WorkspaceWindow>;
75
+ updateWindow(windowId: number, options: { focused?: boolean }): Promise<WorkspaceWindow>;
76
+ closeWindow(windowId: number): Promise<void>;
77
+ getGroup(groupId: number): Promise<WorkspaceGroup | null>;
78
+ groupTabs(tabIds: number[], groupId?: number): Promise<number>;
79
+ updateGroup(groupId: number, options: { title?: string; color?: WorkspaceColor; collapsed?: boolean }): Promise<WorkspaceGroup>;
80
+ }
81
+
82
+ export interface WorkspaceEnsureOptions {
83
+ workspaceId?: string;
84
+ focus?: boolean;
85
+ initialUrl?: string;
86
+ }
87
+
88
+ export interface WorkspaceOpenTabOptions {
89
+ workspaceId?: string;
90
+ url?: string;
91
+ active?: boolean;
92
+ focus?: boolean;
93
+ }
94
+
95
+ export interface WorkspaceResolveTargetOptions {
96
+ tabId?: number;
97
+ workspaceId?: string;
98
+ createIfMissing?: boolean;
99
+ }
100
+
101
+ export class WorkspaceManager {
102
+ private readonly storage: WorkspaceStorage;
103
+ private readonly browser: WorkspaceBrowser;
104
+
105
+ constructor(storage: WorkspaceStorage, browser: WorkspaceBrowser) {
106
+ this.storage = storage;
107
+ this.browser = browser;
108
+ }
109
+
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;
117
+ }
118
+
119
+ async ensureWorkspace(options: WorkspaceEnsureOptions = {}): Promise<WorkspaceEnsureResult> {
120
+ const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
121
+ const repairActions: string[] = [];
122
+ const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
123
+ const persisted = await this.storage.load();
124
+ const created = !persisted;
125
+ let state = this.normalizeState(persisted, workspaceId);
126
+
127
+ let window = state.windowId !== null ? await this.browser.getWindow(state.windowId) : null;
128
+ let tabs: WorkspaceTab[] = [];
129
+ if (!window) {
130
+ const createdWindow = await this.browser.createWindow({
131
+ url: initialUrl,
132
+ focused: options.focus === true
133
+ });
134
+ state.windowId = createdWindow.id;
135
+ state.groupId = null;
136
+ state.tabIds = [];
137
+ state.activeTabId = null;
138
+ state.primaryTabId = null;
139
+ window = createdWindow;
140
+ tabs = await this.waitForWindowTabs(createdWindow.id);
141
+ state.tabIds = tabs.map((tab) => tab.id);
142
+ if (state.primaryTabId === null) {
143
+ state.primaryTabId = tabs[0]?.id ?? null;
144
+ }
145
+ if (state.activeTabId === null) {
146
+ state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
147
+ }
148
+ repairActions.push(created ? 'created-window' : 'recreated-window');
149
+ }
150
+
151
+ tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
152
+ const recoveredTabs = await this.recoverWorkspaceTabs(state, tabs);
153
+ if (recoveredTabs.length > tabs.length) {
154
+ tabs = recoveredTabs;
155
+ repairActions.push('recovered-tracked-tabs');
156
+ }
157
+ if (tabs.length !== state.tabIds.length) {
158
+ repairActions.push('pruned-missing-tabs');
159
+ }
160
+ state.tabIds = tabs.map((tab) => tab.id);
161
+
162
+ if (tabs.length === 0) {
163
+ const primary = await this.createWorkspaceTab({
164
+ windowId: state.windowId,
165
+ url: initialUrl,
166
+ active: true
167
+ });
168
+ tabs = [primary];
169
+ state.tabIds = [primary.id];
170
+ state.primaryTabId = primary.id;
171
+ state.activeTabId = primary.id;
172
+ repairActions.push('created-primary-tab');
173
+ }
174
+
175
+ if (state.primaryTabId === null || !tabs.some((tab) => tab.id === state.primaryTabId)) {
176
+ state.primaryTabId = tabs[0]?.id ?? null;
177
+ repairActions.push('reassigned-primary-tab');
178
+ }
179
+
180
+ if (state.activeTabId === null || !tabs.some((tab) => tab.id === state.activeTabId)) {
181
+ state.activeTabId = state.primaryTabId ?? tabs[0]?.id ?? null;
182
+ repairActions.push('reassigned-active-tab');
183
+ }
184
+
185
+ let group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
186
+ if (!group || group.windowId !== state.windowId) {
187
+ const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
188
+ group = await this.browser.updateGroup(groupId, {
189
+ title: state.label,
190
+ color: state.color,
191
+ collapsed: false
192
+ });
193
+ state.groupId = group.id;
194
+ repairActions.push('recreated-group');
195
+ } else {
196
+ await this.browser.updateGroup(group.id, {
197
+ title: state.label,
198
+ color: state.color,
199
+ collapsed: false
200
+ });
201
+ }
202
+
203
+ const ungroupedIds = tabs.filter((tab) => tab.groupId !== state.groupId).map((tab) => tab.id);
204
+ if (ungroupedIds.length > 0) {
205
+ await this.browser.groupTabs(ungroupedIds, state.groupId ?? undefined);
206
+ repairActions.push('regrouped-tabs');
207
+ }
208
+
209
+ tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
210
+ tabs = await this.recoverWorkspaceTabs(state, tabs);
211
+ const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
212
+ if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
213
+ tabs = [...tabs, activeTab];
214
+ }
215
+ state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
216
+
217
+ if (options.focus === true && state.activeTabId !== null) {
218
+ await this.browser.updateTab(state.activeTabId, { active: true });
219
+ window = await this.browser.updateWindow(state.windowId!, { focused: true });
220
+ void window;
221
+ repairActions.push('focused-window');
222
+ }
223
+
224
+ await this.storage.save(state);
225
+
226
+ return {
227
+ workspace: {
228
+ ...state,
229
+ tabs
230
+ },
231
+ created,
232
+ repaired: repairActions.length > 0,
233
+ repairActions
234
+ };
235
+ }
236
+
237
+ async openTab(options: WorkspaceOpenTabOptions = {}): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab }> {
238
+ const ensured = await this.ensureWorkspace({
239
+ workspaceId: options.workspaceId,
240
+ focus: false,
241
+ initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
242
+ });
243
+ const state = { ...ensured.workspace };
244
+ const active = options.active === true;
245
+ 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
+ });
263
+ const nextTabIds = [...new Set([...state.tabIds, createdTab.id])];
264
+ const groupId = await this.browser.groupTabs([createdTab.id], state.groupId ?? undefined);
265
+ await this.browser.updateGroup(groupId, {
266
+ title: state.label,
267
+ color: state.color,
268
+ collapsed: false
269
+ });
270
+ const nextState: WorkspaceRecord = {
271
+ id: state.id,
272
+ label: state.label,
273
+ color: state.color,
274
+ windowId: state.windowId,
275
+ groupId,
276
+ tabIds: nextTabIds,
277
+ activeTabId: createdTab.id,
278
+ primaryTabId: state.primaryTabId ?? createdTab.id
279
+ };
280
+
281
+ if (options.focus === true) {
282
+ await this.browser.updateTab(createdTab.id, { active: true });
283
+ await this.browser.updateWindow(state.windowId!, { focused: true });
284
+ }
285
+
286
+ await this.storage.save(nextState);
287
+ const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
288
+ const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
289
+ return {
290
+ workspace: {
291
+ ...nextState,
292
+ tabs
293
+ },
294
+ tab
295
+ };
296
+ }
297
+
298
+ async listTabs(workspaceId = DEFAULT_WORKSPACE_ID): Promise<{ workspace: WorkspaceInfo; tabs: WorkspaceTab[] }> {
299
+ const ensured = await this.ensureWorkspace({ workspaceId });
300
+ return {
301
+ workspace: ensured.workspace,
302
+ tabs: ensured.workspace.tabs
303
+ };
304
+ }
305
+
306
+ async getActiveTab(workspaceId = DEFAULT_WORKSPACE_ID): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab | null }> {
307
+ const ensured = await this.ensureWorkspace({ workspaceId });
308
+ return {
309
+ workspace: ensured.workspace,
310
+ tab: ensured.workspace.tabs.find((tab) => tab.id === ensured.workspace.activeTabId) ?? null
311
+ };
312
+ }
313
+
314
+ async setActiveTab(tabId: number, workspaceId = DEFAULT_WORKSPACE_ID): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab }> {
315
+ const ensured = await this.ensureWorkspace({ workspaceId });
316
+ if (!ensured.workspace.tabIds.includes(tabId)) {
317
+ throw new Error(`Tab ${tabId} does not belong to workspace ${workspaceId}`);
318
+ }
319
+ const nextState: WorkspaceRecord = {
320
+ id: ensured.workspace.id,
321
+ label: ensured.workspace.label,
322
+ color: ensured.workspace.color,
323
+ windowId: ensured.workspace.windowId,
324
+ groupId: ensured.workspace.groupId,
325
+ tabIds: [...ensured.workspace.tabIds],
326
+ activeTabId: tabId,
327
+ primaryTabId: ensured.workspace.primaryTabId ?? tabId
328
+ };
329
+ await this.storage.save(nextState);
330
+ const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
331
+ const tab = tabs.find((item) => item.id === tabId);
332
+ if (!tab) {
333
+ throw new Error(`Tab ${tabId} is missing from workspace ${workspaceId}`);
334
+ }
335
+ return {
336
+ workspace: {
337
+ ...nextState,
338
+ tabs
339
+ },
340
+ tab
341
+ };
342
+ }
343
+
344
+ async focus(workspaceId = DEFAULT_WORKSPACE_ID): Promise<{ ok: true; workspace: WorkspaceInfo }> {
345
+ const ensured = await this.ensureWorkspace({ workspaceId, focus: false });
346
+ if (ensured.workspace.activeTabId !== null) {
347
+ await this.browser.updateTab(ensured.workspace.activeTabId, { active: true });
348
+ }
349
+ if (ensured.workspace.windowId !== null) {
350
+ await this.browser.updateWindow(ensured.workspace.windowId, { focused: true });
351
+ }
352
+ const refreshed = await this.ensureWorkspace({ workspaceId, focus: false });
353
+ return { ok: true, workspace: refreshed.workspace };
354
+ }
355
+
356
+ async reset(options: WorkspaceEnsureOptions = {}): Promise<WorkspaceEnsureResult> {
357
+ await this.close(options.workspaceId);
358
+ return this.ensureWorkspace(options);
359
+ }
360
+
361
+ async close(workspaceId = DEFAULT_WORKSPACE_ID): Promise<{ ok: true }> {
362
+ const state = await this.storage.load();
363
+ if (!state || state.id !== workspaceId) {
364
+ await this.storage.save(null);
365
+ return { ok: true };
366
+ }
367
+ if (state.windowId !== null) {
368
+ const existingWindow = await this.browser.getWindow(state.windowId);
369
+ if (existingWindow) {
370
+ await this.browser.closeWindow(state.windowId);
371
+ }
372
+ }
373
+ await this.storage.save(null);
374
+ return { ok: true };
375
+ }
376
+
377
+ async resolveTarget(options: WorkspaceResolveTargetOptions = {}): Promise<WorkspaceTargetResolution> {
378
+ if (typeof options.tabId === 'number') {
379
+ const explicitTab = await this.browser.getTab(options.tabId);
380
+ if (!explicitTab) {
381
+ throw new Error(`No tab with id ${options.tabId}`);
382
+ }
383
+ return {
384
+ tab: explicitTab,
385
+ workspace: null,
386
+ resolution: 'explicit-tab',
387
+ createdWorkspace: false,
388
+ repaired: false,
389
+ repairActions: []
390
+ };
391
+ }
392
+
393
+ const explicitWorkspaceId = typeof options.workspaceId === 'string' ? this.normalizeWorkspaceId(options.workspaceId) : undefined;
394
+ if (explicitWorkspaceId) {
395
+ const ensured = await this.ensureWorkspace({
396
+ workspaceId: explicitWorkspaceId,
397
+ focus: false
398
+ });
399
+ return this.buildWorkspaceResolution(ensured, 'explicit-workspace');
400
+ }
401
+
402
+ const existingWorkspace = await this.loadWorkspaceRecord(DEFAULT_WORKSPACE_ID);
403
+ if (existingWorkspace) {
404
+ const ensured = await this.ensureWorkspace({
405
+ workspaceId: existingWorkspace.id,
406
+ focus: false
407
+ });
408
+ return this.buildWorkspaceResolution(ensured, 'default-workspace');
409
+ }
410
+
411
+ if (options.createIfMissing !== true) {
412
+ const activeTab = await this.browser.getActiveTab();
413
+ if (!activeTab) {
414
+ throw new Error('No active tab');
415
+ }
416
+ return {
417
+ tab: activeTab,
418
+ workspace: null,
419
+ resolution: 'browser-active',
420
+ createdWorkspace: false,
421
+ repaired: false,
422
+ repairActions: []
423
+ };
424
+ }
425
+
426
+ const ensured = await this.ensureWorkspace({
427
+ workspaceId: DEFAULT_WORKSPACE_ID,
428
+ focus: false
429
+ });
430
+ return this.buildWorkspaceResolution(ensured, 'default-workspace');
431
+ }
432
+
433
+ private normalizeWorkspaceId(workspaceId?: string): string {
434
+ const candidate = workspaceId?.trim();
435
+ if (!candidate) {
436
+ return DEFAULT_WORKSPACE_ID;
437
+ }
438
+ if (candidate !== DEFAULT_WORKSPACE_ID) {
439
+ throw new Error(`Unsupported workspace id: ${candidate}`);
440
+ }
441
+ return candidate;
442
+ }
443
+
444
+ private normalizeState(state: WorkspaceRecord | null, workspaceId: string): WorkspaceRecord {
445
+ return {
446
+ id: workspaceId,
447
+ label: state?.label ?? DEFAULT_WORKSPACE_LABEL,
448
+ color: state?.color ?? DEFAULT_WORKSPACE_COLOR,
449
+ windowId: state?.windowId ?? null,
450
+ groupId: state?.groupId ?? null,
451
+ tabIds: state?.tabIds ?? [],
452
+ activeTabId: state?.activeTabId ?? null,
453
+ primaryTabId: state?.primaryTabId ?? null
454
+ };
455
+ }
456
+
457
+ private async loadWorkspaceRecord(workspaceId = DEFAULT_WORKSPACE_ID): Promise<WorkspaceRecord | null> {
458
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
459
+ const state = await this.storage.load();
460
+ if (!state || state.id !== normalizedWorkspaceId) {
461
+ return null;
462
+ }
463
+ return this.normalizeState(state, normalizedWorkspaceId);
464
+ }
465
+
466
+ private async buildWorkspaceResolution(
467
+ ensured: WorkspaceEnsureResult,
468
+ resolution: 'explicit-workspace' | 'default-workspace'
469
+ ): Promise<WorkspaceTargetResolution> {
470
+ const tab = ensured.workspace.tabs.find((item) => item.id === ensured.workspace.activeTabId) ?? ensured.workspace.tabs[0] ?? null;
471
+ if (tab) {
472
+ return {
473
+ tab,
474
+ workspace: ensured.workspace,
475
+ resolution,
476
+ createdWorkspace: ensured.created,
477
+ repaired: ensured.repaired,
478
+ repairActions: ensured.repairActions
479
+ };
480
+ }
481
+
482
+ if (ensured.workspace.activeTabId !== null) {
483
+ const activeWorkspaceTab = await this.waitForTrackedTab(ensured.workspace.activeTabId, ensured.workspace.windowId);
484
+ if (activeWorkspaceTab) {
485
+ return {
486
+ tab: activeWorkspaceTab,
487
+ workspace: ensured.workspace,
488
+ resolution,
489
+ createdWorkspace: ensured.created,
490
+ repaired: ensured.repaired,
491
+ repairActions: ensured.repairActions
492
+ };
493
+ }
494
+ }
495
+
496
+ const activeTab = await this.browser.getActiveTab();
497
+ if (!activeTab) {
498
+ throw new Error('No active tab');
499
+ }
500
+ return {
501
+ tab: activeTab,
502
+ workspace: null,
503
+ resolution: 'browser-active',
504
+ createdWorkspace: ensured.created,
505
+ repaired: ensured.repaired,
506
+ repairActions: ensured.repairActions
507
+ };
508
+ }
509
+
510
+ private async readTrackedTabs(tabIds: number[], windowId: number | null): Promise<WorkspaceTab[]> {
511
+ const tabs = (
512
+ await Promise.all(
513
+ tabIds.map(async (tabId) => {
514
+ const tab = await this.browser.getTab(tabId);
515
+ if (!tab) {
516
+ return null;
517
+ }
518
+ if (windowId !== null && tab.windowId !== windowId) {
519
+ return null;
520
+ }
521
+ return tab;
522
+ })
523
+ )
524
+ ).filter((tab): tab is WorkspaceTab => tab !== null);
525
+ return tabs;
526
+ }
527
+
528
+ private async recoverWorkspaceTabs(state: WorkspaceRecord, existingTabs: WorkspaceTab[]): Promise<WorkspaceTab[]> {
529
+ if (state.windowId === null) {
530
+ return existingTabs;
531
+ }
532
+
533
+ const candidates = await this.waitForWindowTabs(state.windowId, 500);
534
+ if (candidates.length === 0) {
535
+ return existingTabs;
536
+ }
537
+
538
+ const trackedIds = new Set(state.tabIds);
539
+ const trackedTabs = candidates.filter((tab) => trackedIds.has(tab.id));
540
+ if (trackedTabs.length > existingTabs.length) {
541
+ return trackedTabs;
542
+ }
543
+
544
+ if (state.groupId !== null) {
545
+ const groupedTabs = candidates.filter((tab) => tab.groupId === state.groupId);
546
+ if (groupedTabs.length > 0) {
547
+ return groupedTabs;
548
+ }
549
+ }
550
+
551
+ const preferredIds = new Set([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number'));
552
+ const preferredTabs = candidates.filter((tab) => preferredIds.has(tab.id));
553
+ if (preferredTabs.length > existingTabs.length) {
554
+ return preferredTabs;
555
+ }
556
+
557
+ return existingTabs;
558
+ }
559
+
560
+ private async createWorkspaceTab(options: { windowId: number | null; url: string; active: boolean }): Promise<WorkspaceTab> {
561
+ if (options.windowId === null) {
562
+ throw new Error('Workspace window is unavailable');
563
+ }
564
+
565
+ const deadline = Date.now() + 1_500;
566
+ let lastError: Error | null = null;
567
+
568
+ 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
+ try {
577
+ return await this.browser.createTab({
578
+ windowId: options.windowId,
579
+ url: options.url,
580
+ active: options.active
581
+ });
582
+ } catch (error) {
583
+ if (!this.isMissingWindowError(error)) {
584
+ throw error;
585
+ }
586
+ lastError = error instanceof Error ? error : new Error(String(error));
587
+ await this.delay(50);
588
+ }
589
+ }
590
+
591
+ throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
592
+ }
593
+
594
+ private async waitForTrackedTab(tabId: number, windowId: number | null, timeoutMs = 1_000): Promise<WorkspaceTab | null> {
595
+ const deadline = Date.now() + timeoutMs;
596
+ while (Date.now() < deadline) {
597
+ const tab = await this.browser.getTab(tabId);
598
+ if (tab && (windowId === null || tab.windowId === windowId)) {
599
+ return tab;
600
+ }
601
+ await this.delay(50);
602
+ }
603
+ return null;
604
+ }
605
+
606
+ private async waitForWindowTabs(windowId: number, timeoutMs = 1_000): Promise<WorkspaceTab[]> {
607
+ const deadline = Date.now() + timeoutMs;
608
+ while (Date.now() < deadline) {
609
+ const tabs = await this.browser.listTabs({ windowId });
610
+ if (tabs.length > 0) {
611
+ return tabs;
612
+ }
613
+ await this.delay(50);
614
+ }
615
+ return [];
616
+ }
617
+
618
+ private async delay(ms: number): Promise<void> {
619
+ await new Promise((resolve) => setTimeout(resolve, ms));
620
+ }
621
+
622
+ private isMissingWindowError(error: unknown): boolean {
623
+ const message = error instanceof Error ? error.message : String(error);
624
+ return message.toLowerCase().includes('no window with id');
625
+ }
626
+ }