@flrande/bak-extension 0.6.3 → 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.
- package/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +178 -27
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/background.ts +142 -35
- package/src/session-binding.ts +145 -74
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-12T15:53:45.600Z
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
// package.json
|
|
63
63
|
var package_default = {
|
|
64
64
|
name: "@flrande/bak-extension",
|
|
65
|
-
version: "0.6.
|
|
65
|
+
version: "0.6.4",
|
|
66
66
|
type: "module",
|
|
67
67
|
scripts: {
|
|
68
68
|
build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
|
|
@@ -548,7 +548,7 @@
|
|
|
548
548
|
const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
|
|
549
549
|
const persisted = await this.storage.load(bindingId);
|
|
550
550
|
const created = !persisted;
|
|
551
|
-
let state = this.normalizeState(persisted, bindingId);
|
|
551
|
+
let state = this.normalizeState(persisted, bindingId, options.label);
|
|
552
552
|
const originalWindowId = state.windowId;
|
|
553
553
|
let window2 = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
554
554
|
let tabs = [];
|
|
@@ -681,7 +681,8 @@
|
|
|
681
681
|
const ensured = await this.ensureBinding({
|
|
682
682
|
bindingId,
|
|
683
683
|
focus: false,
|
|
684
|
-
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
|
|
685
686
|
});
|
|
686
687
|
let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
|
|
687
688
|
if (state.windowId !== null && state.tabs.length === 0) {
|
|
@@ -715,7 +716,8 @@
|
|
|
715
716
|
const repaired = await this.ensureBinding({
|
|
716
717
|
bindingId,
|
|
717
718
|
focus: false,
|
|
718
|
-
initialUrl: desiredUrl
|
|
719
|
+
initialUrl: desiredUrl,
|
|
720
|
+
label: options.label
|
|
719
721
|
});
|
|
720
722
|
state = { ...repaired.binding };
|
|
721
723
|
reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
|
|
@@ -827,6 +829,56 @@
|
|
|
827
829
|
const refreshed = await this.ensureBinding({ bindingId, focus: false });
|
|
828
830
|
return { ok: true, binding: refreshed.binding };
|
|
829
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
|
+
}
|
|
830
882
|
async reset(options = {}) {
|
|
831
883
|
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
832
884
|
await this.close(bindingId);
|
|
@@ -842,10 +894,11 @@
|
|
|
842
894
|
return { ok: true };
|
|
843
895
|
}
|
|
844
896
|
await this.storage.delete(bindingId);
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
await this.browser.
|
|
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 {
|
|
849
902
|
}
|
|
850
903
|
}
|
|
851
904
|
return { ok: true };
|
|
@@ -896,10 +949,10 @@
|
|
|
896
949
|
}
|
|
897
950
|
return candidate;
|
|
898
951
|
}
|
|
899
|
-
normalizeState(state, bindingId) {
|
|
952
|
+
normalizeState(state, bindingId, label) {
|
|
900
953
|
return {
|
|
901
954
|
id: bindingId,
|
|
902
|
-
label: state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
|
|
955
|
+
label: label?.trim() ? label.trim() : state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
|
|
903
956
|
color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
|
|
904
957
|
windowId: state?.windowId ?? null,
|
|
905
958
|
groupId: state?.groupId ?? null,
|
|
@@ -1066,6 +1119,10 @@
|
|
|
1066
1119
|
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
1067
1120
|
state.primaryTabId = nextPrimaryTabId;
|
|
1068
1121
|
state.activeTabId = nextActiveTabId;
|
|
1122
|
+
await this.storage.save({
|
|
1123
|
+
...state,
|
|
1124
|
+
tabIds: [...state.tabIds]
|
|
1125
|
+
});
|
|
1069
1126
|
for (const bindingTab of ownership.bindingTabs) {
|
|
1070
1127
|
await this.browser.closeTab(bindingTab.id);
|
|
1071
1128
|
}
|
|
@@ -1280,6 +1337,7 @@
|
|
|
1280
1337
|
var lastError = null;
|
|
1281
1338
|
var manualDisconnect = false;
|
|
1282
1339
|
var sessionBindingStateMutationQueue = Promise.resolve();
|
|
1340
|
+
var preserveHumanFocusDepth = 0;
|
|
1283
1341
|
async function getConfig() {
|
|
1284
1342
|
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
1285
1343
|
return {
|
|
@@ -1322,6 +1380,18 @@
|
|
|
1322
1380
|
ws.send(JSON.stringify(payload));
|
|
1323
1381
|
}
|
|
1324
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
|
+
}
|
|
1325
1395
|
function toError(code, message, data) {
|
|
1326
1396
|
return { code, message, data };
|
|
1327
1397
|
}
|
|
@@ -1409,6 +1479,26 @@
|
|
|
1409
1479
|
delete stateMap[bindingId];
|
|
1410
1480
|
});
|
|
1411
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
|
+
}
|
|
1412
1502
|
var sessionBindingBrowser = {
|
|
1413
1503
|
async getTab(tabId) {
|
|
1414
1504
|
try {
|
|
@@ -1756,10 +1846,15 @@
|
|
|
1756
1846
|
return action();
|
|
1757
1847
|
}
|
|
1758
1848
|
const focusContext = await captureFocusContext();
|
|
1849
|
+
preserveHumanFocusDepth += 1;
|
|
1759
1850
|
try {
|
|
1760
1851
|
return await action();
|
|
1761
1852
|
} finally {
|
|
1762
|
-
|
|
1853
|
+
try {
|
|
1854
|
+
await restoreFocusContext(focusContext);
|
|
1855
|
+
} finally {
|
|
1856
|
+
preserveHumanFocusDepth = Math.max(0, preserveHumanFocusDepth - 1);
|
|
1857
|
+
}
|
|
1763
1858
|
}
|
|
1764
1859
|
}
|
|
1765
1860
|
function requireRpcEnvelope(method, value) {
|
|
@@ -2622,11 +2717,13 @@
|
|
|
2622
2717
|
const result = await bindingManager.ensureBinding({
|
|
2623
2718
|
bindingId: String(params.bindingId ?? ""),
|
|
2624
2719
|
focus: params.focus === true,
|
|
2625
|
-
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
|
|
2626
2722
|
});
|
|
2627
2723
|
for (const tab of result.binding.tabs) {
|
|
2628
2724
|
void ensureNetworkDebugger(tab.id).catch(() => void 0);
|
|
2629
2725
|
}
|
|
2726
|
+
emitSessionBindingUpdated(result.binding.id, "ensure", result.binding);
|
|
2630
2727
|
return {
|
|
2631
2728
|
browser: result.binding,
|
|
2632
2729
|
created: result.created,
|
|
@@ -2647,11 +2744,13 @@
|
|
|
2647
2744
|
bindingId: String(params.bindingId ?? ""),
|
|
2648
2745
|
url: expectedUrl,
|
|
2649
2746
|
active: params.active === true,
|
|
2650
|
-
focus: params.focus === true
|
|
2747
|
+
focus: params.focus === true,
|
|
2748
|
+
label: typeof params.label === "string" ? params.label : void 0
|
|
2651
2749
|
});
|
|
2652
2750
|
});
|
|
2653
2751
|
const finalized = await finalizeOpenedSessionBindingTab(opened, expectedUrl);
|
|
2654
2752
|
void ensureNetworkDebugger(finalized.tab.id).catch(() => void 0);
|
|
2753
|
+
emitSessionBindingUpdated(finalized.binding.id, "open-tab", finalized.binding);
|
|
2655
2754
|
return {
|
|
2656
2755
|
browser: finalized.binding,
|
|
2657
2756
|
tab: finalized.tab
|
|
@@ -2674,6 +2773,7 @@
|
|
|
2674
2773
|
case "sessionBinding.setActiveTab": {
|
|
2675
2774
|
const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.bindingId ?? ""));
|
|
2676
2775
|
void ensureNetworkDebugger(result.tab.id).catch(() => void 0);
|
|
2776
|
+
emitSessionBindingUpdated(result.binding.id, "set-active-tab", result.binding);
|
|
2677
2777
|
return {
|
|
2678
2778
|
browser: result.binding,
|
|
2679
2779
|
tab: result.tab
|
|
@@ -2691,8 +2791,10 @@
|
|
|
2691
2791
|
const result = await bindingManager.reset({
|
|
2692
2792
|
bindingId: String(params.bindingId ?? ""),
|
|
2693
2793
|
focus: params.focus === true,
|
|
2694
|
-
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
|
|
2695
2796
|
});
|
|
2797
|
+
emitSessionBindingUpdated(result.binding.id, "reset", result.binding);
|
|
2696
2798
|
return {
|
|
2697
2799
|
browser: result.binding,
|
|
2698
2800
|
created: result.created,
|
|
@@ -2701,8 +2803,22 @@
|
|
|
2701
2803
|
};
|
|
2702
2804
|
});
|
|
2703
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
|
+
}
|
|
2704
2817
|
case "sessionBinding.close": {
|
|
2705
|
-
|
|
2818
|
+
const bindingId = String(params.bindingId ?? "");
|
|
2819
|
+
const result = await bindingManager.close(bindingId);
|
|
2820
|
+
emitSessionBindingUpdated(bindingId, "close", null);
|
|
2821
|
+
return result;
|
|
2706
2822
|
}
|
|
2707
2823
|
case "page.goto": {
|
|
2708
2824
|
return await preserveHumanFocus(typeof target.tabId !== "number", async () => {
|
|
@@ -3263,40 +3379,68 @@
|
|
|
3263
3379
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
3264
3380
|
dropNetworkCapture(tabId);
|
|
3265
3381
|
void mutateSessionBindingStateMap((stateMap) => {
|
|
3382
|
+
const updates = [];
|
|
3266
3383
|
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
3267
3384
|
if (!state.tabIds.includes(tabId)) {
|
|
3268
3385
|
continue;
|
|
3269
3386
|
}
|
|
3270
3387
|
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
3271
|
-
|
|
3388
|
+
const fallbackTabId = nextTabIds[0] ?? null;
|
|
3389
|
+
const nextState = {
|
|
3272
3390
|
...state,
|
|
3273
3391
|
tabIds: nextTabIds,
|
|
3274
|
-
activeTabId: state.activeTabId === tabId ?
|
|
3275
|
-
primaryTabId: state.primaryTabId === tabId ?
|
|
3392
|
+
activeTabId: state.activeTabId === tabId ? fallbackTabId : state.activeTabId,
|
|
3393
|
+
primaryTabId: state.primaryTabId === tabId ? fallbackTabId : state.primaryTabId
|
|
3276
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
|
+
});
|
|
3277
3404
|
}
|
|
3278
3405
|
});
|
|
3279
3406
|
});
|
|
3280
3407
|
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
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 });
|
|
3285
3427
|
}
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3428
|
+
return updates;
|
|
3429
|
+
});
|
|
3430
|
+
}).then((updates) => {
|
|
3431
|
+
for (const update of updates) {
|
|
3432
|
+
emitSessionBindingUpdated(update.bindingId, "tab-activated", update.state);
|
|
3290
3433
|
}
|
|
3291
3434
|
});
|
|
3292
3435
|
});
|
|
3293
3436
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
3294
3437
|
void mutateSessionBindingStateMap((stateMap) => {
|
|
3438
|
+
const updates = [];
|
|
3295
3439
|
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
3296
3440
|
if (state.windowId !== windowId) {
|
|
3297
3441
|
continue;
|
|
3298
3442
|
}
|
|
3299
|
-
|
|
3443
|
+
const nextState = {
|
|
3300
3444
|
...state,
|
|
3301
3445
|
windowId: null,
|
|
3302
3446
|
groupId: null,
|
|
@@ -3304,6 +3448,13 @@
|
|
|
3304
3448
|
activeTabId: null,
|
|
3305
3449
|
primaryTabId: null
|
|
3306
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);
|
|
3307
3458
|
}
|
|
3308
3459
|
});
|
|
3309
3460
|
});
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
package/src/background.ts
CHANGED
|
@@ -116,6 +116,7 @@ let reconnectAttempt = 0;
|
|
|
116
116
|
let lastError: RuntimeErrorDetails | null = null;
|
|
117
117
|
let manualDisconnect = false;
|
|
118
118
|
let sessionBindingStateMutationQueue: Promise<void> = Promise.resolve();
|
|
119
|
+
let preserveHumanFocusDepth = 0;
|
|
119
120
|
|
|
120
121
|
async function getConfig(): Promise<ExtensionConfig> {
|
|
121
122
|
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
@@ -158,11 +159,24 @@ function clearReconnectTimer(): void {
|
|
|
158
159
|
nextReconnectInMs = null;
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
function sendResponse(payload: CliResponse): void {
|
|
162
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
163
|
-
ws.send(JSON.stringify(payload));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
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
|
+
}
|
|
166
180
|
|
|
167
181
|
function toError(code: string, message: string, data?: Record<string, unknown>): CliResponse['error'] {
|
|
168
182
|
return { code, message, data };
|
|
@@ -265,6 +279,33 @@ async function deleteSessionBindingState(bindingId: string): Promise<void> {
|
|
|
265
279
|
delete stateMap[bindingId];
|
|
266
280
|
});
|
|
267
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
|
+
}
|
|
268
309
|
|
|
269
310
|
const sessionBindingBrowser: SessionBindingBrowser = {
|
|
270
311
|
async getTab(tabId) {
|
|
@@ -673,18 +714,23 @@ async function restoreFocusContext(context: FocusContext): Promise<void> {
|
|
|
673
714
|
}
|
|
674
715
|
}
|
|
675
716
|
|
|
676
|
-
async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
|
|
677
|
-
if (!enabled) {
|
|
678
|
-
return action();
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const focusContext = await captureFocusContext();
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
+
}
|
|
688
734
|
|
|
689
735
|
function requireRpcEnvelope(
|
|
690
736
|
method: string,
|
|
@@ -1686,11 +1732,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1686
1732
|
const result = await bindingManager.ensureBinding({
|
|
1687
1733
|
bindingId: String(params.bindingId ?? ''),
|
|
1688
1734
|
focus: params.focus === true,
|
|
1689
|
-
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
|
|
1690
1737
|
});
|
|
1691
1738
|
for (const tab of result.binding.tabs) {
|
|
1692
1739
|
void ensureNetworkDebugger(tab.id).catch(() => undefined);
|
|
1693
1740
|
}
|
|
1741
|
+
emitSessionBindingUpdated(result.binding.id, 'ensure', result.binding);
|
|
1694
1742
|
return {
|
|
1695
1743
|
browser: result.binding,
|
|
1696
1744
|
created: result.created,
|
|
@@ -1711,11 +1759,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1711
1759
|
bindingId: String(params.bindingId ?? ''),
|
|
1712
1760
|
url: expectedUrl,
|
|
1713
1761
|
active: params.active === true,
|
|
1714
|
-
focus: params.focus === true
|
|
1762
|
+
focus: params.focus === true,
|
|
1763
|
+
label: typeof params.label === 'string' ? params.label : undefined
|
|
1715
1764
|
});
|
|
1716
1765
|
});
|
|
1717
1766
|
const finalized = await finalizeOpenedSessionBindingTab(opened, expectedUrl);
|
|
1718
1767
|
void ensureNetworkDebugger(finalized.tab.id).catch(() => undefined);
|
|
1768
|
+
emitSessionBindingUpdated(finalized.binding.id, 'open-tab', finalized.binding);
|
|
1719
1769
|
return {
|
|
1720
1770
|
browser: finalized.binding,
|
|
1721
1771
|
tab: finalized.tab
|
|
@@ -1738,6 +1788,7 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1738
1788
|
case 'sessionBinding.setActiveTab': {
|
|
1739
1789
|
const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.bindingId ?? ''));
|
|
1740
1790
|
void ensureNetworkDebugger(result.tab.id).catch(() => undefined);
|
|
1791
|
+
emitSessionBindingUpdated(result.binding.id, 'set-active-tab', result.binding);
|
|
1741
1792
|
return {
|
|
1742
1793
|
browser: result.binding,
|
|
1743
1794
|
tab: result.tab
|
|
@@ -1755,8 +1806,10 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1755
1806
|
const result = await bindingManager.reset({
|
|
1756
1807
|
bindingId: String(params.bindingId ?? ''),
|
|
1757
1808
|
focus: params.focus === true,
|
|
1758
|
-
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
|
|
1759
1811
|
});
|
|
1812
|
+
emitSessionBindingUpdated(result.binding.id, 'reset', result.binding);
|
|
1760
1813
|
return {
|
|
1761
1814
|
browser: result.binding,
|
|
1762
1815
|
created: result.created,
|
|
@@ -1765,8 +1818,22 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1765
1818
|
};
|
|
1766
1819
|
});
|
|
1767
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
|
+
}
|
|
1768
1832
|
case 'sessionBinding.close': {
|
|
1769
|
-
|
|
1833
|
+
const bindingId = String(params.bindingId ?? '');
|
|
1834
|
+
const result = await bindingManager.close(bindingId);
|
|
1835
|
+
emitSessionBindingUpdated(bindingId, 'close', null);
|
|
1836
|
+
return result;
|
|
1770
1837
|
}
|
|
1771
1838
|
case 'page.goto': {
|
|
1772
1839
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
@@ -2352,42 +2419,75 @@ async function connectWebSocket(): Promise<void> {
|
|
|
2352
2419
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
2353
2420
|
dropNetworkCapture(tabId);
|
|
2354
2421
|
void mutateSessionBindingStateMap((stateMap) => {
|
|
2422
|
+
const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
|
|
2355
2423
|
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2356
2424
|
if (!state.tabIds.includes(tabId)) {
|
|
2357
2425
|
continue;
|
|
2358
2426
|
}
|
|
2359
2427
|
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
2360
|
-
|
|
2428
|
+
const fallbackTabId = nextTabIds[0] ?? null;
|
|
2429
|
+
const nextState: SessionBindingRecord = {
|
|
2361
2430
|
...state,
|
|
2362
2431
|
tabIds: nextTabIds,
|
|
2363
|
-
activeTabId: state.activeTabId === tabId ?
|
|
2364
|
-
primaryTabId: state.primaryTabId === tabId ?
|
|
2432
|
+
activeTabId: state.activeTabId === tabId ? fallbackTabId : state.activeTabId,
|
|
2433
|
+
primaryTabId: state.primaryTabId === tabId ? fallbackTabId : state.primaryTabId
|
|
2365
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
|
+
});
|
|
2366
2444
|
}
|
|
2367
2445
|
});
|
|
2368
2446
|
});
|
|
2369
2447
|
|
|
2370
2448
|
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
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 }>;
|
|
2375
2459
|
}
|
|
2376
|
-
stateMap
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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
|
+
});
|
|
2382
2481
|
});
|
|
2383
2482
|
|
|
2384
2483
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
2385
2484
|
void mutateSessionBindingStateMap((stateMap) => {
|
|
2485
|
+
const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
|
|
2386
2486
|
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2387
2487
|
if (state.windowId !== windowId) {
|
|
2388
2488
|
continue;
|
|
2389
2489
|
}
|
|
2390
|
-
|
|
2490
|
+
const nextState: SessionBindingRecord = {
|
|
2391
2491
|
...state,
|
|
2392
2492
|
windowId: null,
|
|
2393
2493
|
groupId: null,
|
|
@@ -2395,6 +2495,13 @@ chrome.windows.onRemoved.addListener((windowId) => {
|
|
|
2395
2495
|
activeTabId: null,
|
|
2396
2496
|
primaryTabId: null
|
|
2397
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);
|
|
2398
2505
|
}
|
|
2399
2506
|
});
|
|
2400
2507
|
});
|
package/src/session-binding.ts
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
437
|
-
const
|
|
438
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
await this.browser.
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
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
|
}
|