@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.
- package/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +202 -28
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/public/manifest.json +1 -1
- package/scripts/copy-assets.mjs +14 -4
- package/src/background.ts +158 -50
- package/src/network-debugger.ts +2 -1
- package/src/session-binding.ts +145 -74
- package/src/version.ts +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
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:
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
3388
|
+
const fallbackTabId = nextTabIds[0] ?? null;
|
|
3389
|
+
const nextState = {
|
|
3249
3390
|
...state,
|
|
3250
3391
|
tabIds: nextTabIds,
|
|
3251
|
-
activeTabId: state.activeTabId === tabId ?
|
|
3252
|
-
primaryTabId: state.primaryTabId === tabId ?
|
|
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
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
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
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
package/public/manifest.json
CHANGED
package/scripts/copy-assets.mjs
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
+
if (!extensionVersion) {
|
|
15
|
+
throw new Error('Cannot determine extension version from package.json');
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2428
|
+
const fallbackTabId = nextTabIds[0] ?? null;
|
|
2429
|
+
const nextState: SessionBindingRecord = {
|
|
2360
2430
|
...state,
|
|
2361
2431
|
tabIds: nextTabIds,
|
|
2362
|
-
activeTabId: state.activeTabId === tabId ?
|
|
2363
|
-
primaryTabId: state.primaryTabId === tabId ?
|
|
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
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
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
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/network-debugger.ts
CHANGED
|
@@ -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:
|
|
450
|
+
version: EXTENSION_VERSION
|
|
450
451
|
},
|
|
451
452
|
entries: entries.map((entry) => ({
|
|
452
453
|
startedDateTime: new Date(entry.startedAt ?? entry.ts).toISOString(),
|
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
|
}
|
package/src/version.ts
ADDED