@flrande/bak-extension 0.3.8 → 0.5.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 +169 -111
- package/dist/content.global.js +29 -0
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/public/manifest.json +1 -1
- package/src/background.ts +1178 -1157
- package/src/content.ts +1778 -1747
- package/src/session-binding-storage.ts +68 -0
- package/src/workspace.ts +912 -917
package/src/background.ts
CHANGED
|
@@ -1,1160 +1,1181 @@
|
|
|
1
|
-
import type { ConsoleEntry, Locator } from '@flrande/bak-protocol';
|
|
2
|
-
import { isSupportedAutomationUrl } from './url-policy.js';
|
|
3
|
-
import { computeReconnectDelayMs } from './reconnect.js';
|
|
4
|
-
import {
|
|
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
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
let
|
|
55
|
-
let
|
|
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
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
async
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
await
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
return { ok: true };
|
|
688
|
-
}
|
|
689
|
-
case 'tabs.
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
return {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const
|
|
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
|
-
return await
|
|
749
|
-
workspaceId:
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
case 'workspace.
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
return await
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
case 'workspace.
|
|
779
|
-
return await
|
|
780
|
-
}
|
|
781
|
-
case '
|
|
782
|
-
return await
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
await
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
await
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
await
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
case 'element.
|
|
887
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
888
|
-
const tab = await withTab(target);
|
|
889
|
-
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
890
|
-
type: 'bak.performAction',
|
|
891
|
-
action: '
|
|
892
|
-
locator: params.locator as Locator,
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
});
|
|
928
|
-
if (!response.ok) {
|
|
929
|
-
throw response.error ?? toError('
|
|
930
|
-
}
|
|
931
|
-
return { ok: true };
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
case '
|
|
935
|
-
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
936
|
-
const tab = await withTab(target);
|
|
937
|
-
const response = await sendToContent<{
|
|
938
|
-
type: 'bak.
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
ws.
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
chrome.tabs.
|
|
1078
|
-
void
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
})
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1
|
+
import type { ConsoleEntry, Locator } from '@flrande/bak-protocol';
|
|
2
|
+
import { isSupportedAutomationUrl } from './url-policy.js';
|
|
3
|
+
import { computeReconnectDelayMs } from './reconnect.js';
|
|
4
|
+
import {
|
|
5
|
+
LEGACY_STORAGE_KEY_WORKSPACE,
|
|
6
|
+
LEGACY_STORAGE_KEY_WORKSPACES,
|
|
7
|
+
resolveSessionBindingStateMap,
|
|
8
|
+
STORAGE_KEY_SESSION_BINDINGS
|
|
9
|
+
} from './session-binding-storage.js';
|
|
10
|
+
import {
|
|
11
|
+
type WorkspaceBrowser as SessionBindingBrowser,
|
|
12
|
+
type WorkspaceColor as SessionBindingColor,
|
|
13
|
+
type WorkspaceRecord as SessionBindingRecord,
|
|
14
|
+
type WorkspaceTab as SessionBindingTab,
|
|
15
|
+
type WorkspaceWindow as SessionBindingWindow,
|
|
16
|
+
WorkspaceManager as SessionBindingManager
|
|
17
|
+
} from './workspace.js';
|
|
18
|
+
|
|
19
|
+
interface CliRequest {
|
|
20
|
+
id: string;
|
|
21
|
+
method: string;
|
|
22
|
+
params?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CliResponse {
|
|
26
|
+
id: string;
|
|
27
|
+
ok: boolean;
|
|
28
|
+
result?: unknown;
|
|
29
|
+
error?: {
|
|
30
|
+
code: string;
|
|
31
|
+
message: string;
|
|
32
|
+
data?: Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ExtensionConfig {
|
|
37
|
+
token: string;
|
|
38
|
+
port: number;
|
|
39
|
+
debugRichText: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RuntimeErrorDetails {
|
|
43
|
+
message: string;
|
|
44
|
+
context: 'config' | 'socket' | 'request' | 'parse';
|
|
45
|
+
at: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_PORT = 17373;
|
|
49
|
+
const STORAGE_KEY_TOKEN = 'pairToken';
|
|
50
|
+
const STORAGE_KEY_PORT = 'cliPort';
|
|
51
|
+
const STORAGE_KEY_DEBUG_RICH_TEXT = 'debugRichText';
|
|
52
|
+
const DEFAULT_TAB_LOAD_TIMEOUT_MS = 40_000;
|
|
53
|
+
|
|
54
|
+
let ws: WebSocket | null = null;
|
|
55
|
+
let reconnectTimer: number | null = null;
|
|
56
|
+
let nextReconnectInMs: number | null = null;
|
|
57
|
+
let reconnectAttempt = 0;
|
|
58
|
+
let lastError: RuntimeErrorDetails | null = null;
|
|
59
|
+
let manualDisconnect = false;
|
|
60
|
+
|
|
61
|
+
async function getConfig(): Promise<ExtensionConfig> {
|
|
62
|
+
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
63
|
+
return {
|
|
64
|
+
token: typeof stored[STORAGE_KEY_TOKEN] === 'string' ? stored[STORAGE_KEY_TOKEN] : '',
|
|
65
|
+
port: typeof stored[STORAGE_KEY_PORT] === 'number' ? stored[STORAGE_KEY_PORT] : DEFAULT_PORT,
|
|
66
|
+
debugRichText: stored[STORAGE_KEY_DEBUG_RICH_TEXT] === true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function setConfig(config: Partial<ExtensionConfig>): Promise<void> {
|
|
71
|
+
const payload: Record<string, unknown> = {};
|
|
72
|
+
if (typeof config.token === 'string') {
|
|
73
|
+
payload[STORAGE_KEY_TOKEN] = config.token;
|
|
74
|
+
}
|
|
75
|
+
if (typeof config.port === 'number') {
|
|
76
|
+
payload[STORAGE_KEY_PORT] = config.port;
|
|
77
|
+
}
|
|
78
|
+
if (typeof config.debugRichText === 'boolean') {
|
|
79
|
+
payload[STORAGE_KEY_DEBUG_RICH_TEXT] = config.debugRichText;
|
|
80
|
+
}
|
|
81
|
+
if (Object.keys(payload).length > 0) {
|
|
82
|
+
await chrome.storage.local.set(payload);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setRuntimeError(message: string, context: RuntimeErrorDetails['context']): void {
|
|
87
|
+
lastError = {
|
|
88
|
+
message,
|
|
89
|
+
context,
|
|
90
|
+
at: Date.now()
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function clearReconnectTimer(): void {
|
|
95
|
+
if (reconnectTimer !== null) {
|
|
96
|
+
clearTimeout(reconnectTimer);
|
|
97
|
+
reconnectTimer = null;
|
|
98
|
+
}
|
|
99
|
+
nextReconnectInMs = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sendResponse(payload: CliResponse): void {
|
|
103
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
104
|
+
ws.send(JSON.stringify(payload));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toError(code: string, message: string, data?: Record<string, unknown>): CliResponse['error'] {
|
|
109
|
+
return { code, message, data };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function normalizeUnhandledError(error: unknown): CliResponse['error'] {
|
|
113
|
+
if (typeof error === 'object' && error !== null && 'code' in error) {
|
|
114
|
+
return error as CliResponse['error'];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
const lower = message.toLowerCase();
|
|
119
|
+
|
|
120
|
+
if (lower.includes('no tab with id') || lower.includes('no window with id')) {
|
|
121
|
+
return toError('E_NOT_FOUND', message);
|
|
122
|
+
}
|
|
123
|
+
if (lower.includes('workspace') && lower.includes('does not exist')) {
|
|
124
|
+
return toError('E_NOT_FOUND', message);
|
|
125
|
+
}
|
|
126
|
+
if (lower.includes('does not belong to workspace') || lower.includes('is missing from workspace')) {
|
|
127
|
+
return toError('E_NOT_FOUND', message);
|
|
128
|
+
}
|
|
129
|
+
if (lower.includes('invalid url') || lower.includes('url is invalid')) {
|
|
130
|
+
return toError('E_INVALID_PARAMS', message);
|
|
131
|
+
}
|
|
132
|
+
if (lower.includes('cannot access contents of url') || lower.includes('permission denied')) {
|
|
133
|
+
return toError('E_PERMISSION', message);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return toError('E_INTERNAL', message);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toTabInfo(tab: chrome.tabs.Tab): SessionBindingTab {
|
|
140
|
+
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
141
|
+
throw new Error('Tab is missing runtime identifiers');
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
id: tab.id,
|
|
145
|
+
title: tab.title ?? '',
|
|
146
|
+
url: tab.url ?? '',
|
|
147
|
+
active: Boolean(tab.active),
|
|
148
|
+
windowId: tab.windowId,
|
|
149
|
+
groupId: typeof tab.groupId === 'number' && tab.groupId >= 0 ? tab.groupId : null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function loadWorkspaceStateMap(): Promise<Record<string, SessionBindingRecord>> {
|
|
154
|
+
const stored = await chrome.storage.local.get([
|
|
155
|
+
STORAGE_KEY_SESSION_BINDINGS,
|
|
156
|
+
LEGACY_STORAGE_KEY_WORKSPACES,
|
|
157
|
+
LEGACY_STORAGE_KEY_WORKSPACE
|
|
158
|
+
]);
|
|
159
|
+
return resolveSessionBindingStateMap(stored);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function loadWorkspaceState(workspaceId: string): Promise<SessionBindingRecord | null> {
|
|
163
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
164
|
+
return stateMap[workspaceId] ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function listWorkspaceStates(): Promise<SessionBindingRecord[]> {
|
|
168
|
+
return Object.values(await loadWorkspaceStateMap());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function saveWorkspaceState(state: SessionBindingRecord): Promise<void> {
|
|
172
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
173
|
+
stateMap[state.id] = state;
|
|
174
|
+
await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
|
|
175
|
+
await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function deleteWorkspaceState(workspaceId: string): Promise<void> {
|
|
179
|
+
const stateMap = await loadWorkspaceStateMap();
|
|
180
|
+
delete stateMap[workspaceId];
|
|
181
|
+
if (Object.keys(stateMap).length === 0) {
|
|
182
|
+
await chrome.storage.local.remove([STORAGE_KEY_SESSION_BINDINGS, LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await chrome.storage.local.set({ [STORAGE_KEY_SESSION_BINDINGS]: stateMap });
|
|
186
|
+
await chrome.storage.local.remove([LEGACY_STORAGE_KEY_WORKSPACES, LEGACY_STORAGE_KEY_WORKSPACE]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const workspaceBrowser: SessionBindingBrowser = {
|
|
190
|
+
async getTab(tabId) {
|
|
191
|
+
try {
|
|
192
|
+
return toTabInfo(await chrome.tabs.get(tabId));
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
async getActiveTab() {
|
|
198
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
199
|
+
const tab = tabs[0];
|
|
200
|
+
if (!tab) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return toTabInfo(tab);
|
|
204
|
+
},
|
|
205
|
+
async listTabs(filter) {
|
|
206
|
+
const tabs = await chrome.tabs.query(filter?.windowId ? { windowId: filter.windowId } : {});
|
|
207
|
+
return tabs
|
|
208
|
+
.filter((tab): tab is chrome.tabs.Tab => typeof tab.id === 'number' && typeof tab.windowId === 'number')
|
|
209
|
+
.map((tab) => toTabInfo(tab));
|
|
210
|
+
},
|
|
211
|
+
async createTab(options) {
|
|
212
|
+
const createdTab = await chrome.tabs.create({
|
|
213
|
+
windowId: options.windowId,
|
|
214
|
+
url: options.url ?? 'about:blank',
|
|
215
|
+
active: options.active
|
|
216
|
+
});
|
|
217
|
+
if (!createdTab) {
|
|
218
|
+
throw new Error('Tab creation returned no tab');
|
|
219
|
+
}
|
|
220
|
+
return toTabInfo(createdTab);
|
|
221
|
+
},
|
|
222
|
+
async updateTab(tabId, options) {
|
|
223
|
+
const updatedTab = await chrome.tabs.update(tabId, {
|
|
224
|
+
active: options.active,
|
|
225
|
+
url: options.url
|
|
226
|
+
});
|
|
227
|
+
if (!updatedTab) {
|
|
228
|
+
throw new Error(`Tab update returned no tab for ${tabId}`);
|
|
229
|
+
}
|
|
230
|
+
return toTabInfo(updatedTab);
|
|
231
|
+
},
|
|
232
|
+
async closeTab(tabId) {
|
|
233
|
+
await chrome.tabs.remove(tabId);
|
|
234
|
+
},
|
|
235
|
+
async getWindow(windowId) {
|
|
236
|
+
try {
|
|
237
|
+
const window = await chrome.windows.get(windowId);
|
|
238
|
+
return {
|
|
239
|
+
id: window.id!,
|
|
240
|
+
focused: Boolean(window.focused)
|
|
241
|
+
} satisfies SessionBindingWindow;
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
async createWindow(options) {
|
|
247
|
+
const previouslyFocusedWindow =
|
|
248
|
+
options.focused === true
|
|
249
|
+
? null
|
|
250
|
+
: (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === 'number') ?? null;
|
|
251
|
+
const previouslyFocusedTab =
|
|
252
|
+
previouslyFocusedWindow?.id !== undefined
|
|
253
|
+
? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === 'number') ?? null
|
|
254
|
+
: null;
|
|
255
|
+
const created = await chrome.windows.create({
|
|
256
|
+
url: options.url ?? 'about:blank',
|
|
257
|
+
focused: true
|
|
258
|
+
});
|
|
259
|
+
if (!created || typeof created.id !== 'number') {
|
|
260
|
+
throw new Error('Window missing id');
|
|
261
|
+
}
|
|
262
|
+
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
263
|
+
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
264
|
+
if (typeof previouslyFocusedTab?.id === 'number') {
|
|
265
|
+
await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const finalWindow = await chrome.windows.get(created.id);
|
|
269
|
+
return {
|
|
270
|
+
id: finalWindow.id!,
|
|
271
|
+
focused: Boolean(finalWindow.focused)
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
async updateWindow(windowId, options) {
|
|
275
|
+
const updated = await chrome.windows.update(windowId, {
|
|
276
|
+
focused: options.focused
|
|
277
|
+
});
|
|
278
|
+
if (!updated || typeof updated.id !== 'number') {
|
|
279
|
+
throw new Error('Window missing id');
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
id: updated.id,
|
|
283
|
+
focused: Boolean(updated.focused)
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
async closeWindow(windowId) {
|
|
287
|
+
await chrome.windows.remove(windowId);
|
|
288
|
+
},
|
|
289
|
+
async getGroup(groupId) {
|
|
290
|
+
try {
|
|
291
|
+
const group = await chrome.tabGroups.get(groupId);
|
|
292
|
+
return {
|
|
293
|
+
id: group.id,
|
|
294
|
+
windowId: group.windowId,
|
|
295
|
+
title: group.title ?? '',
|
|
296
|
+
color: group.color as SessionBindingColor,
|
|
297
|
+
collapsed: Boolean(group.collapsed)
|
|
298
|
+
};
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
async groupTabs(tabIds, groupId) {
|
|
304
|
+
return await chrome.tabs.group({
|
|
305
|
+
tabIds: tabIds as [number, ...number[]],
|
|
306
|
+
groupId
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
async updateGroup(groupId, options) {
|
|
310
|
+
const updated = await chrome.tabGroups.update(groupId, {
|
|
311
|
+
title: options.title,
|
|
312
|
+
color: options.color,
|
|
313
|
+
collapsed: options.collapsed
|
|
314
|
+
});
|
|
315
|
+
if (!updated) {
|
|
316
|
+
throw new Error(`Tab group update returned no group for ${groupId}`);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
id: updated.id,
|
|
320
|
+
windowId: updated.windowId,
|
|
321
|
+
title: updated.title ?? '',
|
|
322
|
+
color: updated.color as SessionBindingColor,
|
|
323
|
+
collapsed: Boolean(updated.collapsed)
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const bindingManager = new SessionBindingManager(
|
|
329
|
+
{
|
|
330
|
+
load: loadWorkspaceState,
|
|
331
|
+
save: saveWorkspaceState,
|
|
332
|
+
delete: deleteWorkspaceState,
|
|
333
|
+
list: listWorkspaceStates
|
|
334
|
+
},
|
|
335
|
+
workspaceBrowser
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
async function waitForTabComplete(tabId: number, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS): Promise<void> {
|
|
339
|
+
try {
|
|
340
|
+
const current = await chrome.tabs.get(tabId);
|
|
341
|
+
if (current.status === 'complete') {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await new Promise<void>((resolve, reject) => {
|
|
349
|
+
let done = false;
|
|
350
|
+
const probeStatus = (): void => {
|
|
351
|
+
void chrome.tabs
|
|
352
|
+
.get(tabId)
|
|
353
|
+
.then((tab) => {
|
|
354
|
+
if (tab.status === 'complete') {
|
|
355
|
+
finish();
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
.catch(() => {
|
|
359
|
+
finish(new Error(`tab removed before load complete: ${tabId}`));
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const finish = (error?: Error): void => {
|
|
364
|
+
if (done) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
done = true;
|
|
368
|
+
clearTimeout(timeoutTimer);
|
|
369
|
+
clearInterval(pollTimer);
|
|
370
|
+
chrome.tabs.onUpdated.removeListener(onUpdated);
|
|
371
|
+
chrome.tabs.onRemoved.removeListener(onRemoved);
|
|
372
|
+
if (error) {
|
|
373
|
+
reject(error);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
resolve();
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const onUpdated = (updatedTabId: number, changeInfo: { status?: string }): void => {
|
|
380
|
+
if (updatedTabId !== tabId) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (changeInfo.status === 'complete') {
|
|
384
|
+
finish();
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const onRemoved = (removedTabId: number): void => {
|
|
389
|
+
if (removedTabId === tabId) {
|
|
390
|
+
finish(new Error(`tab removed before load complete: ${tabId}`));
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const pollTimer = setInterval(probeStatus, 250);
|
|
395
|
+
const timeoutTimer = setTimeout(() => {
|
|
396
|
+
finish(new Error(`tab load timeout: ${tabId}`));
|
|
397
|
+
}, timeoutMs);
|
|
398
|
+
|
|
399
|
+
chrome.tabs.onUpdated.addListener(onUpdated);
|
|
400
|
+
chrome.tabs.onRemoved.addListener(onRemoved);
|
|
401
|
+
probeStatus();
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function waitForTabUrl(tabId: number, expectedUrl: string, timeoutMs = 10_000): Promise<void> {
|
|
406
|
+
const normalizedExpectedUrl = normalizeComparableTabUrl(expectedUrl);
|
|
407
|
+
const deadline = Date.now() + timeoutMs;
|
|
408
|
+
while (Date.now() < deadline) {
|
|
409
|
+
try {
|
|
410
|
+
const tab = await chrome.tabs.get(tabId);
|
|
411
|
+
const currentUrl = tab.url ?? '';
|
|
412
|
+
const pendingUrl = 'pendingUrl' in tab && typeof tab.pendingUrl === 'string' ? tab.pendingUrl : '';
|
|
413
|
+
if (
|
|
414
|
+
normalizeComparableTabUrl(currentUrl) === normalizedExpectedUrl ||
|
|
415
|
+
normalizeComparableTabUrl(pendingUrl) === normalizedExpectedUrl
|
|
416
|
+
) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
// Ignore transient lookup failures while the tab is navigating.
|
|
421
|
+
}
|
|
422
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function normalizeComparableTabUrl(url: string): string {
|
|
429
|
+
const raw = url.trim();
|
|
430
|
+
if (!raw) {
|
|
431
|
+
return raw;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const parsed = new URL(raw);
|
|
435
|
+
parsed.hash = '';
|
|
436
|
+
return parsed.href;
|
|
437
|
+
} catch {
|
|
438
|
+
return raw;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function finalizeOpenedWorkspaceTab(
|
|
443
|
+
opened: Awaited<ReturnType<SessionBindingManager['openTab']>>,
|
|
444
|
+
expectedUrl?: string
|
|
445
|
+
): Promise<Awaited<ReturnType<SessionBindingManager['openTab']>>> {
|
|
446
|
+
if (expectedUrl && expectedUrl !== 'about:blank') {
|
|
447
|
+
await waitForTabUrl(opened.tab.id, expectedUrl).catch(() => undefined);
|
|
448
|
+
}
|
|
449
|
+
let refreshedTab = opened.tab;
|
|
450
|
+
try {
|
|
451
|
+
const rawTab = await chrome.tabs.get(opened.tab.id);
|
|
452
|
+
const pendingUrl = 'pendingUrl' in rawTab && typeof rawTab.pendingUrl === 'string' ? rawTab.pendingUrl : '';
|
|
453
|
+
const currentUrl = rawTab.url ?? '';
|
|
454
|
+
const effectiveUrl =
|
|
455
|
+
currentUrl && currentUrl !== 'about:blank'
|
|
456
|
+
? currentUrl
|
|
457
|
+
: pendingUrl && pendingUrl !== 'about:blank'
|
|
458
|
+
? pendingUrl
|
|
459
|
+
: currentUrl || pendingUrl || opened.tab.url;
|
|
460
|
+
refreshedTab = {
|
|
461
|
+
...toTabInfo(rawTab),
|
|
462
|
+
url: effectiveUrl
|
|
463
|
+
};
|
|
464
|
+
} catch {
|
|
465
|
+
refreshedTab = (await workspaceBrowser.getTab(opened.tab.id)) ?? opened.tab;
|
|
466
|
+
}
|
|
467
|
+
const refreshedWorkspace = (await bindingManager.getWorkspaceInfo(opened.workspace.id)) ?? {
|
|
468
|
+
...opened.workspace,
|
|
469
|
+
tabs: opened.workspace.tabs.map((tab) => (tab.id === refreshedTab.id ? refreshedTab : tab))
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
workspace: refreshedWorkspace,
|
|
474
|
+
tab: refreshedTab
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
interface WithTabOptions {
|
|
479
|
+
requireSupportedAutomationUrl?: boolean;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function withTab(target: { tabId?: number; workspaceId?: string } = {}, options: WithTabOptions = {}): Promise<chrome.tabs.Tab> {
|
|
483
|
+
const requireSupportedAutomationUrl = options.requireSupportedAutomationUrl !== false;
|
|
484
|
+
const validate = (tab: chrome.tabs.Tab): chrome.tabs.Tab => {
|
|
485
|
+
if (!tab.id) {
|
|
486
|
+
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
487
|
+
}
|
|
488
|
+
const pendingUrl = 'pendingUrl' in tab && typeof tab.pendingUrl === 'string' ? tab.pendingUrl : '';
|
|
489
|
+
if (requireSupportedAutomationUrl && !isSupportedAutomationUrl(tab.url) && !isSupportedAutomationUrl(pendingUrl)) {
|
|
490
|
+
throw toError('E_PERMISSION', 'Unsupported tab URL: only http/https pages can be automated', {
|
|
491
|
+
url: tab.url ?? pendingUrl ?? ''
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return tab;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
if (typeof target.tabId === 'number') {
|
|
498
|
+
const tab = await chrome.tabs.get(target.tabId);
|
|
499
|
+
return validate(tab);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const resolved = await bindingManager.resolveTarget({
|
|
503
|
+
tabId: target.tabId,
|
|
504
|
+
workspaceId: typeof target.workspaceId === 'string' ? target.workspaceId : undefined,
|
|
505
|
+
createIfMissing: false
|
|
506
|
+
});
|
|
507
|
+
const tab = await chrome.tabs.get(resolved.tab.id);
|
|
508
|
+
return validate(tab);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function captureAlignedTabScreenshot(tab: chrome.tabs.Tab): Promise<string> {
|
|
512
|
+
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
513
|
+
throw toError('E_NOT_FOUND', 'Tab screenshot requires tab id and window id');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const activeTabs = await chrome.tabs.query({ windowId: tab.windowId, active: true });
|
|
517
|
+
const activeTab = activeTabs[0];
|
|
518
|
+
const shouldSwitch = activeTab?.id !== tab.id;
|
|
519
|
+
|
|
520
|
+
if (shouldSwitch) {
|
|
521
|
+
await chrome.tabs.update(tab.id, { active: true });
|
|
522
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
return await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
|
|
527
|
+
} finally {
|
|
528
|
+
if (shouldSwitch && typeof activeTab?.id === 'number') {
|
|
529
|
+
try {
|
|
530
|
+
await chrome.tabs.update(activeTab.id, { active: true });
|
|
531
|
+
} catch {
|
|
532
|
+
// Ignore restore errors if the original tab no longer exists.
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function sendToContent<TResponse>(tabId: number, message: Record<string, unknown>): Promise<TResponse> {
|
|
539
|
+
const maxAttempts = 10;
|
|
540
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
541
|
+
try {
|
|
542
|
+
const response = await chrome.tabs.sendMessage(tabId, message);
|
|
543
|
+
if (typeof response === 'undefined') {
|
|
544
|
+
throw new Error('Content script returned undefined response');
|
|
545
|
+
}
|
|
546
|
+
return response as TResponse;
|
|
547
|
+
} catch (error) {
|
|
548
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
549
|
+
const retriable =
|
|
550
|
+
detail.includes('Receiving end does not exist') ||
|
|
551
|
+
detail.includes('Could not establish connection') ||
|
|
552
|
+
detail.includes('No tab with id') ||
|
|
553
|
+
detail.includes('message port closed before a response was received') ||
|
|
554
|
+
detail.includes('Content script returned undefined response');
|
|
555
|
+
if (!retriable || attempt >= maxAttempts) {
|
|
556
|
+
throw toError('E_NOT_READY', 'Content script unavailable', { detail });
|
|
557
|
+
}
|
|
558
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
throw toError('E_NOT_READY', 'Content script unavailable');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
interface FocusContext {
|
|
566
|
+
windowId: number | null;
|
|
567
|
+
tabId: number | null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function captureFocusContext(): Promise<FocusContext> {
|
|
571
|
+
const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
572
|
+
const activeTab = activeTabs.find((tab) => typeof tab.id === 'number' && typeof tab.windowId === 'number') ?? null;
|
|
573
|
+
return {
|
|
574
|
+
windowId: activeTab?.windowId ?? null,
|
|
575
|
+
tabId: activeTab?.id ?? null
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function restoreFocusContext(context: FocusContext): Promise<void> {
|
|
580
|
+
if (context.windowId !== null) {
|
|
581
|
+
try {
|
|
582
|
+
await chrome.windows.update(context.windowId, { focused: true });
|
|
583
|
+
} catch {
|
|
584
|
+
// Ignore restore errors if the original window no longer exists.
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (context.tabId !== null) {
|
|
588
|
+
try {
|
|
589
|
+
await chrome.tabs.update(context.tabId, { active: true });
|
|
590
|
+
} catch {
|
|
591
|
+
// Ignore restore errors if the original tab no longer exists.
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
|
|
597
|
+
if (!enabled) {
|
|
598
|
+
return action();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const focusContext = await captureFocusContext();
|
|
602
|
+
try {
|
|
603
|
+
return await action();
|
|
604
|
+
} finally {
|
|
605
|
+
await restoreFocusContext(focusContext);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function requireRpcEnvelope(
|
|
610
|
+
method: string,
|
|
611
|
+
value: unknown
|
|
612
|
+
): { ok: boolean; result?: unknown; error?: CliResponse['error'] } {
|
|
613
|
+
if (typeof value !== 'object' || value === null || typeof (value as { ok?: unknown }).ok !== 'boolean') {
|
|
614
|
+
throw toError('E_NOT_READY', `Content script returned malformed response for ${method}`);
|
|
615
|
+
}
|
|
616
|
+
return value as { ok: boolean; result?: unknown; error?: CliResponse['error'] };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function forwardContentRpc(
|
|
620
|
+
tabId: number,
|
|
621
|
+
method: string,
|
|
622
|
+
params: Record<string, unknown>
|
|
623
|
+
): Promise<unknown> {
|
|
624
|
+
const raw = await sendToContent<unknown>(tabId, {
|
|
625
|
+
type: 'bak.rpc',
|
|
626
|
+
method,
|
|
627
|
+
params
|
|
628
|
+
});
|
|
629
|
+
const response = requireRpcEnvelope(method, raw);
|
|
630
|
+
|
|
631
|
+
if (!response.ok) {
|
|
632
|
+
throw response.error ?? toError('E_INTERNAL', `${method} failed`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return response.result;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
639
|
+
const params = request.params ?? {};
|
|
640
|
+
const target = {
|
|
641
|
+
tabId: typeof params.tabId === 'number' ? params.tabId : undefined,
|
|
642
|
+
workspaceId: typeof params.workspaceId === 'string' ? params.workspaceId : undefined
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const rpcForwardMethods = new Set([
|
|
646
|
+
'page.title',
|
|
647
|
+
'page.url',
|
|
648
|
+
'page.text',
|
|
649
|
+
'page.dom',
|
|
650
|
+
'page.accessibilityTree',
|
|
651
|
+
'page.scrollTo',
|
|
652
|
+
'page.metrics',
|
|
653
|
+
'element.hover',
|
|
654
|
+
'element.doubleClick',
|
|
655
|
+
'element.rightClick',
|
|
656
|
+
'element.dragDrop',
|
|
657
|
+
'element.select',
|
|
658
|
+
'element.check',
|
|
659
|
+
'element.uncheck',
|
|
660
|
+
'element.scrollIntoView',
|
|
661
|
+
'element.focus',
|
|
662
|
+
'element.blur',
|
|
663
|
+
'element.get',
|
|
664
|
+
'keyboard.press',
|
|
665
|
+
'keyboard.type',
|
|
666
|
+
'keyboard.hotkey',
|
|
667
|
+
'mouse.move',
|
|
668
|
+
'mouse.click',
|
|
669
|
+
'mouse.wheel',
|
|
670
|
+
'file.upload',
|
|
671
|
+
'context.get',
|
|
672
|
+
'context.set',
|
|
673
|
+
'context.enterFrame',
|
|
674
|
+
'context.exitFrame',
|
|
675
|
+
'context.enterShadow',
|
|
676
|
+
'context.exitShadow',
|
|
677
|
+
'context.reset',
|
|
678
|
+
'network.list',
|
|
679
|
+
'network.get',
|
|
680
|
+
'network.waitFor',
|
|
681
|
+
'network.clear',
|
|
682
|
+
'debug.dumpState'
|
|
683
|
+
]);
|
|
684
|
+
|
|
685
|
+
switch (request.method) {
|
|
686
|
+
case 'session.ping': {
|
|
687
|
+
return { ok: true, ts: Date.now() };
|
|
688
|
+
}
|
|
689
|
+
case 'tabs.list': {
|
|
690
|
+
const tabs = await chrome.tabs.query({});
|
|
691
|
+
return {
|
|
692
|
+
tabs: tabs
|
|
693
|
+
.filter((tab): tab is chrome.tabs.Tab => typeof tab.id === 'number' && typeof tab.windowId === 'number')
|
|
694
|
+
.map((tab) => toTabInfo(tab))
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
case 'tabs.getActive': {
|
|
698
|
+
const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
699
|
+
const tab = tabs[0];
|
|
700
|
+
if (!tab || typeof tab.id !== 'number') {
|
|
701
|
+
return { tab: null };
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
tab: toTabInfo(tab)
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
case 'tabs.get': {
|
|
708
|
+
const tabId = Number(params.tabId);
|
|
709
|
+
const tab = await chrome.tabs.get(tabId);
|
|
710
|
+
if (typeof tab.id !== 'number') {
|
|
711
|
+
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
tab: toTabInfo(tab)
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
case 'tabs.focus': {
|
|
718
|
+
const tabId = Number(params.tabId);
|
|
719
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
720
|
+
return { ok: true };
|
|
721
|
+
}
|
|
722
|
+
case 'tabs.new': {
|
|
723
|
+
const tab = await chrome.tabs.create({
|
|
724
|
+
url: (params.url as string | undefined) ?? 'about:blank',
|
|
725
|
+
windowId: typeof params.windowId === 'number' ? params.windowId : undefined,
|
|
726
|
+
active: params.active === true
|
|
727
|
+
});
|
|
728
|
+
if (params.addToGroup === true && typeof tab.id === 'number') {
|
|
729
|
+
const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
|
|
730
|
+
return {
|
|
731
|
+
tabId: tab.id,
|
|
732
|
+
windowId: tab.windowId,
|
|
733
|
+
groupId
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
return {
|
|
737
|
+
tabId: tab.id,
|
|
738
|
+
windowId: tab.windowId
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
case 'tabs.close': {
|
|
742
|
+
const tabId = Number(params.tabId);
|
|
743
|
+
await chrome.tabs.remove(tabId);
|
|
744
|
+
return { ok: true };
|
|
745
|
+
}
|
|
746
|
+
case 'workspace.ensure': {
|
|
747
|
+
return preserveHumanFocus(params.focus !== true, async () => {
|
|
748
|
+
return await bindingManager.ensureWorkspace({
|
|
749
|
+
workspaceId: String(params.workspaceId ?? ''),
|
|
750
|
+
focus: params.focus === true,
|
|
751
|
+
initialUrl: typeof params.url === 'string' ? params.url : undefined
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
case 'workspace.info': {
|
|
756
|
+
return {
|
|
757
|
+
workspace: await bindingManager.getWorkspaceInfo(String(params.workspaceId ?? ''))
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
case 'workspace.openTab': {
|
|
761
|
+
const expectedUrl = typeof params.url === 'string' ? params.url : undefined;
|
|
762
|
+
const opened = await preserveHumanFocus(params.focus !== true, async () => {
|
|
763
|
+
return await bindingManager.openTab({
|
|
764
|
+
workspaceId: String(params.workspaceId ?? ''),
|
|
765
|
+
url: expectedUrl,
|
|
766
|
+
active: params.active === true,
|
|
767
|
+
focus: params.focus === true
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
return await finalizeOpenedWorkspaceTab(opened, expectedUrl);
|
|
771
|
+
}
|
|
772
|
+
case 'workspace.listTabs': {
|
|
773
|
+
return await bindingManager.listTabs(String(params.workspaceId ?? ''));
|
|
774
|
+
}
|
|
775
|
+
case 'workspace.getActiveTab': {
|
|
776
|
+
return await bindingManager.getActiveTab(String(params.workspaceId ?? ''));
|
|
777
|
+
}
|
|
778
|
+
case 'workspace.setActiveTab': {
|
|
779
|
+
return await bindingManager.setActiveTab(Number(params.tabId), String(params.workspaceId ?? ''));
|
|
780
|
+
}
|
|
781
|
+
case 'workspace.focus': {
|
|
782
|
+
return await bindingManager.focus(String(params.workspaceId ?? ''));
|
|
783
|
+
}
|
|
784
|
+
case 'workspace.reset': {
|
|
785
|
+
return await preserveHumanFocus(params.focus !== true, async () => {
|
|
786
|
+
return await bindingManager.reset({
|
|
787
|
+
workspaceId: String(params.workspaceId ?? ''),
|
|
788
|
+
focus: params.focus === true,
|
|
789
|
+
initialUrl: typeof params.url === 'string' ? params.url : undefined
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
case 'workspace.close': {
|
|
794
|
+
return await bindingManager.close(String(params.workspaceId ?? ''));
|
|
795
|
+
}
|
|
796
|
+
case 'page.goto': {
|
|
797
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
798
|
+
const tab = await withTab(target, {
|
|
799
|
+
requireSupportedAutomationUrl: false
|
|
800
|
+
});
|
|
801
|
+
const url = String(params.url ?? 'about:blank');
|
|
802
|
+
await chrome.tabs.update(tab.id!, { url });
|
|
803
|
+
await waitForTabUrl(tab.id!, url);
|
|
804
|
+
await forwardContentRpc(tab.id!, 'page.url', { tabId: tab.id }).catch(() => undefined);
|
|
805
|
+
await waitForTabComplete(tab.id!, 5_000).catch(() => undefined);
|
|
806
|
+
return { ok: true };
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
case 'page.back': {
|
|
810
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
811
|
+
const tab = await withTab(target);
|
|
812
|
+
await chrome.tabs.goBack(tab.id!);
|
|
813
|
+
await waitForTabComplete(tab.id!);
|
|
814
|
+
return { ok: true };
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
case 'page.forward': {
|
|
818
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
819
|
+
const tab = await withTab(target);
|
|
820
|
+
await chrome.tabs.goForward(tab.id!);
|
|
821
|
+
await waitForTabComplete(tab.id!);
|
|
822
|
+
return { ok: true };
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
case 'page.reload': {
|
|
826
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
827
|
+
const tab = await withTab(target);
|
|
828
|
+
await chrome.tabs.reload(tab.id!);
|
|
829
|
+
await waitForTabComplete(tab.id!);
|
|
830
|
+
return { ok: true };
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
case 'page.viewport': {
|
|
834
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
835
|
+
const tab = await withTab(target, {
|
|
836
|
+
requireSupportedAutomationUrl: false
|
|
837
|
+
});
|
|
838
|
+
if (typeof tab.windowId !== 'number') {
|
|
839
|
+
throw toError('E_NOT_FOUND', 'Tab window unavailable');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const width = typeof params.width === 'number' ? Math.max(320, Math.floor(params.width)) : undefined;
|
|
843
|
+
const height = typeof params.height === 'number' ? Math.max(320, Math.floor(params.height)) : undefined;
|
|
844
|
+
if (width || height) {
|
|
845
|
+
await chrome.windows.update(tab.windowId, {
|
|
846
|
+
width,
|
|
847
|
+
height
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const viewport = (await forwardContentRpc(tab.id!, 'page.viewport', {})) as {
|
|
852
|
+
width: number;
|
|
853
|
+
height: number;
|
|
854
|
+
devicePixelRatio: number;
|
|
855
|
+
};
|
|
856
|
+
const viewWidth = typeof width === 'number' ? width : viewport.width ?? tab.width ?? 0;
|
|
857
|
+
const viewHeight = typeof height === 'number' ? height : viewport.height ?? tab.height ?? 0;
|
|
858
|
+
return {
|
|
859
|
+
width: viewWidth,
|
|
860
|
+
height: viewHeight,
|
|
861
|
+
devicePixelRatio: viewport.devicePixelRatio
|
|
862
|
+
};
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
case 'page.snapshot': {
|
|
866
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
867
|
+
const tab = await withTab(target);
|
|
868
|
+
if (typeof tab.id !== 'number' || typeof tab.windowId !== 'number') {
|
|
869
|
+
throw toError('E_NOT_FOUND', 'Tab missing id');
|
|
870
|
+
}
|
|
871
|
+
const includeBase64 = params.includeBase64 !== false;
|
|
872
|
+
const config = await getConfig();
|
|
873
|
+
const elements = await sendToContent<{ elements: unknown[] }>(tab.id, {
|
|
874
|
+
type: 'bak.collectElements',
|
|
875
|
+
debugRichText: config.debugRichText
|
|
876
|
+
});
|
|
877
|
+
const imageData = await captureAlignedTabScreenshot(tab);
|
|
878
|
+
return {
|
|
879
|
+
imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, '') : '',
|
|
880
|
+
elements: elements.elements,
|
|
881
|
+
tabId: tab.id,
|
|
882
|
+
url: tab.url ?? ''
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
case 'element.click': {
|
|
887
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
888
|
+
const tab = await withTab(target);
|
|
889
|
+
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
890
|
+
type: 'bak.performAction',
|
|
891
|
+
action: 'click',
|
|
892
|
+
locator: params.locator as Locator,
|
|
893
|
+
requiresConfirm: params.requiresConfirm === true
|
|
894
|
+
});
|
|
895
|
+
if (!response.ok) {
|
|
896
|
+
throw response.error ?? toError('E_INTERNAL', 'element.click failed');
|
|
897
|
+
}
|
|
898
|
+
return { ok: true };
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
case 'element.type': {
|
|
902
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
903
|
+
const tab = await withTab(target);
|
|
904
|
+
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
905
|
+
type: 'bak.performAction',
|
|
906
|
+
action: 'type',
|
|
907
|
+
locator: params.locator as Locator,
|
|
908
|
+
text: String(params.text ?? ''),
|
|
909
|
+
clear: Boolean(params.clear),
|
|
910
|
+
requiresConfirm: params.requiresConfirm === true
|
|
911
|
+
});
|
|
912
|
+
if (!response.ok) {
|
|
913
|
+
throw response.error ?? toError('E_INTERNAL', 'element.type failed');
|
|
914
|
+
}
|
|
915
|
+
return { ok: true };
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
case 'element.scroll': {
|
|
919
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
920
|
+
const tab = await withTab(target);
|
|
921
|
+
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
922
|
+
type: 'bak.performAction',
|
|
923
|
+
action: 'scroll',
|
|
924
|
+
locator: params.locator as Locator,
|
|
925
|
+
dx: Number(params.dx ?? 0),
|
|
926
|
+
dy: Number(params.dy ?? 320)
|
|
927
|
+
});
|
|
928
|
+
if (!response.ok) {
|
|
929
|
+
throw response.error ?? toError('E_INTERNAL', 'element.scroll failed');
|
|
930
|
+
}
|
|
931
|
+
return { ok: true };
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
case 'page.wait': {
|
|
935
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
936
|
+
const tab = await withTab(target);
|
|
937
|
+
const response = await sendToContent<{ ok: boolean; error?: CliResponse['error'] }>(tab.id!, {
|
|
938
|
+
type: 'bak.waitFor',
|
|
939
|
+
mode: String(params.mode ?? 'selector'),
|
|
940
|
+
value: String(params.value ?? ''),
|
|
941
|
+
timeoutMs: Number(params.timeoutMs ?? 5000)
|
|
942
|
+
});
|
|
943
|
+
if (!response.ok) {
|
|
944
|
+
throw response.error ?? toError('E_TIMEOUT', 'page.wait failed');
|
|
945
|
+
}
|
|
946
|
+
return { ok: true };
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
case 'debug.getConsole': {
|
|
950
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
951
|
+
const tab = await withTab(target);
|
|
952
|
+
const response = await sendToContent<{ entries: ConsoleEntry[] }>(tab.id!, {
|
|
953
|
+
type: 'bak.getConsole',
|
|
954
|
+
limit: Number(params.limit ?? 50)
|
|
955
|
+
});
|
|
956
|
+
return { entries: response.entries };
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
case 'ui.selectCandidate': {
|
|
960
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
961
|
+
const tab = await withTab(target);
|
|
962
|
+
const response = await sendToContent<{ ok: boolean; selectedEid?: string; error?: CliResponse['error'] }>(
|
|
963
|
+
tab.id!,
|
|
964
|
+
{
|
|
965
|
+
type: 'bak.selectCandidate',
|
|
966
|
+
candidates: params.candidates
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
if (!response.ok || !response.selectedEid) {
|
|
970
|
+
throw response.error ?? toError('E_NEED_USER_CONFIRM', 'User did not confirm candidate');
|
|
971
|
+
}
|
|
972
|
+
return { selectedEid: response.selectedEid };
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
default:
|
|
976
|
+
if (rpcForwardMethods.has(request.method)) {
|
|
977
|
+
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
978
|
+
const tab = await withTab(target);
|
|
979
|
+
return await forwardContentRpc(tab.id!, request.method, {
|
|
980
|
+
...params,
|
|
981
|
+
tabId: tab.id
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
throw toError('E_NOT_FOUND', `Unsupported method from CLI bridge: ${request.method}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function scheduleReconnect(reason: string): void {
|
|
990
|
+
if (manualDisconnect) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (reconnectTimer !== null) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const delayMs = computeReconnectDelayMs(reconnectAttempt);
|
|
998
|
+
reconnectAttempt += 1;
|
|
999
|
+
nextReconnectInMs = delayMs;
|
|
1000
|
+
reconnectTimer = setTimeout(() => {
|
|
1001
|
+
reconnectTimer = null;
|
|
1002
|
+
nextReconnectInMs = null;
|
|
1003
|
+
void connectWebSocket();
|
|
1004
|
+
}, delayMs) as unknown as number;
|
|
1005
|
+
|
|
1006
|
+
if (!lastError) {
|
|
1007
|
+
setRuntimeError(`Reconnect scheduled: ${reason}`, 'socket');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function connectWebSocket(): Promise<void> {
|
|
1012
|
+
clearReconnectTimer();
|
|
1013
|
+
if (manualDisconnect) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const config = await getConfig();
|
|
1022
|
+
if (!config.token) {
|
|
1023
|
+
setRuntimeError('Pair token is empty', 'config');
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
|
|
1028
|
+
ws = new WebSocket(url);
|
|
1029
|
+
|
|
1030
|
+
ws.addEventListener('open', () => {
|
|
1031
|
+
manualDisconnect = false;
|
|
1032
|
+
reconnectAttempt = 0;
|
|
1033
|
+
lastError = null;
|
|
1034
|
+
ws?.send(JSON.stringify({
|
|
1035
|
+
type: 'hello',
|
|
1036
|
+
role: 'extension',
|
|
1037
|
+
version: '0.5.0',
|
|
1038
|
+
ts: Date.now()
|
|
1039
|
+
}));
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
ws.addEventListener('message', (event) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const request = JSON.parse(String(event.data)) as CliRequest;
|
|
1045
|
+
if (!request.id || !request.method) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
void handleRequest(request)
|
|
1049
|
+
.then((result) => {
|
|
1050
|
+
sendResponse({ id: request.id, ok: true, result });
|
|
1051
|
+
})
|
|
1052
|
+
.catch((error: unknown) => {
|
|
1053
|
+
const normalized = normalizeUnhandledError(error);
|
|
1054
|
+
sendResponse({ id: request.id, ok: false, error: normalized });
|
|
1055
|
+
});
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
setRuntimeError(error instanceof Error ? error.message : String(error), 'parse');
|
|
1058
|
+
sendResponse({
|
|
1059
|
+
id: 'parse-error',
|
|
1060
|
+
ok: false,
|
|
1061
|
+
error: toError('E_INTERNAL', error instanceof Error ? error.message : String(error))
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
ws.addEventListener('close', () => {
|
|
1067
|
+
ws = null;
|
|
1068
|
+
scheduleReconnect('socket-closed');
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
ws.addEventListener('error', () => {
|
|
1072
|
+
setRuntimeError('Cannot connect to bak cli', 'socket');
|
|
1073
|
+
ws?.close();
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1078
|
+
void listWorkspaceStates().then(async (states) => {
|
|
1079
|
+
for (const state of states) {
|
|
1080
|
+
if (!state.tabIds.includes(tabId)) {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
1084
|
+
await saveWorkspaceState({
|
|
1085
|
+
...state,
|
|
1086
|
+
tabIds: nextTabIds,
|
|
1087
|
+
activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
|
|
1088
|
+
primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
1095
|
+
void listWorkspaceStates().then(async (states) => {
|
|
1096
|
+
for (const state of states) {
|
|
1097
|
+
if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
await saveWorkspaceState({
|
|
1101
|
+
...state,
|
|
1102
|
+
activeTabId: activeInfo.tabId
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
chrome.windows.onRemoved.addListener((windowId) => {
|
|
1109
|
+
void listWorkspaceStates().then(async (states) => {
|
|
1110
|
+
for (const state of states) {
|
|
1111
|
+
if (state.windowId !== windowId) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
await saveWorkspaceState({
|
|
1115
|
+
...state,
|
|
1116
|
+
windowId: null,
|
|
1117
|
+
groupId: null,
|
|
1118
|
+
tabIds: [],
|
|
1119
|
+
activeTabId: null,
|
|
1120
|
+
primaryTabId: null
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1127
|
+
void setConfig({ port: DEFAULT_PORT, debugRichText: false });
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
1131
|
+
void connectWebSocket();
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
void connectWebSocket();
|
|
1135
|
+
|
|
1136
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
1137
|
+
if (message?.type === 'bak.updateConfig') {
|
|
1138
|
+
manualDisconnect = false;
|
|
1139
|
+
void setConfig({
|
|
1140
|
+
token: message.token,
|
|
1141
|
+
port: Number(message.port ?? DEFAULT_PORT),
|
|
1142
|
+
debugRichText: message.debugRichText === true
|
|
1143
|
+
}).then(() => {
|
|
1144
|
+
ws?.close();
|
|
1145
|
+
void connectWebSocket().then(() => sendResponse({ ok: true }));
|
|
1146
|
+
});
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (message?.type === 'bak.getState') {
|
|
1151
|
+
void getConfig().then((config) => {
|
|
1152
|
+
sendResponse({
|
|
1153
|
+
ok: true,
|
|
1154
|
+
connected: ws?.readyState === WebSocket.OPEN,
|
|
1155
|
+
hasToken: Boolean(config.token),
|
|
1156
|
+
port: config.port,
|
|
1157
|
+
debugRichText: config.debugRichText,
|
|
1158
|
+
lastError: lastError?.message ?? null,
|
|
1159
|
+
lastErrorAt: lastError?.at ?? null,
|
|
1160
|
+
lastErrorContext: lastError?.context ?? null,
|
|
1161
|
+
reconnectAttempt,
|
|
1162
|
+
nextReconnectInMs
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (message?.type === 'bak.disconnect') {
|
|
1169
|
+
manualDisconnect = true;
|
|
1170
|
+
clearReconnectTimer();
|
|
1171
|
+
reconnectAttempt = 0;
|
|
1172
|
+
ws?.close();
|
|
1173
|
+
ws = null;
|
|
1174
|
+
sendResponse({ ok: true });
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return false;
|
|
1158
1179
|
});
|
|
1159
1180
|
|
|
1160
1181
|
|