@flrande/bak-extension 0.2.5 → 0.3.1

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.
@@ -30,11 +30,560 @@
30
30
  return Math.min(maxDelayMs, baseDelayMs * 2 ** safeAttempt);
31
31
  }
32
32
 
33
+ // src/workspace.ts
34
+ var DEFAULT_WORKSPACE_ID = "default";
35
+ var DEFAULT_WORKSPACE_LABEL = "bak agent";
36
+ var DEFAULT_WORKSPACE_COLOR = "blue";
37
+ var DEFAULT_WORKSPACE_URL = "about:blank";
38
+ var WorkspaceManager = class {
39
+ storage;
40
+ browser;
41
+ constructor(storage, browser) {
42
+ this.storage = storage;
43
+ this.browser = browser;
44
+ }
45
+ async getWorkspaceInfo(workspaceId = DEFAULT_WORKSPACE_ID) {
46
+ return this.inspectWorkspace(workspaceId);
47
+ }
48
+ async ensureWorkspace(options = {}) {
49
+ const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
50
+ const repairActions = [];
51
+ const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
52
+ const persisted = await this.storage.load();
53
+ const created = !persisted;
54
+ let state = this.normalizeState(persisted, workspaceId);
55
+ let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
56
+ let tabs = [];
57
+ if (!window) {
58
+ const createdWindow = await this.browser.createWindow({
59
+ url: initialUrl,
60
+ focused: options.focus === true
61
+ });
62
+ state.windowId = createdWindow.id;
63
+ state.groupId = null;
64
+ state.tabIds = [];
65
+ state.activeTabId = null;
66
+ state.primaryTabId = null;
67
+ window = createdWindow;
68
+ tabs = await this.waitForWindowTabs(createdWindow.id);
69
+ state.tabIds = tabs.map((tab) => tab.id);
70
+ if (state.primaryTabId === null) {
71
+ state.primaryTabId = tabs[0]?.id ?? null;
72
+ }
73
+ if (state.activeTabId === null) {
74
+ state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
75
+ }
76
+ repairActions.push(created ? "created-window" : "recreated-window");
77
+ }
78
+ tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
79
+ const recoveredTabs = await this.recoverWorkspaceTabs(state, tabs);
80
+ if (recoveredTabs.length > tabs.length) {
81
+ tabs = recoveredTabs;
82
+ repairActions.push("recovered-tracked-tabs");
83
+ }
84
+ if (tabs.length !== state.tabIds.length) {
85
+ repairActions.push("pruned-missing-tabs");
86
+ }
87
+ state.tabIds = tabs.map((tab) => tab.id);
88
+ if (tabs.length === 0) {
89
+ const primary = await this.createWorkspaceTab({
90
+ windowId: state.windowId,
91
+ url: initialUrl,
92
+ active: true
93
+ });
94
+ tabs = [primary];
95
+ state.tabIds = [primary.id];
96
+ state.primaryTabId = primary.id;
97
+ state.activeTabId = primary.id;
98
+ repairActions.push("created-primary-tab");
99
+ }
100
+ if (state.primaryTabId === null || !tabs.some((tab) => tab.id === state.primaryTabId)) {
101
+ state.primaryTabId = tabs[0]?.id ?? null;
102
+ repairActions.push("reassigned-primary-tab");
103
+ }
104
+ if (state.activeTabId === null || !tabs.some((tab) => tab.id === state.activeTabId)) {
105
+ state.activeTabId = state.primaryTabId ?? tabs[0]?.id ?? null;
106
+ repairActions.push("reassigned-active-tab");
107
+ }
108
+ let group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
109
+ if (!group || group.windowId !== state.windowId) {
110
+ const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
111
+ group = await this.browser.updateGroup(groupId, {
112
+ title: state.label,
113
+ color: state.color,
114
+ collapsed: false
115
+ });
116
+ state.groupId = group.id;
117
+ repairActions.push("recreated-group");
118
+ } else {
119
+ await this.browser.updateGroup(group.id, {
120
+ title: state.label,
121
+ color: state.color,
122
+ collapsed: false
123
+ });
124
+ }
125
+ const ungroupedIds = tabs.filter((tab) => tab.groupId !== state.groupId).map((tab) => tab.id);
126
+ if (ungroupedIds.length > 0) {
127
+ await this.browser.groupTabs(ungroupedIds, state.groupId ?? void 0);
128
+ repairActions.push("regrouped-tabs");
129
+ }
130
+ tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
131
+ tabs = await this.recoverWorkspaceTabs(state, tabs);
132
+ const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
133
+ if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
134
+ tabs = [...tabs, activeTab];
135
+ }
136
+ if (tabs.length === 0 && state.primaryTabId !== null) {
137
+ const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId);
138
+ if (primaryTab) {
139
+ tabs = [primaryTab];
140
+ }
141
+ }
142
+ state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
143
+ if (options.focus === true && state.activeTabId !== null) {
144
+ await this.browser.updateTab(state.activeTabId, { active: true });
145
+ window = await this.browser.updateWindow(state.windowId, { focused: true });
146
+ void window;
147
+ repairActions.push("focused-window");
148
+ }
149
+ await this.storage.save(state);
150
+ return {
151
+ workspace: {
152
+ ...state,
153
+ tabs
154
+ },
155
+ created,
156
+ repaired: repairActions.length > 0,
157
+ repairActions
158
+ };
159
+ }
160
+ async openTab(options = {}) {
161
+ const ensured = await this.ensureWorkspace({
162
+ workspaceId: options.workspaceId,
163
+ focus: false,
164
+ initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
165
+ });
166
+ let state = { ...ensured.workspace };
167
+ const active = options.active === true;
168
+ const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
169
+ let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
170
+ state,
171
+ ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
172
+ );
173
+ let createdTab;
174
+ try {
175
+ createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
176
+ url: desiredUrl,
177
+ active
178
+ }) : await this.createWorkspaceTab({
179
+ windowId: state.windowId,
180
+ url: desiredUrl,
181
+ active
182
+ });
183
+ } catch (error) {
184
+ if (!this.isMissingWindowError(error)) {
185
+ throw error;
186
+ }
187
+ const repaired = await this.ensureWorkspace({
188
+ workspaceId: options.workspaceId,
189
+ focus: false,
190
+ initialUrl: desiredUrl
191
+ });
192
+ state = { ...repaired.workspace };
193
+ reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
194
+ createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
195
+ url: desiredUrl,
196
+ active
197
+ }) : await this.createWorkspaceTab({
198
+ windowId: state.windowId,
199
+ url: desiredUrl,
200
+ active
201
+ });
202
+ }
203
+ const nextTabIds = [.../* @__PURE__ */ new Set([...state.tabIds, createdTab.id])];
204
+ const groupId = await this.browser.groupTabs([createdTab.id], state.groupId ?? void 0);
205
+ await this.browser.updateGroup(groupId, {
206
+ title: state.label,
207
+ color: state.color,
208
+ collapsed: false
209
+ });
210
+ const nextState = {
211
+ id: state.id,
212
+ label: state.label,
213
+ color: state.color,
214
+ windowId: state.windowId,
215
+ groupId,
216
+ tabIds: nextTabIds,
217
+ activeTabId: createdTab.id,
218
+ primaryTabId: state.primaryTabId ?? createdTab.id
219
+ };
220
+ if (options.focus === true) {
221
+ await this.browser.updateTab(createdTab.id, { active: true });
222
+ await this.browser.updateWindow(state.windowId, { focused: true });
223
+ }
224
+ await this.storage.save(nextState);
225
+ const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
226
+ const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
227
+ return {
228
+ workspace: {
229
+ ...nextState,
230
+ tabs
231
+ },
232
+ tab
233
+ };
234
+ }
235
+ async listTabs(workspaceId = DEFAULT_WORKSPACE_ID) {
236
+ const ensured = await this.inspectWorkspace(workspaceId);
237
+ if (!ensured) {
238
+ throw new Error(`Workspace ${workspaceId} does not exist`);
239
+ }
240
+ return {
241
+ workspace: ensured,
242
+ tabs: ensured.tabs
243
+ };
244
+ }
245
+ async getActiveTab(workspaceId = DEFAULT_WORKSPACE_ID) {
246
+ const ensured = await this.inspectWorkspace(workspaceId);
247
+ if (!ensured) {
248
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
249
+ return {
250
+ workspace: {
251
+ ...this.normalizeState(null, normalizedWorkspaceId),
252
+ tabs: []
253
+ },
254
+ tab: null
255
+ };
256
+ }
257
+ return {
258
+ workspace: ensured,
259
+ tab: ensured.tabs.find((tab) => tab.id === ensured.activeTabId) ?? null
260
+ };
261
+ }
262
+ async setActiveTab(tabId, workspaceId = DEFAULT_WORKSPACE_ID) {
263
+ const ensured = await this.ensureWorkspace({ workspaceId });
264
+ if (!ensured.workspace.tabIds.includes(tabId)) {
265
+ throw new Error(`Tab ${tabId} does not belong to workspace ${workspaceId}`);
266
+ }
267
+ const nextState = {
268
+ id: ensured.workspace.id,
269
+ label: ensured.workspace.label,
270
+ color: ensured.workspace.color,
271
+ windowId: ensured.workspace.windowId,
272
+ groupId: ensured.workspace.groupId,
273
+ tabIds: [...ensured.workspace.tabIds],
274
+ activeTabId: tabId,
275
+ primaryTabId: ensured.workspace.primaryTabId ?? tabId
276
+ };
277
+ await this.storage.save(nextState);
278
+ const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
279
+ const tab = tabs.find((item) => item.id === tabId);
280
+ if (!tab) {
281
+ throw new Error(`Tab ${tabId} is missing from workspace ${workspaceId}`);
282
+ }
283
+ return {
284
+ workspace: {
285
+ ...nextState,
286
+ tabs
287
+ },
288
+ tab
289
+ };
290
+ }
291
+ async focus(workspaceId = DEFAULT_WORKSPACE_ID) {
292
+ const ensured = await this.ensureWorkspace({ workspaceId, focus: false });
293
+ if (ensured.workspace.activeTabId !== null) {
294
+ await this.browser.updateTab(ensured.workspace.activeTabId, { active: true });
295
+ }
296
+ if (ensured.workspace.windowId !== null) {
297
+ await this.browser.updateWindow(ensured.workspace.windowId, { focused: true });
298
+ }
299
+ const refreshed = await this.ensureWorkspace({ workspaceId, focus: false });
300
+ return { ok: true, workspace: refreshed.workspace };
301
+ }
302
+ async reset(options = {}) {
303
+ await this.close(options.workspaceId);
304
+ return this.ensureWorkspace(options);
305
+ }
306
+ async close(workspaceId = DEFAULT_WORKSPACE_ID) {
307
+ const state = await this.storage.load();
308
+ if (!state || state.id !== workspaceId) {
309
+ await this.storage.save(null);
310
+ return { ok: true };
311
+ }
312
+ if (state.windowId !== null) {
313
+ const existingWindow = await this.browser.getWindow(state.windowId);
314
+ if (existingWindow) {
315
+ await this.browser.closeWindow(state.windowId);
316
+ }
317
+ }
318
+ await this.storage.save(null);
319
+ return { ok: true };
320
+ }
321
+ async resolveTarget(options = {}) {
322
+ if (typeof options.tabId === "number") {
323
+ const explicitTab = await this.browser.getTab(options.tabId);
324
+ if (!explicitTab) {
325
+ throw new Error(`No tab with id ${options.tabId}`);
326
+ }
327
+ return {
328
+ tab: explicitTab,
329
+ workspace: null,
330
+ resolution: "explicit-tab",
331
+ createdWorkspace: false,
332
+ repaired: false,
333
+ repairActions: []
334
+ };
335
+ }
336
+ const explicitWorkspaceId = typeof options.workspaceId === "string" ? this.normalizeWorkspaceId(options.workspaceId) : void 0;
337
+ if (explicitWorkspaceId) {
338
+ const ensured2 = await this.ensureWorkspace({
339
+ workspaceId: explicitWorkspaceId,
340
+ focus: false
341
+ });
342
+ return this.buildWorkspaceResolution(ensured2, "explicit-workspace");
343
+ }
344
+ const existingWorkspace = await this.loadWorkspaceRecord(DEFAULT_WORKSPACE_ID);
345
+ if (existingWorkspace) {
346
+ const ensured2 = await this.ensureWorkspace({
347
+ workspaceId: existingWorkspace.id,
348
+ focus: false
349
+ });
350
+ return this.buildWorkspaceResolution(ensured2, "default-workspace");
351
+ }
352
+ if (options.createIfMissing !== true) {
353
+ const activeTab = await this.browser.getActiveTab();
354
+ if (!activeTab) {
355
+ throw new Error("No active tab");
356
+ }
357
+ return {
358
+ tab: activeTab,
359
+ workspace: null,
360
+ resolution: "browser-active",
361
+ createdWorkspace: false,
362
+ repaired: false,
363
+ repairActions: []
364
+ };
365
+ }
366
+ const ensured = await this.ensureWorkspace({
367
+ workspaceId: DEFAULT_WORKSPACE_ID,
368
+ focus: false
369
+ });
370
+ return this.buildWorkspaceResolution(ensured, "default-workspace");
371
+ }
372
+ normalizeWorkspaceId(workspaceId) {
373
+ const candidate = workspaceId?.trim();
374
+ if (!candidate) {
375
+ return DEFAULT_WORKSPACE_ID;
376
+ }
377
+ if (candidate !== DEFAULT_WORKSPACE_ID) {
378
+ throw new Error(`Unsupported workspace id: ${candidate}`);
379
+ }
380
+ return candidate;
381
+ }
382
+ normalizeState(state, workspaceId) {
383
+ return {
384
+ id: workspaceId,
385
+ label: state?.label ?? DEFAULT_WORKSPACE_LABEL,
386
+ color: state?.color ?? DEFAULT_WORKSPACE_COLOR,
387
+ windowId: state?.windowId ?? null,
388
+ groupId: state?.groupId ?? null,
389
+ tabIds: state?.tabIds ?? [],
390
+ activeTabId: state?.activeTabId ?? null,
391
+ primaryTabId: state?.primaryTabId ?? null
392
+ };
393
+ }
394
+ async loadWorkspaceRecord(workspaceId = DEFAULT_WORKSPACE_ID) {
395
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
396
+ const state = await this.storage.load();
397
+ if (!state || state.id !== normalizedWorkspaceId) {
398
+ return null;
399
+ }
400
+ return this.normalizeState(state, normalizedWorkspaceId);
401
+ }
402
+ async buildWorkspaceResolution(ensured, resolution) {
403
+ const tab = ensured.workspace.tabs.find((item) => item.id === ensured.workspace.activeTabId) ?? ensured.workspace.tabs[0] ?? null;
404
+ if (tab) {
405
+ return {
406
+ tab,
407
+ workspace: ensured.workspace,
408
+ resolution,
409
+ createdWorkspace: ensured.created,
410
+ repaired: ensured.repaired,
411
+ repairActions: ensured.repairActions
412
+ };
413
+ }
414
+ if (ensured.workspace.activeTabId !== null) {
415
+ const activeWorkspaceTab = await this.waitForTrackedTab(ensured.workspace.activeTabId, ensured.workspace.windowId);
416
+ if (activeWorkspaceTab) {
417
+ return {
418
+ tab: activeWorkspaceTab,
419
+ workspace: ensured.workspace,
420
+ resolution,
421
+ createdWorkspace: ensured.created,
422
+ repaired: ensured.repaired,
423
+ repairActions: ensured.repairActions
424
+ };
425
+ }
426
+ }
427
+ const activeTab = await this.browser.getActiveTab();
428
+ if (!activeTab) {
429
+ throw new Error("No active tab");
430
+ }
431
+ return {
432
+ tab: activeTab,
433
+ workspace: null,
434
+ resolution: "browser-active",
435
+ createdWorkspace: ensured.created,
436
+ repaired: ensured.repaired,
437
+ repairActions: ensured.repairActions
438
+ };
439
+ }
440
+ async readTrackedTabs(tabIds, windowId) {
441
+ const tabs = (await Promise.all(
442
+ tabIds.map(async (tabId) => {
443
+ const tab = await this.browser.getTab(tabId);
444
+ if (!tab) {
445
+ return null;
446
+ }
447
+ if (windowId !== null && tab.windowId !== windowId) {
448
+ return null;
449
+ }
450
+ return tab;
451
+ })
452
+ )).filter((tab) => tab !== null);
453
+ return tabs;
454
+ }
455
+ async recoverWorkspaceTabs(state, existingTabs) {
456
+ if (state.windowId === null) {
457
+ return existingTabs;
458
+ }
459
+ const candidates = await this.waitForWindowTabs(state.windowId, 500);
460
+ if (candidates.length === 0) {
461
+ return existingTabs;
462
+ }
463
+ const trackedIds = new Set(state.tabIds);
464
+ const trackedTabs = candidates.filter((tab) => trackedIds.has(tab.id));
465
+ if (trackedTabs.length > existingTabs.length) {
466
+ return trackedTabs;
467
+ }
468
+ if (state.groupId !== null) {
469
+ const groupedTabs = candidates.filter((tab) => tab.groupId === state.groupId);
470
+ if (groupedTabs.length > 0) {
471
+ return groupedTabs;
472
+ }
473
+ }
474
+ const preferredIds = new Set([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number"));
475
+ const preferredTabs = candidates.filter((tab) => preferredIds.has(tab.id));
476
+ if (preferredTabs.length > existingTabs.length) {
477
+ return preferredTabs;
478
+ }
479
+ return existingTabs;
480
+ }
481
+ async createWorkspaceTab(options) {
482
+ if (options.windowId === null) {
483
+ throw new Error("Workspace window is unavailable");
484
+ }
485
+ const deadline = Date.now() + 1500;
486
+ let lastError2 = null;
487
+ while (Date.now() < deadline) {
488
+ try {
489
+ return await this.browser.createTab({
490
+ windowId: options.windowId,
491
+ url: options.url,
492
+ active: options.active
493
+ });
494
+ } catch (error) {
495
+ if (!this.isMissingWindowError(error)) {
496
+ throw error;
497
+ }
498
+ lastError2 = error instanceof Error ? error : new Error(String(error));
499
+ await this.delay(50);
500
+ }
501
+ }
502
+ throw lastError2 ?? new Error(`No window with id: ${options.windowId}.`);
503
+ }
504
+ async inspectWorkspace(workspaceId = DEFAULT_WORKSPACE_ID) {
505
+ const state = await this.loadWorkspaceRecord(workspaceId);
506
+ if (!state) {
507
+ return null;
508
+ }
509
+ let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
510
+ const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
511
+ if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
512
+ tabs = [...tabs, activeTab];
513
+ }
514
+ if (tabs.length === 0 && state.primaryTabId !== null) {
515
+ const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
516
+ if (primaryTab) {
517
+ tabs = [primaryTab];
518
+ }
519
+ }
520
+ return {
521
+ ...state,
522
+ tabIds: [...new Set(state.tabIds.concat(tabs.map((tab) => tab.id)))],
523
+ tabs
524
+ };
525
+ }
526
+ async resolveReusablePrimaryTab(workspace, allowReuse) {
527
+ if (!allowReuse || workspace.windowId === null) {
528
+ return null;
529
+ }
530
+ if (workspace.primaryTabId !== null) {
531
+ const trackedPrimary = workspace.tabs.find((tab) => tab.id === workspace.primaryTabId) ?? await this.waitForTrackedTab(workspace.primaryTabId, workspace.windowId);
532
+ if (trackedPrimary) {
533
+ return trackedPrimary;
534
+ }
535
+ }
536
+ const windowTabs = await this.waitForWindowTabs(workspace.windowId, 750);
537
+ return windowTabs.length === 1 ? windowTabs[0] : null;
538
+ }
539
+ async waitForWindow(windowId, timeoutMs = 750) {
540
+ const deadline = Date.now() + timeoutMs;
541
+ while (Date.now() < deadline) {
542
+ const window = await this.browser.getWindow(windowId);
543
+ if (window) {
544
+ return window;
545
+ }
546
+ await this.delay(50);
547
+ }
548
+ return null;
549
+ }
550
+ async waitForTrackedTab(tabId, windowId, timeoutMs = 1e3) {
551
+ const deadline = Date.now() + timeoutMs;
552
+ while (Date.now() < deadline) {
553
+ const tab = await this.browser.getTab(tabId);
554
+ if (tab && (windowId === null || tab.windowId === windowId)) {
555
+ return tab;
556
+ }
557
+ await this.delay(50);
558
+ }
559
+ return null;
560
+ }
561
+ async waitForWindowTabs(windowId, timeoutMs = 1e3) {
562
+ const deadline = Date.now() + timeoutMs;
563
+ while (Date.now() < deadline) {
564
+ const tabs = await this.browser.listTabs({ windowId });
565
+ if (tabs.length > 0) {
566
+ return tabs;
567
+ }
568
+ await this.delay(50);
569
+ }
570
+ return [];
571
+ }
572
+ async delay(ms) {
573
+ await new Promise((resolve) => setTimeout(resolve, ms));
574
+ }
575
+ isMissingWindowError(error) {
576
+ const message = error instanceof Error ? error.message : String(error);
577
+ return message.toLowerCase().includes("no window with id");
578
+ }
579
+ };
580
+
33
581
  // src/background.ts
34
582
  var DEFAULT_PORT = 17373;
35
583
  var STORAGE_KEY_TOKEN = "pairToken";
36
584
  var STORAGE_KEY_PORT = "cliPort";
37
585
  var STORAGE_KEY_DEBUG_RICH_TEXT = "debugRichText";
586
+ var STORAGE_KEY_WORKSPACE = "agentWorkspace";
38
587
  var DEFAULT_TAB_LOAD_TIMEOUT_MS = 4e4;
39
588
  var ws = null;
40
589
  var reconnectTimer = null;
@@ -104,6 +653,171 @@
104
653
  }
105
654
  return toError("E_INTERNAL", message);
106
655
  }
656
+ function toTabInfo(tab) {
657
+ if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
658
+ throw new Error("Tab is missing runtime identifiers");
659
+ }
660
+ return {
661
+ id: tab.id,
662
+ title: tab.title ?? "",
663
+ url: tab.url ?? "",
664
+ active: Boolean(tab.active),
665
+ windowId: tab.windowId,
666
+ groupId: typeof tab.groupId === "number" && tab.groupId >= 0 ? tab.groupId : null
667
+ };
668
+ }
669
+ async function loadWorkspaceState() {
670
+ const stored = await chrome.storage.local.get(STORAGE_KEY_WORKSPACE);
671
+ const state = stored[STORAGE_KEY_WORKSPACE];
672
+ if (!state || typeof state !== "object") {
673
+ return null;
674
+ }
675
+ return state;
676
+ }
677
+ async function saveWorkspaceState(state) {
678
+ if (state === null) {
679
+ await chrome.storage.local.remove(STORAGE_KEY_WORKSPACE);
680
+ return;
681
+ }
682
+ await chrome.storage.local.set({ [STORAGE_KEY_WORKSPACE]: state });
683
+ }
684
+ var workspaceBrowser = {
685
+ async getTab(tabId) {
686
+ try {
687
+ return toTabInfo(await chrome.tabs.get(tabId));
688
+ } catch {
689
+ return null;
690
+ }
691
+ },
692
+ async getActiveTab() {
693
+ const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
694
+ const tab = tabs[0];
695
+ if (!tab) {
696
+ return null;
697
+ }
698
+ return toTabInfo(tab);
699
+ },
700
+ async listTabs(filter) {
701
+ const tabs = await chrome.tabs.query(filter?.windowId ? { windowId: filter.windowId } : {});
702
+ return tabs.filter((tab) => typeof tab.id === "number" && typeof tab.windowId === "number").map((tab) => toTabInfo(tab));
703
+ },
704
+ async createTab(options) {
705
+ const createdTab = await chrome.tabs.create({
706
+ windowId: options.windowId,
707
+ url: options.url ?? "about:blank",
708
+ active: options.active
709
+ });
710
+ if (!createdTab) {
711
+ throw new Error("Tab creation returned no tab");
712
+ }
713
+ return toTabInfo(createdTab);
714
+ },
715
+ async updateTab(tabId, options) {
716
+ const updatedTab = await chrome.tabs.update(tabId, {
717
+ active: options.active,
718
+ url: options.url
719
+ });
720
+ if (!updatedTab) {
721
+ throw new Error(`Tab update returned no tab for ${tabId}`);
722
+ }
723
+ return toTabInfo(updatedTab);
724
+ },
725
+ async closeTab(tabId) {
726
+ await chrome.tabs.remove(tabId);
727
+ },
728
+ async getWindow(windowId) {
729
+ try {
730
+ const window = await chrome.windows.get(windowId);
731
+ return {
732
+ id: window.id,
733
+ focused: Boolean(window.focused)
734
+ };
735
+ } catch {
736
+ return null;
737
+ }
738
+ },
739
+ async createWindow(options) {
740
+ const previouslyFocusedWindow = options.focused === true ? null : (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === "number") ?? null;
741
+ const previouslyFocusedTab = previouslyFocusedWindow?.id !== void 0 ? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === "number") ?? null : null;
742
+ const created = await chrome.windows.create({
743
+ url: options.url ?? "about:blank",
744
+ focused: true
745
+ });
746
+ if (!created || typeof created.id !== "number") {
747
+ throw new Error("Window missing id");
748
+ }
749
+ if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
750
+ await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
751
+ if (typeof previouslyFocusedTab?.id === "number") {
752
+ await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
753
+ }
754
+ }
755
+ const finalWindow = await chrome.windows.get(created.id);
756
+ return {
757
+ id: finalWindow.id,
758
+ focused: Boolean(finalWindow.focused)
759
+ };
760
+ },
761
+ async updateWindow(windowId, options) {
762
+ const updated = await chrome.windows.update(windowId, {
763
+ focused: options.focused
764
+ });
765
+ if (!updated || typeof updated.id !== "number") {
766
+ throw new Error("Window missing id");
767
+ }
768
+ return {
769
+ id: updated.id,
770
+ focused: Boolean(updated.focused)
771
+ };
772
+ },
773
+ async closeWindow(windowId) {
774
+ await chrome.windows.remove(windowId);
775
+ },
776
+ async getGroup(groupId) {
777
+ try {
778
+ const group = await chrome.tabGroups.get(groupId);
779
+ return {
780
+ id: group.id,
781
+ windowId: group.windowId,
782
+ title: group.title ?? "",
783
+ color: group.color,
784
+ collapsed: Boolean(group.collapsed)
785
+ };
786
+ } catch {
787
+ return null;
788
+ }
789
+ },
790
+ async groupTabs(tabIds, groupId) {
791
+ return await chrome.tabs.group({
792
+ tabIds,
793
+ groupId
794
+ });
795
+ },
796
+ async updateGroup(groupId, options) {
797
+ const updated = await chrome.tabGroups.update(groupId, {
798
+ title: options.title,
799
+ color: options.color,
800
+ collapsed: options.collapsed
801
+ });
802
+ if (!updated) {
803
+ throw new Error(`Tab group update returned no group for ${groupId}`);
804
+ }
805
+ return {
806
+ id: updated.id,
807
+ windowId: updated.windowId,
808
+ title: updated.title ?? "",
809
+ color: updated.color,
810
+ collapsed: Boolean(updated.collapsed)
811
+ };
812
+ }
813
+ };
814
+ var workspaceManager = new WorkspaceManager(
815
+ {
816
+ load: loadWorkspaceState,
817
+ save: saveWorkspaceState
818
+ },
819
+ workspaceBrowser
820
+ );
107
821
  async function waitForTabComplete(tabId, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS) {
108
822
  try {
109
823
  const current = await chrome.tabs.get(tabId);
@@ -161,28 +875,46 @@
161
875
  probeStatus();
162
876
  });
163
877
  }
164
- async function withTab(tabId, options = {}) {
878
+ async function waitForTabUrl(tabId, expectedUrl, timeoutMs = 1e4) {
879
+ const deadline = Date.now() + timeoutMs;
880
+ while (Date.now() < deadline) {
881
+ try {
882
+ const tab = await chrome.tabs.get(tabId);
883
+ const currentUrl = tab.url ?? "";
884
+ const pendingUrl = "pendingUrl" in tab && typeof tab.pendingUrl === "string" ? tab.pendingUrl : "";
885
+ if (currentUrl === expectedUrl || pendingUrl === expectedUrl) {
886
+ return;
887
+ }
888
+ } catch {
889
+ }
890
+ await new Promise((resolve) => setTimeout(resolve, 100));
891
+ }
892
+ throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
893
+ }
894
+ async function withTab(target = {}, options = {}) {
165
895
  const requireSupportedAutomationUrl = options.requireSupportedAutomationUrl !== false;
166
896
  const validate = (tab2) => {
167
897
  if (!tab2.id) {
168
898
  throw toError("E_NOT_FOUND", "Tab missing id");
169
899
  }
170
- if (requireSupportedAutomationUrl && !isSupportedAutomationUrl(tab2.url)) {
900
+ const pendingUrl = "pendingUrl" in tab2 && typeof tab2.pendingUrl === "string" ? tab2.pendingUrl : "";
901
+ if (requireSupportedAutomationUrl && !isSupportedAutomationUrl(tab2.url) && !isSupportedAutomationUrl(pendingUrl)) {
171
902
  throw toError("E_PERMISSION", "Unsupported tab URL: only http/https pages can be automated", {
172
- url: tab2.url ?? ""
903
+ url: tab2.url ?? pendingUrl ?? ""
173
904
  });
174
905
  }
175
906
  return tab2;
176
907
  };
177
- if (typeof tabId === "number") {
178
- const tab2 = await chrome.tabs.get(tabId);
908
+ if (typeof target.tabId === "number") {
909
+ const tab2 = await chrome.tabs.get(target.tabId);
179
910
  return validate(tab2);
180
911
  }
181
- const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
182
- const tab = tabs[0];
183
- if (!tab) {
184
- throw toError("E_NOT_FOUND", "No active tab");
185
- }
912
+ const resolved = await workspaceManager.resolveTarget({
913
+ tabId: target.tabId,
914
+ workspaceId: typeof target.workspaceId === "string" ? target.workspaceId : void 0,
915
+ createIfMissing: false
916
+ });
917
+ const tab = await chrome.tabs.get(resolved.tab.id);
186
918
  return validate(tab);
187
919
  }
188
920
  async function captureAlignedTabScreenshot(tab) {
@@ -208,7 +940,7 @@
208
940
  }
209
941
  }
210
942
  async function sendToContent(tabId, message) {
211
- const maxAttempts = 6;
943
+ const maxAttempts = 10;
212
944
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
213
945
  try {
214
946
  const response = await chrome.tabs.sendMessage(tabId, message);
@@ -222,11 +954,44 @@
222
954
  if (!retriable || attempt >= maxAttempts) {
223
955
  throw toError("E_NOT_READY", "Content script unavailable", { detail });
224
956
  }
225
- await new Promise((resolve) => setTimeout(resolve, 150 * attempt));
957
+ await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
226
958
  }
227
959
  }
228
960
  throw toError("E_NOT_READY", "Content script unavailable");
229
961
  }
962
+ async function captureFocusContext() {
963
+ const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
964
+ const activeTab = activeTabs.find((tab) => typeof tab.id === "number" && typeof tab.windowId === "number") ?? null;
965
+ return {
966
+ windowId: activeTab?.windowId ?? null,
967
+ tabId: activeTab?.id ?? null
968
+ };
969
+ }
970
+ async function restoreFocusContext(context) {
971
+ if (context.windowId !== null) {
972
+ try {
973
+ await chrome.windows.update(context.windowId, { focused: true });
974
+ } catch {
975
+ }
976
+ }
977
+ if (context.tabId !== null) {
978
+ try {
979
+ await chrome.tabs.update(context.tabId, { active: true });
980
+ } catch {
981
+ }
982
+ }
983
+ }
984
+ async function preserveHumanFocus(enabled, action) {
985
+ if (!enabled) {
986
+ return action();
987
+ }
988
+ const focusContext = await captureFocusContext();
989
+ try {
990
+ return await action();
991
+ } finally {
992
+ await restoreFocusContext(focusContext);
993
+ }
994
+ }
230
995
  function requireRpcEnvelope(method, value) {
231
996
  if (typeof value !== "object" || value === null || typeof value.ok !== "boolean") {
232
997
  throw toError("E_NOT_READY", `Content script returned malformed response for ${method}`);
@@ -247,6 +1012,10 @@
247
1012
  }
248
1013
  async function handleRequest(request) {
249
1014
  const params = request.params ?? {};
1015
+ const target = {
1016
+ tabId: typeof params.tabId === "number" ? params.tabId : void 0,
1017
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0
1018
+ };
250
1019
  const rpcForwardMethods = /* @__PURE__ */ new Set([
251
1020
  "page.title",
252
1021
  "page.url",
@@ -291,12 +1060,7 @@
291
1060
  case "tabs.list": {
292
1061
  const tabs = await chrome.tabs.query({});
293
1062
  return {
294
- tabs: tabs.filter((tab) => typeof tab.id === "number").map((tab) => ({
295
- id: tab.id,
296
- title: tab.title ?? "",
297
- url: tab.url ?? "",
298
- active: tab.active
299
- }))
1063
+ tabs: tabs.filter((tab) => typeof tab.id === "number" && typeof tab.windowId === "number").map((tab) => toTabInfo(tab))
300
1064
  };
301
1065
  }
302
1066
  case "tabs.getActive": {
@@ -306,12 +1070,7 @@
306
1070
  return { tab: null };
307
1071
  }
308
1072
  return {
309
- tab: {
310
- id: tab.id,
311
- title: tab.title ?? "",
312
- url: tab.url ?? "",
313
- active: Boolean(tab.active)
314
- }
1073
+ tab: toTabInfo(tab)
315
1074
  };
316
1075
  }
317
1076
  case "tabs.get": {
@@ -321,12 +1080,7 @@
321
1080
  throw toError("E_NOT_FOUND", "Tab missing id");
322
1081
  }
323
1082
  return {
324
- tab: {
325
- id: tab.id,
326
- title: tab.title ?? "",
327
- url: tab.url ?? "",
328
- active: Boolean(tab.active)
329
- }
1083
+ tab: toTabInfo(tab)
330
1084
  };
331
1085
  }
332
1086
  case "tabs.focus": {
@@ -335,164 +1089,275 @@
335
1089
  return { ok: true };
336
1090
  }
337
1091
  case "tabs.new": {
338
- const tab = await chrome.tabs.create({ url: params.url ?? "about:blank" });
339
- return { tabId: tab.id };
1092
+ if (typeof params.workspaceId === "string" || params.windowId === void 0) {
1093
+ const opened = await preserveHumanFocus(true, async () => {
1094
+ return await workspaceManager.openTab({
1095
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : DEFAULT_WORKSPACE_ID,
1096
+ url: params.url ?? "about:blank",
1097
+ active: params.active === true,
1098
+ focus: false
1099
+ });
1100
+ });
1101
+ return {
1102
+ tabId: opened.tab.id,
1103
+ windowId: opened.tab.windowId,
1104
+ groupId: opened.workspace.groupId,
1105
+ workspaceId: opened.workspace.id
1106
+ };
1107
+ }
1108
+ const tab = await chrome.tabs.create({
1109
+ url: params.url ?? "about:blank",
1110
+ windowId: typeof params.windowId === "number" ? params.windowId : void 0,
1111
+ active: params.active === true
1112
+ });
1113
+ if (params.addToGroup === true && typeof tab.id === "number") {
1114
+ const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
1115
+ return {
1116
+ tabId: tab.id,
1117
+ windowId: tab.windowId,
1118
+ groupId
1119
+ };
1120
+ }
1121
+ return {
1122
+ tabId: tab.id,
1123
+ windowId: tab.windowId
1124
+ };
340
1125
  }
341
1126
  case "tabs.close": {
342
1127
  const tabId = Number(params.tabId);
343
1128
  await chrome.tabs.remove(tabId);
344
1129
  return { ok: true };
345
1130
  }
1131
+ case "workspace.ensure": {
1132
+ return preserveHumanFocus(params.focus !== true, async () => {
1133
+ return await workspaceManager.ensureWorkspace({
1134
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
1135
+ focus: params.focus === true,
1136
+ initialUrl: typeof params.url === "string" ? params.url : void 0
1137
+ });
1138
+ });
1139
+ }
1140
+ case "workspace.info": {
1141
+ return {
1142
+ workspace: await workspaceManager.getWorkspaceInfo(typeof params.workspaceId === "string" ? params.workspaceId : void 0)
1143
+ };
1144
+ }
1145
+ case "workspace.openTab": {
1146
+ return await preserveHumanFocus(params.focus !== true, async () => {
1147
+ return await workspaceManager.openTab({
1148
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
1149
+ url: typeof params.url === "string" ? params.url : void 0,
1150
+ active: params.active === true,
1151
+ focus: params.focus === true
1152
+ });
1153
+ });
1154
+ }
1155
+ case "workspace.listTabs": {
1156
+ return await workspaceManager.listTabs(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1157
+ }
1158
+ case "workspace.getActiveTab": {
1159
+ return await workspaceManager.getActiveTab(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1160
+ }
1161
+ case "workspace.setActiveTab": {
1162
+ return await workspaceManager.setActiveTab(Number(params.tabId), typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1163
+ }
1164
+ case "workspace.focus": {
1165
+ return await workspaceManager.focus(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1166
+ }
1167
+ case "workspace.reset": {
1168
+ return await preserveHumanFocus(params.focus !== true, async () => {
1169
+ return await workspaceManager.reset({
1170
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
1171
+ focus: params.focus === true,
1172
+ initialUrl: typeof params.url === "string" ? params.url : void 0
1173
+ });
1174
+ });
1175
+ }
1176
+ case "workspace.close": {
1177
+ return await workspaceManager.close(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1178
+ }
346
1179
  case "page.goto": {
347
- const tab = await withTab(params.tabId, {
348
- requireSupportedAutomationUrl: false
1180
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1181
+ const tab = await withTab(target, {
1182
+ requireSupportedAutomationUrl: false
1183
+ });
1184
+ const url = String(params.url ?? "about:blank");
1185
+ await chrome.tabs.update(tab.id, { url });
1186
+ await waitForTabUrl(tab.id, url);
1187
+ await forwardContentRpc(tab.id, "page.url", { tabId: tab.id }).catch(() => void 0);
1188
+ await waitForTabComplete(tab.id, 5e3).catch(() => void 0);
1189
+ return { ok: true };
349
1190
  });
350
- await chrome.tabs.update(tab.id, { url: String(params.url ?? "about:blank") });
351
- await waitForTabComplete(tab.id);
352
- return { ok: true };
353
1191
  }
354
1192
  case "page.back": {
355
- const tab = await withTab(params.tabId);
356
- await chrome.tabs.goBack(tab.id);
357
- await waitForTabComplete(tab.id);
358
- return { ok: true };
1193
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1194
+ const tab = await withTab(target);
1195
+ await chrome.tabs.goBack(tab.id);
1196
+ await waitForTabComplete(tab.id);
1197
+ return { ok: true };
1198
+ });
359
1199
  }
360
1200
  case "page.forward": {
361
- const tab = await withTab(params.tabId);
362
- await chrome.tabs.goForward(tab.id);
363
- await waitForTabComplete(tab.id);
364
- return { ok: true };
1201
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1202
+ const tab = await withTab(target);
1203
+ await chrome.tabs.goForward(tab.id);
1204
+ await waitForTabComplete(tab.id);
1205
+ return { ok: true };
1206
+ });
365
1207
  }
366
1208
  case "page.reload": {
367
- const tab = await withTab(params.tabId);
368
- await chrome.tabs.reload(tab.id);
369
- await waitForTabComplete(tab.id);
370
- return { ok: true };
1209
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1210
+ const tab = await withTab(target);
1211
+ await chrome.tabs.reload(tab.id);
1212
+ await waitForTabComplete(tab.id);
1213
+ return { ok: true };
1214
+ });
371
1215
  }
372
1216
  case "page.viewport": {
373
- const tab = await withTab(params.tabId, {
374
- requireSupportedAutomationUrl: false
375
- });
376
- if (typeof tab.windowId !== "number") {
377
- throw toError("E_NOT_FOUND", "Tab window unavailable");
378
- }
379
- const width = typeof params.width === "number" ? Math.max(320, Math.floor(params.width)) : void 0;
380
- const height = typeof params.height === "number" ? Math.max(320, Math.floor(params.height)) : void 0;
381
- if (width || height) {
382
- await chrome.windows.update(tab.windowId, {
383
- width,
384
- height
1217
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1218
+ const tab = await withTab(target, {
1219
+ requireSupportedAutomationUrl: false
385
1220
  });
386
- }
387
- const viewport = await forwardContentRpc(tab.id, "page.viewport", {});
388
- const viewWidth = typeof width === "number" ? width : viewport.width ?? tab.width ?? 0;
389
- const viewHeight = typeof height === "number" ? height : viewport.height ?? tab.height ?? 0;
390
- return {
391
- width: viewWidth,
392
- height: viewHeight,
393
- devicePixelRatio: viewport.devicePixelRatio
394
- };
1221
+ if (typeof tab.windowId !== "number") {
1222
+ throw toError("E_NOT_FOUND", "Tab window unavailable");
1223
+ }
1224
+ const width = typeof params.width === "number" ? Math.max(320, Math.floor(params.width)) : void 0;
1225
+ const height = typeof params.height === "number" ? Math.max(320, Math.floor(params.height)) : void 0;
1226
+ if (width || height) {
1227
+ await chrome.windows.update(tab.windowId, {
1228
+ width,
1229
+ height
1230
+ });
1231
+ }
1232
+ const viewport = await forwardContentRpc(tab.id, "page.viewport", {});
1233
+ const viewWidth = typeof width === "number" ? width : viewport.width ?? tab.width ?? 0;
1234
+ const viewHeight = typeof height === "number" ? height : viewport.height ?? tab.height ?? 0;
1235
+ return {
1236
+ width: viewWidth,
1237
+ height: viewHeight,
1238
+ devicePixelRatio: viewport.devicePixelRatio
1239
+ };
1240
+ });
395
1241
  }
396
1242
  case "page.snapshot": {
397
- const tab = await withTab(params.tabId);
398
- if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
399
- throw toError("E_NOT_FOUND", "Tab missing id");
400
- }
401
- const includeBase64 = params.includeBase64 !== false;
402
- const config = await getConfig();
403
- const elements = await sendToContent(tab.id, {
404
- type: "bak.collectElements",
405
- debugRichText: config.debugRichText
1243
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1244
+ const tab = await withTab(target);
1245
+ if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
1246
+ throw toError("E_NOT_FOUND", "Tab missing id");
1247
+ }
1248
+ const includeBase64 = params.includeBase64 !== false;
1249
+ const config = await getConfig();
1250
+ const elements = await sendToContent(tab.id, {
1251
+ type: "bak.collectElements",
1252
+ debugRichText: config.debugRichText
1253
+ });
1254
+ const imageData = await captureAlignedTabScreenshot(tab);
1255
+ return {
1256
+ imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, "") : "",
1257
+ elements: elements.elements,
1258
+ tabId: tab.id,
1259
+ url: tab.url ?? ""
1260
+ };
406
1261
  });
407
- const imageData = await captureAlignedTabScreenshot(tab);
408
- return {
409
- imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, "") : "",
410
- elements: elements.elements,
411
- tabId: tab.id,
412
- url: tab.url ?? ""
413
- };
414
1262
  }
415
1263
  case "element.click": {
416
- const tab = await withTab(params.tabId);
417
- const response = await sendToContent(tab.id, {
418
- type: "bak.performAction",
419
- action: "click",
420
- locator: params.locator,
421
- requiresConfirm: params.requiresConfirm === true
1264
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1265
+ const tab = await withTab(target);
1266
+ const response = await sendToContent(tab.id, {
1267
+ type: "bak.performAction",
1268
+ action: "click",
1269
+ locator: params.locator,
1270
+ requiresConfirm: params.requiresConfirm === true
1271
+ });
1272
+ if (!response.ok) {
1273
+ throw response.error ?? toError("E_INTERNAL", "element.click failed");
1274
+ }
1275
+ return { ok: true };
422
1276
  });
423
- if (!response.ok) {
424
- throw response.error ?? toError("E_INTERNAL", "element.click failed");
425
- }
426
- return { ok: true };
427
1277
  }
428
1278
  case "element.type": {
429
- const tab = await withTab(params.tabId);
430
- const response = await sendToContent(tab.id, {
431
- type: "bak.performAction",
432
- action: "type",
433
- locator: params.locator,
434
- text: String(params.text ?? ""),
435
- clear: Boolean(params.clear),
436
- requiresConfirm: params.requiresConfirm === true
1279
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1280
+ const tab = await withTab(target);
1281
+ const response = await sendToContent(tab.id, {
1282
+ type: "bak.performAction",
1283
+ action: "type",
1284
+ locator: params.locator,
1285
+ text: String(params.text ?? ""),
1286
+ clear: Boolean(params.clear),
1287
+ requiresConfirm: params.requiresConfirm === true
1288
+ });
1289
+ if (!response.ok) {
1290
+ throw response.error ?? toError("E_INTERNAL", "element.type failed");
1291
+ }
1292
+ return { ok: true };
437
1293
  });
438
- if (!response.ok) {
439
- throw response.error ?? toError("E_INTERNAL", "element.type failed");
440
- }
441
- return { ok: true };
442
1294
  }
443
1295
  case "element.scroll": {
444
- const tab = await withTab(params.tabId);
445
- const response = await sendToContent(tab.id, {
446
- type: "bak.performAction",
447
- action: "scroll",
448
- locator: params.locator,
449
- dx: Number(params.dx ?? 0),
450
- dy: Number(params.dy ?? 320)
1296
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1297
+ const tab = await withTab(target);
1298
+ const response = await sendToContent(tab.id, {
1299
+ type: "bak.performAction",
1300
+ action: "scroll",
1301
+ locator: params.locator,
1302
+ dx: Number(params.dx ?? 0),
1303
+ dy: Number(params.dy ?? 320)
1304
+ });
1305
+ if (!response.ok) {
1306
+ throw response.error ?? toError("E_INTERNAL", "element.scroll failed");
1307
+ }
1308
+ return { ok: true };
451
1309
  });
452
- if (!response.ok) {
453
- throw response.error ?? toError("E_INTERNAL", "element.scroll failed");
454
- }
455
- return { ok: true };
456
1310
  }
457
1311
  case "page.wait": {
458
- const tab = await withTab(params.tabId);
459
- const response = await sendToContent(tab.id, {
460
- type: "bak.waitFor",
461
- mode: String(params.mode ?? "selector"),
462
- value: String(params.value ?? ""),
463
- timeoutMs: Number(params.timeoutMs ?? 5e3)
1312
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1313
+ const tab = await withTab(target);
1314
+ const response = await sendToContent(tab.id, {
1315
+ type: "bak.waitFor",
1316
+ mode: String(params.mode ?? "selector"),
1317
+ value: String(params.value ?? ""),
1318
+ timeoutMs: Number(params.timeoutMs ?? 5e3)
1319
+ });
1320
+ if (!response.ok) {
1321
+ throw response.error ?? toError("E_TIMEOUT", "page.wait failed");
1322
+ }
1323
+ return { ok: true };
464
1324
  });
465
- if (!response.ok) {
466
- throw response.error ?? toError("E_TIMEOUT", "page.wait failed");
467
- }
468
- return { ok: true };
469
1325
  }
470
1326
  case "debug.getConsole": {
471
- const tab = await withTab(params.tabId);
472
- const response = await sendToContent(tab.id, {
473
- type: "bak.getConsole",
474
- limit: Number(params.limit ?? 50)
1327
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1328
+ const tab = await withTab(target);
1329
+ const response = await sendToContent(tab.id, {
1330
+ type: "bak.getConsole",
1331
+ limit: Number(params.limit ?? 50)
1332
+ });
1333
+ return { entries: response.entries };
475
1334
  });
476
- return { entries: response.entries };
477
1335
  }
478
1336
  case "ui.selectCandidate": {
479
- const tab = await withTab(params.tabId);
480
- const response = await sendToContent(
481
- tab.id,
482
- {
483
- type: "bak.selectCandidate",
484
- candidates: params.candidates
1337
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1338
+ const tab = await withTab(target);
1339
+ const response = await sendToContent(
1340
+ tab.id,
1341
+ {
1342
+ type: "bak.selectCandidate",
1343
+ candidates: params.candidates
1344
+ }
1345
+ );
1346
+ if (!response.ok || !response.selectedEid) {
1347
+ throw response.error ?? toError("E_NEED_USER_CONFIRM", "User did not confirm candidate");
485
1348
  }
486
- );
487
- if (!response.ok || !response.selectedEid) {
488
- throw response.error ?? toError("E_NEED_USER_CONFIRM", "User did not confirm candidate");
489
- }
490
- return { selectedEid: response.selectedEid };
1349
+ return { selectedEid: response.selectedEid };
1350
+ });
491
1351
  }
492
1352
  default:
493
1353
  if (rpcForwardMethods.has(request.method)) {
494
- const tab = await withTab(params.tabId);
495
- return forwardContentRpc(tab.id, request.method, params);
1354
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1355
+ const tab = await withTab(target);
1356
+ return await forwardContentRpc(tab.id, request.method, {
1357
+ ...params,
1358
+ tabId: tab.id
1359
+ });
1360
+ });
496
1361
  }
497
1362
  throw toError("E_NOT_FOUND", `Unsupported method from CLI bridge: ${request.method}`);
498
1363
  }
@@ -572,6 +1437,46 @@
572
1437
  ws?.close();
573
1438
  });
574
1439
  }
1440
+ chrome.tabs.onRemoved.addListener((tabId) => {
1441
+ void loadWorkspaceState().then(async (state) => {
1442
+ if (!state || !state.tabIds.includes(tabId)) {
1443
+ return;
1444
+ }
1445
+ const nextTabIds = state.tabIds.filter((id) => id !== tabId);
1446
+ await saveWorkspaceState({
1447
+ ...state,
1448
+ tabIds: nextTabIds,
1449
+ activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
1450
+ primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
1451
+ });
1452
+ });
1453
+ });
1454
+ chrome.tabs.onActivated.addListener((activeInfo) => {
1455
+ void loadWorkspaceState().then(async (state) => {
1456
+ if (!state || state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
1457
+ return;
1458
+ }
1459
+ await saveWorkspaceState({
1460
+ ...state,
1461
+ activeTabId: activeInfo.tabId
1462
+ });
1463
+ });
1464
+ });
1465
+ chrome.windows.onRemoved.addListener((windowId) => {
1466
+ void loadWorkspaceState().then(async (state) => {
1467
+ if (!state || state.windowId !== windowId) {
1468
+ return;
1469
+ }
1470
+ await saveWorkspaceState({
1471
+ ...state,
1472
+ windowId: null,
1473
+ groupId: null,
1474
+ tabIds: [],
1475
+ activeTabId: null,
1476
+ primaryTabId: null
1477
+ });
1478
+ });
1479
+ });
575
1480
  chrome.runtime.onInstalled.addListener(() => {
576
1481
  void setConfig({ port: DEFAULT_PORT, debugRichText: false });
577
1482
  });