@flrande/bak-extension 0.6.14 → 0.6.15
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 +103 -25
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +11 -8
- package/dist/popup.html +13 -3
- package/package.json +2 -2
- package/public/popup.html +13 -3
- package/src/background.ts +107 -49
- package/src/popup.ts +27 -9
- package/src/session-binding-storage.ts +7 -1
- package/src/session-binding.ts +41 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-14T15:19:38.952Z
|
|
@@ -556,7 +556,7 @@
|
|
|
556
556
|
// package.json
|
|
557
557
|
var package_default = {
|
|
558
558
|
name: "@flrande/bak-extension",
|
|
559
|
-
version: "0.6.
|
|
559
|
+
version: "0.6.15",
|
|
560
560
|
type: "module",
|
|
561
561
|
scripts: {
|
|
562
562
|
build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
|
|
@@ -990,10 +990,16 @@
|
|
|
990
990
|
return typeof candidate.id === "string" && Array.isArray(candidate.tabIds) && (typeof candidate.windowId === "number" || candidate.windowId === null) && (typeof candidate.groupId === "number" || candidate.groupId === null) && (typeof candidate.activeTabId === "number" || candidate.activeTabId === null) && (typeof candidate.primaryTabId === "number" || candidate.primaryTabId === null);
|
|
991
991
|
}
|
|
992
992
|
function cloneSessionBindingRecord(state) {
|
|
993
|
-
|
|
993
|
+
const cloned = {
|
|
994
994
|
...state,
|
|
995
995
|
tabIds: [...state.tabIds]
|
|
996
996
|
};
|
|
997
|
+
if (cloned.tabIds.length === 0) {
|
|
998
|
+
cloned.groupId = null;
|
|
999
|
+
cloned.activeTabId = null;
|
|
1000
|
+
cloned.primaryTabId = null;
|
|
1001
|
+
}
|
|
1002
|
+
return cloned;
|
|
997
1003
|
}
|
|
998
1004
|
function normalizeSessionBindingRecordMap(value) {
|
|
999
1005
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -1376,6 +1382,34 @@
|
|
|
1376
1382
|
};
|
|
1377
1383
|
}
|
|
1378
1384
|
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
1385
|
+
if (tabs.length === 0) {
|
|
1386
|
+
const liveWindow = binding.windowId !== null ? await this.waitForWindow(binding.windowId, 300) : null;
|
|
1387
|
+
if (!liveWindow) {
|
|
1388
|
+
await this.storage.delete(binding.id);
|
|
1389
|
+
return {
|
|
1390
|
+
binding: null,
|
|
1391
|
+
closedTabId: resolvedTabId
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
const nextState2 = {
|
|
1395
|
+
id: binding.id,
|
|
1396
|
+
label: binding.label,
|
|
1397
|
+
color: binding.color,
|
|
1398
|
+
windowId: liveWindow.id,
|
|
1399
|
+
groupId: null,
|
|
1400
|
+
tabIds: [],
|
|
1401
|
+
activeTabId: null,
|
|
1402
|
+
primaryTabId: null
|
|
1403
|
+
};
|
|
1404
|
+
await this.storage.save(nextState2);
|
|
1405
|
+
return {
|
|
1406
|
+
binding: {
|
|
1407
|
+
...nextState2,
|
|
1408
|
+
tabs: []
|
|
1409
|
+
},
|
|
1410
|
+
closedTabId: resolvedTabId
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1379
1413
|
const nextPrimaryTabId = binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
|
|
1380
1414
|
const nextActiveTabId = binding.activeTabId === resolvedTabId ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null : binding.activeTabId;
|
|
1381
1415
|
const nextState = {
|
|
@@ -2041,38 +2075,82 @@
|
|
|
2041
2075
|
async function listSessionBindingStates() {
|
|
2042
2076
|
return Object.values(await loadSessionBindingStateMap());
|
|
2043
2077
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
tabCount: state.tabIds.length,
|
|
2054
|
-
activeTabId: state.activeTabId,
|
|
2055
|
-
activeTabTitle: activeTab?.title ?? null,
|
|
2056
|
-
activeTabUrl: activeTab?.url ?? null,
|
|
2057
|
-
windowId: state.windowId,
|
|
2058
|
-
groupId: state.groupId,
|
|
2059
|
-
detached,
|
|
2060
|
-
lastBindingUpdateAt: bindingUpdate?.at ?? null,
|
|
2061
|
-
lastBindingUpdateReason: bindingUpdate?.reason ?? null
|
|
2062
|
-
};
|
|
2078
|
+
function collectPopupSessionBindingTabIds(state) {
|
|
2079
|
+
return [
|
|
2080
|
+
...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value) => typeof value === "number")))
|
|
2081
|
+
];
|
|
2082
|
+
}
|
|
2083
|
+
async function inspectPopupSessionBinding(state) {
|
|
2084
|
+
const trackedTabs = (await Promise.all(
|
|
2085
|
+
collectPopupSessionBindingTabIds(state).map(async (tabId) => {
|
|
2086
|
+
return await sessionBindingBrowser.getTab(tabId);
|
|
2063
2087
|
})
|
|
2064
|
-
);
|
|
2088
|
+
)).filter((tab) => tab !== null);
|
|
2089
|
+
const liveWindow = typeof state.windowId === "number" ? await sessionBindingBrowser.getWindow(state.windowId) : null;
|
|
2090
|
+
const activeTab = trackedTabs.find((tab) => tab.id === state.activeTabId) ?? trackedTabs.find((tab) => tab.active) ?? trackedTabs[0] ?? null;
|
|
2091
|
+
const status = trackedTabs.length > 0 ? "attached" : liveWindow ? "window-only" : "detached";
|
|
2092
|
+
const bindingUpdate = bindingUpdateMetadata.get(state.id);
|
|
2093
|
+
return {
|
|
2094
|
+
summary: {
|
|
2095
|
+
id: state.id,
|
|
2096
|
+
label: state.label,
|
|
2097
|
+
tabCount: trackedTabs.length,
|
|
2098
|
+
activeTabId: activeTab?.id ?? null,
|
|
2099
|
+
activeTabTitle: activeTab?.title ?? null,
|
|
2100
|
+
activeTabUrl: activeTab?.url ?? null,
|
|
2101
|
+
windowId: activeTab?.windowId ?? trackedTabs[0]?.windowId ?? liveWindow?.id ?? state.windowId,
|
|
2102
|
+
groupId: trackedTabs.length > 0 ? activeTab?.groupId ?? trackedTabs.find((tab) => tab.groupId !== null)?.groupId ?? state.groupId : null,
|
|
2103
|
+
status,
|
|
2104
|
+
lastBindingUpdateAt: bindingUpdate?.at ?? null,
|
|
2105
|
+
lastBindingUpdateReason: bindingUpdate?.reason ?? null
|
|
2106
|
+
},
|
|
2107
|
+
prune: status === "detached"
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
async function summarizeSessionBindings() {
|
|
2111
|
+
const statusRank = {
|
|
2112
|
+
attached: 0,
|
|
2113
|
+
"window-only": 1,
|
|
2114
|
+
detached: 2
|
|
2115
|
+
};
|
|
2116
|
+
const items = await mutateSessionBindingStateMap(async (stateMap) => {
|
|
2117
|
+
const inspected = await Promise.all(
|
|
2118
|
+
Object.entries(stateMap).map(async ([bindingId, state]) => {
|
|
2119
|
+
return {
|
|
2120
|
+
bindingId,
|
|
2121
|
+
inspected: await inspectPopupSessionBinding(state)
|
|
2122
|
+
};
|
|
2123
|
+
})
|
|
2124
|
+
);
|
|
2125
|
+
for (const entry of inspected) {
|
|
2126
|
+
if (entry.inspected.prune) {
|
|
2127
|
+
delete stateMap[entry.bindingId];
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
return inspected.filter((entry) => !entry.inspected.prune).map((entry) => entry.inspected.summary).sort((left, right) => {
|
|
2131
|
+
const byStatus = statusRank[left.status] - statusRank[right.status];
|
|
2132
|
+
if (byStatus !== 0) {
|
|
2133
|
+
return byStatus;
|
|
2134
|
+
}
|
|
2135
|
+
const byUpdate = (right.lastBindingUpdateAt ?? 0) - (left.lastBindingUpdateAt ?? 0);
|
|
2136
|
+
if (byUpdate !== 0) {
|
|
2137
|
+
return byUpdate;
|
|
2138
|
+
}
|
|
2139
|
+
return left.label.localeCompare(right.label);
|
|
2140
|
+
});
|
|
2141
|
+
});
|
|
2065
2142
|
return {
|
|
2066
2143
|
count: items.length,
|
|
2067
|
-
attachedCount: items.filter((item) =>
|
|
2068
|
-
|
|
2144
|
+
attachedCount: items.filter((item) => item.status === "attached").length,
|
|
2145
|
+
windowOnlyCount: items.filter((item) => item.status === "window-only").length,
|
|
2146
|
+
detachedCount: items.filter((item) => item.status === "detached").length,
|
|
2069
2147
|
tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
|
|
2070
2148
|
items
|
|
2071
2149
|
};
|
|
2072
2150
|
}
|
|
2073
2151
|
async function buildPopupState() {
|
|
2074
2152
|
const config = await getConfig();
|
|
2075
|
-
const sessionBindings = await summarizeSessionBindings(
|
|
2153
|
+
const sessionBindings = await summarizeSessionBindings();
|
|
2076
2154
|
const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
|
|
2077
2155
|
let connectionState;
|
|
2078
2156
|
if (!config.token) {
|
package/dist/manifest.json
CHANGED
package/dist/popup.global.js
CHANGED
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
const lastError = `${state.lastErrorContext ?? ""} ${state.lastError ?? ""}`.toLowerCase();
|
|
102
102
|
const runtimeOffline = lastError.includes("cannot connect to bak cli");
|
|
103
103
|
if (state.connected) {
|
|
104
|
-
const body = state.sessionBindings.detachedCount > 0 ? `${pluralize(state.sessionBindings.detachedCount, "remembered session")} are detached. Check the cards below before you continue browser work.` : "The extension bridge is healthy and ready for CLI-driven browser work.";
|
|
104
|
+
const body = state.sessionBindings.detachedCount > 0 ? `${pluralize(state.sessionBindings.detachedCount, "remembered session")} are detached. Check the cards below before you continue browser work.` : state.sessionBindings.windowOnlyCount > 0 ? `${pluralize(state.sessionBindings.windowOnlyCount, "remembered session")} still point at a live window, but do not own tabs yet.` : "The extension bridge is healthy and ready for CLI-driven browser work.";
|
|
105
105
|
return {
|
|
106
106
|
badge: "Ready",
|
|
107
107
|
title: "Connected to the local bak runtime",
|
|
@@ -110,6 +110,9 @@
|
|
|
110
110
|
recoverySteps: state.sessionBindings.detachedCount > 0 ? [
|
|
111
111
|
"Resume or recreate detached work from the bak CLI before sending new page commands.",
|
|
112
112
|
"Use the Sessions panel below to confirm which remembered session lost its owned tabs."
|
|
113
|
+
] : state.sessionBindings.windowOnlyCount > 0 ? [
|
|
114
|
+
"New bak page commands can reuse those remembered windows without disturbing your human tabs.",
|
|
115
|
+
"Use the Sessions panel below to confirm which binding is only waiting for a new owned tab."
|
|
113
116
|
] : [
|
|
114
117
|
"Start browser work from the bak CLI.",
|
|
115
118
|
"Use Reconnect bridge only when you intentionally changed token or port settings."
|
|
@@ -234,7 +237,7 @@
|
|
|
234
237
|
if (state.count === 0) {
|
|
235
238
|
sessionSummaryEl.textContent = "No remembered sessions";
|
|
236
239
|
} else {
|
|
237
|
-
sessionSummaryEl.textContent = `${pluralize(state.count, "session")}, ${pluralize(state.attachedCount, "attached binding")}, ${pluralize(state.detachedCount, "detached binding")}, ${pluralize(state.tabCount, "tracked tab")}`;
|
|
240
|
+
sessionSummaryEl.textContent = `${pluralize(state.count, "session")}, ${pluralize(state.attachedCount, "attached binding")}, ${pluralize(state.windowOnlyCount, "window-only binding")}, ${pluralize(state.detachedCount, "detached binding")}, ${pluralize(state.tabCount, "tracked tab")}`;
|
|
238
241
|
}
|
|
239
242
|
sessionCardsEl.replaceChildren();
|
|
240
243
|
if (state.items.length === 0) {
|
|
@@ -247,7 +250,7 @@
|
|
|
247
250
|
for (const item of state.items) {
|
|
248
251
|
const card = document.createElement("section");
|
|
249
252
|
card.className = "session-card";
|
|
250
|
-
card.dataset.
|
|
253
|
+
card.dataset.status = item.status;
|
|
251
254
|
const header = document.createElement("div");
|
|
252
255
|
header.className = "session-card-header";
|
|
253
256
|
const titleWrap = document.createElement("div");
|
|
@@ -261,16 +264,16 @@
|
|
|
261
264
|
titleWrap.append(title, subtitle);
|
|
262
265
|
const badge = document.createElement("span");
|
|
263
266
|
badge.className = "session-badge";
|
|
264
|
-
badge.dataset.
|
|
265
|
-
badge.textContent = item.
|
|
267
|
+
badge.dataset.status = item.status;
|
|
268
|
+
badge.textContent = item.status === "attached" ? "Attached" : item.status === "window-only" ? "Window only" : "Detached";
|
|
266
269
|
header.append(titleWrap, badge);
|
|
267
270
|
const activeTitle = document.createElement("div");
|
|
268
271
|
activeTitle.className = "session-active-title";
|
|
269
|
-
activeTitle.textContent = item.activeTabTitle ? truncate(item.activeTabTitle, 72) : "No active tab title";
|
|
272
|
+
activeTitle.textContent = item.activeTabTitle ? truncate(item.activeTabTitle, 72) : item.status === "window-only" ? "No owned tab yet" : "No active tab title";
|
|
270
273
|
activeTitle.title = item.activeTabTitle ?? "";
|
|
271
274
|
const activeUrl = document.createElement("div");
|
|
272
275
|
activeUrl.className = "session-active-url";
|
|
273
|
-
activeUrl.textContent = formatUrl(item.activeTabUrl);
|
|
276
|
+
activeUrl.textContent = item.status === "window-only" && !item.activeTabUrl ? "Next bak page command can reuse this window" : formatUrl(item.activeTabUrl);
|
|
274
277
|
activeUrl.title = item.activeTabUrl ?? "";
|
|
275
278
|
const meta = document.createElement("div");
|
|
276
279
|
meta.className = "session-meta-grid";
|
|
@@ -286,7 +289,7 @@
|
|
|
286
289
|
);
|
|
287
290
|
const footer = document.createElement("div");
|
|
288
291
|
footer.className = "session-card-footer";
|
|
289
|
-
footer.textContent = item.
|
|
292
|
+
footer.textContent = item.status === "attached" ? "The saved binding still points at live browser tabs." : item.status === "window-only" ? "bak still remembers the target window for this session, but it does not own any live tabs right now." : "bak still remembers this session, but its owned tabs or window are missing.";
|
|
290
293
|
card.append(header, activeTitle, activeUrl, meta, footer);
|
|
291
294
|
sessionCardsEl.appendChild(card);
|
|
292
295
|
}
|
package/dist/popup.html
CHANGED
|
@@ -245,7 +245,12 @@
|
|
|
245
245
|
background: rgba(255, 255, 255, 0.92);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
.session-card[data-
|
|
248
|
+
.session-card[data-status="window-only"] {
|
|
249
|
+
border-color: #38bdf8;
|
|
250
|
+
background: rgba(240, 249, 255, 0.95);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.session-card[data-status="detached"] {
|
|
249
254
|
border-color: #f59e0b;
|
|
250
255
|
background: rgba(255, 251, 235, 0.95);
|
|
251
256
|
}
|
|
@@ -283,12 +288,17 @@
|
|
|
283
288
|
white-space: nowrap;
|
|
284
289
|
}
|
|
285
290
|
|
|
286
|
-
.session-badge[data-
|
|
291
|
+
.session-badge[data-status="window-only"] {
|
|
292
|
+
background: #e0f2fe;
|
|
293
|
+
color: #0369a1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.session-badge[data-status="detached"] {
|
|
287
297
|
background: #fef3c7;
|
|
288
298
|
color: #b45309;
|
|
289
299
|
}
|
|
290
300
|
|
|
291
|
-
.session-badge[data-
|
|
301
|
+
.session-badge[data-status="attached"] {
|
|
292
302
|
background: #dbeafe;
|
|
293
303
|
color: #1d4ed8;
|
|
294
304
|
}
|
package/package.json
CHANGED
package/public/popup.html
CHANGED
|
@@ -245,7 +245,12 @@
|
|
|
245
245
|
background: rgba(255, 255, 255, 0.92);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
.session-card[data-
|
|
248
|
+
.session-card[data-status="window-only"] {
|
|
249
|
+
border-color: #38bdf8;
|
|
250
|
+
background: rgba(240, 249, 255, 0.95);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.session-card[data-status="detached"] {
|
|
249
254
|
border-color: #f59e0b;
|
|
250
255
|
background: rgba(255, 251, 235, 0.95);
|
|
251
256
|
}
|
|
@@ -283,12 +288,17 @@
|
|
|
283
288
|
white-space: nowrap;
|
|
284
289
|
}
|
|
285
290
|
|
|
286
|
-
.session-badge[data-
|
|
291
|
+
.session-badge[data-status="window-only"] {
|
|
292
|
+
background: #e0f2fe;
|
|
293
|
+
color: #0369a1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.session-badge[data-status="detached"] {
|
|
287
297
|
background: #fef3c7;
|
|
288
298
|
color: #b45309;
|
|
289
299
|
}
|
|
290
300
|
|
|
291
|
-
.session-badge[data-
|
|
301
|
+
.session-badge[data-status="attached"] {
|
|
292
302
|
background: #dbeafe;
|
|
293
303
|
color: #1d4ed8;
|
|
294
304
|
}
|
package/src/background.ts
CHANGED
|
@@ -70,12 +70,14 @@ interface ExtensionConfig {
|
|
|
70
70
|
debugRichText: boolean;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
interface RuntimeErrorDetails {
|
|
74
|
-
message: string;
|
|
75
|
-
context: 'config' | 'socket' | 'request' | 'parse';
|
|
76
|
-
at: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
73
|
+
interface RuntimeErrorDetails {
|
|
74
|
+
message: string;
|
|
75
|
+
context: 'config' | 'socket' | 'request' | 'parse';
|
|
76
|
+
at: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type PopupSessionBindingStatus = 'attached' | 'window-only' | 'detached';
|
|
80
|
+
|
|
79
81
|
interface PopupSessionBindingSummary {
|
|
80
82
|
id: string;
|
|
81
83
|
label: string;
|
|
@@ -85,7 +87,7 @@ interface PopupSessionBindingSummary {
|
|
|
85
87
|
activeTabUrl: string | null;
|
|
86
88
|
windowId: number | null;
|
|
87
89
|
groupId: number | null;
|
|
88
|
-
|
|
90
|
+
status: PopupSessionBindingStatus;
|
|
89
91
|
lastBindingUpdateAt: number | null;
|
|
90
92
|
lastBindingUpdateReason: string | null;
|
|
91
93
|
}
|
|
@@ -107,14 +109,15 @@ interface PopupState {
|
|
|
107
109
|
extensionVersion: string;
|
|
108
110
|
lastBindingUpdateAt: number | null;
|
|
109
111
|
lastBindingUpdateReason: string | null;
|
|
110
|
-
sessionBindings: {
|
|
111
|
-
count: number;
|
|
112
|
-
attachedCount: number;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
112
|
+
sessionBindings: {
|
|
113
|
+
count: number;
|
|
114
|
+
attachedCount: number;
|
|
115
|
+
windowOnlyCount: number;
|
|
116
|
+
detachedCount: number;
|
|
117
|
+
tabCount: number;
|
|
118
|
+
items: PopupSessionBindingSummary[];
|
|
119
|
+
};
|
|
120
|
+
}
|
|
118
121
|
|
|
119
122
|
const DEFAULT_PORT = 17373;
|
|
120
123
|
const STORAGE_KEY_TOKEN = 'pairToken';
|
|
@@ -344,44 +347,99 @@ async function loadSessionBindingState(bindingId: string): Promise<SessionBindin
|
|
|
344
347
|
return stateMap[bindingId] ?? null;
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
348
|
-
return Object.values(await loadSessionBindingStateMap());
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
350
|
+
async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
351
|
+
return Object.values(await loadSessionBindingStateMap());
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function collectPopupSessionBindingTabIds(state: SessionBindingRecord): number[] {
|
|
355
|
+
return [
|
|
356
|
+
...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function inspectPopupSessionBinding(
|
|
361
|
+
state: SessionBindingRecord
|
|
362
|
+
): Promise<{ summary: PopupSessionBindingSummary; prune: boolean }> {
|
|
363
|
+
const trackedTabs = (
|
|
364
|
+
await Promise.all(
|
|
365
|
+
collectPopupSessionBindingTabIds(state).map(async (tabId) => {
|
|
366
|
+
return await sessionBindingBrowser.getTab(tabId);
|
|
367
|
+
})
|
|
368
|
+
)
|
|
369
|
+
).filter((tab): tab is SessionBindingTab => tab !== null);
|
|
370
|
+
const liveWindow = typeof state.windowId === 'number' ? await sessionBindingBrowser.getWindow(state.windowId) : null;
|
|
371
|
+
const activeTab = trackedTabs.find((tab) => tab.id === state.activeTabId) ?? trackedTabs.find((tab) => tab.active) ?? trackedTabs[0] ?? null;
|
|
372
|
+
const status: PopupSessionBindingStatus = trackedTabs.length > 0 ? 'attached' : liveWindow ? 'window-only' : 'detached';
|
|
373
|
+
const bindingUpdate = bindingUpdateMetadata.get(state.id);
|
|
374
|
+
return {
|
|
375
|
+
summary: {
|
|
376
|
+
id: state.id,
|
|
377
|
+
label: state.label,
|
|
378
|
+
tabCount: trackedTabs.length,
|
|
379
|
+
activeTabId: activeTab?.id ?? null,
|
|
380
|
+
activeTabTitle: activeTab?.title ?? null,
|
|
381
|
+
activeTabUrl: activeTab?.url ?? null,
|
|
382
|
+
windowId: activeTab?.windowId ?? trackedTabs[0]?.windowId ?? liveWindow?.id ?? state.windowId,
|
|
383
|
+
groupId:
|
|
384
|
+
trackedTabs.length > 0
|
|
385
|
+
? activeTab?.groupId ?? trackedTabs.find((tab) => tab.groupId !== null)?.groupId ?? state.groupId
|
|
386
|
+
: null,
|
|
387
|
+
status,
|
|
388
|
+
lastBindingUpdateAt: bindingUpdate?.at ?? null,
|
|
389
|
+
lastBindingUpdateReason: bindingUpdate?.reason ?? null
|
|
390
|
+
},
|
|
391
|
+
prune: status === 'detached'
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function summarizeSessionBindings(): Promise<PopupState['sessionBindings']> {
|
|
396
|
+
const statusRank: Record<PopupSessionBindingStatus, number> = {
|
|
397
|
+
attached: 0,
|
|
398
|
+
'window-only': 1,
|
|
399
|
+
detached: 2
|
|
400
|
+
};
|
|
401
|
+
const items = await mutateSessionBindingStateMap(async (stateMap) => {
|
|
402
|
+
const inspected = await Promise.all(
|
|
403
|
+
Object.entries(stateMap).map(async ([bindingId, state]) => {
|
|
404
|
+
return {
|
|
405
|
+
bindingId,
|
|
406
|
+
inspected: await inspectPopupSessionBinding(state)
|
|
407
|
+
};
|
|
408
|
+
})
|
|
409
|
+
);
|
|
410
|
+
for (const entry of inspected) {
|
|
411
|
+
if (entry.inspected.prune) {
|
|
412
|
+
delete stateMap[entry.bindingId];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return inspected
|
|
416
|
+
.filter((entry) => !entry.inspected.prune)
|
|
417
|
+
.map((entry) => entry.inspected.summary)
|
|
418
|
+
.sort((left, right) => {
|
|
419
|
+
const byStatus = statusRank[left.status] - statusRank[right.status];
|
|
420
|
+
if (byStatus !== 0) {
|
|
421
|
+
return byStatus;
|
|
422
|
+
}
|
|
423
|
+
const byUpdate = (right.lastBindingUpdateAt ?? 0) - (left.lastBindingUpdateAt ?? 0);
|
|
424
|
+
if (byUpdate !== 0) {
|
|
425
|
+
return byUpdate;
|
|
426
|
+
}
|
|
427
|
+
return left.label.localeCompare(right.label);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
373
430
|
return {
|
|
374
431
|
count: items.length,
|
|
375
|
-
attachedCount: items.filter((item) =>
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
items
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
432
|
+
attachedCount: items.filter((item) => item.status === 'attached').length,
|
|
433
|
+
windowOnlyCount: items.filter((item) => item.status === 'window-only').length,
|
|
434
|
+
detachedCount: items.filter((item) => item.status === 'detached').length,
|
|
435
|
+
tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
|
|
436
|
+
items
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
382
440
|
async function buildPopupState(): Promise<PopupState> {
|
|
383
441
|
const config = await getConfig();
|
|
384
|
-
const sessionBindings = await summarizeSessionBindings(
|
|
442
|
+
const sessionBindings = await summarizeSessionBindings();
|
|
385
443
|
const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
|
|
386
444
|
let connectionState: PopupState['connectionState'];
|
|
387
445
|
if (!config.token) {
|
package/src/popup.ts
CHANGED
|
@@ -21,6 +21,7 @@ const sessionSummaryEl = document.getElementById('sessionSummary') as HTMLDivEle
|
|
|
21
21
|
const sessionCardsEl = document.getElementById('sessionCards') as HTMLDivElement;
|
|
22
22
|
|
|
23
23
|
type PopupTone = 'neutral' | 'success' | 'warning' | 'error';
|
|
24
|
+
type PopupSessionBindingStatus = 'attached' | 'window-only' | 'detached';
|
|
24
25
|
|
|
25
26
|
interface PopupState {
|
|
26
27
|
ok: boolean;
|
|
@@ -42,6 +43,7 @@ interface PopupState {
|
|
|
42
43
|
sessionBindings: {
|
|
43
44
|
count: number;
|
|
44
45
|
attachedCount: number;
|
|
46
|
+
windowOnlyCount: number;
|
|
45
47
|
detachedCount: number;
|
|
46
48
|
tabCount: number;
|
|
47
49
|
items: Array<{
|
|
@@ -53,7 +55,7 @@ interface PopupState {
|
|
|
53
55
|
activeTabUrl: string | null;
|
|
54
56
|
windowId: number | null;
|
|
55
57
|
groupId: number | null;
|
|
56
|
-
|
|
58
|
+
status: PopupSessionBindingStatus;
|
|
57
59
|
lastBindingUpdateAt: number | null;
|
|
58
60
|
lastBindingUpdateReason: string | null;
|
|
59
61
|
}>;
|
|
@@ -158,6 +160,8 @@ function describeStatus(state: PopupState): StatusDescriptor {
|
|
|
158
160
|
const body =
|
|
159
161
|
state.sessionBindings.detachedCount > 0
|
|
160
162
|
? `${pluralize(state.sessionBindings.detachedCount, 'remembered session')} are detached. Check the cards below before you continue browser work.`
|
|
163
|
+
: state.sessionBindings.windowOnlyCount > 0
|
|
164
|
+
? `${pluralize(state.sessionBindings.windowOnlyCount, 'remembered session')} still point at a live window, but do not own tabs yet.`
|
|
161
165
|
: 'The extension bridge is healthy and ready for CLI-driven browser work.';
|
|
162
166
|
return {
|
|
163
167
|
badge: 'Ready',
|
|
@@ -169,6 +173,11 @@ function describeStatus(state: PopupState): StatusDescriptor {
|
|
|
169
173
|
'Resume or recreate detached work from the bak CLI before sending new page commands.',
|
|
170
174
|
'Use the Sessions panel below to confirm which remembered session lost its owned tabs.'
|
|
171
175
|
]
|
|
176
|
+
: state.sessionBindings.windowOnlyCount > 0
|
|
177
|
+
? [
|
|
178
|
+
'New bak page commands can reuse those remembered windows without disturbing your human tabs.',
|
|
179
|
+
'Use the Sessions panel below to confirm which binding is only waiting for a new owned tab.'
|
|
180
|
+
]
|
|
172
181
|
: [
|
|
173
182
|
'Start browser work from the bak CLI.',
|
|
174
183
|
'Use Reconnect bridge only when you intentionally changed token or port settings.'
|
|
@@ -311,6 +320,7 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
311
320
|
sessionSummaryEl.textContent =
|
|
312
321
|
`${pluralize(state.count, 'session')}, ` +
|
|
313
322
|
`${pluralize(state.attachedCount, 'attached binding')}, ` +
|
|
323
|
+
`${pluralize(state.windowOnlyCount, 'window-only binding')}, ` +
|
|
314
324
|
`${pluralize(state.detachedCount, 'detached binding')}, ` +
|
|
315
325
|
`${pluralize(state.tabCount, 'tracked tab')}`;
|
|
316
326
|
}
|
|
@@ -328,7 +338,7 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
328
338
|
for (const item of state.items) {
|
|
329
339
|
const card = document.createElement('section');
|
|
330
340
|
card.className = 'session-card';
|
|
331
|
-
card.dataset.
|
|
341
|
+
card.dataset.status = item.status;
|
|
332
342
|
|
|
333
343
|
const header = document.createElement('div');
|
|
334
344
|
header.className = 'session-card-header';
|
|
@@ -348,19 +358,24 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
348
358
|
|
|
349
359
|
const badge = document.createElement('span');
|
|
350
360
|
badge.className = 'session-badge';
|
|
351
|
-
badge.dataset.
|
|
352
|
-
badge.textContent = item.
|
|
361
|
+
badge.dataset.status = item.status;
|
|
362
|
+
badge.textContent = item.status === 'attached' ? 'Attached' : item.status === 'window-only' ? 'Window only' : 'Detached';
|
|
353
363
|
|
|
354
364
|
header.append(titleWrap, badge);
|
|
355
365
|
|
|
356
366
|
const activeTitle = document.createElement('div');
|
|
357
367
|
activeTitle.className = 'session-active-title';
|
|
358
|
-
activeTitle.textContent =
|
|
368
|
+
activeTitle.textContent =
|
|
369
|
+
item.activeTabTitle
|
|
370
|
+
? truncate(item.activeTabTitle, 72)
|
|
371
|
+
: item.status === 'window-only'
|
|
372
|
+
? 'No owned tab yet'
|
|
373
|
+
: 'No active tab title';
|
|
359
374
|
activeTitle.title = item.activeTabTitle ?? '';
|
|
360
375
|
|
|
361
376
|
const activeUrl = document.createElement('div');
|
|
362
377
|
activeUrl.className = 'session-active-url';
|
|
363
|
-
activeUrl.textContent = formatUrl(item.activeTabUrl);
|
|
378
|
+
activeUrl.textContent = item.status === 'window-only' && !item.activeTabUrl ? 'Next bak page command can reuse this window' : formatUrl(item.activeTabUrl);
|
|
364
379
|
activeUrl.title = item.activeTabUrl ?? '';
|
|
365
380
|
|
|
366
381
|
const meta = document.createElement('div');
|
|
@@ -380,9 +395,12 @@ function renderSessionBindings(state: PopupState['sessionBindings']): void {
|
|
|
380
395
|
|
|
381
396
|
const footer = document.createElement('div');
|
|
382
397
|
footer.className = 'session-card-footer';
|
|
383
|
-
footer.textContent =
|
|
384
|
-
|
|
385
|
-
|
|
398
|
+
footer.textContent =
|
|
399
|
+
item.status === 'attached'
|
|
400
|
+
? 'The saved binding still points at live browser tabs.'
|
|
401
|
+
: item.status === 'window-only'
|
|
402
|
+
? 'bak still remembers the target window for this session, but it does not own any live tabs right now.'
|
|
403
|
+
: 'bak still remembers this session, but its owned tabs or window are missing.';
|
|
386
404
|
|
|
387
405
|
card.append(header, activeTitle, activeUrl, meta, footer);
|
|
388
406
|
sessionCardsEl.appendChild(card);
|
|
@@ -18,10 +18,16 @@ function isSessionBindingRecord(value: unknown): value is SessionBindingRecord {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function cloneSessionBindingRecord(state: SessionBindingRecord): SessionBindingRecord {
|
|
21
|
-
|
|
21
|
+
const cloned: SessionBindingRecord = {
|
|
22
22
|
...state,
|
|
23
23
|
tabIds: [...state.tabIds]
|
|
24
24
|
};
|
|
25
|
+
if (cloned.tabIds.length === 0) {
|
|
26
|
+
cloned.groupId = null;
|
|
27
|
+
cloned.activeTabId = null;
|
|
28
|
+
cloned.primaryTabId = null;
|
|
29
|
+
}
|
|
30
|
+
return cloned;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
function normalizeSessionBindingRecordMap(value: unknown): { found: boolean; map: Record<string, SessionBindingRecord> } {
|
package/src/session-binding.ts
CHANGED
|
@@ -482,10 +482,10 @@ class SessionBindingManager {
|
|
|
482
482
|
};
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
|
|
486
|
-
const binding = await this.inspectBinding(bindingId);
|
|
487
|
-
if (!binding) {
|
|
488
|
-
throw new Error(`Binding ${bindingId} does not exist`);
|
|
485
|
+
async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
|
|
486
|
+
const binding = await this.inspectBinding(bindingId);
|
|
487
|
+
if (!binding) {
|
|
488
|
+
throw new Error(`Binding ${bindingId} does not exist`);
|
|
489
489
|
}
|
|
490
490
|
const resolvedTabId =
|
|
491
491
|
typeof tabId === 'number'
|
|
@@ -505,13 +505,43 @@ class SessionBindingManager {
|
|
|
505
505
|
closedTabId: resolvedTabId
|
|
506
506
|
};
|
|
507
507
|
}
|
|
508
|
-
|
|
509
|
-
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
510
|
-
|
|
511
|
-
binding.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
508
|
+
|
|
509
|
+
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
510
|
+
if (tabs.length === 0) {
|
|
511
|
+
const liveWindow = binding.windowId !== null ? await this.waitForWindow(binding.windowId, 300) : null;
|
|
512
|
+
if (!liveWindow) {
|
|
513
|
+
await this.storage.delete(binding.id);
|
|
514
|
+
return {
|
|
515
|
+
binding: null,
|
|
516
|
+
closedTabId: resolvedTabId
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const nextState: SessionBindingRecord = {
|
|
521
|
+
id: binding.id,
|
|
522
|
+
label: binding.label,
|
|
523
|
+
color: binding.color,
|
|
524
|
+
windowId: liveWindow.id,
|
|
525
|
+
groupId: null,
|
|
526
|
+
tabIds: [],
|
|
527
|
+
activeTabId: null,
|
|
528
|
+
primaryTabId: null
|
|
529
|
+
};
|
|
530
|
+
await this.storage.save(nextState);
|
|
531
|
+
return {
|
|
532
|
+
binding: {
|
|
533
|
+
...nextState,
|
|
534
|
+
tabs: []
|
|
535
|
+
},
|
|
536
|
+
closedTabId: resolvedTabId
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const nextPrimaryTabId =
|
|
541
|
+
binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
|
|
542
|
+
const nextActiveTabId =
|
|
543
|
+
binding.activeTabId === resolvedTabId
|
|
544
|
+
? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
|
|
515
545
|
: binding.activeTabId;
|
|
516
546
|
const nextState: SessionBindingRecord = {
|
|
517
547
|
id: binding.id,
|