@flrande/bak-extension 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,11 +30,481 @@
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
+ const state = await this.loadWorkspaceRecord(workspaceId);
47
+ if (!state) {
48
+ return null;
49
+ }
50
+ const repaired = await this.ensureWorkspace({ workspaceId, focus: false, initialUrl: DEFAULT_WORKSPACE_URL });
51
+ return repaired.workspace;
52
+ }
53
+ async ensureWorkspace(options = {}) {
54
+ const workspaceId = this.normalizeWorkspaceId(options.workspaceId);
55
+ const repairActions = [];
56
+ const initialUrl = options.initialUrl ?? DEFAULT_WORKSPACE_URL;
57
+ const persisted = await this.storage.load();
58
+ const created = !persisted;
59
+ let state = this.normalizeState(persisted, workspaceId);
60
+ let window = state.windowId !== null ? await this.browser.getWindow(state.windowId) : null;
61
+ let tabs = [];
62
+ if (!window) {
63
+ const createdWindow = await this.browser.createWindow({
64
+ url: initialUrl,
65
+ focused: options.focus === true
66
+ });
67
+ state.windowId = createdWindow.id;
68
+ state.groupId = null;
69
+ state.tabIds = [];
70
+ state.activeTabId = null;
71
+ state.primaryTabId = null;
72
+ window = createdWindow;
73
+ tabs = await this.waitForWindowTabs(createdWindow.id);
74
+ state.tabIds = tabs.map((tab) => tab.id);
75
+ if (state.primaryTabId === null) {
76
+ state.primaryTabId = tabs[0]?.id ?? null;
77
+ }
78
+ if (state.activeTabId === null) {
79
+ state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
80
+ }
81
+ repairActions.push(created ? "created-window" : "recreated-window");
82
+ }
83
+ tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
84
+ const recoveredTabs = await this.recoverWorkspaceTabs(state, tabs);
85
+ if (recoveredTabs.length > tabs.length) {
86
+ tabs = recoveredTabs;
87
+ repairActions.push("recovered-tracked-tabs");
88
+ }
89
+ if (tabs.length !== state.tabIds.length) {
90
+ repairActions.push("pruned-missing-tabs");
91
+ }
92
+ state.tabIds = tabs.map((tab) => tab.id);
93
+ if (tabs.length === 0) {
94
+ const primary = await this.createWorkspaceTab({
95
+ windowId: state.windowId,
96
+ url: initialUrl,
97
+ active: true
98
+ });
99
+ tabs = [primary];
100
+ state.tabIds = [primary.id];
101
+ state.primaryTabId = primary.id;
102
+ state.activeTabId = primary.id;
103
+ repairActions.push("created-primary-tab");
104
+ }
105
+ if (state.primaryTabId === null || !tabs.some((tab) => tab.id === state.primaryTabId)) {
106
+ state.primaryTabId = tabs[0]?.id ?? null;
107
+ repairActions.push("reassigned-primary-tab");
108
+ }
109
+ if (state.activeTabId === null || !tabs.some((tab) => tab.id === state.activeTabId)) {
110
+ state.activeTabId = state.primaryTabId ?? tabs[0]?.id ?? null;
111
+ repairActions.push("reassigned-active-tab");
112
+ }
113
+ let group = state.groupId !== null ? await this.browser.getGroup(state.groupId) : null;
114
+ if (!group || group.windowId !== state.windowId) {
115
+ const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
116
+ group = await this.browser.updateGroup(groupId, {
117
+ title: state.label,
118
+ color: state.color,
119
+ collapsed: false
120
+ });
121
+ state.groupId = group.id;
122
+ repairActions.push("recreated-group");
123
+ } else {
124
+ await this.browser.updateGroup(group.id, {
125
+ title: state.label,
126
+ color: state.color,
127
+ collapsed: false
128
+ });
129
+ }
130
+ const ungroupedIds = tabs.filter((tab) => tab.groupId !== state.groupId).map((tab) => tab.id);
131
+ if (ungroupedIds.length > 0) {
132
+ await this.browser.groupTabs(ungroupedIds, state.groupId ?? void 0);
133
+ repairActions.push("regrouped-tabs");
134
+ }
135
+ tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
136
+ tabs = await this.recoverWorkspaceTabs(state, tabs);
137
+ const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId) : null;
138
+ if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
139
+ tabs = [...tabs, activeTab];
140
+ }
141
+ state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
142
+ if (options.focus === true && state.activeTabId !== null) {
143
+ await this.browser.updateTab(state.activeTabId, { active: true });
144
+ window = await this.browser.updateWindow(state.windowId, { focused: true });
145
+ void window;
146
+ repairActions.push("focused-window");
147
+ }
148
+ await this.storage.save(state);
149
+ return {
150
+ workspace: {
151
+ ...state,
152
+ tabs
153
+ },
154
+ created,
155
+ repaired: repairActions.length > 0,
156
+ repairActions
157
+ };
158
+ }
159
+ async openTab(options = {}) {
160
+ const ensured = await this.ensureWorkspace({
161
+ workspaceId: options.workspaceId,
162
+ focus: false,
163
+ initialUrl: options.url ?? DEFAULT_WORKSPACE_URL
164
+ });
165
+ const state = { ...ensured.workspace };
166
+ const active = options.active === true;
167
+ const desiredUrl = options.url ?? DEFAULT_WORKSPACE_URL;
168
+ const reusablePrimaryTab = (ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")) && state.tabs.length === 1 && state.primaryTabId !== null ? state.tabs.find((tab2) => tab2.id === state.primaryTabId) ?? null : null;
169
+ const createdTab = reusablePrimaryTab ? await this.browser.updateTab(reusablePrimaryTab.id, {
170
+ url: desiredUrl,
171
+ active
172
+ }) : await this.createWorkspaceTab({
173
+ windowId: state.windowId,
174
+ url: desiredUrl,
175
+ active
176
+ });
177
+ const nextTabIds = [.../* @__PURE__ */ new Set([...state.tabIds, createdTab.id])];
178
+ const groupId = await this.browser.groupTabs([createdTab.id], state.groupId ?? void 0);
179
+ await this.browser.updateGroup(groupId, {
180
+ title: state.label,
181
+ color: state.color,
182
+ collapsed: false
183
+ });
184
+ const nextState = {
185
+ id: state.id,
186
+ label: state.label,
187
+ color: state.color,
188
+ windowId: state.windowId,
189
+ groupId,
190
+ tabIds: nextTabIds,
191
+ activeTabId: createdTab.id,
192
+ primaryTabId: state.primaryTabId ?? createdTab.id
193
+ };
194
+ if (options.focus === true) {
195
+ await this.browser.updateTab(createdTab.id, { active: true });
196
+ await this.browser.updateWindow(state.windowId, { focused: true });
197
+ }
198
+ await this.storage.save(nextState);
199
+ const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
200
+ const tab = tabs.find((item) => item.id === createdTab.id) ?? createdTab;
201
+ return {
202
+ workspace: {
203
+ ...nextState,
204
+ tabs
205
+ },
206
+ tab
207
+ };
208
+ }
209
+ async listTabs(workspaceId = DEFAULT_WORKSPACE_ID) {
210
+ const ensured = await this.ensureWorkspace({ workspaceId });
211
+ return {
212
+ workspace: ensured.workspace,
213
+ tabs: ensured.workspace.tabs
214
+ };
215
+ }
216
+ async getActiveTab(workspaceId = DEFAULT_WORKSPACE_ID) {
217
+ const ensured = await this.ensureWorkspace({ workspaceId });
218
+ return {
219
+ workspace: ensured.workspace,
220
+ tab: ensured.workspace.tabs.find((tab) => tab.id === ensured.workspace.activeTabId) ?? null
221
+ };
222
+ }
223
+ async setActiveTab(tabId, workspaceId = DEFAULT_WORKSPACE_ID) {
224
+ const ensured = await this.ensureWorkspace({ workspaceId });
225
+ if (!ensured.workspace.tabIds.includes(tabId)) {
226
+ throw new Error(`Tab ${tabId} does not belong to workspace ${workspaceId}`);
227
+ }
228
+ const nextState = {
229
+ id: ensured.workspace.id,
230
+ label: ensured.workspace.label,
231
+ color: ensured.workspace.color,
232
+ windowId: ensured.workspace.windowId,
233
+ groupId: ensured.workspace.groupId,
234
+ tabIds: [...ensured.workspace.tabIds],
235
+ activeTabId: tabId,
236
+ primaryTabId: ensured.workspace.primaryTabId ?? tabId
237
+ };
238
+ await this.storage.save(nextState);
239
+ const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
240
+ const tab = tabs.find((item) => item.id === tabId);
241
+ if (!tab) {
242
+ throw new Error(`Tab ${tabId} is missing from workspace ${workspaceId}`);
243
+ }
244
+ return {
245
+ workspace: {
246
+ ...nextState,
247
+ tabs
248
+ },
249
+ tab
250
+ };
251
+ }
252
+ async focus(workspaceId = DEFAULT_WORKSPACE_ID) {
253
+ const ensured = await this.ensureWorkspace({ workspaceId, focus: false });
254
+ if (ensured.workspace.activeTabId !== null) {
255
+ await this.browser.updateTab(ensured.workspace.activeTabId, { active: true });
256
+ }
257
+ if (ensured.workspace.windowId !== null) {
258
+ await this.browser.updateWindow(ensured.workspace.windowId, { focused: true });
259
+ }
260
+ const refreshed = await this.ensureWorkspace({ workspaceId, focus: false });
261
+ return { ok: true, workspace: refreshed.workspace };
262
+ }
263
+ async reset(options = {}) {
264
+ await this.close(options.workspaceId);
265
+ return this.ensureWorkspace(options);
266
+ }
267
+ async close(workspaceId = DEFAULT_WORKSPACE_ID) {
268
+ const state = await this.storage.load();
269
+ if (!state || state.id !== workspaceId) {
270
+ await this.storage.save(null);
271
+ return { ok: true };
272
+ }
273
+ if (state.windowId !== null) {
274
+ const existingWindow = await this.browser.getWindow(state.windowId);
275
+ if (existingWindow) {
276
+ await this.browser.closeWindow(state.windowId);
277
+ }
278
+ }
279
+ await this.storage.save(null);
280
+ return { ok: true };
281
+ }
282
+ async resolveTarget(options = {}) {
283
+ if (typeof options.tabId === "number") {
284
+ const explicitTab = await this.browser.getTab(options.tabId);
285
+ if (!explicitTab) {
286
+ throw new Error(`No tab with id ${options.tabId}`);
287
+ }
288
+ return {
289
+ tab: explicitTab,
290
+ workspace: null,
291
+ resolution: "explicit-tab",
292
+ createdWorkspace: false,
293
+ repaired: false,
294
+ repairActions: []
295
+ };
296
+ }
297
+ const explicitWorkspaceId = typeof options.workspaceId === "string" ? this.normalizeWorkspaceId(options.workspaceId) : void 0;
298
+ if (explicitWorkspaceId) {
299
+ const ensured2 = await this.ensureWorkspace({
300
+ workspaceId: explicitWorkspaceId,
301
+ focus: false
302
+ });
303
+ return this.buildWorkspaceResolution(ensured2, "explicit-workspace");
304
+ }
305
+ const existingWorkspace = await this.loadWorkspaceRecord(DEFAULT_WORKSPACE_ID);
306
+ if (existingWorkspace) {
307
+ const ensured2 = await this.ensureWorkspace({
308
+ workspaceId: existingWorkspace.id,
309
+ focus: false
310
+ });
311
+ return this.buildWorkspaceResolution(ensured2, "default-workspace");
312
+ }
313
+ if (options.createIfMissing !== true) {
314
+ const activeTab = await this.browser.getActiveTab();
315
+ if (!activeTab) {
316
+ throw new Error("No active tab");
317
+ }
318
+ return {
319
+ tab: activeTab,
320
+ workspace: null,
321
+ resolution: "browser-active",
322
+ createdWorkspace: false,
323
+ repaired: false,
324
+ repairActions: []
325
+ };
326
+ }
327
+ const ensured = await this.ensureWorkspace({
328
+ workspaceId: DEFAULT_WORKSPACE_ID,
329
+ focus: false
330
+ });
331
+ return this.buildWorkspaceResolution(ensured, "default-workspace");
332
+ }
333
+ normalizeWorkspaceId(workspaceId) {
334
+ const candidate = workspaceId?.trim();
335
+ if (!candidate) {
336
+ return DEFAULT_WORKSPACE_ID;
337
+ }
338
+ if (candidate !== DEFAULT_WORKSPACE_ID) {
339
+ throw new Error(`Unsupported workspace id: ${candidate}`);
340
+ }
341
+ return candidate;
342
+ }
343
+ normalizeState(state, workspaceId) {
344
+ return {
345
+ id: workspaceId,
346
+ label: state?.label ?? DEFAULT_WORKSPACE_LABEL,
347
+ color: state?.color ?? DEFAULT_WORKSPACE_COLOR,
348
+ windowId: state?.windowId ?? null,
349
+ groupId: state?.groupId ?? null,
350
+ tabIds: state?.tabIds ?? [],
351
+ activeTabId: state?.activeTabId ?? null,
352
+ primaryTabId: state?.primaryTabId ?? null
353
+ };
354
+ }
355
+ async loadWorkspaceRecord(workspaceId = DEFAULT_WORKSPACE_ID) {
356
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
357
+ const state = await this.storage.load();
358
+ if (!state || state.id !== normalizedWorkspaceId) {
359
+ return null;
360
+ }
361
+ return this.normalizeState(state, normalizedWorkspaceId);
362
+ }
363
+ async buildWorkspaceResolution(ensured, resolution) {
364
+ const tab = ensured.workspace.tabs.find((item) => item.id === ensured.workspace.activeTabId) ?? ensured.workspace.tabs[0] ?? null;
365
+ if (tab) {
366
+ return {
367
+ tab,
368
+ workspace: ensured.workspace,
369
+ resolution,
370
+ createdWorkspace: ensured.created,
371
+ repaired: ensured.repaired,
372
+ repairActions: ensured.repairActions
373
+ };
374
+ }
375
+ if (ensured.workspace.activeTabId !== null) {
376
+ const activeWorkspaceTab = await this.waitForTrackedTab(ensured.workspace.activeTabId, ensured.workspace.windowId);
377
+ if (activeWorkspaceTab) {
378
+ return {
379
+ tab: activeWorkspaceTab,
380
+ workspace: ensured.workspace,
381
+ resolution,
382
+ createdWorkspace: ensured.created,
383
+ repaired: ensured.repaired,
384
+ repairActions: ensured.repairActions
385
+ };
386
+ }
387
+ }
388
+ const activeTab = await this.browser.getActiveTab();
389
+ if (!activeTab) {
390
+ throw new Error("No active tab");
391
+ }
392
+ return {
393
+ tab: activeTab,
394
+ workspace: null,
395
+ resolution: "browser-active",
396
+ createdWorkspace: ensured.created,
397
+ repaired: ensured.repaired,
398
+ repairActions: ensured.repairActions
399
+ };
400
+ }
401
+ async readTrackedTabs(tabIds, windowId) {
402
+ const tabs = (await Promise.all(
403
+ tabIds.map(async (tabId) => {
404
+ const tab = await this.browser.getTab(tabId);
405
+ if (!tab) {
406
+ return null;
407
+ }
408
+ if (windowId !== null && tab.windowId !== windowId) {
409
+ return null;
410
+ }
411
+ return tab;
412
+ })
413
+ )).filter((tab) => tab !== null);
414
+ return tabs;
415
+ }
416
+ async recoverWorkspaceTabs(state, existingTabs) {
417
+ if (state.windowId === null) {
418
+ return existingTabs;
419
+ }
420
+ const candidates = await this.waitForWindowTabs(state.windowId, 500);
421
+ if (candidates.length === 0) {
422
+ return existingTabs;
423
+ }
424
+ const trackedIds = new Set(state.tabIds);
425
+ const trackedTabs = candidates.filter((tab) => trackedIds.has(tab.id));
426
+ if (trackedTabs.length > existingTabs.length) {
427
+ return trackedTabs;
428
+ }
429
+ if (state.groupId !== null) {
430
+ const groupedTabs = candidates.filter((tab) => tab.groupId === state.groupId);
431
+ if (groupedTabs.length > 0) {
432
+ return groupedTabs;
433
+ }
434
+ }
435
+ const preferredIds = new Set([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number"));
436
+ const preferredTabs = candidates.filter((tab) => preferredIds.has(tab.id));
437
+ if (preferredTabs.length > existingTabs.length) {
438
+ return preferredTabs;
439
+ }
440
+ return existingTabs;
441
+ }
442
+ async createWorkspaceTab(options) {
443
+ if (options.windowId === null) {
444
+ throw new Error("Workspace window is unavailable");
445
+ }
446
+ const deadline = Date.now() + 1500;
447
+ let lastError2 = null;
448
+ while (Date.now() < deadline) {
449
+ const window = await this.browser.getWindow(options.windowId);
450
+ if (!window) {
451
+ lastError2 = new Error(`No window with id: ${options.windowId}.`);
452
+ await this.delay(50);
453
+ continue;
454
+ }
455
+ try {
456
+ return await this.browser.createTab({
457
+ windowId: options.windowId,
458
+ url: options.url,
459
+ active: options.active
460
+ });
461
+ } catch (error) {
462
+ if (!this.isMissingWindowError(error)) {
463
+ throw error;
464
+ }
465
+ lastError2 = error instanceof Error ? error : new Error(String(error));
466
+ await this.delay(50);
467
+ }
468
+ }
469
+ throw lastError2 ?? new Error(`No window with id: ${options.windowId}.`);
470
+ }
471
+ async waitForTrackedTab(tabId, windowId, timeoutMs = 1e3) {
472
+ const deadline = Date.now() + timeoutMs;
473
+ while (Date.now() < deadline) {
474
+ const tab = await this.browser.getTab(tabId);
475
+ if (tab && (windowId === null || tab.windowId === windowId)) {
476
+ return tab;
477
+ }
478
+ await this.delay(50);
479
+ }
480
+ return null;
481
+ }
482
+ async waitForWindowTabs(windowId, timeoutMs = 1e3) {
483
+ const deadline = Date.now() + timeoutMs;
484
+ while (Date.now() < deadline) {
485
+ const tabs = await this.browser.listTabs({ windowId });
486
+ if (tabs.length > 0) {
487
+ return tabs;
488
+ }
489
+ await this.delay(50);
490
+ }
491
+ return [];
492
+ }
493
+ async delay(ms) {
494
+ await new Promise((resolve) => setTimeout(resolve, ms));
495
+ }
496
+ isMissingWindowError(error) {
497
+ const message = error instanceof Error ? error.message : String(error);
498
+ return message.toLowerCase().includes("no window with id");
499
+ }
500
+ };
501
+
33
502
  // src/background.ts
34
503
  var DEFAULT_PORT = 17373;
35
504
  var STORAGE_KEY_TOKEN = "pairToken";
36
505
  var STORAGE_KEY_PORT = "cliPort";
37
506
  var STORAGE_KEY_DEBUG_RICH_TEXT = "debugRichText";
507
+ var STORAGE_KEY_WORKSPACE = "agentWorkspace";
38
508
  var DEFAULT_TAB_LOAD_TIMEOUT_MS = 4e4;
39
509
  var ws = null;
40
510
  var reconnectTimer = null;
@@ -104,6 +574,171 @@
104
574
  }
105
575
  return toError("E_INTERNAL", message);
106
576
  }
577
+ function toTabInfo(tab) {
578
+ if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
579
+ throw new Error("Tab is missing runtime identifiers");
580
+ }
581
+ return {
582
+ id: tab.id,
583
+ title: tab.title ?? "",
584
+ url: tab.url ?? "",
585
+ active: Boolean(tab.active),
586
+ windowId: tab.windowId,
587
+ groupId: typeof tab.groupId === "number" && tab.groupId >= 0 ? tab.groupId : null
588
+ };
589
+ }
590
+ async function loadWorkspaceState() {
591
+ const stored = await chrome.storage.local.get(STORAGE_KEY_WORKSPACE);
592
+ const state = stored[STORAGE_KEY_WORKSPACE];
593
+ if (!state || typeof state !== "object") {
594
+ return null;
595
+ }
596
+ return state;
597
+ }
598
+ async function saveWorkspaceState(state) {
599
+ if (state === null) {
600
+ await chrome.storage.local.remove(STORAGE_KEY_WORKSPACE);
601
+ return;
602
+ }
603
+ await chrome.storage.local.set({ [STORAGE_KEY_WORKSPACE]: state });
604
+ }
605
+ var workspaceBrowser = {
606
+ async getTab(tabId) {
607
+ try {
608
+ return toTabInfo(await chrome.tabs.get(tabId));
609
+ } catch {
610
+ return null;
611
+ }
612
+ },
613
+ async getActiveTab() {
614
+ const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
615
+ const tab = tabs[0];
616
+ if (!tab) {
617
+ return null;
618
+ }
619
+ return toTabInfo(tab);
620
+ },
621
+ async listTabs(filter) {
622
+ const tabs = await chrome.tabs.query(filter?.windowId ? { windowId: filter.windowId } : {});
623
+ return tabs.filter((tab) => typeof tab.id === "number" && typeof tab.windowId === "number").map((tab) => toTabInfo(tab));
624
+ },
625
+ async createTab(options) {
626
+ const createdTab = await chrome.tabs.create({
627
+ windowId: options.windowId,
628
+ url: options.url ?? "about:blank",
629
+ active: options.active
630
+ });
631
+ if (!createdTab) {
632
+ throw new Error("Tab creation returned no tab");
633
+ }
634
+ return toTabInfo(createdTab);
635
+ },
636
+ async updateTab(tabId, options) {
637
+ const updatedTab = await chrome.tabs.update(tabId, {
638
+ active: options.active,
639
+ url: options.url
640
+ });
641
+ if (!updatedTab) {
642
+ throw new Error(`Tab update returned no tab for ${tabId}`);
643
+ }
644
+ return toTabInfo(updatedTab);
645
+ },
646
+ async closeTab(tabId) {
647
+ await chrome.tabs.remove(tabId);
648
+ },
649
+ async getWindow(windowId) {
650
+ try {
651
+ const window = await chrome.windows.get(windowId);
652
+ return {
653
+ id: window.id,
654
+ focused: Boolean(window.focused)
655
+ };
656
+ } catch {
657
+ return null;
658
+ }
659
+ },
660
+ async createWindow(options) {
661
+ const previouslyFocusedWindow = options.focused === true ? null : (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === "number") ?? null;
662
+ const previouslyFocusedTab = previouslyFocusedWindow?.id !== void 0 ? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === "number") ?? null : null;
663
+ const created = await chrome.windows.create({
664
+ url: options.url ?? "about:blank",
665
+ focused: true
666
+ });
667
+ if (!created || typeof created.id !== "number") {
668
+ throw new Error("Window missing id");
669
+ }
670
+ if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
671
+ await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
672
+ if (typeof previouslyFocusedTab?.id === "number") {
673
+ await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
674
+ }
675
+ }
676
+ const finalWindow = await chrome.windows.get(created.id);
677
+ return {
678
+ id: finalWindow.id,
679
+ focused: Boolean(finalWindow.focused)
680
+ };
681
+ },
682
+ async updateWindow(windowId, options) {
683
+ const updated = await chrome.windows.update(windowId, {
684
+ focused: options.focused
685
+ });
686
+ if (!updated || typeof updated.id !== "number") {
687
+ throw new Error("Window missing id");
688
+ }
689
+ return {
690
+ id: updated.id,
691
+ focused: Boolean(updated.focused)
692
+ };
693
+ },
694
+ async closeWindow(windowId) {
695
+ await chrome.windows.remove(windowId);
696
+ },
697
+ async getGroup(groupId) {
698
+ try {
699
+ const group = await chrome.tabGroups.get(groupId);
700
+ return {
701
+ id: group.id,
702
+ windowId: group.windowId,
703
+ title: group.title ?? "",
704
+ color: group.color,
705
+ collapsed: Boolean(group.collapsed)
706
+ };
707
+ } catch {
708
+ return null;
709
+ }
710
+ },
711
+ async groupTabs(tabIds, groupId) {
712
+ return await chrome.tabs.group({
713
+ tabIds,
714
+ groupId
715
+ });
716
+ },
717
+ async updateGroup(groupId, options) {
718
+ const updated = await chrome.tabGroups.update(groupId, {
719
+ title: options.title,
720
+ color: options.color,
721
+ collapsed: options.collapsed
722
+ });
723
+ if (!updated) {
724
+ throw new Error(`Tab group update returned no group for ${groupId}`);
725
+ }
726
+ return {
727
+ id: updated.id,
728
+ windowId: updated.windowId,
729
+ title: updated.title ?? "",
730
+ color: updated.color,
731
+ collapsed: Boolean(updated.collapsed)
732
+ };
733
+ }
734
+ };
735
+ var workspaceManager = new WorkspaceManager(
736
+ {
737
+ load: loadWorkspaceState,
738
+ save: saveWorkspaceState
739
+ },
740
+ workspaceBrowser
741
+ );
107
742
  async function waitForTabComplete(tabId, timeoutMs = DEFAULT_TAB_LOAD_TIMEOUT_MS) {
108
743
  try {
109
744
  const current = await chrome.tabs.get(tabId);
@@ -161,28 +796,46 @@
161
796
  probeStatus();
162
797
  });
163
798
  }
164
- async function withTab(tabId, options = {}) {
799
+ async function waitForTabUrl(tabId, expectedUrl, timeoutMs = 1e4) {
800
+ const deadline = Date.now() + timeoutMs;
801
+ while (Date.now() < deadline) {
802
+ try {
803
+ const tab = await chrome.tabs.get(tabId);
804
+ const currentUrl = tab.url ?? "";
805
+ const pendingUrl = "pendingUrl" in tab && typeof tab.pendingUrl === "string" ? tab.pendingUrl : "";
806
+ if (currentUrl === expectedUrl || pendingUrl === expectedUrl) {
807
+ return;
808
+ }
809
+ } catch {
810
+ }
811
+ await new Promise((resolve) => setTimeout(resolve, 100));
812
+ }
813
+ throw new Error(`tab url timeout: ${tabId} -> ${expectedUrl}`);
814
+ }
815
+ async function withTab(target = {}, options = {}) {
165
816
  const requireSupportedAutomationUrl = options.requireSupportedAutomationUrl !== false;
166
817
  const validate = (tab2) => {
167
818
  if (!tab2.id) {
168
819
  throw toError("E_NOT_FOUND", "Tab missing id");
169
820
  }
170
- if (requireSupportedAutomationUrl && !isSupportedAutomationUrl(tab2.url)) {
821
+ const pendingUrl = "pendingUrl" in tab2 && typeof tab2.pendingUrl === "string" ? tab2.pendingUrl : "";
822
+ if (requireSupportedAutomationUrl && !isSupportedAutomationUrl(tab2.url) && !isSupportedAutomationUrl(pendingUrl)) {
171
823
  throw toError("E_PERMISSION", "Unsupported tab URL: only http/https pages can be automated", {
172
- url: tab2.url ?? ""
824
+ url: tab2.url ?? pendingUrl ?? ""
173
825
  });
174
826
  }
175
827
  return tab2;
176
828
  };
177
- if (typeof tabId === "number") {
178
- const tab2 = await chrome.tabs.get(tabId);
829
+ if (typeof target.tabId === "number") {
830
+ const tab2 = await chrome.tabs.get(target.tabId);
179
831
  return validate(tab2);
180
832
  }
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
- }
833
+ const resolved = await workspaceManager.resolveTarget({
834
+ tabId: target.tabId,
835
+ workspaceId: typeof target.workspaceId === "string" ? target.workspaceId : void 0,
836
+ createIfMissing: false
837
+ });
838
+ const tab = await chrome.tabs.get(resolved.tab.id);
186
839
  return validate(tab);
187
840
  }
188
841
  async function captureAlignedTabScreenshot(tab) {
@@ -208,7 +861,7 @@
208
861
  }
209
862
  }
210
863
  async function sendToContent(tabId, message) {
211
- const maxAttempts = 6;
864
+ const maxAttempts = 10;
212
865
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
213
866
  try {
214
867
  const response = await chrome.tabs.sendMessage(tabId, message);
@@ -222,11 +875,44 @@
222
875
  if (!retriable || attempt >= maxAttempts) {
223
876
  throw toError("E_NOT_READY", "Content script unavailable", { detail });
224
877
  }
225
- await new Promise((resolve) => setTimeout(resolve, 150 * attempt));
878
+ await new Promise((resolve) => setTimeout(resolve, 100 * attempt));
226
879
  }
227
880
  }
228
881
  throw toError("E_NOT_READY", "Content script unavailable");
229
882
  }
883
+ async function captureFocusContext() {
884
+ const activeTabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
885
+ const activeTab = activeTabs.find((tab) => typeof tab.id === "number" && typeof tab.windowId === "number") ?? null;
886
+ return {
887
+ windowId: activeTab?.windowId ?? null,
888
+ tabId: activeTab?.id ?? null
889
+ };
890
+ }
891
+ async function restoreFocusContext(context) {
892
+ if (context.windowId !== null) {
893
+ try {
894
+ await chrome.windows.update(context.windowId, { focused: true });
895
+ } catch {
896
+ }
897
+ }
898
+ if (context.tabId !== null) {
899
+ try {
900
+ await chrome.tabs.update(context.tabId, { active: true });
901
+ } catch {
902
+ }
903
+ }
904
+ }
905
+ async function preserveHumanFocus(enabled, action) {
906
+ if (!enabled) {
907
+ return action();
908
+ }
909
+ const focusContext = await captureFocusContext();
910
+ try {
911
+ return await action();
912
+ } finally {
913
+ await restoreFocusContext(focusContext);
914
+ }
915
+ }
230
916
  function requireRpcEnvelope(method, value) {
231
917
  if (typeof value !== "object" || value === null || typeof value.ok !== "boolean") {
232
918
  throw toError("E_NOT_READY", `Content script returned malformed response for ${method}`);
@@ -247,6 +933,10 @@
247
933
  }
248
934
  async function handleRequest(request) {
249
935
  const params = request.params ?? {};
936
+ const target = {
937
+ tabId: typeof params.tabId === "number" ? params.tabId : void 0,
938
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0
939
+ };
250
940
  const rpcForwardMethods = /* @__PURE__ */ new Set([
251
941
  "page.title",
252
942
  "page.url",
@@ -291,12 +981,7 @@
291
981
  case "tabs.list": {
292
982
  const tabs = await chrome.tabs.query({});
293
983
  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
- }))
984
+ tabs: tabs.filter((tab) => typeof tab.id === "number" && typeof tab.windowId === "number").map((tab) => toTabInfo(tab))
300
985
  };
301
986
  }
302
987
  case "tabs.getActive": {
@@ -306,12 +991,7 @@
306
991
  return { tab: null };
307
992
  }
308
993
  return {
309
- tab: {
310
- id: tab.id,
311
- title: tab.title ?? "",
312
- url: tab.url ?? "",
313
- active: Boolean(tab.active)
314
- }
994
+ tab: toTabInfo(tab)
315
995
  };
316
996
  }
317
997
  case "tabs.get": {
@@ -321,12 +1001,7 @@
321
1001
  throw toError("E_NOT_FOUND", "Tab missing id");
322
1002
  }
323
1003
  return {
324
- tab: {
325
- id: tab.id,
326
- title: tab.title ?? "",
327
- url: tab.url ?? "",
328
- active: Boolean(tab.active)
329
- }
1004
+ tab: toTabInfo(tab)
330
1005
  };
331
1006
  }
332
1007
  case "tabs.focus": {
@@ -335,164 +1010,275 @@
335
1010
  return { ok: true };
336
1011
  }
337
1012
  case "tabs.new": {
338
- const tab = await chrome.tabs.create({ url: params.url ?? "about:blank" });
339
- return { tabId: tab.id };
1013
+ if (typeof params.workspaceId === "string" || params.windowId === void 0) {
1014
+ const opened = await preserveHumanFocus(true, async () => {
1015
+ return await workspaceManager.openTab({
1016
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : DEFAULT_WORKSPACE_ID,
1017
+ url: params.url ?? "about:blank",
1018
+ active: params.active === true,
1019
+ focus: false
1020
+ });
1021
+ });
1022
+ return {
1023
+ tabId: opened.tab.id,
1024
+ windowId: opened.tab.windowId,
1025
+ groupId: opened.workspace.groupId,
1026
+ workspaceId: opened.workspace.id
1027
+ };
1028
+ }
1029
+ const tab = await chrome.tabs.create({
1030
+ url: params.url ?? "about:blank",
1031
+ windowId: typeof params.windowId === "number" ? params.windowId : void 0,
1032
+ active: params.active === true
1033
+ });
1034
+ if (params.addToGroup === true && typeof tab.id === "number") {
1035
+ const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
1036
+ return {
1037
+ tabId: tab.id,
1038
+ windowId: tab.windowId,
1039
+ groupId
1040
+ };
1041
+ }
1042
+ return {
1043
+ tabId: tab.id,
1044
+ windowId: tab.windowId
1045
+ };
340
1046
  }
341
1047
  case "tabs.close": {
342
1048
  const tabId = Number(params.tabId);
343
1049
  await chrome.tabs.remove(tabId);
344
1050
  return { ok: true };
345
1051
  }
1052
+ case "workspace.ensure": {
1053
+ return preserveHumanFocus(params.focus !== true, async () => {
1054
+ return await workspaceManager.ensureWorkspace({
1055
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
1056
+ focus: params.focus === true,
1057
+ initialUrl: typeof params.url === "string" ? params.url : void 0
1058
+ });
1059
+ });
1060
+ }
1061
+ case "workspace.info": {
1062
+ return {
1063
+ workspace: await workspaceManager.getWorkspaceInfo(typeof params.workspaceId === "string" ? params.workspaceId : void 0)
1064
+ };
1065
+ }
1066
+ case "workspace.openTab": {
1067
+ return await preserveHumanFocus(params.focus !== true, async () => {
1068
+ return await workspaceManager.openTab({
1069
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
1070
+ url: typeof params.url === "string" ? params.url : void 0,
1071
+ active: params.active === true,
1072
+ focus: params.focus === true
1073
+ });
1074
+ });
1075
+ }
1076
+ case "workspace.listTabs": {
1077
+ return await workspaceManager.listTabs(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1078
+ }
1079
+ case "workspace.getActiveTab": {
1080
+ return await workspaceManager.getActiveTab(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1081
+ }
1082
+ case "workspace.setActiveTab": {
1083
+ return await workspaceManager.setActiveTab(Number(params.tabId), typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1084
+ }
1085
+ case "workspace.focus": {
1086
+ return await workspaceManager.focus(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1087
+ }
1088
+ case "workspace.reset": {
1089
+ return await preserveHumanFocus(params.focus !== true, async () => {
1090
+ return await workspaceManager.reset({
1091
+ workspaceId: typeof params.workspaceId === "string" ? params.workspaceId : void 0,
1092
+ focus: params.focus === true,
1093
+ initialUrl: typeof params.url === "string" ? params.url : void 0
1094
+ });
1095
+ });
1096
+ }
1097
+ case "workspace.close": {
1098
+ return await workspaceManager.close(typeof params.workspaceId === "string" ? params.workspaceId : void 0);
1099
+ }
346
1100
  case "page.goto": {
347
- const tab = await withTab(params.tabId, {
348
- requireSupportedAutomationUrl: false
1101
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1102
+ const tab = await withTab(target, {
1103
+ requireSupportedAutomationUrl: false
1104
+ });
1105
+ const url = String(params.url ?? "about:blank");
1106
+ await chrome.tabs.update(tab.id, { url });
1107
+ await waitForTabUrl(tab.id, url);
1108
+ await forwardContentRpc(tab.id, "page.url", { tabId: tab.id }).catch(() => void 0);
1109
+ await waitForTabComplete(tab.id, 5e3).catch(() => void 0);
1110
+ return { ok: true };
349
1111
  });
350
- await chrome.tabs.update(tab.id, { url: String(params.url ?? "about:blank") });
351
- await waitForTabComplete(tab.id);
352
- return { ok: true };
353
1112
  }
354
1113
  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 };
1114
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1115
+ const tab = await withTab(target);
1116
+ await chrome.tabs.goBack(tab.id);
1117
+ await waitForTabComplete(tab.id);
1118
+ return { ok: true };
1119
+ });
359
1120
  }
360
1121
  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 };
1122
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1123
+ const tab = await withTab(target);
1124
+ await chrome.tabs.goForward(tab.id);
1125
+ await waitForTabComplete(tab.id);
1126
+ return { ok: true };
1127
+ });
365
1128
  }
366
1129
  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 };
1130
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1131
+ const tab = await withTab(target);
1132
+ await chrome.tabs.reload(tab.id);
1133
+ await waitForTabComplete(tab.id);
1134
+ return { ok: true };
1135
+ });
371
1136
  }
372
1137
  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
1138
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1139
+ const tab = await withTab(target, {
1140
+ requireSupportedAutomationUrl: false
385
1141
  });
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
- };
1142
+ if (typeof tab.windowId !== "number") {
1143
+ throw toError("E_NOT_FOUND", "Tab window unavailable");
1144
+ }
1145
+ const width = typeof params.width === "number" ? Math.max(320, Math.floor(params.width)) : void 0;
1146
+ const height = typeof params.height === "number" ? Math.max(320, Math.floor(params.height)) : void 0;
1147
+ if (width || height) {
1148
+ await chrome.windows.update(tab.windowId, {
1149
+ width,
1150
+ height
1151
+ });
1152
+ }
1153
+ const viewport = await forwardContentRpc(tab.id, "page.viewport", {});
1154
+ const viewWidth = typeof width === "number" ? width : viewport.width ?? tab.width ?? 0;
1155
+ const viewHeight = typeof height === "number" ? height : viewport.height ?? tab.height ?? 0;
1156
+ return {
1157
+ width: viewWidth,
1158
+ height: viewHeight,
1159
+ devicePixelRatio: viewport.devicePixelRatio
1160
+ };
1161
+ });
395
1162
  }
396
1163
  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
1164
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1165
+ const tab = await withTab(target);
1166
+ if (typeof tab.id !== "number" || typeof tab.windowId !== "number") {
1167
+ throw toError("E_NOT_FOUND", "Tab missing id");
1168
+ }
1169
+ const includeBase64 = params.includeBase64 !== false;
1170
+ const config = await getConfig();
1171
+ const elements = await sendToContent(tab.id, {
1172
+ type: "bak.collectElements",
1173
+ debugRichText: config.debugRichText
1174
+ });
1175
+ const imageData = await captureAlignedTabScreenshot(tab);
1176
+ return {
1177
+ imageBase64: includeBase64 ? imageData.replace(/^data:image\/png;base64,/, "") : "",
1178
+ elements: elements.elements,
1179
+ tabId: tab.id,
1180
+ url: tab.url ?? ""
1181
+ };
406
1182
  });
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
1183
  }
415
1184
  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
1185
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1186
+ const tab = await withTab(target);
1187
+ const response = await sendToContent(tab.id, {
1188
+ type: "bak.performAction",
1189
+ action: "click",
1190
+ locator: params.locator,
1191
+ requiresConfirm: params.requiresConfirm === true
1192
+ });
1193
+ if (!response.ok) {
1194
+ throw response.error ?? toError("E_INTERNAL", "element.click failed");
1195
+ }
1196
+ return { ok: true };
422
1197
  });
423
- if (!response.ok) {
424
- throw response.error ?? toError("E_INTERNAL", "element.click failed");
425
- }
426
- return { ok: true };
427
1198
  }
428
1199
  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
1200
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1201
+ const tab = await withTab(target);
1202
+ const response = await sendToContent(tab.id, {
1203
+ type: "bak.performAction",
1204
+ action: "type",
1205
+ locator: params.locator,
1206
+ text: String(params.text ?? ""),
1207
+ clear: Boolean(params.clear),
1208
+ requiresConfirm: params.requiresConfirm === true
1209
+ });
1210
+ if (!response.ok) {
1211
+ throw response.error ?? toError("E_INTERNAL", "element.type failed");
1212
+ }
1213
+ return { ok: true };
437
1214
  });
438
- if (!response.ok) {
439
- throw response.error ?? toError("E_INTERNAL", "element.type failed");
440
- }
441
- return { ok: true };
442
1215
  }
443
1216
  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)
1217
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1218
+ const tab = await withTab(target);
1219
+ const response = await sendToContent(tab.id, {
1220
+ type: "bak.performAction",
1221
+ action: "scroll",
1222
+ locator: params.locator,
1223
+ dx: Number(params.dx ?? 0),
1224
+ dy: Number(params.dy ?? 320)
1225
+ });
1226
+ if (!response.ok) {
1227
+ throw response.error ?? toError("E_INTERNAL", "element.scroll failed");
1228
+ }
1229
+ return { ok: true };
451
1230
  });
452
- if (!response.ok) {
453
- throw response.error ?? toError("E_INTERNAL", "element.scroll failed");
454
- }
455
- return { ok: true };
456
1231
  }
457
1232
  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)
1233
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1234
+ const tab = await withTab(target);
1235
+ const response = await sendToContent(tab.id, {
1236
+ type: "bak.waitFor",
1237
+ mode: String(params.mode ?? "selector"),
1238
+ value: String(params.value ?? ""),
1239
+ timeoutMs: Number(params.timeoutMs ?? 5e3)
1240
+ });
1241
+ if (!response.ok) {
1242
+ throw response.error ?? toError("E_TIMEOUT", "page.wait failed");
1243
+ }
1244
+ return { ok: true };
464
1245
  });
465
- if (!response.ok) {
466
- throw response.error ?? toError("E_TIMEOUT", "page.wait failed");
467
- }
468
- return { ok: true };
469
1246
  }
470
1247
  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)
1248
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1249
+ const tab = await withTab(target);
1250
+ const response = await sendToContent(tab.id, {
1251
+ type: "bak.getConsole",
1252
+ limit: Number(params.limit ?? 50)
1253
+ });
1254
+ return { entries: response.entries };
475
1255
  });
476
- return { entries: response.entries };
477
1256
  }
478
1257
  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
1258
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1259
+ const tab = await withTab(target);
1260
+ const response = await sendToContent(
1261
+ tab.id,
1262
+ {
1263
+ type: "bak.selectCandidate",
1264
+ candidates: params.candidates
1265
+ }
1266
+ );
1267
+ if (!response.ok || !response.selectedEid) {
1268
+ throw response.error ?? toError("E_NEED_USER_CONFIRM", "User did not confirm candidate");
485
1269
  }
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 };
1270
+ return { selectedEid: response.selectedEid };
1271
+ });
491
1272
  }
492
1273
  default:
493
1274
  if (rpcForwardMethods.has(request.method)) {
494
- const tab = await withTab(params.tabId);
495
- return forwardContentRpc(tab.id, request.method, params);
1275
+ return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
1276
+ const tab = await withTab(target);
1277
+ return await forwardContentRpc(tab.id, request.method, {
1278
+ ...params,
1279
+ tabId: tab.id
1280
+ });
1281
+ });
496
1282
  }
497
1283
  throw toError("E_NOT_FOUND", `Unsupported method from CLI bridge: ${request.method}`);
498
1284
  }
@@ -572,6 +1358,46 @@
572
1358
  ws?.close();
573
1359
  });
574
1360
  }
1361
+ chrome.tabs.onRemoved.addListener((tabId) => {
1362
+ void loadWorkspaceState().then(async (state) => {
1363
+ if (!state || !state.tabIds.includes(tabId)) {
1364
+ return;
1365
+ }
1366
+ const nextTabIds = state.tabIds.filter((id) => id !== tabId);
1367
+ await saveWorkspaceState({
1368
+ ...state,
1369
+ tabIds: nextTabIds,
1370
+ activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
1371
+ primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
1372
+ });
1373
+ });
1374
+ });
1375
+ chrome.tabs.onActivated.addListener((activeInfo) => {
1376
+ void loadWorkspaceState().then(async (state) => {
1377
+ if (!state || state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
1378
+ return;
1379
+ }
1380
+ await saveWorkspaceState({
1381
+ ...state,
1382
+ activeTabId: activeInfo.tabId
1383
+ });
1384
+ });
1385
+ });
1386
+ chrome.windows.onRemoved.addListener((windowId) => {
1387
+ void loadWorkspaceState().then(async (state) => {
1388
+ if (!state || state.windowId !== windowId) {
1389
+ return;
1390
+ }
1391
+ await saveWorkspaceState({
1392
+ ...state,
1393
+ windowId: null,
1394
+ groupId: null,
1395
+ tabIds: [],
1396
+ activeTabId: null,
1397
+ primaryTabId: null
1398
+ });
1399
+ });
1400
+ });
575
1401
  chrome.runtime.onInstalled.addListener(() => {
576
1402
  void setConfig({ port: DEFAULT_PORT, debugRichText: false });
577
1403
  });