@flrande/bak-extension 0.3.8 → 0.6.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.
- package/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +1305 -140
- package/dist/content.global.js +820 -3
- package/dist/manifest.json +2 -2
- package/package.json +2 -2
- package/public/manifest.json +2 -2
- package/src/background.ts +1762 -992
- package/src/content.ts +2419 -1593
- package/src/network-debugger.ts +495 -0
- package/src/privacy.ts +112 -1
- package/src/session-binding-storage.ts +68 -0
- package/src/workspace.ts +912 -917
package/src/workspace.ts
CHANGED
|
@@ -1,917 +1,912 @@
|
|
|
1
|
-
export const
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
private readonly
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
let
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
state.
|
|
147
|
-
state.
|
|
148
|
-
state.
|
|
149
|
-
state.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
state.
|
|
193
|
-
state.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
tabs = await this.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
repairActions
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
state.
|
|
279
|
-
state.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
ensured.repairActions.includes('
|
|
289
|
-
ensured.repairActions.includes('
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
await this.browser.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
await this.browser.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
return
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
private
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
recreatedTabs
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
if (
|
|
860
|
-
return
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
private isMissingWindowError(error: unknown): boolean {
|
|
914
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
915
|
-
return message.toLowerCase().includes('no window with id');
|
|
916
|
-
}
|
|
917
|
-
}
|
|
1
|
+
export const DEFAULT_WORKSPACE_LABEL = 'bak agent';
|
|
2
|
+
export const DEFAULT_WORKSPACE_COLOR = 'blue';
|
|
3
|
+
export const DEFAULT_WORKSPACE_URL = 'about:blank';
|
|
4
|
+
|
|
5
|
+
export type WorkspaceColor = 'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange';
|
|
6
|
+
|
|
7
|
+
export interface WorkspaceTab {
|
|
8
|
+
id: number;
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
active: boolean;
|
|
12
|
+
windowId: number;
|
|
13
|
+
groupId: number | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WorkspaceWindow {
|
|
17
|
+
id: number;
|
|
18
|
+
focused: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkspaceGroup {
|
|
22
|
+
id: number;
|
|
23
|
+
windowId: number;
|
|
24
|
+
title: string;
|
|
25
|
+
color: WorkspaceColor;
|
|
26
|
+
collapsed: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkspaceRecord {
|
|
30
|
+
id: string;
|
|
31
|
+
label: string;
|
|
32
|
+
color: WorkspaceColor;
|
|
33
|
+
windowId: number | null;
|
|
34
|
+
groupId: number | null;
|
|
35
|
+
tabIds: number[];
|
|
36
|
+
activeTabId: number | null;
|
|
37
|
+
primaryTabId: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkspaceInfo extends WorkspaceRecord {
|
|
41
|
+
tabs: WorkspaceTab[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WorkspaceEnsureResult {
|
|
45
|
+
workspace: WorkspaceInfo;
|
|
46
|
+
created: boolean;
|
|
47
|
+
repaired: boolean;
|
|
48
|
+
repairActions: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WorkspaceTargetResolution {
|
|
52
|
+
tab: WorkspaceTab;
|
|
53
|
+
workspace: WorkspaceInfo | null;
|
|
54
|
+
resolution: 'explicit-tab' | 'explicit-workspace' | 'default-workspace' | 'browser-active';
|
|
55
|
+
createdWorkspace: boolean;
|
|
56
|
+
repaired: boolean;
|
|
57
|
+
repairActions: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
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[]>;
|
|
65
|
+
}
|
|
66
|
+
|
|
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>;
|
|
73
|
+
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
|
+
closeWindow(windowId: number): Promise<void>;
|
|
78
|
+
getGroup(groupId: number): Promise<WorkspaceGroup | null>;
|
|
79
|
+
groupTabs(tabIds: number[], groupId?: number): Promise<number>;
|
|
80
|
+
updateGroup(groupId: number, options: { title?: string; color?: WorkspaceColor; collapsed?: boolean }): Promise<WorkspaceGroup>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface WorkspaceWindowOwnership {
|
|
84
|
+
workspaceTabs: WorkspaceTab[];
|
|
85
|
+
foreignTabs: WorkspaceTab[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface WorkspaceEnsureOptions {
|
|
89
|
+
workspaceId?: string;
|
|
90
|
+
focus?: boolean;
|
|
91
|
+
initialUrl?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface WorkspaceOpenTabOptions {
|
|
95
|
+
workspaceId?: string;
|
|
96
|
+
url?: string;
|
|
97
|
+
active?: boolean;
|
|
98
|
+
focus?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface WorkspaceResolveTargetOptions {
|
|
102
|
+
tabId?: number;
|
|
103
|
+
workspaceId?: string;
|
|
104
|
+
createIfMissing?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class SessionBindingManager {
|
|
108
|
+
private readonly storage: WorkspaceStorage;
|
|
109
|
+
private readonly browser: WorkspaceBrowser;
|
|
110
|
+
|
|
111
|
+
constructor(storage: WorkspaceStorage, browser: WorkspaceBrowser) {
|
|
112
|
+
this.storage = storage;
|
|
113
|
+
this.browser = browser;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getWorkspaceInfo(workspaceId: string): Promise<WorkspaceInfo | null> {
|
|
117
|
+
return this.inspectWorkspace(workspaceId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async ensureWorkspace(options: WorkspaceEnsureOptions = {}): Promise<WorkspaceEnsureResult> {
|
|
121
|
+
const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
|
|
122
|
+
const repairActions: string[] = [];
|
|
123
|
+
const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
|
|
124
|
+
const persisted = await this.storage.load(workspaceId);
|
|
125
|
+
const created = !persisted;
|
|
126
|
+
let state = this.normalizeState(persisted, workspaceId);
|
|
127
|
+
|
|
128
|
+
const originalWindowId = state.windowId;
|
|
129
|
+
let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
130
|
+
let tabs: WorkspaceTab[] = [];
|
|
131
|
+
if (!window) {
|
|
132
|
+
const rebound = await this.rebindWorkspaceWindow(state);
|
|
133
|
+
if (rebound) {
|
|
134
|
+
window = rebound.window;
|
|
135
|
+
tabs = rebound.tabs;
|
|
136
|
+
if (originalWindowId !== rebound.window.id) {
|
|
137
|
+
repairActions.push('rebound-window');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!window) {
|
|
142
|
+
const createdWindow = await this.browser.createWindow({
|
|
143
|
+
url: initialUrl,
|
|
144
|
+
focused: options.focus === true
|
|
145
|
+
});
|
|
146
|
+
state.windowId = createdWindow.id;
|
|
147
|
+
state.groupId = null;
|
|
148
|
+
state.tabIds = [];
|
|
149
|
+
state.activeTabId = null;
|
|
150
|
+
state.primaryTabId = null;
|
|
151
|
+
window = createdWindow;
|
|
152
|
+
tabs = await this.waitForWindowTabs(createdWindow.id);
|
|
153
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
154
|
+
if (state.primaryTabId === null) {
|
|
155
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
156
|
+
}
|
|
157
|
+
if (state.activeTabId === null) {
|
|
158
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
|
|
159
|
+
}
|
|
160
|
+
repairActions.push(created ? 'created-window' : 'recreated-window');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
164
|
+
const recoveredTabs = await this.recoverWorkspaceTabs(state, tabs);
|
|
165
|
+
if (recoveredTabs.length > tabs.length) {
|
|
166
|
+
tabs = recoveredTabs;
|
|
167
|
+
repairActions.push('recovered-tracked-tabs');
|
|
168
|
+
}
|
|
169
|
+
if (tabs.length !== state.tabIds.length) {
|
|
170
|
+
repairActions.push('pruned-missing-tabs');
|
|
171
|
+
}
|
|
172
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
173
|
+
|
|
174
|
+
if (state.windowId !== null) {
|
|
175
|
+
const ownership = await this.inspectWorkspaceWindowOwnership(state, state.windowId);
|
|
176
|
+
if (ownership.foreignTabs.length > 0) {
|
|
177
|
+
const migrated = await this.moveWorkspaceIntoDedicatedWindow(state, ownership, initialUrl);
|
|
178
|
+
window = migrated.window;
|
|
179
|
+
tabs = migrated.tabs;
|
|
180
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
181
|
+
repairActions.push('migrated-dirty-window');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (tabs.length === 0) {
|
|
186
|
+
const primary = await this.createWorkspaceTab({
|
|
187
|
+
windowId: state.windowId,
|
|
188
|
+
url: initialUrl,
|
|
189
|
+
active: true
|
|
190
|
+
});
|
|
191
|
+
tabs = [primary];
|
|
192
|
+
state.tabIds = [primary.id];
|
|
193
|
+
state.primaryTabId = primary.id;
|
|
194
|
+
state.activeTabId = primary.id;
|
|
195
|
+
repairActions.push('created-primary-tab');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (state.primaryTabId === null || !tabs.some((tab) => tab.id === state.primaryTabId)) {
|
|
199
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
200
|
+
repairActions.push('reassigned-primary-tab');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (state.activeTabId === null || !tabs.some((tab) => tab.id === state.activeTabId)) {
|
|
204
|
+
state.activeTabId = state.primaryTabId ?? tabs[0]?.id ?? null;
|
|
205
|
+
repairActions.push('reassigned-active-tab');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
|
|
209
|
+
if (!group || group.windowId !== state.windowId) {
|
|
210
|
+
const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
|
|
211
|
+
group = await this.browser.updateGroup(groupId, {
|
|
212
|
+
title: state.label,
|
|
213
|
+
color: state.color,
|
|
214
|
+
collapsed: false
|
|
215
|
+
});
|
|
216
|
+
state.groupId = group.id;
|
|
217
|
+
repairActions.push('recreated-group');
|
|
218
|
+
} else {
|
|
219
|
+
await this.browser.updateGroup(group.id, {
|
|
220
|
+
title: state.label,
|
|
221
|
+
color: state.color,
|
|
222
|
+
collapsed: false
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const ungroupedIds = tabs.filter((tab) => tab.groupId !== state.groupId).map((tab) => tab.id);
|
|
227
|
+
if (ungroupedIds.length > 0) {
|
|
228
|
+
await this.browser.groupTabs(ungroupedIds, state.groupId ?? undefined);
|
|
229
|
+
repairActions.push('regrouped-tabs');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
233
|
+
tabs = await this.recoverWorkspaceTabs(state, tabs);
|
|
234
|
+
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
|
|
235
|
+
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
236
|
+
tabs = [...tabs, activeTab];
|
|
237
|
+
}
|
|
238
|
+
if (tabs.length === 0 && state.primaryTabId !== null) {
|
|
239
|
+
const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId);
|
|
240
|
+
if (primaryTab) {
|
|
241
|
+
tabs = [primaryTab];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
245
|
+
|
|
246
|
+
if (options.focus === true && state.activeTabId !== null) {
|
|
247
|
+
await this.browser.updateTab(state.activeTabId, { active: true });
|
|
248
|
+
window = await this.browser.updateWindow(state.windowId!, { focused: true });
|
|
249
|
+
void window;
|
|
250
|
+
repairActions.push('focused-window');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await this.storage.save(state);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
workspace: {
|
|
257
|
+
...state,
|
|
258
|
+
tabs
|
|
259
|
+
},
|
|
260
|
+
created,
|
|
261
|
+
repaired: repairActions.length > 0,
|
|
262
|
+
repairActions
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
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,
|
|
271
|
+
focus: false,
|
|
272
|
+
initialUrl: hadWorkspace ? options.url ?? DEFAULT_WORKSPACE_URL : DEFAULT_WORKSPACE_URL
|
|
273
|
+
});
|
|
274
|
+
let state = { ...ensured.workspace, tabIds: [...ensured.workspace.tabIds], tabs: [...ensured.workspace.tabs] };
|
|
275
|
+
if (state.windowId !== null && state.tabs.length === 0) {
|
|
276
|
+
const rebound = await this.rebindWorkspaceWindow(state);
|
|
277
|
+
if (rebound) {
|
|
278
|
+
state.windowId = rebound.window.id;
|
|
279
|
+
state.tabs = rebound.tabs;
|
|
280
|
+
state.tabIds = [...new Set(rebound.tabs.map((tab) => tab.id))];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const active = options.active === true;
|
|
284
|
+
const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
|
|
285
|
+
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
286
|
+
state,
|
|
287
|
+
ensured.created ||
|
|
288
|
+
ensured.repairActions.includes('recreated-window') ||
|
|
289
|
+
ensured.repairActions.includes('created-primary-tab') ||
|
|
290
|
+
ensured.repairActions.includes('migrated-dirty-window')
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
let createdTab: WorkspaceTab;
|
|
294
|
+
try {
|
|
295
|
+
createdTab = reusablePrimaryTab
|
|
296
|
+
? await this.browser.updateTab(reusablePrimaryTab.id, {
|
|
297
|
+
url: desiredUrl,
|
|
298
|
+
active
|
|
299
|
+
})
|
|
300
|
+
: await this.createWorkspaceTab({
|
|
301
|
+
windowId: state.windowId,
|
|
302
|
+
url: desiredUrl,
|
|
303
|
+
active
|
|
304
|
+
});
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (!this.isMissingWindowError(error)) {
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
const repaired = await this.ensureWorkspace({
|
|
310
|
+
workspaceId,
|
|
311
|
+
focus: false,
|
|
312
|
+
initialUrl: desiredUrl
|
|
313
|
+
});
|
|
314
|
+
state = { ...repaired.workspace };
|
|
315
|
+
reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
|
|
316
|
+
createdTab = reusablePrimaryTab
|
|
317
|
+
? await this.browser.updateTab(reusablePrimaryTab.id, {
|
|
318
|
+
url: desiredUrl,
|
|
319
|
+
active
|
|
320
|
+
})
|
|
321
|
+
: await this.createWorkspaceTab({
|
|
322
|
+
windowId: state.windowId,
|
|
323
|
+
url: desiredUrl,
|
|
324
|
+
active
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const nextTabIds = [...new Set([...state.tabIds, createdTab.id])];
|
|
328
|
+
const groupId = await this.browser.groupTabs([createdTab.id], state.groupId ?? undefined);
|
|
329
|
+
await this.browser.updateGroup(groupId, {
|
|
330
|
+
title: state.label,
|
|
331
|
+
color: state.color,
|
|
332
|
+
collapsed: false
|
|
333
|
+
});
|
|
334
|
+
const nextState: WorkspaceRecord = {
|
|
335
|
+
id: state.id,
|
|
336
|
+
label: state.label,
|
|
337
|
+
color: state.color,
|
|
338
|
+
windowId: state.windowId,
|
|
339
|
+
groupId,
|
|
340
|
+
tabIds: nextTabIds,
|
|
341
|
+
activeTabId: createdTab.id,
|
|
342
|
+
primaryTabId: state.primaryTabId ?? createdTab.id
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (options.focus === true) {
|
|
346
|
+
await this.browser.updateTab(createdTab.id, { active: true });
|
|
347
|
+
await this.browser.updateWindow(state.windowId!, { focused: true });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await this.storage.save(nextState);
|
|
351
|
+
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
352
|
+
const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
|
|
353
|
+
return {
|
|
354
|
+
workspace: {
|
|
355
|
+
...nextState,
|
|
356
|
+
tabs
|
|
357
|
+
},
|
|
358
|
+
tab
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async listTabs(workspaceId: string): Promise<{ workspace: WorkspaceInfo; tabs: WorkspaceTab[] }> {
|
|
363
|
+
const ensured = await this.inspectWorkspace(workspaceId);
|
|
364
|
+
if (!ensured) {
|
|
365
|
+
throw new Error(`Workspace ${workspaceId} does not exist`);
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
workspace: ensured,
|
|
369
|
+
tabs: ensured.tabs
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async getActiveTab(workspaceId: string): Promise<{ workspace: WorkspaceInfo; tab: WorkspaceTab | null }> {
|
|
374
|
+
const ensured = await this.inspectWorkspace(workspaceId);
|
|
375
|
+
if (!ensured) {
|
|
376
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
377
|
+
return {
|
|
378
|
+
workspace: {
|
|
379
|
+
...this.normalizeState(null, normalizedWorkspaceId),
|
|
380
|
+
tabs: []
|
|
381
|
+
},
|
|
382
|
+
tab: null
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
workspace: ensured,
|
|
387
|
+
tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
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}`);
|
|
395
|
+
}
|
|
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],
|
|
403
|
+
activeTabId: tabId,
|
|
404
|
+
primaryTabId: ensured.workspace.primaryTabId ?? tabId
|
|
405
|
+
};
|
|
406
|
+
await this.storage.save(nextState);
|
|
407
|
+
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
408
|
+
const tab = tabs.find((item) => item.id === tabId);
|
|
409
|
+
if (!tab) {
|
|
410
|
+
throw new Error(`Tab ${tabId} is missing from workspace ${workspaceId}`);
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
workspace: {
|
|
414
|
+
...nextState,
|
|
415
|
+
tabs
|
|
416
|
+
},
|
|
417
|
+
tab
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
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 });
|
|
425
|
+
}
|
|
426
|
+
if (ensured.workspace.windowId !== null) {
|
|
427
|
+
await this.browser.updateWindow(ensured.workspace.windowId, { focused: true });
|
|
428
|
+
}
|
|
429
|
+
const refreshed = await this.ensureWorkspace({ workspaceId, focus: false });
|
|
430
|
+
return { ok: true, workspace: refreshed.workspace };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async reset(options: WorkspaceEnsureOptions = {}): Promise<WorkspaceEnsureResult> {
|
|
434
|
+
const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
|
|
435
|
+
await this.close(workspaceId);
|
|
436
|
+
return this.ensureWorkspace({
|
|
437
|
+
...options,
|
|
438
|
+
workspaceId
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async close(workspaceId: string): Promise<{ ok: true }> {
|
|
443
|
+
const state = await this.loadWorkspaceRecord(workspaceId);
|
|
444
|
+
if (!state) {
|
|
445
|
+
await this.storage.delete(workspaceId);
|
|
446
|
+
return { ok: true };
|
|
447
|
+
}
|
|
448
|
+
// 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);
|
|
451
|
+
if (state.windowId !== null) {
|
|
452
|
+
const existingWindow = await this.browser.getWindow(state.windowId);
|
|
453
|
+
if (existingWindow) {
|
|
454
|
+
await this.browser.closeWindow(state.windowId);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return { ok: true };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async resolveTarget(options: WorkspaceResolveTargetOptions = {}): Promise<WorkspaceTargetResolution> {
|
|
461
|
+
if (typeof options.tabId === 'number') {
|
|
462
|
+
const explicitTab = await this.browser.getTab(options.tabId);
|
|
463
|
+
if (!explicitTab) {
|
|
464
|
+
throw new Error(`No tab with id ${options.tabId}`);
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
tab: explicitTab,
|
|
468
|
+
workspace: null,
|
|
469
|
+
resolution: 'explicit-tab',
|
|
470
|
+
createdWorkspace: false,
|
|
471
|
+
repaired: false,
|
|
472
|
+
repairActions: []
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const explicitWorkspaceId = typeof options.workspaceId === 'string' ? this.normalizeWorkspaceId(options.workspaceId) : undefined;
|
|
477
|
+
if (explicitWorkspaceId) {
|
|
478
|
+
const ensured = await this.ensureWorkspace({
|
|
479
|
+
workspaceId: explicitWorkspaceId,
|
|
480
|
+
focus: false
|
|
481
|
+
});
|
|
482
|
+
return this.buildWorkspaceResolution(ensured, 'explicit-workspace');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (options.createIfMissing !== true) {
|
|
486
|
+
const activeTab = await this.browser.getActiveTab();
|
|
487
|
+
if (!activeTab) {
|
|
488
|
+
throw new Error('No active tab');
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
tab: activeTab,
|
|
492
|
+
workspace: null,
|
|
493
|
+
resolution: 'browser-active',
|
|
494
|
+
createdWorkspace: false,
|
|
495
|
+
repaired: false,
|
|
496
|
+
repairActions: []
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
throw new Error('workspaceId is required when createIfMissing is true');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private normalizeWorkspaceId(workspaceId?: string): string {
|
|
504
|
+
const candidate = workspaceId?.trim();
|
|
505
|
+
if (!candidate) {
|
|
506
|
+
throw new Error('workspaceId is required');
|
|
507
|
+
}
|
|
508
|
+
return candidate;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private normalizeState(state: WorkspaceRecord | null, workspaceId: string): WorkspaceRecord {
|
|
512
|
+
return {
|
|
513
|
+
id: workspaceId,
|
|
514
|
+
label: state?.label ?? DEFAULT_WORKSPACE_LABEL,
|
|
515
|
+
color: state?.color ?? DEFAULT_WORKSPACE_COLOR,
|
|
516
|
+
windowId: state?.windowId ?? null,
|
|
517
|
+
groupId: state?.groupId ?? null,
|
|
518
|
+
tabIds: state?.tabIds ?? [],
|
|
519
|
+
activeTabId: state?.activeTabId ?? null,
|
|
520
|
+
primaryTabId: state?.primaryTabId ?? null
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async listWorkspaceRecords(): Promise<WorkspaceRecord[]> {
|
|
525
|
+
return await this.storage.list();
|
|
526
|
+
}
|
|
527
|
+
|
|
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) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
return this.normalizeState(state, normalizedWorkspaceId);
|
|
535
|
+
}
|
|
536
|
+
|
|
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;
|
|
542
|
+
if (tab) {
|
|
543
|
+
return {
|
|
544
|
+
tab,
|
|
545
|
+
workspace: ensured.workspace,
|
|
546
|
+
resolution,
|
|
547
|
+
createdWorkspace: ensured.created,
|
|
548
|
+
repaired: ensured.repaired,
|
|
549
|
+
repairActions: ensured.repairActions
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (ensured.workspace.activeTabId !== null) {
|
|
554
|
+
const activeWorkspaceTab = await this.waitForTrackedTab(ensured.workspace.activeTabId, ensured.workspace.windowId);
|
|
555
|
+
if (activeWorkspaceTab) {
|
|
556
|
+
return {
|
|
557
|
+
tab: activeWorkspaceTab,
|
|
558
|
+
workspace: ensured.workspace,
|
|
559
|
+
resolution,
|
|
560
|
+
createdWorkspace: ensured.created,
|
|
561
|
+
repaired: ensured.repaired,
|
|
562
|
+
repairActions: ensured.repairActions
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const activeTab = await this.browser.getActiveTab();
|
|
568
|
+
if (!activeTab) {
|
|
569
|
+
throw new Error('No active tab');
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
tab: activeTab,
|
|
573
|
+
workspace: null,
|
|
574
|
+
resolution: 'browser-active',
|
|
575
|
+
createdWorkspace: ensured.created,
|
|
576
|
+
repaired: ensured.repaired,
|
|
577
|
+
repairActions: ensured.repairActions
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private async readTrackedTabs(tabIds: number[], windowId: number | null): Promise<WorkspaceTab[]> {
|
|
582
|
+
const tabs = (
|
|
583
|
+
await Promise.all(
|
|
584
|
+
tabIds.map(async (tabId) => {
|
|
585
|
+
const tab = await this.browser.getTab(tabId);
|
|
586
|
+
if (!tab) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
if (windowId !== null && tab.windowId !== windowId) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return tab;
|
|
593
|
+
})
|
|
594
|
+
)
|
|
595
|
+
).filter((tab): tab is WorkspaceTab => tab !== null);
|
|
596
|
+
return tabs;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private async readLooseTrackedTabs(tabIds: number[]): Promise<WorkspaceTab[]> {
|
|
600
|
+
const tabs = (
|
|
601
|
+
await Promise.all(
|
|
602
|
+
tabIds.map(async (tabId) => {
|
|
603
|
+
return await this.browser.getTab(tabId);
|
|
604
|
+
})
|
|
605
|
+
)
|
|
606
|
+
).filter((tab): tab is WorkspaceTab => tab !== null);
|
|
607
|
+
return tabs;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private collectCandidateTabIds(state: WorkspaceRecord): number[] {
|
|
611
|
+
return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private async rebindWorkspaceWindow(state: WorkspaceRecord): Promise<{ window: WorkspaceWindow; tabs: WorkspaceTab[] } | null> {
|
|
615
|
+
const candidateWindowIds: number[] = [];
|
|
616
|
+
const pushWindowId = (windowId: number | null | undefined): void => {
|
|
617
|
+
if (typeof windowId !== 'number') {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (!candidateWindowIds.includes(windowId)) {
|
|
621
|
+
candidateWindowIds.push(windowId);
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
|
|
626
|
+
pushWindowId(group?.windowId);
|
|
627
|
+
|
|
628
|
+
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
629
|
+
for (const tab of trackedTabs) {
|
|
630
|
+
pushWindowId(tab.windowId);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
for (const candidateWindowId of candidateWindowIds) {
|
|
634
|
+
const window = await this.waitForWindow(candidateWindowId);
|
|
635
|
+
if (!window) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
|
|
639
|
+
if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
|
|
640
|
+
const windowTabs = await this.waitForWindowTabs(candidateWindowId, 750);
|
|
641
|
+
tabs = windowTabs.filter((tab) => tab.groupId === group.id);
|
|
642
|
+
}
|
|
643
|
+
if (tabs.length === 0) {
|
|
644
|
+
tabs = trackedTabs.filter((tab) => tab.windowId === candidateWindowId);
|
|
645
|
+
}
|
|
646
|
+
state.windowId = candidateWindowId;
|
|
647
|
+
if (tabs.length > 0) {
|
|
648
|
+
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
649
|
+
if (state.primaryTabId === null || !state.tabIds.includes(state.primaryTabId)) {
|
|
650
|
+
state.primaryTabId = tabs[0]?.id ?? null;
|
|
651
|
+
}
|
|
652
|
+
if (state.activeTabId === null || !state.tabIds.includes(state.activeTabId)) {
|
|
653
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? state.primaryTabId;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return { window, tabs };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private async inspectWorkspaceWindowOwnership(state: WorkspaceRecord, windowId: number): Promise<WorkspaceWindowOwnership> {
|
|
663
|
+
const windowTabs = await this.waitForWindowTabs(windowId, 500);
|
|
664
|
+
const trackedIds = new Set(this.collectCandidateTabIds(state));
|
|
665
|
+
return {
|
|
666
|
+
workspaceTabs: windowTabs.filter((tab) => trackedIds.has(tab.id) || (state.groupId !== null && tab.groupId === state.groupId)),
|
|
667
|
+
foreignTabs: windowTabs.filter((tab) => !trackedIds.has(tab.id) && (state.groupId === null || tab.groupId !== state.groupId))
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private async moveWorkspaceIntoDedicatedWindow(
|
|
672
|
+
state: WorkspaceRecord,
|
|
673
|
+
ownership: WorkspaceWindowOwnership,
|
|
674
|
+
initialUrl: string
|
|
675
|
+
): Promise<{ window: WorkspaceWindow; tabs: WorkspaceTab[] }> {
|
|
676
|
+
const sourceTabs = this.orderWorkspaceTabsForMigration(state, ownership.workspaceTabs);
|
|
677
|
+
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
678
|
+
const window = await this.browser.createWindow({
|
|
679
|
+
url: seedUrl || DEFAULT_WORKSPACE_URL,
|
|
680
|
+
focused: false
|
|
681
|
+
});
|
|
682
|
+
const recreatedTabs = await this.waitForWindowTabs(window.id);
|
|
683
|
+
const firstTab = recreatedTabs[0] ?? null;
|
|
684
|
+
const tabIdMap = new Map<number, number>();
|
|
685
|
+
if (sourceTabs[0] && firstTab) {
|
|
686
|
+
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
for (const sourceTab of sourceTabs.slice(1)) {
|
|
690
|
+
const recreated = await this.createWorkspaceTab({
|
|
691
|
+
windowId: window.id,
|
|
692
|
+
url: sourceTab.url,
|
|
693
|
+
active: false
|
|
694
|
+
});
|
|
695
|
+
recreatedTabs.push(recreated);
|
|
696
|
+
tabIdMap.set(sourceTab.id, recreated.id);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const nextPrimaryTabId =
|
|
700
|
+
(state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : undefined) ??
|
|
701
|
+
firstTab?.id ??
|
|
702
|
+
recreatedTabs[0]?.id ??
|
|
703
|
+
null;
|
|
704
|
+
const nextActiveTabId =
|
|
705
|
+
(state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : undefined) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
|
|
706
|
+
if (nextActiveTabId !== null) {
|
|
707
|
+
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
state.windowId = window.id;
|
|
711
|
+
state.groupId = null;
|
|
712
|
+
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
713
|
+
state.primaryTabId = nextPrimaryTabId;
|
|
714
|
+
state.activeTabId = nextActiveTabId;
|
|
715
|
+
|
|
716
|
+
for (const workspaceTab of ownership.workspaceTabs) {
|
|
717
|
+
await this.browser.closeTab(workspaceTab.id);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
window,
|
|
722
|
+
tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private orderWorkspaceTabsForMigration(state: WorkspaceRecord, tabs: WorkspaceTab[]): WorkspaceTab[] {
|
|
727
|
+
const ordered: WorkspaceTab[] = [];
|
|
728
|
+
const seen = new Set<number>();
|
|
729
|
+
const pushById = (tabId: number | null): void => {
|
|
730
|
+
if (typeof tabId !== 'number') {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const tab = tabs.find((candidate) => candidate.id === tabId);
|
|
734
|
+
if (!tab || seen.has(tab.id)) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
ordered.push(tab);
|
|
738
|
+
seen.add(tab.id);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
pushById(state.primaryTabId);
|
|
742
|
+
pushById(state.activeTabId);
|
|
743
|
+
for (const tab of tabs) {
|
|
744
|
+
if (seen.has(tab.id)) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
ordered.push(tab);
|
|
748
|
+
seen.add(tab.id);
|
|
749
|
+
}
|
|
750
|
+
return ordered;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private async recoverWorkspaceTabs(state: WorkspaceRecord, existingTabs: WorkspaceTab[]): Promise<WorkspaceTab[]> {
|
|
754
|
+
if (state.windowId === null) {
|
|
755
|
+
return existingTabs;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const candidates = await this.waitForWindowTabs(state.windowId, 500);
|
|
759
|
+
if (candidates.length === 0) {
|
|
760
|
+
return existingTabs;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const trackedIds = new Set(state.tabIds);
|
|
764
|
+
const trackedTabs = candidates.filter((tab) => trackedIds.has(tab.id));
|
|
765
|
+
if (trackedTabs.length > existingTabs.length) {
|
|
766
|
+
return trackedTabs;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (state.groupId !== null) {
|
|
770
|
+
const groupedTabs = candidates.filter((tab) => tab.groupId === state.groupId);
|
|
771
|
+
if (groupedTabs.length > 0) {
|
|
772
|
+
return groupedTabs;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const preferredIds = new Set([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number'));
|
|
777
|
+
const preferredTabs = candidates.filter((tab) => preferredIds.has(tab.id));
|
|
778
|
+
if (preferredTabs.length > existingTabs.length) {
|
|
779
|
+
return preferredTabs;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return existingTabs;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private async createWorkspaceTab(options: { windowId: number | null; url: string; active: boolean }): Promise<WorkspaceTab> {
|
|
786
|
+
if (options.windowId === null) {
|
|
787
|
+
throw new Error('Workspace window is unavailable');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const deadline = Date.now() + 1_500;
|
|
791
|
+
let lastError: Error | null = null;
|
|
792
|
+
|
|
793
|
+
while (Date.now() < deadline) {
|
|
794
|
+
try {
|
|
795
|
+
return await this.browser.createTab({
|
|
796
|
+
windowId: options.windowId,
|
|
797
|
+
url: options.url,
|
|
798
|
+
active: options.active
|
|
799
|
+
});
|
|
800
|
+
} catch (error) {
|
|
801
|
+
if (!this.isMissingWindowError(error)) {
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
805
|
+
await this.delay(50);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
private async inspectWorkspace(workspaceId: string): Promise<WorkspaceInfo | null> {
|
|
813
|
+
const state = await this.loadWorkspaceRecord(workspaceId);
|
|
814
|
+
if (!state) {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
819
|
+
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
|
|
820
|
+
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
821
|
+
tabs = [...tabs, activeTab];
|
|
822
|
+
}
|
|
823
|
+
if (tabs.length === 0 && state.primaryTabId !== null) {
|
|
824
|
+
const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
|
|
825
|
+
if (primaryTab) {
|
|
826
|
+
tabs = [primaryTab];
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
...state,
|
|
832
|
+
tabIds: [...new Set(state.tabIds.concat(tabs.map((tab) => tab.id)))],
|
|
833
|
+
tabs
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
private async resolveReusablePrimaryTab(workspace: WorkspaceInfo, allowReuse: boolean): Promise<WorkspaceTab | null> {
|
|
838
|
+
if (workspace.windowId === null) {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
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
|
+
return trackedPrimary;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
|
|
848
|
+
if (windowTabs.length !== 1) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
const candidate = windowTabs[0]!;
|
|
852
|
+
if (allowReuse || this.isReusableBlankWorkspaceTab(candidate, workspace)) {
|
|
853
|
+
return candidate;
|
|
854
|
+
}
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private isReusableBlankWorkspaceTab(tab: WorkspaceTab, workspace: WorkspaceInfo): boolean {
|
|
859
|
+
if (workspace.tabIds.length > 1) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
const normalizedUrl = tab.url.trim().toLowerCase();
|
|
863
|
+
return normalizedUrl === '' || normalizedUrl === DEFAULT_WORKSPACE_URL;
|
|
864
|
+
}
|
|
865
|
+
|
|
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;
|
|
872
|
+
}
|
|
873
|
+
await this.delay(50);
|
|
874
|
+
}
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private async waitForTrackedTab(tabId: number, windowId: number | null, timeoutMs = 1_000): Promise<WorkspaceTab | null> {
|
|
879
|
+
const deadline = Date.now() + timeoutMs;
|
|
880
|
+
while (Date.now() < deadline) {
|
|
881
|
+
const tab = await this.browser.getTab(tabId);
|
|
882
|
+
if (tab && (windowId === null || tab.windowId === windowId)) {
|
|
883
|
+
return tab;
|
|
884
|
+
}
|
|
885
|
+
await this.delay(50);
|
|
886
|
+
}
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private async waitForWindowTabs(windowId: number, timeoutMs = 1_000): Promise<WorkspaceTab[]> {
|
|
891
|
+
const deadline = Date.now() + timeoutMs;
|
|
892
|
+
while (Date.now() < deadline) {
|
|
893
|
+
const tabs = await this.browser.listTabs({ windowId });
|
|
894
|
+
if (tabs.length > 0) {
|
|
895
|
+
return tabs;
|
|
896
|
+
}
|
|
897
|
+
await this.delay(50);
|
|
898
|
+
}
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private async delay(ms: number): Promise<void> {
|
|
903
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private isMissingWindowError(error: unknown): boolean {
|
|
907
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
908
|
+
return message.toLowerCase().includes('no window with id');
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export { SessionBindingManager, SessionBindingManager as WorkspaceManager };
|