@flrande/bak-extension 0.6.11 → 0.6.13
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 +588 -90
- package/dist/content.global.js +530 -12
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +233 -101
- package/dist/popup.html +260 -37
- package/package.json +2 -2
- package/public/popup.html +260 -37
- package/src/background.ts +344 -296
- package/src/content.ts +390 -22
- package/src/dynamic-data-tools.ts +790 -0
- package/src/popup.ts +291 -108
package/dist/popup.global.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
3
|
// src/popup.ts
|
|
4
|
+
var statusPanelEl = document.getElementById("statusPanel");
|
|
5
|
+
var statusBadgeEl = document.getElementById("statusBadge");
|
|
4
6
|
var statusEl = document.getElementById("status");
|
|
5
|
-
var
|
|
7
|
+
var statusBodyEl = document.getElementById("statusBody");
|
|
8
|
+
var recoveryListEl = document.getElementById("recoveryList");
|
|
6
9
|
var tokenInput = document.getElementById("token");
|
|
7
10
|
var portInput = document.getElementById("port");
|
|
8
11
|
var debugRichTextInput = document.getElementById("debugRichText");
|
|
@@ -18,24 +21,8 @@
|
|
|
18
21
|
var lastBindingUpdateEl = document.getElementById("lastBindingUpdate");
|
|
19
22
|
var extensionVersionEl = document.getElementById("extensionVersion");
|
|
20
23
|
var sessionSummaryEl = document.getElementById("sessionSummary");
|
|
21
|
-
var
|
|
24
|
+
var sessionCardsEl = document.getElementById("sessionCards");
|
|
22
25
|
var latestState = null;
|
|
23
|
-
function setStatus(text, tone = "neutral") {
|
|
24
|
-
statusEl.textContent = text;
|
|
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
26
|
function pluralize(count, singular, plural = `${singular}s`) {
|
|
40
27
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
41
28
|
}
|
|
@@ -55,24 +42,29 @@
|
|
|
55
42
|
return `${deltaMinutes}m ago`;
|
|
56
43
|
}
|
|
57
44
|
const deltaHours = Math.round(deltaMinutes / 60);
|
|
58
|
-
|
|
45
|
+
if (deltaHours < 48) {
|
|
46
|
+
return `${deltaHours}h ago`;
|
|
47
|
+
}
|
|
48
|
+
const deltaDays = Math.round(deltaHours / 24);
|
|
49
|
+
return `${deltaDays}d ago`;
|
|
59
50
|
}
|
|
60
|
-
function
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
} else {
|
|
64
|
-
sessionSummaryEl.textContent = `${pluralize(state.count, "session")}, ${pluralize(state.attachedCount, "attached binding")}, ${pluralize(state.tabCount, "tab")}, ${pluralize(state.detachedCount, "detached binding")}`;
|
|
51
|
+
function truncate(value, maxLength) {
|
|
52
|
+
if (value.length <= maxLength) {
|
|
53
|
+
return value;
|
|
65
54
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
55
|
+
return `${value.slice(0, Math.max(0, maxLength - 1))}...`;
|
|
56
|
+
}
|
|
57
|
+
function formatUrl(url) {
|
|
58
|
+
if (!url) {
|
|
59
|
+
return "No active tab URL";
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const parsed = new URL(url);
|
|
63
|
+
const trimmedPath = parsed.pathname === "/" ? "" : parsed.pathname;
|
|
64
|
+
const trimmedQuery = parsed.search.length > 0 ? parsed.search : "";
|
|
65
|
+
return truncate(`${parsed.host}${trimmedPath}${trimmedQuery}`, 64);
|
|
66
|
+
} catch {
|
|
67
|
+
return truncate(url, 64);
|
|
76
68
|
}
|
|
77
69
|
}
|
|
78
70
|
function describeConnectionState(connectionState) {
|
|
@@ -92,6 +84,113 @@
|
|
|
92
84
|
return "disconnected";
|
|
93
85
|
}
|
|
94
86
|
}
|
|
87
|
+
function setStatus(descriptor) {
|
|
88
|
+
statusPanelEl.dataset.tone = descriptor.tone;
|
|
89
|
+
statusBadgeEl.dataset.tone = descriptor.tone;
|
|
90
|
+
statusBadgeEl.textContent = descriptor.badge;
|
|
91
|
+
statusEl.textContent = descriptor.title;
|
|
92
|
+
statusBodyEl.textContent = descriptor.body;
|
|
93
|
+
recoveryListEl.replaceChildren();
|
|
94
|
+
for (const step of descriptor.recoverySteps) {
|
|
95
|
+
const li = document.createElement("li");
|
|
96
|
+
li.textContent = step;
|
|
97
|
+
recoveryListEl.appendChild(li);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function describeStatus(state) {
|
|
101
|
+
const lastError = `${state.lastErrorContext ?? ""} ${state.lastError ?? ""}`.toLowerCase();
|
|
102
|
+
const runtimeOffline = lastError.includes("cannot connect to bak cli");
|
|
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.";
|
|
105
|
+
return {
|
|
106
|
+
badge: "Ready",
|
|
107
|
+
title: "Connected to the local bak runtime",
|
|
108
|
+
body,
|
|
109
|
+
tone: "success",
|
|
110
|
+
recoverySteps: state.sessionBindings.detachedCount > 0 ? [
|
|
111
|
+
"Resume or recreate detached work from the bak CLI before sending new page commands.",
|
|
112
|
+
"Use the Sessions panel below to confirm which remembered session lost its owned tabs."
|
|
113
|
+
] : [
|
|
114
|
+
"Start browser work from the bak CLI.",
|
|
115
|
+
"Use Reconnect bridge only when you intentionally changed token or port settings."
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (state.connectionState === "missing-token") {
|
|
120
|
+
return {
|
|
121
|
+
badge: "Action needed",
|
|
122
|
+
title: "Pair token is required",
|
|
123
|
+
body: "This browser profile does not have a saved token yet, so the extension cannot pair with bak.",
|
|
124
|
+
tone: "error",
|
|
125
|
+
recoverySteps: [
|
|
126
|
+
"Run `bak setup` if you need a fresh token.",
|
|
127
|
+
`Paste the token above, keep CLI port ${state.port}, and click Save settings.`,
|
|
128
|
+
"If the bridge still stays disconnected after saving, click Reconnect bridge below."
|
|
129
|
+
]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (state.connectionState === "manual") {
|
|
133
|
+
return {
|
|
134
|
+
badge: "Paused",
|
|
135
|
+
title: "Extension bridge is paused",
|
|
136
|
+
body: "The bridge was manually disconnected. It will stay idle until you reconnect it.",
|
|
137
|
+
tone: "warning",
|
|
138
|
+
recoverySteps: [
|
|
139
|
+
"Click Reconnect bridge below when you want the extension live again.",
|
|
140
|
+
"If you changed the token or port, save the new settings first."
|
|
141
|
+
]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (runtimeOffline) {
|
|
145
|
+
return {
|
|
146
|
+
badge: "Runtime offline",
|
|
147
|
+
title: "The local bak runtime is not reachable",
|
|
148
|
+
body: `The extension cannot reach ${state.wsUrl}, so browser work cannot start yet.`,
|
|
149
|
+
tone: "warning",
|
|
150
|
+
recoverySteps: [
|
|
151
|
+
`Run \`bak doctor --port ${state.port}\` in PowerShell 7.`,
|
|
152
|
+
"If you just upgraded bak, reload the unpacked extension in chrome://extensions or edge://extensions.",
|
|
153
|
+
"Leave this popup open for a moment after doctor so the bridge can retry."
|
|
154
|
+
]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (state.connectionState === "reconnecting") {
|
|
158
|
+
return {
|
|
159
|
+
badge: "Retrying",
|
|
160
|
+
title: "The extension is trying to reconnect",
|
|
161
|
+
body: "The bridge is retrying in the background. You only need to intervene if the retry loop keeps failing.",
|
|
162
|
+
tone: "warning",
|
|
163
|
+
recoverySteps: [
|
|
164
|
+
"Wait for the current retry window to finish.",
|
|
165
|
+
"If you changed the token or port, click Save settings.",
|
|
166
|
+
"Use Reconnect bridge below if you want to retry immediately."
|
|
167
|
+
]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (state.lastError) {
|
|
171
|
+
return {
|
|
172
|
+
badge: "Check setup",
|
|
173
|
+
title: "The bridge needs attention",
|
|
174
|
+
body: `Last error: ${state.lastError}`,
|
|
175
|
+
tone: "error",
|
|
176
|
+
recoverySteps: [
|
|
177
|
+
`Confirm the saved token and CLI port ${state.port}.`,
|
|
178
|
+
`Run \`bak doctor --port ${state.port}\` from the bak CLI.`,
|
|
179
|
+
"Reload the unpacked extension if the runtime and token both look correct."
|
|
180
|
+
]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
badge: "Waiting",
|
|
185
|
+
title: "Not connected yet",
|
|
186
|
+
body: "The extension is ready to pair, but it is still waiting for the local bak runtime to come online.",
|
|
187
|
+
tone: "neutral",
|
|
188
|
+
recoverySteps: [
|
|
189
|
+
`Run \`bak doctor --port ${state.port}\` or another bak CLI command to wake the runtime.`,
|
|
190
|
+
"Return here and confirm the bridge reconnects automatically."
|
|
191
|
+
]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
95
194
|
function renderConnectionDetails(state) {
|
|
96
195
|
connectionStateEl.textContent = describeConnectionState(state.connectionState);
|
|
97
196
|
tokenStateEl.textContent = state.hasToken ? "configured" : "missing";
|
|
@@ -119,6 +218,79 @@
|
|
|
119
218
|
lastBindingUpdateEl.textContent = "none";
|
|
120
219
|
}
|
|
121
220
|
}
|
|
221
|
+
function createMetaRow(label, value) {
|
|
222
|
+
const row = document.createElement("div");
|
|
223
|
+
row.className = "session-meta-row";
|
|
224
|
+
const labelEl = document.createElement("span");
|
|
225
|
+
labelEl.className = "session-meta-label";
|
|
226
|
+
labelEl.textContent = label;
|
|
227
|
+
const valueEl = document.createElement("span");
|
|
228
|
+
valueEl.className = "session-meta-value";
|
|
229
|
+
valueEl.textContent = value;
|
|
230
|
+
row.append(labelEl, valueEl);
|
|
231
|
+
return row;
|
|
232
|
+
}
|
|
233
|
+
function renderSessionBindings(state) {
|
|
234
|
+
if (state.count === 0) {
|
|
235
|
+
sessionSummaryEl.textContent = "No remembered sessions";
|
|
236
|
+
} else {
|
|
237
|
+
sessionSummaryEl.textContent = `${pluralize(state.count, "session")}, ${pluralize(state.attachedCount, "attached binding")}, ${pluralize(state.detachedCount, "detached binding")}, ${pluralize(state.tabCount, "tracked tab")}`;
|
|
238
|
+
}
|
|
239
|
+
sessionCardsEl.replaceChildren();
|
|
240
|
+
if (state.items.length === 0) {
|
|
241
|
+
const empty = document.createElement("div");
|
|
242
|
+
empty.className = "session-empty";
|
|
243
|
+
empty.textContent = "No tracked sessions yet. Resolve a session from the bak CLI and it will appear here.";
|
|
244
|
+
sessionCardsEl.appendChild(empty);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
for (const item of state.items) {
|
|
248
|
+
const card = document.createElement("section");
|
|
249
|
+
card.className = "session-card";
|
|
250
|
+
card.dataset.detached = item.detached ? "true" : "false";
|
|
251
|
+
const header = document.createElement("div");
|
|
252
|
+
header.className = "session-card-header";
|
|
253
|
+
const titleWrap = document.createElement("div");
|
|
254
|
+
titleWrap.className = "session-card-title-wrap";
|
|
255
|
+
const title = document.createElement("div");
|
|
256
|
+
title.className = "session-card-title";
|
|
257
|
+
title.textContent = item.label;
|
|
258
|
+
const subtitle = document.createElement("div");
|
|
259
|
+
subtitle.className = "session-card-subtitle";
|
|
260
|
+
subtitle.textContent = item.id;
|
|
261
|
+
titleWrap.append(title, subtitle);
|
|
262
|
+
const badge = document.createElement("span");
|
|
263
|
+
badge.className = "session-badge";
|
|
264
|
+
badge.dataset.detached = item.detached ? "true" : "false";
|
|
265
|
+
badge.textContent = item.detached ? "Detached" : "Attached";
|
|
266
|
+
header.append(titleWrap, badge);
|
|
267
|
+
const activeTitle = document.createElement("div");
|
|
268
|
+
activeTitle.className = "session-active-title";
|
|
269
|
+
activeTitle.textContent = item.activeTabTitle ? truncate(item.activeTabTitle, 72) : "No active tab title";
|
|
270
|
+
activeTitle.title = item.activeTabTitle ?? "";
|
|
271
|
+
const activeUrl = document.createElement("div");
|
|
272
|
+
activeUrl.className = "session-active-url";
|
|
273
|
+
activeUrl.textContent = formatUrl(item.activeTabUrl);
|
|
274
|
+
activeUrl.title = item.activeTabUrl ?? "";
|
|
275
|
+
const meta = document.createElement("div");
|
|
276
|
+
meta.className = "session-meta-grid";
|
|
277
|
+
meta.append(
|
|
278
|
+
createMetaRow("Active tab", item.activeTabId === null ? "none" : `${item.activeTabId}`),
|
|
279
|
+
createMetaRow("Tabs", `${item.tabCount}`),
|
|
280
|
+
createMetaRow("Window", item.windowId === null ? "none" : `${item.windowId}`),
|
|
281
|
+
createMetaRow("Group", item.groupId === null ? "none" : `${item.groupId}`),
|
|
282
|
+
createMetaRow(
|
|
283
|
+
"Last binding",
|
|
284
|
+
item.lastBindingUpdateReason ? `${item.lastBindingUpdateReason} (${formatTimeAgo(item.lastBindingUpdateAt)})` : "none this session"
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
const footer = document.createElement("div");
|
|
288
|
+
footer.className = "session-card-footer";
|
|
289
|
+
footer.textContent = item.detached ? "bak still remembers this session, but its owned tabs or window are missing." : "The saved binding still points at live browser tabs.";
|
|
290
|
+
card.append(header, activeTitle, activeUrl, meta, footer);
|
|
291
|
+
sessionCardsEl.appendChild(card);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
122
294
|
function parsePortValue() {
|
|
123
295
|
const port = Number.parseInt(portInput.value.trim(), 10);
|
|
124
296
|
return Number.isInteger(port) && port > 0 ? port : null;
|
|
@@ -145,84 +317,44 @@
|
|
|
145
317
|
saveBtn.disabled = !dirty || validationError !== null;
|
|
146
318
|
saveBtn.textContent = state?.hasToken ? "Save settings" : "Save token";
|
|
147
319
|
}
|
|
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
|
-
}
|
|
199
320
|
async function refreshState() {
|
|
200
321
|
const state = await chrome.runtime.sendMessage({ type: "bak.getState" });
|
|
201
|
-
if (state.ok) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
renderSessionBindings(state.sessionBindings);
|
|
211
|
-
updateSaveState(state);
|
|
212
|
-
const status = describeStatus(state);
|
|
213
|
-
setStatus(status.text, status.tone);
|
|
214
|
-
statusNoteEl.textContent = status.note;
|
|
322
|
+
if (!state.ok) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const shouldSyncForm = !isFormDirty(latestState);
|
|
326
|
+
latestState = state;
|
|
327
|
+
if (shouldSyncForm) {
|
|
328
|
+
portInput.value = String(state.port);
|
|
329
|
+
debugRichTextInput.checked = Boolean(state.debugRichText);
|
|
330
|
+
tokenInput.value = "";
|
|
215
331
|
}
|
|
332
|
+
renderConnectionDetails(state);
|
|
333
|
+
renderSessionBindings(state.sessionBindings);
|
|
334
|
+
updateSaveState(state);
|
|
335
|
+
setStatus(describeStatus(state));
|
|
216
336
|
}
|
|
217
337
|
saveBtn.addEventListener("click", async () => {
|
|
218
338
|
const token = tokenInput.value.trim();
|
|
219
339
|
const port = parsePortValue();
|
|
220
340
|
if (!token && latestState?.hasToken !== true) {
|
|
221
|
-
setStatus(
|
|
341
|
+
setStatus({
|
|
342
|
+
badge: "Action needed",
|
|
343
|
+
title: "Pair token is required",
|
|
344
|
+
body: "Save a token before the extension can reconnect.",
|
|
345
|
+
tone: "error",
|
|
346
|
+
recoverySteps: ["Paste a valid token above, then click Save settings."]
|
|
347
|
+
});
|
|
222
348
|
return;
|
|
223
349
|
}
|
|
224
350
|
if (port === null) {
|
|
225
|
-
setStatus(
|
|
351
|
+
setStatus({
|
|
352
|
+
badge: "Invalid input",
|
|
353
|
+
title: "CLI port is invalid",
|
|
354
|
+
body: "Use a positive integer port before saving settings.",
|
|
355
|
+
tone: "error",
|
|
356
|
+
recoverySteps: ["Correct the port value above and try again."]
|
|
357
|
+
});
|
|
226
358
|
return;
|
|
227
359
|
}
|
|
228
360
|
await chrome.runtime.sendMessage({
|