@flrande/bak-extension 0.6.2 → 0.6.4

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.
@@ -1 +1 @@
1
- 2026-03-12T11:56:10.714Z
1
+ 2026-03-12T15:53:45.600Z
@@ -59,6 +59,29 @@
59
59
  return Object.keys(result).length > 0 ? result : void 0;
60
60
  }
61
61
 
62
+ // package.json
63
+ var package_default = {
64
+ name: "@flrande/bak-extension",
65
+ version: "0.6.4",
66
+ type: "module",
67
+ scripts: {
68
+ build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
69
+ dev: "node scripts/copy-assets.mjs && tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --watch",
70
+ typecheck: "tsc -p tsconfig.json --noEmit",
71
+ lint: "eslint src --ext .ts"
72
+ },
73
+ dependencies: {
74
+ "@flrande/bak-protocol": "workspace:*"
75
+ },
76
+ devDependencies: {
77
+ "@types/chrome": "^0.1.14",
78
+ tsup: "^8.5.0"
79
+ }
80
+ };
81
+
82
+ // src/version.ts
83
+ var EXTENSION_VERSION = package_default.version;
84
+
62
85
  // src/network-debugger.ts
63
86
  var DEBUGGER_VERSION = "1.3";
64
87
  var MAX_ENTRIES = 1e3;
@@ -389,7 +412,7 @@
389
412
  version: "1.2",
390
413
  creator: {
391
414
  name: "bak",
392
- version: "0.6.1"
415
+ version: EXTENSION_VERSION
393
416
  },
394
417
  entries: entries.map((entry) => ({
395
418
  startedDateTime: new Date(entry.startedAt ?? entry.ts).toISOString(),
@@ -525,7 +548,7 @@
525
548
  const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
526
549
  const persisted = await this.storage.load(bindingId);
527
550
  const created = !persisted;
528
- let state = this.normalizeState(persisted, bindingId);
551
+ let state = this.normalizeState(persisted, bindingId, options.label);
529
552
  const originalWindowId = state.windowId;
530
553
  let window2 = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
531
554
  let tabs = [];
@@ -658,7 +681,8 @@
658
681
  const ensured = await this.ensureBinding({
659
682
  bindingId,
660
683
  focus: false,
661
- initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL
684
+ initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL,
685
+ label: options.label
662
686
  });
663
687
  let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
664
688
  if (state.windowId !== null && state.tabs.length === 0) {
@@ -692,7 +716,8 @@
692
716
  const repaired = await this.ensureBinding({
693
717
  bindingId,
694
718
  focus: false,
695
- initialUrl: desiredUrl
719
+ initialUrl: desiredUrl,
720
+ label: options.label
696
721
  });
697
722
  state = { ...repaired.binding };
698
723
  reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
@@ -804,6 +829,56 @@
804
829
  const refreshed = await this.ensureBinding({ bindingId, focus: false });
805
830
  return { ok: true, binding: refreshed.binding };
806
831
  }
832
+ async closeTab(bindingId, tabId) {
833
+ const ensured = await this.ensureBinding({ bindingId, focus: false });
834
+ const resolvedTabId = typeof tabId === "number" ? tabId : ensured.binding.activeTabId ?? ensured.binding.primaryTabId ?? ensured.binding.tabs[0]?.id;
835
+ if (typeof resolvedTabId !== "number" || !ensured.binding.tabIds.includes(resolvedTabId)) {
836
+ throw new Error(`Tab ${tabId ?? "active"} does not belong to binding ${bindingId}`);
837
+ }
838
+ await this.browser.closeTab(resolvedTabId);
839
+ const remainingTabIds = ensured.binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
840
+ if (remainingTabIds.length === 0) {
841
+ const emptied = {
842
+ id: ensured.binding.id,
843
+ label: ensured.binding.label,
844
+ color: ensured.binding.color,
845
+ windowId: null,
846
+ groupId: null,
847
+ tabIds: [],
848
+ activeTabId: null,
849
+ primaryTabId: null
850
+ };
851
+ await this.storage.save(emptied);
852
+ return {
853
+ binding: {
854
+ ...emptied,
855
+ tabs: []
856
+ },
857
+ closedTabId: resolvedTabId
858
+ };
859
+ }
860
+ const tabs = await this.readLooseTrackedTabs(remainingTabIds);
861
+ const nextPrimaryTabId = ensured.binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : ensured.binding.primaryTabId;
862
+ const nextActiveTabId = ensured.binding.activeTabId === resolvedTabId ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null : ensured.binding.activeTabId;
863
+ const nextState = {
864
+ id: ensured.binding.id,
865
+ label: ensured.binding.label,
866
+ color: ensured.binding.color,
867
+ windowId: tabs[0]?.windowId ?? ensured.binding.windowId,
868
+ groupId: tabs[0]?.groupId ?? ensured.binding.groupId,
869
+ tabIds: tabs.map((candidate) => candidate.id),
870
+ activeTabId: nextActiveTabId,
871
+ primaryTabId: nextPrimaryTabId
872
+ };
873
+ await this.storage.save(nextState);
874
+ return {
875
+ binding: {
876
+ ...nextState,
877
+ tabs
878
+ },
879
+ closedTabId: resolvedTabId
880
+ };
881
+ }
807
882
  async reset(options = {}) {
808
883
  const bindingId = this.normalizeBindingId(options.bindingId);
809
884
  await this.close(bindingId);
@@ -819,10 +894,11 @@
819
894
  return { ok: true };
820
895
  }
821
896
  await this.storage.delete(bindingId);
822
- if (state.windowId !== null) {
823
- const existingWindow = await this.browser.getWindow(state.windowId);
824
- if (existingWindow) {
825
- await this.browser.closeWindow(state.windowId);
897
+ const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
898
+ for (const trackedTab of trackedTabs) {
899
+ try {
900
+ await this.browser.closeTab(trackedTab.id);
901
+ } catch {
826
902
  }
827
903
  }
828
904
  return { ok: true };
@@ -873,10 +949,10 @@
873
949
  }
874
950
  return candidate;
875
951
  }
876
- normalizeState(state, bindingId) {
952
+ normalizeState(state, bindingId, label) {
877
953
  return {
878
954
  id: bindingId,
879
- label: state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
955
+ label: label?.trim() ? label.trim() : state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
880
956
  color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
881
957
  windowId: state?.windowId ?? null,
882
958
  groupId: state?.groupId ?? null,
@@ -1043,6 +1119,10 @@
1043
1119
  state.tabIds = recreatedTabs.map((tab) => tab.id);
1044
1120
  state.primaryTabId = nextPrimaryTabId;
1045
1121
  state.activeTabId = nextActiveTabId;
1122
+ await this.storage.save({
1123
+ ...state,
1124
+ tabIds: [...state.tabIds]
1125
+ });
1046
1126
  for (const bindingTab of ownership.bindingTabs) {
1047
1127
  await this.browser.closeTab(bindingTab.id);
1048
1128
  }
@@ -1257,6 +1337,7 @@
1257
1337
  var lastError = null;
1258
1338
  var manualDisconnect = false;
1259
1339
  var sessionBindingStateMutationQueue = Promise.resolve();
1340
+ var preserveHumanFocusDepth = 0;
1260
1341
  async function getConfig() {
1261
1342
  const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
1262
1343
  return {
@@ -1299,6 +1380,18 @@
1299
1380
  ws.send(JSON.stringify(payload));
1300
1381
  }
1301
1382
  }
1383
+ function sendEvent(event, data) {
1384
+ if (ws && ws.readyState === WebSocket.OPEN) {
1385
+ ws.send(
1386
+ JSON.stringify({
1387
+ type: "event",
1388
+ event,
1389
+ data,
1390
+ ts: Date.now()
1391
+ })
1392
+ );
1393
+ }
1394
+ }
1302
1395
  function toError(code, message, data) {
1303
1396
  return { code, message, data };
1304
1397
  }
@@ -1386,6 +1479,26 @@
1386
1479
  delete stateMap[bindingId];
1387
1480
  });
1388
1481
  }
1482
+ function toSessionBindingEventBrowser(state) {
1483
+ if (!state) {
1484
+ return null;
1485
+ }
1486
+ return {
1487
+ windowId: state.windowId,
1488
+ groupId: state.groupId,
1489
+ tabIds: [...state.tabIds],
1490
+ activeTabId: state.activeTabId,
1491
+ primaryTabId: state.primaryTabId
1492
+ };
1493
+ }
1494
+ function emitSessionBindingUpdated(bindingId, reason, state, extras = {}) {
1495
+ sendEvent("sessionBinding.updated", {
1496
+ bindingId,
1497
+ reason,
1498
+ browser: toSessionBindingEventBrowser(state),
1499
+ ...extras
1500
+ });
1501
+ }
1389
1502
  var sessionBindingBrowser = {
1390
1503
  async getTab(tabId) {
1391
1504
  try {
@@ -1733,10 +1846,15 @@
1733
1846
  return action();
1734
1847
  }
1735
1848
  const focusContext = await captureFocusContext();
1849
+ preserveHumanFocusDepth += 1;
1736
1850
  try {
1737
1851
  return await action();
1738
1852
  } finally {
1739
- await restoreFocusContext(focusContext);
1853
+ try {
1854
+ await restoreFocusContext(focusContext);
1855
+ } finally {
1856
+ preserveHumanFocusDepth = Math.max(0, preserveHumanFocusDepth - 1);
1857
+ }
1740
1858
  }
1741
1859
  }
1742
1860
  function requireRpcEnvelope(method, value) {
@@ -2599,11 +2717,13 @@
2599
2717
  const result = await bindingManager.ensureBinding({
2600
2718
  bindingId: String(params.bindingId ?? ""),
2601
2719
  focus: params.focus === true,
2602
- initialUrl: typeof params.url === "string" ? params.url : void 0
2720
+ initialUrl: typeof params.url === "string" ? params.url : void 0,
2721
+ label: typeof params.label === "string" ? params.label : void 0
2603
2722
  });
2604
2723
  for (const tab of result.binding.tabs) {
2605
2724
  void ensureNetworkDebugger(tab.id).catch(() => void 0);
2606
2725
  }
2726
+ emitSessionBindingUpdated(result.binding.id, "ensure", result.binding);
2607
2727
  return {
2608
2728
  browser: result.binding,
2609
2729
  created: result.created,
@@ -2624,11 +2744,13 @@
2624
2744
  bindingId: String(params.bindingId ?? ""),
2625
2745
  url: expectedUrl,
2626
2746
  active: params.active === true,
2627
- focus: params.focus === true
2747
+ focus: params.focus === true,
2748
+ label: typeof params.label === "string" ? params.label : void 0
2628
2749
  });
2629
2750
  });
2630
2751
  const finalized = await finalizeOpenedSessionBindingTab(opened, expectedUrl);
2631
2752
  void ensureNetworkDebugger(finalized.tab.id).catch(() => void 0);
2753
+ emitSessionBindingUpdated(finalized.binding.id, "open-tab", finalized.binding);
2632
2754
  return {
2633
2755
  browser: finalized.binding,
2634
2756
  tab: finalized.tab
@@ -2651,6 +2773,7 @@
2651
2773
  case "sessionBinding.setActiveTab": {
2652
2774
  const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.bindingId ?? ""));
2653
2775
  void ensureNetworkDebugger(result.tab.id).catch(() => void 0);
2776
+ emitSessionBindingUpdated(result.binding.id, "set-active-tab", result.binding);
2654
2777
  return {
2655
2778
  browser: result.binding,
2656
2779
  tab: result.tab
@@ -2668,8 +2791,10 @@
2668
2791
  const result = await bindingManager.reset({
2669
2792
  bindingId: String(params.bindingId ?? ""),
2670
2793
  focus: params.focus === true,
2671
- initialUrl: typeof params.url === "string" ? params.url : void 0
2794
+ initialUrl: typeof params.url === "string" ? params.url : void 0,
2795
+ label: typeof params.label === "string" ? params.label : void 0
2672
2796
  });
2797
+ emitSessionBindingUpdated(result.binding.id, "reset", result.binding);
2673
2798
  return {
2674
2799
  browser: result.binding,
2675
2800
  created: result.created,
@@ -2678,8 +2803,22 @@
2678
2803
  };
2679
2804
  });
2680
2805
  }
2806
+ case "sessionBinding.closeTab": {
2807
+ const bindingId = String(params.bindingId ?? "");
2808
+ const result = await bindingManager.closeTab(bindingId, typeof params.tabId === "number" ? params.tabId : void 0);
2809
+ emitSessionBindingUpdated(bindingId, "close-tab", result.binding, {
2810
+ closedTabId: result.closedTabId
2811
+ });
2812
+ return {
2813
+ browser: result.binding,
2814
+ closedTabId: result.closedTabId
2815
+ };
2816
+ }
2681
2817
  case "sessionBinding.close": {
2682
- return await bindingManager.close(String(params.bindingId ?? ""));
2818
+ const bindingId = String(params.bindingId ?? "");
2819
+ const result = await bindingManager.close(bindingId);
2820
+ emitSessionBindingUpdated(bindingId, "close", null);
2821
+ return result;
2683
2822
  }
2684
2823
  case "page.goto": {
2685
2824
  return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
@@ -3203,7 +3342,7 @@
3203
3342
  ws?.send(JSON.stringify({
3204
3343
  type: "hello",
3205
3344
  role: "extension",
3206
- version: "0.6.1",
3345
+ version: EXTENSION_VERSION,
3207
3346
  ts: Date.now()
3208
3347
  }));
3209
3348
  });
@@ -3240,40 +3379,68 @@
3240
3379
  chrome.tabs.onRemoved.addListener((tabId) => {
3241
3380
  dropNetworkCapture(tabId);
3242
3381
  void mutateSessionBindingStateMap((stateMap) => {
3382
+ const updates = [];
3243
3383
  for (const [bindingId, state] of Object.entries(stateMap)) {
3244
3384
  if (!state.tabIds.includes(tabId)) {
3245
3385
  continue;
3246
3386
  }
3247
3387
  const nextTabIds = state.tabIds.filter((id) => id !== tabId);
3248
- stateMap[bindingId] = {
3388
+ const fallbackTabId = nextTabIds[0] ?? null;
3389
+ const nextState = {
3249
3390
  ...state,
3250
3391
  tabIds: nextTabIds,
3251
- activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
3252
- primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
3392
+ activeTabId: state.activeTabId === tabId ? fallbackTabId : state.activeTabId,
3393
+ primaryTabId: state.primaryTabId === tabId ? fallbackTabId : state.primaryTabId
3253
3394
  };
3395
+ stateMap[bindingId] = nextState;
3396
+ updates.push({ bindingId, state: nextState });
3397
+ }
3398
+ return updates;
3399
+ }).then((updates) => {
3400
+ for (const update of updates) {
3401
+ emitSessionBindingUpdated(update.bindingId, "tab-removed", update.state, {
3402
+ closedTabId: tabId
3403
+ });
3254
3404
  }
3255
3405
  });
3256
3406
  });
3257
3407
  chrome.tabs.onActivated.addListener((activeInfo) => {
3258
- void mutateSessionBindingStateMap((stateMap) => {
3259
- for (const [bindingId, state] of Object.entries(stateMap)) {
3260
- if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
3261
- continue;
3408
+ if (preserveHumanFocusDepth > 0) {
3409
+ return;
3410
+ }
3411
+ void chrome.windows.get(activeInfo.windowId).then((window2) => window2.focused === true).catch(() => false).then((windowFocused) => {
3412
+ if (!windowFocused) {
3413
+ return [];
3414
+ }
3415
+ return mutateSessionBindingStateMap((stateMap) => {
3416
+ const updates = [];
3417
+ for (const [bindingId, state] of Object.entries(stateMap)) {
3418
+ if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
3419
+ continue;
3420
+ }
3421
+ const nextState = {
3422
+ ...state,
3423
+ activeTabId: activeInfo.tabId
3424
+ };
3425
+ stateMap[bindingId] = nextState;
3426
+ updates.push({ bindingId, state: nextState });
3262
3427
  }
3263
- stateMap[bindingId] = {
3264
- ...state,
3265
- activeTabId: activeInfo.tabId
3266
- };
3428
+ return updates;
3429
+ });
3430
+ }).then((updates) => {
3431
+ for (const update of updates) {
3432
+ emitSessionBindingUpdated(update.bindingId, "tab-activated", update.state);
3267
3433
  }
3268
3434
  });
3269
3435
  });
3270
3436
  chrome.windows.onRemoved.addListener((windowId) => {
3271
3437
  void mutateSessionBindingStateMap((stateMap) => {
3438
+ const updates = [];
3272
3439
  for (const [bindingId, state] of Object.entries(stateMap)) {
3273
3440
  if (state.windowId !== windowId) {
3274
3441
  continue;
3275
3442
  }
3276
- stateMap[bindingId] = {
3443
+ const nextState = {
3277
3444
  ...state,
3278
3445
  windowId: null,
3279
3446
  groupId: null,
@@ -3281,6 +3448,13 @@
3281
3448
  activeTabId: null,
3282
3449
  primaryTabId: null
3283
3450
  };
3451
+ stateMap[bindingId] = nextState;
3452
+ updates.push({ bindingId, state: nextState });
3453
+ }
3454
+ return updates;
3455
+ }).then((updates) => {
3456
+ for (const update of updates) {
3457
+ emitSessionBindingUpdated(update.bindingId, "window-removed", update.state);
3284
3458
  }
3285
3459
  });
3286
3460
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Agent Kit",
4
- "version": "0.6.1",
4
+ "version": "0.6.4",
5
5
  "action": {
6
6
  "default_popup": "popup.html"
7
7
  },
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@flrande/bak-extension",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@flrande/bak-protocol": "0.6.2"
6
+ "@flrande/bak-protocol": "0.6.4"
7
7
  },
8
8
  "devDependencies": {
9
9
  "@types/chrome": "^0.1.14",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Agent Kit",
4
- "version": "0.6.1",
4
+ "version": "__BAK_EXTENSION_VERSION__",
5
5
  "action": {
6
6
  "default_popup": "popup.html"
7
7
  },
@@ -1,16 +1,26 @@
1
- import { cpSync, existsSync, mkdirSync } from 'node:fs';
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
4
  const root = resolve(import.meta.dirname, '..');
5
5
  const fromDir = resolve(root, 'public');
6
6
  const distDir = resolve(root, 'dist');
7
+ const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
8
+ const extensionVersion = String(packageJson.version ?? '').trim();
7
9
 
8
10
  if (!existsSync(distDir)) {
9
11
  mkdirSync(distDir, { recursive: true });
10
12
  }
11
13
 
12
- for (const file of ['manifest.json', 'popup.html']) {
13
- cpSync(resolve(fromDir, file), resolve(distDir, file), { force: true });
14
+ if (!extensionVersion) {
15
+ throw new Error('Cannot determine extension version from package.json');
14
16
  }
15
17
 
16
- console.log('Copied extension assets to dist');
18
+ const manifestTemplate = readFileSync(resolve(fromDir, 'manifest.json'), 'utf8');
19
+ writeFileSync(
20
+ resolve(distDir, 'manifest.json'),
21
+ manifestTemplate.replace('__BAK_EXTENSION_VERSION__', extensionVersion),
22
+ 'utf8'
23
+ );
24
+ cpSync(resolve(fromDir, 'popup.html'), resolve(distDir, 'popup.html'), { force: true });
25
+
26
+ console.log('Copied extension assets to dist');
package/src/background.ts CHANGED
@@ -26,14 +26,15 @@ import { isSupportedAutomationUrl } from './url-policy.js';
26
26
  import { computeReconnectDelayMs } from './reconnect.js';
27
27
  import { resolveSessionBindingStateMap, STORAGE_KEY_SESSION_BINDINGS } from './session-binding-storage.js';
28
28
  import { containsRedactionMarker } from './privacy.js';
29
- import {
30
- type SessionBindingBrowser,
31
- type SessionBindingColor,
32
- type SessionBindingRecord,
33
- type SessionBindingTab,
34
- type SessionBindingWindow,
35
- SessionBindingManager
36
- } from './session-binding.js';
29
+ import {
30
+ type SessionBindingBrowser,
31
+ type SessionBindingColor,
32
+ type SessionBindingRecord,
33
+ type SessionBindingTab,
34
+ type SessionBindingWindow,
35
+ SessionBindingManager
36
+ } from './session-binding.js';
37
+ import { EXTENSION_VERSION } from './version.js';
37
38
 
38
39
  interface CliRequest {
39
40
  id: string;
@@ -115,6 +116,7 @@ let reconnectAttempt = 0;
115
116
  let lastError: RuntimeErrorDetails | null = null;
116
117
  let manualDisconnect = false;
117
118
  let sessionBindingStateMutationQueue: Promise<void> = Promise.resolve();
119
+ let preserveHumanFocusDepth = 0;
118
120
 
119
121
  async function getConfig(): Promise<ExtensionConfig> {
120
122
  const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
@@ -157,11 +159,24 @@ function clearReconnectTimer(): void {
157
159
  nextReconnectInMs = null;
158
160
  }
159
161
 
160
- function sendResponse(payload: CliResponse): void {
161
- if (ws && ws.readyState === WebSocket.OPEN) {
162
- ws.send(JSON.stringify(payload));
163
- }
164
- }
162
+ function sendResponse(payload: CliResponse): void {
163
+ if (ws && ws.readyState === WebSocket.OPEN) {
164
+ ws.send(JSON.stringify(payload));
165
+ }
166
+ }
167
+
168
+ function sendEvent(event: string, data: Record<string, unknown>): void {
169
+ if (ws && ws.readyState === WebSocket.OPEN) {
170
+ ws.send(
171
+ JSON.stringify({
172
+ type: 'event',
173
+ event,
174
+ data,
175
+ ts: Date.now()
176
+ })
177
+ );
178
+ }
179
+ }
165
180
 
166
181
  function toError(code: string, message: string, data?: Record<string, unknown>): CliResponse['error'] {
167
182
  return { code, message, data };
@@ -264,6 +279,33 @@ async function deleteSessionBindingState(bindingId: string): Promise<void> {
264
279
  delete stateMap[bindingId];
265
280
  });
266
281
  }
282
+
283
+ function toSessionBindingEventBrowser(state: SessionBindingRecord | null): Record<string, unknown> | null {
284
+ if (!state) {
285
+ return null;
286
+ }
287
+ return {
288
+ windowId: state.windowId,
289
+ groupId: state.groupId,
290
+ tabIds: [...state.tabIds],
291
+ activeTabId: state.activeTabId,
292
+ primaryTabId: state.primaryTabId
293
+ };
294
+ }
295
+
296
+ function emitSessionBindingUpdated(
297
+ bindingId: string,
298
+ reason: string,
299
+ state: SessionBindingRecord | null,
300
+ extras: Record<string, unknown> = {}
301
+ ): void {
302
+ sendEvent('sessionBinding.updated', {
303
+ bindingId,
304
+ reason,
305
+ browser: toSessionBindingEventBrowser(state),
306
+ ...extras
307
+ });
308
+ }
267
309
 
268
310
  const sessionBindingBrowser: SessionBindingBrowser = {
269
311
  async getTab(tabId) {
@@ -672,18 +714,23 @@ async function restoreFocusContext(context: FocusContext): Promise<void> {
672
714
  }
673
715
  }
674
716
 
675
- async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
676
- if (!enabled) {
677
- return action();
678
- }
679
-
680
- const focusContext = await captureFocusContext();
681
- try {
682
- return await action();
683
- } finally {
684
- await restoreFocusContext(focusContext);
685
- }
686
- }
717
+ async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
718
+ if (!enabled) {
719
+ return action();
720
+ }
721
+
722
+ const focusContext = await captureFocusContext();
723
+ preserveHumanFocusDepth += 1;
724
+ try {
725
+ return await action();
726
+ } finally {
727
+ try {
728
+ await restoreFocusContext(focusContext);
729
+ } finally {
730
+ preserveHumanFocusDepth = Math.max(0, preserveHumanFocusDepth - 1);
731
+ }
732
+ }
733
+ }
687
734
 
688
735
  function requireRpcEnvelope(
689
736
  method: string,
@@ -1685,11 +1732,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1685
1732
  const result = await bindingManager.ensureBinding({
1686
1733
  bindingId: String(params.bindingId ?? ''),
1687
1734
  focus: params.focus === true,
1688
- initialUrl: typeof params.url === 'string' ? params.url : undefined
1735
+ initialUrl: typeof params.url === 'string' ? params.url : undefined,
1736
+ label: typeof params.label === 'string' ? params.label : undefined
1689
1737
  });
1690
1738
  for (const tab of result.binding.tabs) {
1691
1739
  void ensureNetworkDebugger(tab.id).catch(() => undefined);
1692
1740
  }
1741
+ emitSessionBindingUpdated(result.binding.id, 'ensure', result.binding);
1693
1742
  return {
1694
1743
  browser: result.binding,
1695
1744
  created: result.created,
@@ -1710,11 +1759,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1710
1759
  bindingId: String(params.bindingId ?? ''),
1711
1760
  url: expectedUrl,
1712
1761
  active: params.active === true,
1713
- focus: params.focus === true
1762
+ focus: params.focus === true,
1763
+ label: typeof params.label === 'string' ? params.label : undefined
1714
1764
  });
1715
1765
  });
1716
1766
  const finalized = await finalizeOpenedSessionBindingTab(opened, expectedUrl);
1717
1767
  void ensureNetworkDebugger(finalized.tab.id).catch(() => undefined);
1768
+ emitSessionBindingUpdated(finalized.binding.id, 'open-tab', finalized.binding);
1718
1769
  return {
1719
1770
  browser: finalized.binding,
1720
1771
  tab: finalized.tab
@@ -1737,6 +1788,7 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1737
1788
  case 'sessionBinding.setActiveTab': {
1738
1789
  const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.bindingId ?? ''));
1739
1790
  void ensureNetworkDebugger(result.tab.id).catch(() => undefined);
1791
+ emitSessionBindingUpdated(result.binding.id, 'set-active-tab', result.binding);
1740
1792
  return {
1741
1793
  browser: result.binding,
1742
1794
  tab: result.tab
@@ -1754,8 +1806,10 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1754
1806
  const result = await bindingManager.reset({
1755
1807
  bindingId: String(params.bindingId ?? ''),
1756
1808
  focus: params.focus === true,
1757
- initialUrl: typeof params.url === 'string' ? params.url : undefined
1809
+ initialUrl: typeof params.url === 'string' ? params.url : undefined,
1810
+ label: typeof params.label === 'string' ? params.label : undefined
1758
1811
  });
1812
+ emitSessionBindingUpdated(result.binding.id, 'reset', result.binding);
1759
1813
  return {
1760
1814
  browser: result.binding,
1761
1815
  created: result.created,
@@ -1764,8 +1818,22 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1764
1818
  };
1765
1819
  });
1766
1820
  }
1821
+ case 'sessionBinding.closeTab': {
1822
+ const bindingId = String(params.bindingId ?? '');
1823
+ const result = await bindingManager.closeTab(bindingId, typeof params.tabId === 'number' ? params.tabId : undefined);
1824
+ emitSessionBindingUpdated(bindingId, 'close-tab', result.binding, {
1825
+ closedTabId: result.closedTabId
1826
+ });
1827
+ return {
1828
+ browser: result.binding,
1829
+ closedTabId: result.closedTabId
1830
+ };
1831
+ }
1767
1832
  case 'sessionBinding.close': {
1768
- return await bindingManager.close(String(params.bindingId ?? ''));
1833
+ const bindingId = String(params.bindingId ?? '');
1834
+ const result = await bindingManager.close(bindingId);
1835
+ emitSessionBindingUpdated(bindingId, 'close', null);
1836
+ return result;
1769
1837
  }
1770
1838
  case 'page.goto': {
1771
1839
  return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
@@ -2305,13 +2373,13 @@ async function connectWebSocket(): Promise<void> {
2305
2373
  manualDisconnect = false;
2306
2374
  reconnectAttempt = 0;
2307
2375
  lastError = null;
2308
- ws?.send(JSON.stringify({
2309
- type: 'hello',
2310
- role: 'extension',
2311
- version: '0.6.1',
2312
- ts: Date.now()
2313
- }));
2314
- });
2376
+ ws?.send(JSON.stringify({
2377
+ type: 'hello',
2378
+ role: 'extension',
2379
+ version: EXTENSION_VERSION,
2380
+ ts: Date.now()
2381
+ }));
2382
+ });
2315
2383
 
2316
2384
  ws.addEventListener('message', (event) => {
2317
2385
  try {
@@ -2351,42 +2419,75 @@ async function connectWebSocket(): Promise<void> {
2351
2419
  chrome.tabs.onRemoved.addListener((tabId) => {
2352
2420
  dropNetworkCapture(tabId);
2353
2421
  void mutateSessionBindingStateMap((stateMap) => {
2422
+ const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
2354
2423
  for (const [bindingId, state] of Object.entries(stateMap)) {
2355
2424
  if (!state.tabIds.includes(tabId)) {
2356
2425
  continue;
2357
2426
  }
2358
2427
  const nextTabIds = state.tabIds.filter((id) => id !== tabId);
2359
- stateMap[bindingId] = {
2428
+ const fallbackTabId = nextTabIds[0] ?? null;
2429
+ const nextState: SessionBindingRecord = {
2360
2430
  ...state,
2361
2431
  tabIds: nextTabIds,
2362
- activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
2363
- primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
2432
+ activeTabId: state.activeTabId === tabId ? fallbackTabId : state.activeTabId,
2433
+ primaryTabId: state.primaryTabId === tabId ? fallbackTabId : state.primaryTabId
2364
2434
  };
2435
+ stateMap[bindingId] = nextState;
2436
+ updates.push({ bindingId, state: nextState });
2437
+ }
2438
+ return updates;
2439
+ }).then((updates) => {
2440
+ for (const update of updates) {
2441
+ emitSessionBindingUpdated(update.bindingId, 'tab-removed', update.state, {
2442
+ closedTabId: tabId
2443
+ });
2365
2444
  }
2366
2445
  });
2367
2446
  });
2368
2447
 
2369
2448
  chrome.tabs.onActivated.addListener((activeInfo) => {
2370
- void mutateSessionBindingStateMap((stateMap) => {
2371
- for (const [bindingId, state] of Object.entries(stateMap)) {
2372
- if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
2373
- continue;
2449
+ if (preserveHumanFocusDepth > 0) {
2450
+ return;
2451
+ }
2452
+ void chrome.windows
2453
+ .get(activeInfo.windowId)
2454
+ .then((window) => window.focused === true)
2455
+ .catch(() => false)
2456
+ .then((windowFocused) => {
2457
+ if (!windowFocused) {
2458
+ return [] as Array<{ bindingId: string; state: SessionBindingRecord }>;
2374
2459
  }
2375
- stateMap[bindingId] = {
2376
- ...state,
2377
- activeTabId: activeInfo.tabId
2378
- };
2379
- }
2380
- });
2460
+ return mutateSessionBindingStateMap((stateMap) => {
2461
+ const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
2462
+ for (const [bindingId, state] of Object.entries(stateMap)) {
2463
+ if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
2464
+ continue;
2465
+ }
2466
+ const nextState: SessionBindingRecord = {
2467
+ ...state,
2468
+ activeTabId: activeInfo.tabId
2469
+ };
2470
+ stateMap[bindingId] = nextState;
2471
+ updates.push({ bindingId, state: nextState });
2472
+ }
2473
+ return updates;
2474
+ });
2475
+ })
2476
+ .then((updates) => {
2477
+ for (const update of updates) {
2478
+ emitSessionBindingUpdated(update.bindingId, 'tab-activated', update.state);
2479
+ }
2480
+ });
2381
2481
  });
2382
2482
 
2383
2483
  chrome.windows.onRemoved.addListener((windowId) => {
2384
2484
  void mutateSessionBindingStateMap((stateMap) => {
2485
+ const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
2385
2486
  for (const [bindingId, state] of Object.entries(stateMap)) {
2386
2487
  if (state.windowId !== windowId) {
2387
2488
  continue;
2388
2489
  }
2389
- stateMap[bindingId] = {
2490
+ const nextState: SessionBindingRecord = {
2390
2491
  ...state,
2391
2492
  windowId: null,
2392
2493
  groupId: null,
@@ -2394,6 +2495,13 @@ chrome.windows.onRemoved.addListener((windowId) => {
2394
2495
  activeTabId: null,
2395
2496
  primaryTabId: null
2396
2497
  };
2498
+ stateMap[bindingId] = nextState;
2499
+ updates.push({ bindingId, state: nextState });
2500
+ }
2501
+ return updates;
2502
+ }).then((updates) => {
2503
+ for (const update of updates) {
2504
+ emitSessionBindingUpdated(update.bindingId, 'window-removed', update.state);
2397
2505
  }
2398
2506
  });
2399
2507
  });
@@ -1,5 +1,6 @@
1
1
  import type { NetworkEntry } from '@flrande/bak-protocol';
2
2
  import { redactHeaderMap, redactTransportText } from './privacy.js';
3
+ import { EXTENSION_VERSION } from './version.js';
3
4
 
4
5
  const DEBUGGER_VERSION = '1.3';
5
6
  const MAX_ENTRIES = 1000;
@@ -446,7 +447,7 @@ export function exportHar(tabId: number, limit = MAX_ENTRIES): Record<string, un
446
447
  version: '1.2',
447
448
  creator: {
448
449
  name: 'bak',
449
- version: '0.6.1'
450
+ version: EXTENSION_VERSION
450
451
  },
451
452
  entries: entries.map((entry) => ({
452
453
  startedDateTime: new Date(entry.startedAt ?? entry.ts).toISOString(),
@@ -88,18 +88,20 @@ interface SessionBindingWindowOwnership {
88
88
  foreignTabs: SessionBindingTab[];
89
89
  }
90
90
 
91
- export interface SessionBindingEnsureOptions {
92
- bindingId?: string;
93
- focus?: boolean;
94
- initialUrl?: string;
95
- }
96
-
97
- export interface SessionBindingOpenTabOptions {
98
- bindingId?: string;
99
- url?: string;
100
- active?: boolean;
101
- focus?: boolean;
102
- }
91
+ export interface SessionBindingEnsureOptions {
92
+ bindingId?: string;
93
+ focus?: boolean;
94
+ initialUrl?: string;
95
+ label?: string;
96
+ }
97
+
98
+ export interface SessionBindingOpenTabOptions {
99
+ bindingId?: string;
100
+ url?: string;
101
+ active?: boolean;
102
+ focus?: boolean;
103
+ label?: string;
104
+ }
103
105
 
104
106
  export interface SessionBindingResolveTargetOptions {
105
107
  tabId?: number;
@@ -120,13 +122,13 @@ class SessionBindingManager {
120
122
  return this.inspectBinding(bindingId);
121
123
  }
122
124
 
123
- async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
124
- const bindingId = this.normalizeBindingId(options.bindingId);
125
- const repairActions: string[] = [];
126
- const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
127
- const persisted = await this.storage.load(bindingId);
128
- const created = !persisted;
129
- let state = this.normalizeState(persisted, bindingId);
125
+ async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
126
+ const bindingId = this.normalizeBindingId(options.bindingId);
127
+ const repairActions: string[] = [];
128
+ const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
129
+ const persisted = await this.storage.load(bindingId);
130
+ const created = !persisted;
131
+ let state = this.normalizeState(persisted, bindingId, options.label);
130
132
 
131
133
  const originalWindowId = state.windowId;
132
134
  let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
@@ -266,14 +268,15 @@ class SessionBindingManager {
266
268
  };
267
269
  }
268
270
 
269
- async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
270
- const bindingId = this.normalizeBindingId(options.bindingId);
271
- const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
272
- const ensured = await this.ensureBinding({
273
- bindingId,
274
- focus: false,
275
- initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL
276
- });
271
+ async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
272
+ const bindingId = this.normalizeBindingId(options.bindingId);
273
+ const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
274
+ const ensured = await this.ensureBinding({
275
+ bindingId,
276
+ focus: false,
277
+ initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL,
278
+ label: options.label
279
+ });
277
280
  let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
278
281
  if (state.windowId !== null && state.tabs.length === 0) {
279
282
  const rebound = await this.rebindBindingWindow(state);
@@ -309,11 +312,12 @@ class SessionBindingManager {
309
312
  if (!this.isMissingWindowError(error)) {
310
313
  throw error;
311
314
  }
312
- const repaired = await this.ensureBinding({
313
- bindingId,
314
- focus: false,
315
- initialUrl: desiredUrl
316
- });
315
+ const repaired = await this.ensureBinding({
316
+ bindingId,
317
+ focus: false,
318
+ initialUrl: desiredUrl,
319
+ label: options.label
320
+ });
317
321
  state = { ...repaired.binding };
318
322
  reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
319
323
  createdTab = reusablePrimaryTab
@@ -421,44 +425,107 @@ class SessionBindingManager {
421
425
  };
422
426
  }
423
427
 
424
- async focus(bindingId: string): Promise<{ ok: true; binding: SessionBindingInfo }> {
425
- const ensured = await this.ensureBinding({ bindingId, focus: false });
426
- if (ensured.binding.activeTabId !== null) {
427
- await this.browser.updateTab(ensured.binding.activeTabId, { active: true });
428
- }
428
+ async focus(bindingId: string): Promise<{ ok: true; binding: SessionBindingInfo }> {
429
+ const ensured = await this.ensureBinding({ bindingId, focus: false });
430
+ if (ensured.binding.activeTabId !== null) {
431
+ await this.browser.updateTab(ensured.binding.activeTabId, { active: true });
432
+ }
429
433
  if (ensured.binding.windowId !== null) {
430
434
  await this.browser.updateWindow(ensured.binding.windowId, { focused: true });
431
435
  }
432
- const refreshed = await this.ensureBinding({ bindingId, focus: false });
433
- return { ok: true, binding: refreshed.binding };
434
- }
435
-
436
- async reset(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
437
- const bindingId = this.normalizeBindingId(options.bindingId);
438
- await this.close(bindingId);
436
+ const refreshed = await this.ensureBinding({ bindingId, focus: false });
437
+ return { ok: true, binding: refreshed.binding };
438
+ }
439
+
440
+ async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
441
+ const ensured = await this.ensureBinding({ bindingId, focus: false });
442
+ const resolvedTabId =
443
+ typeof tabId === 'number'
444
+ ? tabId
445
+ : ensured.binding.activeTabId ?? ensured.binding.primaryTabId ?? ensured.binding.tabs[0]?.id;
446
+ if (typeof resolvedTabId !== 'number' || !ensured.binding.tabIds.includes(resolvedTabId)) {
447
+ throw new Error(`Tab ${tabId ?? 'active'} does not belong to binding ${bindingId}`);
448
+ }
449
+
450
+ await this.browser.closeTab(resolvedTabId);
451
+ const remainingTabIds = ensured.binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
452
+
453
+ if (remainingTabIds.length === 0) {
454
+ const emptied: SessionBindingRecord = {
455
+ id: ensured.binding.id,
456
+ label: ensured.binding.label,
457
+ color: ensured.binding.color,
458
+ windowId: null,
459
+ groupId: null,
460
+ tabIds: [],
461
+ activeTabId: null,
462
+ primaryTabId: null
463
+ };
464
+ await this.storage.save(emptied);
465
+ return {
466
+ binding: {
467
+ ...emptied,
468
+ tabs: []
469
+ },
470
+ closedTabId: resolvedTabId
471
+ };
472
+ }
473
+
474
+ const tabs = await this.readLooseTrackedTabs(remainingTabIds);
475
+ const nextPrimaryTabId =
476
+ ensured.binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : ensured.binding.primaryTabId;
477
+ const nextActiveTabId =
478
+ ensured.binding.activeTabId === resolvedTabId
479
+ ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
480
+ : ensured.binding.activeTabId;
481
+ const nextState: SessionBindingRecord = {
482
+ id: ensured.binding.id,
483
+ label: ensured.binding.label,
484
+ color: ensured.binding.color,
485
+ windowId: tabs[0]?.windowId ?? ensured.binding.windowId,
486
+ groupId: tabs[0]?.groupId ?? ensured.binding.groupId,
487
+ tabIds: tabs.map((candidate) => candidate.id),
488
+ activeTabId: nextActiveTabId,
489
+ primaryTabId: nextPrimaryTabId
490
+ };
491
+ await this.storage.save(nextState);
492
+ return {
493
+ binding: {
494
+ ...nextState,
495
+ tabs
496
+ },
497
+ closedTabId: resolvedTabId
498
+ };
499
+ }
500
+
501
+ async reset(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
502
+ const bindingId = this.normalizeBindingId(options.bindingId);
503
+ await this.close(bindingId);
439
504
  return this.ensureBinding({
440
505
  ...options,
441
506
  bindingId
442
507
  });
443
508
  }
444
509
 
445
- async close(bindingId: string): Promise<{ ok: true }> {
446
- const state = await this.loadBindingRecord(bindingId);
447
- if (!state) {
448
- await this.storage.delete(bindingId);
449
- return { ok: true };
450
- }
451
- // Clear persisted state before closing the window so tab/window removal
452
- // listeners cannot race and resurrect an empty binding record.
453
- await this.storage.delete(bindingId);
454
- if (state.windowId !== null) {
455
- const existingWindow = await this.browser.getWindow(state.windowId);
456
- if (existingWindow) {
457
- await this.browser.closeWindow(state.windowId);
458
- }
459
- }
460
- return { ok: true };
461
- }
510
+ async close(bindingId: string): Promise<{ ok: true }> {
511
+ const state = await this.loadBindingRecord(bindingId);
512
+ if (!state) {
513
+ await this.storage.delete(bindingId);
514
+ return { ok: true };
515
+ }
516
+ // Clear persisted state before closing the window so tab/window removal
517
+ // listeners cannot race and resurrect an empty binding record.
518
+ await this.storage.delete(bindingId);
519
+ const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
520
+ for (const trackedTab of trackedTabs) {
521
+ try {
522
+ await this.browser.closeTab(trackedTab.id);
523
+ } catch {
524
+ // Ignore tabs that were already removed before explicit close.
525
+ }
526
+ }
527
+ return { ok: true };
528
+ }
462
529
 
463
530
  async resolveTarget(options: SessionBindingResolveTargetOptions = {}): Promise<SessionBindingTargetResolution> {
464
531
  if (typeof options.tabId === 'number') {
@@ -511,13 +578,13 @@ class SessionBindingManager {
511
578
  return candidate;
512
579
  }
513
580
 
514
- private normalizeState(state: SessionBindingRecord | null, bindingId: string): SessionBindingRecord {
515
- return {
516
- id: bindingId,
517
- label: state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
518
- color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
519
- windowId: state?.windowId ?? null,
520
- groupId: state?.groupId ?? null,
581
+ private normalizeState(state: SessionBindingRecord | null, bindingId: string, label?: string): SessionBindingRecord {
582
+ return {
583
+ id: bindingId,
584
+ label: label?.trim() ? label.trim() : state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
585
+ color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
586
+ windowId: state?.windowId ?? null,
587
+ groupId: state?.groupId ?? null,
521
588
  tabIds: state?.tabIds ?? [],
522
589
  activeTabId: state?.activeTabId ?? null,
523
590
  primaryTabId: state?.primaryTabId ?? null
@@ -710,12 +777,16 @@ class SessionBindingManager {
710
777
  await this.browser.updateTab(nextActiveTabId, { active: true });
711
778
  }
712
779
 
713
- state.windowId = window.id;
714
- state.groupId = null;
715
- state.tabIds = recreatedTabs.map((tab) => tab.id);
716
- state.primaryTabId = nextPrimaryTabId;
717
- state.activeTabId = nextActiveTabId;
718
-
780
+ state.windowId = window.id;
781
+ state.groupId = null;
782
+ state.tabIds = recreatedTabs.map((tab) => tab.id);
783
+ state.primaryTabId = nextPrimaryTabId;
784
+ state.activeTabId = nextActiveTabId;
785
+ await this.storage.save({
786
+ ...state,
787
+ tabIds: [...state.tabIds]
788
+ });
789
+
719
790
  for (const bindingTab of ownership.bindingTabs) {
720
791
  await this.browser.closeTab(bindingTab.id);
721
792
  }
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ import packageJson from '../package.json' with { type: 'json' };
2
+
3
+ export const EXTENSION_VERSION = packageJson.version;