@flrande/bak-extension 0.6.6 → 0.6.8
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 +166 -49
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +141 -23
- package/dist/popup.html +49 -7
- package/package.json +2 -2
- package/public/popup.html +49 -7
- package/src/popup.ts +160 -23
- package/src/session-binding.ts +298 -169
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-13T08:51:15.417Z
|
|
@@ -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.8",
|
|
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",
|
|
@@ -563,26 +563,37 @@
|
|
|
563
563
|
}
|
|
564
564
|
}
|
|
565
565
|
if (!window2) {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
state.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
state.activeTabId =
|
|
584
|
-
|
|
585
|
-
|
|
566
|
+
const sharedWindow = await this.findSharedBindingWindow(bindingId);
|
|
567
|
+
if (sharedWindow) {
|
|
568
|
+
state.windowId = sharedWindow.id;
|
|
569
|
+
state.groupId = null;
|
|
570
|
+
state.tabIds = [];
|
|
571
|
+
state.activeTabId = null;
|
|
572
|
+
state.primaryTabId = null;
|
|
573
|
+
window2 = sharedWindow;
|
|
574
|
+
repairActions.push("attached-shared-window");
|
|
575
|
+
} else {
|
|
576
|
+
const createdWindow = await this.browser.createWindow({
|
|
577
|
+
url: initialUrl,
|
|
578
|
+
focused: options.focus === true
|
|
579
|
+
});
|
|
580
|
+
state.windowId = createdWindow.id;
|
|
581
|
+
state.groupId = null;
|
|
582
|
+
state.tabIds = [];
|
|
583
|
+
state.activeTabId = null;
|
|
584
|
+
state.primaryTabId = null;
|
|
585
|
+
window2 = createdWindow;
|
|
586
|
+
const initialTab = typeof createdWindow.initialTabId === "number" ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id) : null;
|
|
587
|
+
tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
|
|
588
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
589
|
+
if (state.primaryTabId === null) {
|
|
590
|
+
state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
|
|
591
|
+
}
|
|
592
|
+
if (state.activeTabId === null) {
|
|
593
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
|
|
594
|
+
}
|
|
595
|
+
repairActions.push(created ? "created-window" : "recreated-window");
|
|
596
|
+
}
|
|
586
597
|
}
|
|
587
598
|
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
588
599
|
const recoveredTabs = await this.recoverBindingTabs(state, tabs);
|
|
@@ -596,12 +607,12 @@
|
|
|
596
607
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
597
608
|
if (state.windowId !== null) {
|
|
598
609
|
const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
|
|
599
|
-
if (ownership.foreignTabs.length > 0) {
|
|
600
|
-
const migrated = await this.
|
|
610
|
+
if (ownership.foreignTabs.length > 0 && ownership.bindingTabs.length > 0) {
|
|
611
|
+
const migrated = await this.moveBindingIntoBakWindow(state, ownership, initialUrl);
|
|
601
612
|
window2 = migrated.window;
|
|
602
613
|
tabs = migrated.tabs;
|
|
603
614
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
604
|
-
repairActions.push("
|
|
615
|
+
repairActions.push("evacuated-foreign-window");
|
|
605
616
|
}
|
|
606
617
|
}
|
|
607
618
|
if (tabs.length === 0) {
|
|
@@ -698,7 +709,7 @@
|
|
|
698
709
|
const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
|
|
699
710
|
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
700
711
|
state,
|
|
701
|
-
ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
|
|
712
|
+
ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
|
|
702
713
|
);
|
|
703
714
|
let createdTab;
|
|
704
715
|
try {
|
|
@@ -831,29 +842,32 @@
|
|
|
831
842
|
return { ok: true, binding: refreshed.binding };
|
|
832
843
|
}
|
|
833
844
|
async closeTab(bindingId, tabId) {
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
845
|
+
const binding = await this.inspectBinding(bindingId);
|
|
846
|
+
if (!binding) {
|
|
847
|
+
throw new Error(`Binding ${bindingId} does not exist`);
|
|
848
|
+
}
|
|
849
|
+
const resolvedTabId = typeof tabId === "number" ? tabId : binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
|
|
850
|
+
if (typeof resolvedTabId !== "number" || !binding.tabIds.includes(resolvedTabId)) {
|
|
837
851
|
throw new Error(`Tab ${tabId ?? "active"} does not belong to binding ${bindingId}`);
|
|
838
852
|
}
|
|
839
853
|
await this.browser.closeTab(resolvedTabId);
|
|
840
|
-
const remainingTabIds =
|
|
854
|
+
const remainingTabIds = binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
|
|
841
855
|
if (remainingTabIds.length === 0) {
|
|
842
|
-
await this.storage.delete(
|
|
856
|
+
await this.storage.delete(binding.id);
|
|
843
857
|
return {
|
|
844
858
|
binding: null,
|
|
845
859
|
closedTabId: resolvedTabId
|
|
846
860
|
};
|
|
847
861
|
}
|
|
848
862
|
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
849
|
-
const nextPrimaryTabId =
|
|
850
|
-
const nextActiveTabId =
|
|
863
|
+
const nextPrimaryTabId = binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
|
|
864
|
+
const nextActiveTabId = binding.activeTabId === resolvedTabId ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null : binding.activeTabId;
|
|
851
865
|
const nextState = {
|
|
852
|
-
id:
|
|
853
|
-
label:
|
|
854
|
-
color:
|
|
855
|
-
windowId: tabs[0]?.windowId ??
|
|
856
|
-
groupId: tabs[0]?.groupId ??
|
|
866
|
+
id: binding.id,
|
|
867
|
+
label: binding.label,
|
|
868
|
+
color: binding.color,
|
|
869
|
+
windowId: tabs[0]?.windowId ?? binding.windowId,
|
|
870
|
+
groupId: tabs[0]?.groupId ?? binding.groupId,
|
|
857
871
|
tabIds: tabs.map((candidate) => candidate.id),
|
|
858
872
|
activeTabId: nextActiveTabId,
|
|
859
873
|
primaryTabId: nextPrimaryTabId
|
|
@@ -882,7 +896,7 @@
|
|
|
882
896
|
return { ok: true };
|
|
883
897
|
}
|
|
884
898
|
await this.storage.delete(bindingId);
|
|
885
|
-
const trackedTabs = await this.
|
|
899
|
+
const trackedTabs = await this.collectBindingTabsForClose(state);
|
|
886
900
|
for (const trackedTab of trackedTabs) {
|
|
887
901
|
try {
|
|
888
902
|
await this.browser.closeTab(trackedTab.id);
|
|
@@ -1069,24 +1083,59 @@
|
|
|
1069
1083
|
}
|
|
1070
1084
|
async inspectBindingWindowOwnership(state, windowId) {
|
|
1071
1085
|
const windowTabs = await this.waitForWindowTabs(windowId, 500);
|
|
1072
|
-
const
|
|
1086
|
+
const bindingTabIds = new Set(this.collectCandidateTabIds(state));
|
|
1087
|
+
const peerBindings = (await this.storage.list()).filter((candidate) => candidate.id !== state.id);
|
|
1088
|
+
const peerTabIds = /* @__PURE__ */ new Set();
|
|
1089
|
+
const peerGroupIds = /* @__PURE__ */ new Set();
|
|
1090
|
+
for (const peer of peerBindings) {
|
|
1091
|
+
for (const tabId of this.collectCandidateTabIds(peer)) {
|
|
1092
|
+
peerTabIds.add(tabId);
|
|
1093
|
+
}
|
|
1094
|
+
if (peer.groupId !== null) {
|
|
1095
|
+
peerGroupIds.add(peer.groupId);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const bindingTabs = [];
|
|
1099
|
+
const sharedBindingTabs = [];
|
|
1100
|
+
const foreignTabs = [];
|
|
1101
|
+
for (const tab of windowTabs) {
|
|
1102
|
+
if (bindingTabIds.has(tab.id) || state.groupId !== null && tab.groupId === state.groupId) {
|
|
1103
|
+
bindingTabs.push(tab);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (peerTabIds.has(tab.id) || tab.groupId !== null && peerGroupIds.has(tab.groupId)) {
|
|
1107
|
+
sharedBindingTabs.push(tab);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
foreignTabs.push(tab);
|
|
1111
|
+
}
|
|
1073
1112
|
return {
|
|
1074
|
-
bindingTabs
|
|
1075
|
-
|
|
1113
|
+
bindingTabs,
|
|
1114
|
+
sharedBindingTabs,
|
|
1115
|
+
foreignTabs
|
|
1076
1116
|
};
|
|
1077
1117
|
}
|
|
1078
|
-
async
|
|
1118
|
+
async moveBindingIntoBakWindow(state, ownership, initialUrl) {
|
|
1079
1119
|
const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
|
|
1080
1120
|
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
1081
|
-
const
|
|
1121
|
+
const sharedWindow = await this.findSharedBindingWindow(state.id, state.windowId === null ? [] : [state.windowId]);
|
|
1122
|
+
const window2 = sharedWindow ?? await this.browser.createWindow({
|
|
1082
1123
|
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
1083
1124
|
focused: false
|
|
1084
1125
|
});
|
|
1085
|
-
const initialTab = typeof window2.initialTabId
|
|
1086
|
-
const recreatedTabs =
|
|
1087
|
-
const firstTab = recreatedTabs[0] ?? null;
|
|
1126
|
+
const initialTab = sharedWindow || typeof window2.initialTabId !== "number" ? null : await this.waitForTrackedTab(window2.initialTabId, window2.id);
|
|
1127
|
+
const recreatedTabs = [];
|
|
1088
1128
|
const tabIdMap = /* @__PURE__ */ new Map();
|
|
1089
|
-
if (sourceTabs[0]
|
|
1129
|
+
if (sourceTabs[0]) {
|
|
1130
|
+
const firstTab = initialTab ? await this.browser.updateTab(initialTab.id, {
|
|
1131
|
+
url: sourceTabs[0].url,
|
|
1132
|
+
active: false
|
|
1133
|
+
}) : await this.createBindingTab({
|
|
1134
|
+
windowId: window2.id,
|
|
1135
|
+
url: sourceTabs[0].url,
|
|
1136
|
+
active: false
|
|
1137
|
+
});
|
|
1138
|
+
recreatedTabs.push(firstTab);
|
|
1090
1139
|
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|
|
1091
1140
|
}
|
|
1092
1141
|
for (const sourceTab of sourceTabs.slice(1)) {
|
|
@@ -1098,7 +1147,7 @@
|
|
|
1098
1147
|
recreatedTabs.push(recreated);
|
|
1099
1148
|
tabIdMap.set(sourceTab.id, recreated.id);
|
|
1100
1149
|
}
|
|
1101
|
-
const nextPrimaryTabId = (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : void 0) ??
|
|
1150
|
+
const nextPrimaryTabId = (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : void 0) ?? recreatedTabs[0]?.id ?? null;
|
|
1102
1151
|
const nextActiveTabId = (state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : void 0) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
|
|
1103
1152
|
if (nextActiveTabId !== null) {
|
|
1104
1153
|
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
@@ -1113,7 +1162,10 @@
|
|
|
1113
1162
|
tabIds: [...state.tabIds]
|
|
1114
1163
|
});
|
|
1115
1164
|
for (const bindingTab of ownership.bindingTabs) {
|
|
1116
|
-
|
|
1165
|
+
try {
|
|
1166
|
+
await this.browser.closeTab(bindingTab.id);
|
|
1167
|
+
} catch {
|
|
1168
|
+
}
|
|
1117
1169
|
}
|
|
1118
1170
|
return {
|
|
1119
1171
|
window: window2,
|
|
@@ -1145,6 +1197,57 @@
|
|
|
1145
1197
|
}
|
|
1146
1198
|
return ordered;
|
|
1147
1199
|
}
|
|
1200
|
+
async findSharedBindingWindow(bindingId, excludedWindowIds = []) {
|
|
1201
|
+
const peers = (await this.storage.list()).filter((candidate) => candidate.id !== bindingId);
|
|
1202
|
+
const candidateWindowIds = [];
|
|
1203
|
+
const peerTabIds = /* @__PURE__ */ new Set();
|
|
1204
|
+
const peerGroupIds = /* @__PURE__ */ new Set();
|
|
1205
|
+
const pushWindowId = (windowId) => {
|
|
1206
|
+
if (typeof windowId !== "number") {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
candidateWindowIds.push(windowId);
|
|
1213
|
+
};
|
|
1214
|
+
for (const peer of peers) {
|
|
1215
|
+
pushWindowId(peer.windowId);
|
|
1216
|
+
if (peer.groupId !== null) {
|
|
1217
|
+
peerGroupIds.add(peer.groupId);
|
|
1218
|
+
const group = await this.waitForGroup(peer.groupId, 300);
|
|
1219
|
+
pushWindowId(group?.windowId);
|
|
1220
|
+
}
|
|
1221
|
+
const trackedTabIds = this.collectCandidateTabIds(peer);
|
|
1222
|
+
for (const trackedTabId of trackedTabIds) {
|
|
1223
|
+
peerTabIds.add(trackedTabId);
|
|
1224
|
+
}
|
|
1225
|
+
const trackedTabs = await this.readLooseTrackedTabs(trackedTabIds);
|
|
1226
|
+
for (const tab of trackedTabs) {
|
|
1227
|
+
pushWindowId(tab.windowId);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
for (const windowId of candidateWindowIds) {
|
|
1231
|
+
const window2 = await this.waitForWindow(windowId, 300);
|
|
1232
|
+
if (window2) {
|
|
1233
|
+
const windowTabs = await this.waitForWindowTabs(window2.id, 300);
|
|
1234
|
+
const ownedTabs = windowTabs.filter(
|
|
1235
|
+
(tab) => peerTabIds.has(tab.id) || tab.groupId !== null && peerGroupIds.has(tab.groupId)
|
|
1236
|
+
);
|
|
1237
|
+
if (ownedTabs.length === 0) {
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
const foreignTabs = windowTabs.filter(
|
|
1241
|
+
(tab) => !peerTabIds.has(tab.id) && (tab.groupId === null || !peerGroupIds.has(tab.groupId))
|
|
1242
|
+
);
|
|
1243
|
+
if (foreignTabs.length > 0) {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
return window2;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1148
1251
|
async recoverBindingTabs(state, existingTabs) {
|
|
1149
1252
|
if (state.windowId === null) {
|
|
1150
1253
|
return existingTabs;
|
|
@@ -1171,6 +1274,19 @@
|
|
|
1171
1274
|
}
|
|
1172
1275
|
return existingTabs;
|
|
1173
1276
|
}
|
|
1277
|
+
async collectBindingTabsForClose(state) {
|
|
1278
|
+
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
1279
|
+
if (state.windowId === null || state.groupId === null) {
|
|
1280
|
+
return trackedTabs;
|
|
1281
|
+
}
|
|
1282
|
+
const windowTabs = await this.waitForWindowTabs(state.windowId, 300);
|
|
1283
|
+
const groupedTabs = windowTabs.filter((tab) => tab.groupId === state.groupId);
|
|
1284
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1285
|
+
for (const tab of [...trackedTabs, ...groupedTabs]) {
|
|
1286
|
+
merged.set(tab.id, tab);
|
|
1287
|
+
}
|
|
1288
|
+
return [...merged.values()];
|
|
1289
|
+
}
|
|
1174
1290
|
async createBindingTab(options) {
|
|
1175
1291
|
if (options.windowId === null) {
|
|
1176
1292
|
throw new Error("Binding window is unavailable");
|
|
@@ -1200,6 +1316,7 @@
|
|
|
1200
1316
|
return null;
|
|
1201
1317
|
}
|
|
1202
1318
|
let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
1319
|
+
tabs = await this.recoverBindingTabs(state, tabs);
|
|
1203
1320
|
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
|
|
1204
1321
|
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
1205
1322
|
tabs = [...tabs, activeTab];
|
package/dist/manifest.json
CHANGED
package/dist/popup.global.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
(() => {
|
|
3
3
|
// src/popup.ts
|
|
4
4
|
var statusEl = document.getElementById("status");
|
|
5
|
+
var statusNoteEl = document.getElementById("statusNote");
|
|
5
6
|
var tokenInput = document.getElementById("token");
|
|
6
7
|
var portInput = document.getElementById("port");
|
|
7
8
|
var debugRichTextInput = document.getElementById("debugRichText");
|
|
8
9
|
var saveBtn = document.getElementById("save");
|
|
10
|
+
var saveRowEl = document.getElementById("saveRow");
|
|
9
11
|
var reconnectBtn = document.getElementById("reconnect");
|
|
10
12
|
var disconnectBtn = document.getElementById("disconnect");
|
|
11
13
|
var connectionStateEl = document.getElementById("connectionState");
|
|
@@ -18,9 +20,24 @@
|
|
|
18
20
|
var sessionSummaryEl = document.getElementById("sessionSummary");
|
|
19
21
|
var sessionListEl = document.getElementById("sessionList");
|
|
20
22
|
var latestState = null;
|
|
21
|
-
function setStatus(text,
|
|
23
|
+
function setStatus(text, tone = "neutral") {
|
|
22
24
|
statusEl.textContent = text;
|
|
23
|
-
|
|
25
|
+
if (tone === "success") {
|
|
26
|
+
statusEl.style.color = "#166534";
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (tone === "warning") {
|
|
30
|
+
statusEl.style.color = "#b45309";
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (tone === "error") {
|
|
34
|
+
statusEl.style.color = "#dc2626";
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
statusEl.style.color = "#0f172a";
|
|
38
|
+
}
|
|
39
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
40
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
24
41
|
}
|
|
25
42
|
function formatTimeAgo(at) {
|
|
26
43
|
if (typeof at !== "number") {
|
|
@@ -41,7 +58,11 @@
|
|
|
41
58
|
return `${deltaHours}h ago`;
|
|
42
59
|
}
|
|
43
60
|
function renderSessionBindings(state) {
|
|
44
|
-
|
|
61
|
+
if (state.count === 0) {
|
|
62
|
+
sessionSummaryEl.textContent = "No remembered sessions";
|
|
63
|
+
} else {
|
|
64
|
+
sessionSummaryEl.textContent = `${pluralize(state.count, "session")}, ${pluralize(state.attachedCount, "attached binding")}, ${pluralize(state.tabCount, "tab")}, ${pluralize(state.detachedCount, "detached binding")}`;
|
|
65
|
+
}
|
|
45
66
|
sessionListEl.replaceChildren();
|
|
46
67
|
for (const item of state.items) {
|
|
47
68
|
const li = document.createElement("li");
|
|
@@ -54,8 +75,25 @@
|
|
|
54
75
|
sessionListEl.appendChild(li);
|
|
55
76
|
}
|
|
56
77
|
}
|
|
78
|
+
function describeConnectionState(connectionState) {
|
|
79
|
+
switch (connectionState) {
|
|
80
|
+
case "connected":
|
|
81
|
+
return "connected";
|
|
82
|
+
case "connecting":
|
|
83
|
+
return "waiting for runtime";
|
|
84
|
+
case "reconnecting":
|
|
85
|
+
return "retrying connection";
|
|
86
|
+
case "manual":
|
|
87
|
+
return "manually disconnected";
|
|
88
|
+
case "missing-token":
|
|
89
|
+
return "token required";
|
|
90
|
+
case "disconnected":
|
|
91
|
+
default:
|
|
92
|
+
return "disconnected";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
57
95
|
function renderConnectionDetails(state) {
|
|
58
|
-
connectionStateEl.textContent = state.connectionState;
|
|
96
|
+
connectionStateEl.textContent = describeConnectionState(state.connectionState);
|
|
59
97
|
tokenStateEl.textContent = state.hasToken ? "configured" : "missing";
|
|
60
98
|
connectionUrlEl.textContent = state.wsUrl;
|
|
61
99
|
extensionVersionEl.textContent = state.extensionVersion;
|
|
@@ -81,38 +119,110 @@
|
|
|
81
119
|
lastBindingUpdateEl.textContent = "none";
|
|
82
120
|
}
|
|
83
121
|
}
|
|
122
|
+
function parsePortValue() {
|
|
123
|
+
const port = Number.parseInt(portInput.value.trim(), 10);
|
|
124
|
+
return Number.isInteger(port) && port > 0 ? port : null;
|
|
125
|
+
}
|
|
126
|
+
function isFormDirty(state) {
|
|
127
|
+
if (!state) {
|
|
128
|
+
return tokenInput.value.trim().length > 0;
|
|
129
|
+
}
|
|
130
|
+
return tokenInput.value.trim().length > 0 || portInput.value.trim() !== String(state.port) || debugRichTextInput.checked !== Boolean(state.debugRichText);
|
|
131
|
+
}
|
|
132
|
+
function getConfigValidationMessage(state) {
|
|
133
|
+
if (!tokenInput.value.trim() && state?.hasToken !== true) {
|
|
134
|
+
return "Pair token is required";
|
|
135
|
+
}
|
|
136
|
+
if (parsePortValue() === null) {
|
|
137
|
+
return "Port is invalid";
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function updateSaveState(state) {
|
|
142
|
+
const dirty = isFormDirty(state);
|
|
143
|
+
const validationError = getConfigValidationMessage(state);
|
|
144
|
+
saveRowEl.hidden = !dirty;
|
|
145
|
+
saveBtn.disabled = !dirty || validationError !== null;
|
|
146
|
+
saveBtn.textContent = state?.hasToken ? "Save settings" : "Save token";
|
|
147
|
+
}
|
|
148
|
+
function describeStatus(state) {
|
|
149
|
+
const combinedError = `${state.lastErrorContext ?? ""} ${state.lastError ?? ""}`.toLowerCase();
|
|
150
|
+
const runtimeOffline = combinedError.includes("cannot connect to bak cli");
|
|
151
|
+
if (state.connected) {
|
|
152
|
+
return {
|
|
153
|
+
text: "Connected to local bak runtime",
|
|
154
|
+
note: "Use the bak CLI to start browser work. This popup is mainly for status and configuration.",
|
|
155
|
+
tone: "success"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (state.connectionState === "missing-token") {
|
|
159
|
+
return {
|
|
160
|
+
text: "Pair token is required",
|
|
161
|
+
note: "Paste a token once, then save it. Future reconnects happen automatically.",
|
|
162
|
+
tone: "error"
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (state.connectionState === "manual") {
|
|
166
|
+
return {
|
|
167
|
+
text: "Extension bridge is paused",
|
|
168
|
+
note: "Normal browser work starts from the bak CLI. Open Advanced only if you need to reconnect manually.",
|
|
169
|
+
tone: "warning"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (runtimeOffline) {
|
|
173
|
+
return {
|
|
174
|
+
text: "Waiting for local bak runtime",
|
|
175
|
+
note: "Run any bak command, such as `bak doctor`, and the extension will reconnect automatically.",
|
|
176
|
+
tone: "warning"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (state.connectionState === "reconnecting") {
|
|
180
|
+
return {
|
|
181
|
+
text: "Trying to reconnect",
|
|
182
|
+
note: "The extension is retrying in the background. You usually do not need to press anything here.",
|
|
183
|
+
tone: "warning"
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (state.lastError) {
|
|
187
|
+
return {
|
|
188
|
+
text: "Connection problem",
|
|
189
|
+
note: "Check the last error below. The extension keeps retrying automatically unless you disconnect it manually.",
|
|
190
|
+
tone: "error"
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
text: "Not connected yet",
|
|
195
|
+
note: "Once the local bak runtime is available, the extension reconnects automatically.",
|
|
196
|
+
tone: "neutral"
|
|
197
|
+
};
|
|
198
|
+
}
|
|
84
199
|
async function refreshState() {
|
|
85
200
|
const state = await chrome.runtime.sendMessage({ type: "bak.getState" });
|
|
86
201
|
if (state.ok) {
|
|
202
|
+
const shouldSyncForm = !isFormDirty(latestState);
|
|
87
203
|
latestState = state;
|
|
88
|
-
|
|
89
|
-
|
|
204
|
+
if (shouldSyncForm) {
|
|
205
|
+
portInput.value = String(state.port);
|
|
206
|
+
debugRichTextInput.checked = Boolean(state.debugRichText);
|
|
207
|
+
tokenInput.value = "";
|
|
208
|
+
}
|
|
90
209
|
renderConnectionDetails(state);
|
|
91
210
|
renderSessionBindings(state.sessionBindings);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
} else if (state.connectionState === "manual") {
|
|
97
|
-
setStatus("Disconnected manually");
|
|
98
|
-
} else if (state.connectionState === "reconnecting") {
|
|
99
|
-
setStatus("Reconnecting to bak CLI", true);
|
|
100
|
-
} else if (state.lastError) {
|
|
101
|
-
setStatus(`Disconnected: ${state.lastError}`, true);
|
|
102
|
-
} else {
|
|
103
|
-
setStatus("Disconnected");
|
|
104
|
-
}
|
|
211
|
+
updateSaveState(state);
|
|
212
|
+
const status = describeStatus(state);
|
|
213
|
+
setStatus(status.text, status.tone);
|
|
214
|
+
statusNoteEl.textContent = status.note;
|
|
105
215
|
}
|
|
106
216
|
}
|
|
107
217
|
saveBtn.addEventListener("click", async () => {
|
|
108
218
|
const token = tokenInput.value.trim();
|
|
109
|
-
const port =
|
|
219
|
+
const port = parsePortValue();
|
|
110
220
|
if (!token && latestState?.hasToken !== true) {
|
|
111
|
-
setStatus("Pair token is required",
|
|
221
|
+
setStatus("Pair token is required", "error");
|
|
112
222
|
return;
|
|
113
223
|
}
|
|
114
|
-
if (
|
|
115
|
-
setStatus("Port is invalid",
|
|
224
|
+
if (port === null) {
|
|
225
|
+
setStatus("Port is invalid", "error");
|
|
116
226
|
return;
|
|
117
227
|
}
|
|
118
228
|
await chrome.runtime.sendMessage({
|
|
@@ -132,6 +242,14 @@
|
|
|
132
242
|
await chrome.runtime.sendMessage({ type: "bak.disconnect" });
|
|
133
243
|
await refreshState();
|
|
134
244
|
});
|
|
245
|
+
for (const element of [tokenInput, portInput, debugRichTextInput]) {
|
|
246
|
+
element.addEventListener("input", () => {
|
|
247
|
+
updateSaveState(latestState);
|
|
248
|
+
});
|
|
249
|
+
element.addEventListener("change", () => {
|
|
250
|
+
updateSaveState(latestState);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
135
253
|
void refreshState();
|
|
136
254
|
var refreshInterval = window.setInterval(() => {
|
|
137
255
|
void refreshState();
|
package/dist/popup.html
CHANGED
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
gap: 8px;
|
|
55
55
|
margin-top: 12px;
|
|
56
56
|
}
|
|
57
|
+
.row[hidden] {
|
|
58
|
+
display: none;
|
|
59
|
+
}
|
|
57
60
|
button {
|
|
58
61
|
flex: 1;
|
|
59
62
|
border: none;
|
|
@@ -62,6 +65,10 @@
|
|
|
62
65
|
font-size: 12px;
|
|
63
66
|
cursor: pointer;
|
|
64
67
|
}
|
|
68
|
+
button:disabled {
|
|
69
|
+
opacity: 0.55;
|
|
70
|
+
cursor: default;
|
|
71
|
+
}
|
|
65
72
|
#save {
|
|
66
73
|
background: #0f172a;
|
|
67
74
|
color: #fff;
|
|
@@ -79,6 +86,12 @@
|
|
|
79
86
|
font-size: 12px;
|
|
80
87
|
font-weight: 600;
|
|
81
88
|
}
|
|
89
|
+
#statusNote {
|
|
90
|
+
margin-top: 4px;
|
|
91
|
+
font-size: 11px;
|
|
92
|
+
line-height: 1.45;
|
|
93
|
+
color: #475569;
|
|
94
|
+
}
|
|
82
95
|
.panel {
|
|
83
96
|
margin-top: 12px;
|
|
84
97
|
padding: 10px;
|
|
@@ -120,6 +133,30 @@
|
|
|
120
133
|
font-size: 11px;
|
|
121
134
|
color: #334155;
|
|
122
135
|
}
|
|
136
|
+
.hint.compact {
|
|
137
|
+
margin-top: 8px;
|
|
138
|
+
}
|
|
139
|
+
details.panel {
|
|
140
|
+
padding-bottom: 12px;
|
|
141
|
+
}
|
|
142
|
+
details.panel summary {
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
font-weight: 600;
|
|
146
|
+
list-style: none;
|
|
147
|
+
}
|
|
148
|
+
details.panel summary::-webkit-details-marker {
|
|
149
|
+
display: none;
|
|
150
|
+
}
|
|
151
|
+
details.panel summary::after {
|
|
152
|
+
content: "Show";
|
|
153
|
+
float: right;
|
|
154
|
+
font-size: 11px;
|
|
155
|
+
color: #64748b;
|
|
156
|
+
}
|
|
157
|
+
details.panel[open] summary::after {
|
|
158
|
+
content: "Hide";
|
|
159
|
+
}
|
|
123
160
|
</style>
|
|
124
161
|
</head>
|
|
125
162
|
<body>
|
|
@@ -136,14 +173,11 @@
|
|
|
136
173
|
<input id="debugRichText" type="checkbox" />
|
|
137
174
|
<span class="toggle-text">Allow richer text capture for debugging (still redacted, off by default)</span>
|
|
138
175
|
</label>
|
|
139
|
-
<div class="row">
|
|
140
|
-
<button id="save">Save & Connect</button>
|
|
141
|
-
<button id="reconnect">Reconnect</button>
|
|
142
|
-
</div>
|
|
143
|
-
<div class="row">
|
|
144
|
-
<button id="disconnect">Disconnect</button>
|
|
145
|
-
</div>
|
|
146
176
|
<div id="status">Checking...</div>
|
|
177
|
+
<div id="statusNote">The extension reconnects automatically when the local bak runtime wakes up.</div>
|
|
178
|
+
<div class="row" id="saveRow" hidden>
|
|
179
|
+
<button id="save">Save settings</button>
|
|
180
|
+
</div>
|
|
147
181
|
<div class="panel">
|
|
148
182
|
<h2>Connection</h2>
|
|
149
183
|
<dl class="meta-grid">
|
|
@@ -171,6 +205,14 @@
|
|
|
171
205
|
</dl>
|
|
172
206
|
<ul id="sessionList"></ul>
|
|
173
207
|
</div>
|
|
208
|
+
<details class="panel" id="advancedPanel">
|
|
209
|
+
<summary>Advanced bridge controls</summary>
|
|
210
|
+
<div class="hint compact">These controls are only for debugging the extension bridge. Normal browser work should start from the bak CLI.</div>
|
|
211
|
+
<div class="row">
|
|
212
|
+
<button id="reconnect">Reconnect bridge</button>
|
|
213
|
+
<button id="disconnect">Disconnect bridge</button>
|
|
214
|
+
</div>
|
|
215
|
+
</details>
|
|
174
216
|
<div class="hint">Extension only connects to ws://127.0.0.1</div>
|
|
175
217
|
<script src="./popup.global.js"></script>
|
|
176
218
|
</body>
|
package/package.json
CHANGED