@flrande/bak-extension 0.6.4 → 0.6.6
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 +147 -55
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +92 -2
- package/dist/popup.html +72 -1
- package/package.json +2 -2
- package/public/popup.html +72 -1
- package/src/background.ts +282 -131
- package/src/popup.ts +135 -11
- package/src/session-binding.ts +44 -50
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-13T07:15:48.660Z
|
|
@@ -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.6",
|
|
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",
|
|
@@ -573,13 +573,14 @@
|
|
|
573
573
|
state.activeTabId = null;
|
|
574
574
|
state.primaryTabId = null;
|
|
575
575
|
window2 = createdWindow;
|
|
576
|
-
|
|
576
|
+
const initialTab = typeof createdWindow.initialTabId === "number" ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id) : null;
|
|
577
|
+
tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
|
|
577
578
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
578
579
|
if (state.primaryTabId === null) {
|
|
579
|
-
state.primaryTabId = tabs[0]?.id ?? null;
|
|
580
|
+
state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
|
|
580
581
|
}
|
|
581
582
|
if (state.activeTabId === null) {
|
|
582
|
-
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? tabs[0]?.id ?? null;
|
|
583
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
|
|
583
584
|
}
|
|
584
585
|
repairActions.push(created ? "created-window" : "recreated-window");
|
|
585
586
|
}
|
|
@@ -838,22 +839,9 @@
|
|
|
838
839
|
await this.browser.closeTab(resolvedTabId);
|
|
839
840
|
const remainingTabIds = ensured.binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
|
|
840
841
|
if (remainingTabIds.length === 0) {
|
|
841
|
-
|
|
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);
|
|
842
|
+
await this.storage.delete(ensured.binding.id);
|
|
852
843
|
return {
|
|
853
|
-
binding:
|
|
854
|
-
...emptied,
|
|
855
|
-
tabs: []
|
|
856
|
-
},
|
|
844
|
+
binding: null,
|
|
857
845
|
closedTabId: resolvedTabId
|
|
858
846
|
};
|
|
859
847
|
}
|
|
@@ -1094,7 +1082,8 @@
|
|
|
1094
1082
|
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
1095
1083
|
focused: false
|
|
1096
1084
|
});
|
|
1097
|
-
const
|
|
1085
|
+
const initialTab = typeof window2.initialTabId === "number" ? await this.waitForTrackedTab(window2.initialTabId, window2.id) : null;
|
|
1086
|
+
const recreatedTabs = initialTab ? [initialTab] : await this.waitForWindowTabs(window2.id);
|
|
1098
1087
|
const firstTab = recreatedTabs[0] ?? null;
|
|
1099
1088
|
const tabIdMap = /* @__PURE__ */ new Map();
|
|
1100
1089
|
if (sourceTabs[0] && firstTab) {
|
|
@@ -1333,11 +1322,14 @@
|
|
|
1333
1322
|
var ws = null;
|
|
1334
1323
|
var reconnectTimer = null;
|
|
1335
1324
|
var nextReconnectInMs = null;
|
|
1325
|
+
var nextReconnectAt = null;
|
|
1336
1326
|
var reconnectAttempt = 0;
|
|
1337
1327
|
var lastError = null;
|
|
1338
1328
|
var manualDisconnect = false;
|
|
1339
1329
|
var sessionBindingStateMutationQueue = Promise.resolve();
|
|
1340
1330
|
var preserveHumanFocusDepth = 0;
|
|
1331
|
+
var lastBindingUpdateAt = null;
|
|
1332
|
+
var lastBindingUpdateReason = null;
|
|
1341
1333
|
async function getConfig() {
|
|
1342
1334
|
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
1343
1335
|
return {
|
|
@@ -1374,6 +1366,7 @@
|
|
|
1374
1366
|
reconnectTimer = null;
|
|
1375
1367
|
}
|
|
1376
1368
|
nextReconnectInMs = null;
|
|
1369
|
+
nextReconnectAt = null;
|
|
1377
1370
|
}
|
|
1378
1371
|
function sendResponse(payload) {
|
|
1379
1372
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -1469,6 +1462,65 @@
|
|
|
1469
1462
|
async function listSessionBindingStates() {
|
|
1470
1463
|
return Object.values(await loadSessionBindingStateMap());
|
|
1471
1464
|
}
|
|
1465
|
+
function summarizeSessionBindings(states) {
|
|
1466
|
+
const items = states.map((state) => {
|
|
1467
|
+
const detached = state.windowId === null || state.tabIds.length === 0;
|
|
1468
|
+
return {
|
|
1469
|
+
id: state.id,
|
|
1470
|
+
label: state.label,
|
|
1471
|
+
tabCount: state.tabIds.length,
|
|
1472
|
+
activeTabId: state.activeTabId,
|
|
1473
|
+
windowId: state.windowId,
|
|
1474
|
+
groupId: state.groupId,
|
|
1475
|
+
detached
|
|
1476
|
+
};
|
|
1477
|
+
});
|
|
1478
|
+
return {
|
|
1479
|
+
count: items.length,
|
|
1480
|
+
attachedCount: items.filter((item) => !item.detached).length,
|
|
1481
|
+
detachedCount: items.filter((item) => item.detached).length,
|
|
1482
|
+
tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
|
|
1483
|
+
items
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
async function buildPopupState() {
|
|
1487
|
+
const config = await getConfig();
|
|
1488
|
+
const sessionBindings = summarizeSessionBindings(await listSessionBindingStates());
|
|
1489
|
+
const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
|
|
1490
|
+
let connectionState;
|
|
1491
|
+
if (!config.token) {
|
|
1492
|
+
connectionState = "missing-token";
|
|
1493
|
+
} else if (ws?.readyState === WebSocket.OPEN) {
|
|
1494
|
+
connectionState = "connected";
|
|
1495
|
+
} else if (ws?.readyState === WebSocket.CONNECTING) {
|
|
1496
|
+
connectionState = "connecting";
|
|
1497
|
+
} else if (manualDisconnect) {
|
|
1498
|
+
connectionState = "manual";
|
|
1499
|
+
} else if (nextReconnectInMs !== null) {
|
|
1500
|
+
connectionState = "reconnecting";
|
|
1501
|
+
} else {
|
|
1502
|
+
connectionState = "disconnected";
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
ok: true,
|
|
1506
|
+
connected: ws?.readyState === WebSocket.OPEN,
|
|
1507
|
+
connectionState,
|
|
1508
|
+
hasToken: Boolean(config.token),
|
|
1509
|
+
port: config.port,
|
|
1510
|
+
wsUrl: `ws://127.0.0.1:${config.port}/extension`,
|
|
1511
|
+
debugRichText: config.debugRichText,
|
|
1512
|
+
lastError: lastError?.message ?? null,
|
|
1513
|
+
lastErrorAt: lastError?.at ?? null,
|
|
1514
|
+
lastErrorContext: lastError?.context ?? null,
|
|
1515
|
+
reconnectAttempt,
|
|
1516
|
+
nextReconnectInMs: reconnectRemainingMs,
|
|
1517
|
+
manualDisconnect,
|
|
1518
|
+
extensionVersion: EXTENSION_VERSION,
|
|
1519
|
+
lastBindingUpdateAt,
|
|
1520
|
+
lastBindingUpdateReason,
|
|
1521
|
+
sessionBindings
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1472
1524
|
async function saveSessionBindingState(state) {
|
|
1473
1525
|
await mutateSessionBindingStateMap((stateMap) => {
|
|
1474
1526
|
stateMap[state.id] = state;
|
|
@@ -1492,6 +1544,8 @@
|
|
|
1492
1544
|
};
|
|
1493
1545
|
}
|
|
1494
1546
|
function emitSessionBindingUpdated(bindingId, reason, state, extras = {}) {
|
|
1547
|
+
lastBindingUpdateAt = Date.now();
|
|
1548
|
+
lastBindingUpdateReason = reason;
|
|
1495
1549
|
sendEvent("sessionBinding.updated", {
|
|
1496
1550
|
bindingId,
|
|
1497
1551
|
reason,
|
|
@@ -1556,14 +1610,42 @@
|
|
|
1556
1610
|
},
|
|
1557
1611
|
async createWindow(options) {
|
|
1558
1612
|
const previouslyFocusedWindow = options.focused === true ? null : (await chrome.windows.getAll()).find((window2) => window2.focused === true && typeof window2.id === "number") ?? null;
|
|
1559
|
-
const
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1613
|
+
const previouslyFocusedTabs = previouslyFocusedWindow?.id !== void 0 ? await chrome.tabs.query({ windowId: previouslyFocusedWindow.id }) : [];
|
|
1614
|
+
const previouslyFocusedTabIds = new Set(
|
|
1615
|
+
previouslyFocusedTabs.flatMap((tab) => typeof tab.id === "number" ? [tab.id] : [])
|
|
1616
|
+
);
|
|
1617
|
+
const previouslyFocusedTab = previouslyFocusedTabs.find((tab) => tab.active === true && typeof tab.id === "number") ?? null;
|
|
1618
|
+
const desiredUrl = options.url ?? "about:blank";
|
|
1619
|
+
let created = await chrome.windows.create({
|
|
1620
|
+
url: desiredUrl,
|
|
1562
1621
|
focused: true
|
|
1563
1622
|
});
|
|
1564
1623
|
if (!created || typeof created.id !== "number") {
|
|
1565
1624
|
throw new Error("Window missing id");
|
|
1566
1625
|
}
|
|
1626
|
+
const pickSeedTab = async (windowId) => {
|
|
1627
|
+
const tabs = await chrome.tabs.query({ windowId });
|
|
1628
|
+
const newlyCreatedTab = windowId === previouslyFocusedWindow?.id ? tabs.find((tab) => typeof tab.id === "number" && !previouslyFocusedTabIds.has(tab.id)) : null;
|
|
1629
|
+
const normalizedDesiredUrl = normalizeComparableTabUrl(desiredUrl);
|
|
1630
|
+
return newlyCreatedTab ?? tabs.find((tab) => {
|
|
1631
|
+
const pendingUrl = "pendingUrl" in tab && typeof tab.pendingUrl === "string" ? tab.pendingUrl : "";
|
|
1632
|
+
return normalizeComparableTabUrl(tab.url ?? pendingUrl) === normalizedDesiredUrl;
|
|
1633
|
+
}) ?? tabs.find((tab) => tab.active === true && typeof tab.id === "number") ?? tabs.find((tab) => typeof tab.id === "number") ?? null;
|
|
1634
|
+
};
|
|
1635
|
+
let seedTab = await pickSeedTab(created.id);
|
|
1636
|
+
const createdWindowTabs = await chrome.tabs.query({ windowId: created.id });
|
|
1637
|
+
const createdWindowReusedFocusedWindow = previouslyFocusedWindow?.id === created.id;
|
|
1638
|
+
const createdWindowLooksDirty = createdWindowTabs.length > 1;
|
|
1639
|
+
if ((createdWindowReusedFocusedWindow || createdWindowLooksDirty) && typeof seedTab?.id === "number") {
|
|
1640
|
+
created = await chrome.windows.create({
|
|
1641
|
+
tabId: seedTab.id,
|
|
1642
|
+
focused: true
|
|
1643
|
+
});
|
|
1644
|
+
if (!created || typeof created.id !== "number") {
|
|
1645
|
+
throw new Error("Lifted window missing id");
|
|
1646
|
+
}
|
|
1647
|
+
seedTab = await pickSeedTab(created.id);
|
|
1648
|
+
}
|
|
1567
1649
|
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
1568
1650
|
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
1569
1651
|
if (typeof previouslyFocusedTab?.id === "number") {
|
|
@@ -1573,7 +1655,8 @@
|
|
|
1573
1655
|
const finalWindow = await chrome.windows.get(created.id);
|
|
1574
1656
|
return {
|
|
1575
1657
|
id: finalWindow.id,
|
|
1576
|
-
focused: Boolean(finalWindow.focused)
|
|
1658
|
+
focused: Boolean(finalWindow.focused),
|
|
1659
|
+
initialTabId: seedTab?.id ?? null
|
|
1577
1660
|
};
|
|
1578
1661
|
},
|
|
1579
1662
|
async updateWindow(windowId, options) {
|
|
@@ -3311,9 +3394,11 @@
|
|
|
3311
3394
|
const delayMs = computeReconnectDelayMs(reconnectAttempt);
|
|
3312
3395
|
reconnectAttempt += 1;
|
|
3313
3396
|
nextReconnectInMs = delayMs;
|
|
3397
|
+
nextReconnectAt = Date.now() + delayMs;
|
|
3314
3398
|
reconnectTimer = setTimeout(() => {
|
|
3315
3399
|
reconnectTimer = null;
|
|
3316
3400
|
nextReconnectInMs = null;
|
|
3401
|
+
nextReconnectAt = null;
|
|
3317
3402
|
void connectWebSocket();
|
|
3318
3403
|
}, delayMs);
|
|
3319
3404
|
if (!lastError) {
|
|
@@ -3334,19 +3419,23 @@
|
|
|
3334
3419
|
return;
|
|
3335
3420
|
}
|
|
3336
3421
|
const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
|
|
3337
|
-
|
|
3338
|
-
ws
|
|
3422
|
+
const socket = new WebSocket(url);
|
|
3423
|
+
ws = socket;
|
|
3424
|
+
socket.addEventListener("open", () => {
|
|
3425
|
+
if (ws !== socket) {
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3339
3428
|
manualDisconnect = false;
|
|
3340
3429
|
reconnectAttempt = 0;
|
|
3341
3430
|
lastError = null;
|
|
3342
|
-
|
|
3431
|
+
socket.send(JSON.stringify({
|
|
3343
3432
|
type: "hello",
|
|
3344
3433
|
role: "extension",
|
|
3345
3434
|
version: EXTENSION_VERSION,
|
|
3346
3435
|
ts: Date.now()
|
|
3347
3436
|
}));
|
|
3348
3437
|
});
|
|
3349
|
-
|
|
3438
|
+
socket.addEventListener("message", (event) => {
|
|
3350
3439
|
try {
|
|
3351
3440
|
const request = JSON.parse(String(event.data));
|
|
3352
3441
|
if (!request.id || !request.method) {
|
|
@@ -3367,13 +3456,19 @@
|
|
|
3367
3456
|
});
|
|
3368
3457
|
}
|
|
3369
3458
|
});
|
|
3370
|
-
|
|
3459
|
+
socket.addEventListener("close", () => {
|
|
3460
|
+
if (ws !== socket) {
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3371
3463
|
ws = null;
|
|
3372
3464
|
scheduleReconnect("socket-closed");
|
|
3373
3465
|
});
|
|
3374
|
-
|
|
3466
|
+
socket.addEventListener("error", () => {
|
|
3467
|
+
if (ws !== socket) {
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3375
3470
|
setRuntimeError("Cannot connect to bak cli", "socket");
|
|
3376
|
-
|
|
3471
|
+
socket.close();
|
|
3377
3472
|
});
|
|
3378
3473
|
}
|
|
3379
3474
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
@@ -3385,6 +3480,11 @@
|
|
|
3385
3480
|
continue;
|
|
3386
3481
|
}
|
|
3387
3482
|
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
3483
|
+
if (nextTabIds.length === 0) {
|
|
3484
|
+
delete stateMap[bindingId];
|
|
3485
|
+
updates.push({ bindingId, state: null });
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3388
3488
|
const fallbackTabId = nextTabIds[0] ?? null;
|
|
3389
3489
|
const nextState = {
|
|
3390
3490
|
...state,
|
|
@@ -3440,16 +3540,8 @@
|
|
|
3440
3540
|
if (state.windowId !== windowId) {
|
|
3441
3541
|
continue;
|
|
3442
3542
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
windowId: null,
|
|
3446
|
-
groupId: null,
|
|
3447
|
-
tabIds: [],
|
|
3448
|
-
activeTabId: null,
|
|
3449
|
-
primaryTabId: null
|
|
3450
|
-
};
|
|
3451
|
-
stateMap[bindingId] = nextState;
|
|
3452
|
-
updates.push({ bindingId, state: nextState });
|
|
3543
|
+
delete stateMap[bindingId];
|
|
3544
|
+
updates.push({ bindingId, state: null });
|
|
3453
3545
|
}
|
|
3454
3546
|
return updates;
|
|
3455
3547
|
}).then((updates) => {
|
|
@@ -3468,8 +3560,9 @@
|
|
|
3468
3560
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse2) => {
|
|
3469
3561
|
if (message?.type === "bak.updateConfig") {
|
|
3470
3562
|
manualDisconnect = false;
|
|
3563
|
+
const token = typeof message.token === "string" ? message.token.trim() : "";
|
|
3471
3564
|
void setConfig({
|
|
3472
|
-
token:
|
|
3565
|
+
...token ? { token } : {},
|
|
3473
3566
|
port: Number(message.port ?? DEFAULT_PORT),
|
|
3474
3567
|
debugRichText: message.debugRichText === true
|
|
3475
3568
|
}).then(() => {
|
|
@@ -3479,19 +3572,8 @@
|
|
|
3479
3572
|
return true;
|
|
3480
3573
|
}
|
|
3481
3574
|
if (message?.type === "bak.getState") {
|
|
3482
|
-
void
|
|
3483
|
-
sendResponse2(
|
|
3484
|
-
ok: true,
|
|
3485
|
-
connected: ws?.readyState === WebSocket.OPEN,
|
|
3486
|
-
hasToken: Boolean(config.token),
|
|
3487
|
-
port: config.port,
|
|
3488
|
-
debugRichText: config.debugRichText,
|
|
3489
|
-
lastError: lastError?.message ?? null,
|
|
3490
|
-
lastErrorAt: lastError?.at ?? null,
|
|
3491
|
-
lastErrorContext: lastError?.context ?? null,
|
|
3492
|
-
reconnectAttempt,
|
|
3493
|
-
nextReconnectInMs
|
|
3494
|
-
});
|
|
3575
|
+
void buildPopupState().then((state) => {
|
|
3576
|
+
sendResponse2(state);
|
|
3495
3577
|
});
|
|
3496
3578
|
return true;
|
|
3497
3579
|
}
|
|
@@ -3499,11 +3581,21 @@
|
|
|
3499
3581
|
manualDisconnect = true;
|
|
3500
3582
|
clearReconnectTimer();
|
|
3501
3583
|
reconnectAttempt = 0;
|
|
3584
|
+
lastError = null;
|
|
3502
3585
|
ws?.close();
|
|
3503
3586
|
ws = null;
|
|
3504
3587
|
sendResponse2({ ok: true });
|
|
3505
3588
|
return false;
|
|
3506
3589
|
}
|
|
3590
|
+
if (message?.type === "bak.reconnectNow") {
|
|
3591
|
+
manualDisconnect = false;
|
|
3592
|
+
clearReconnectTimer();
|
|
3593
|
+
reconnectAttempt = 0;
|
|
3594
|
+
ws?.close();
|
|
3595
|
+
ws = null;
|
|
3596
|
+
void connectWebSocket().then(() => sendResponse2({ ok: true }));
|
|
3597
|
+
return true;
|
|
3598
|
+
}
|
|
3507
3599
|
return false;
|
|
3508
3600
|
});
|
|
3509
3601
|
})();
|
package/dist/manifest.json
CHANGED
package/dist/popup.global.js
CHANGED
|
@@ -6,18 +6,97 @@
|
|
|
6
6
|
var portInput = document.getElementById("port");
|
|
7
7
|
var debugRichTextInput = document.getElementById("debugRichText");
|
|
8
8
|
var saveBtn = document.getElementById("save");
|
|
9
|
+
var reconnectBtn = document.getElementById("reconnect");
|
|
9
10
|
var disconnectBtn = document.getElementById("disconnect");
|
|
11
|
+
var connectionStateEl = document.getElementById("connectionState");
|
|
12
|
+
var tokenStateEl = document.getElementById("tokenState");
|
|
13
|
+
var reconnectStateEl = document.getElementById("reconnectState");
|
|
14
|
+
var connectionUrlEl = document.getElementById("connectionUrl");
|
|
15
|
+
var lastErrorEl = document.getElementById("lastError");
|
|
16
|
+
var lastBindingUpdateEl = document.getElementById("lastBindingUpdate");
|
|
17
|
+
var extensionVersionEl = document.getElementById("extensionVersion");
|
|
18
|
+
var sessionSummaryEl = document.getElementById("sessionSummary");
|
|
19
|
+
var sessionListEl = document.getElementById("sessionList");
|
|
20
|
+
var latestState = null;
|
|
10
21
|
function setStatus(text, bad = false) {
|
|
11
22
|
statusEl.textContent = text;
|
|
12
23
|
statusEl.style.color = bad ? "#dc2626" : "#0f172a";
|
|
13
24
|
}
|
|
25
|
+
function formatTimeAgo(at) {
|
|
26
|
+
if (typeof at !== "number") {
|
|
27
|
+
return "never";
|
|
28
|
+
}
|
|
29
|
+
const deltaSeconds = Math.max(0, Math.round((Date.now() - at) / 1e3));
|
|
30
|
+
if (deltaSeconds < 5) {
|
|
31
|
+
return "just now";
|
|
32
|
+
}
|
|
33
|
+
if (deltaSeconds < 60) {
|
|
34
|
+
return `${deltaSeconds}s ago`;
|
|
35
|
+
}
|
|
36
|
+
const deltaMinutes = Math.round(deltaSeconds / 60);
|
|
37
|
+
if (deltaMinutes < 60) {
|
|
38
|
+
return `${deltaMinutes}m ago`;
|
|
39
|
+
}
|
|
40
|
+
const deltaHours = Math.round(deltaMinutes / 60);
|
|
41
|
+
return `${deltaHours}h ago`;
|
|
42
|
+
}
|
|
43
|
+
function renderSessionBindings(state) {
|
|
44
|
+
sessionSummaryEl.textContent = `${state.count} sessions, ${state.attachedCount} attached, ${state.tabCount} tabs, ${state.detachedCount} detached`;
|
|
45
|
+
sessionListEl.replaceChildren();
|
|
46
|
+
for (const item of state.items) {
|
|
47
|
+
const li = document.createElement("li");
|
|
48
|
+
const location = item.windowId === null ? "no window" : `window ${item.windowId}`;
|
|
49
|
+
const active = item.activeTabId === null ? "no active tab" : `active ${item.activeTabId}`;
|
|
50
|
+
li.textContent = `${item.label}: ${item.tabCount} tabs, ${location}, ${active}`;
|
|
51
|
+
if (item.detached) {
|
|
52
|
+
li.style.color = "#b45309";
|
|
53
|
+
}
|
|
54
|
+
sessionListEl.appendChild(li);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function renderConnectionDetails(state) {
|
|
58
|
+
connectionStateEl.textContent = state.connectionState;
|
|
59
|
+
tokenStateEl.textContent = state.hasToken ? "configured" : "missing";
|
|
60
|
+
connectionUrlEl.textContent = state.wsUrl;
|
|
61
|
+
extensionVersionEl.textContent = state.extensionVersion;
|
|
62
|
+
if (state.manualDisconnect) {
|
|
63
|
+
reconnectStateEl.textContent = "manual disconnect";
|
|
64
|
+
} else if (typeof state.nextReconnectInMs === "number") {
|
|
65
|
+
const seconds = Math.max(0, Math.ceil(state.nextReconnectInMs / 100) / 10);
|
|
66
|
+
reconnectStateEl.textContent = `attempt ${state.reconnectAttempt}, retry in ${seconds}s`;
|
|
67
|
+
} else if (state.connected) {
|
|
68
|
+
reconnectStateEl.textContent = "connected";
|
|
69
|
+
} else {
|
|
70
|
+
reconnectStateEl.textContent = "idle";
|
|
71
|
+
}
|
|
72
|
+
if (state.lastError) {
|
|
73
|
+
const context = state.lastErrorContext ? `${state.lastErrorContext}: ` : "";
|
|
74
|
+
lastErrorEl.textContent = `${context}${state.lastError} (${formatTimeAgo(state.lastErrorAt)})`;
|
|
75
|
+
} else {
|
|
76
|
+
lastErrorEl.textContent = "none";
|
|
77
|
+
}
|
|
78
|
+
if (state.lastBindingUpdateReason) {
|
|
79
|
+
lastBindingUpdateEl.textContent = `${state.lastBindingUpdateReason} (${formatTimeAgo(state.lastBindingUpdateAt)})`;
|
|
80
|
+
} else {
|
|
81
|
+
lastBindingUpdateEl.textContent = "none";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
14
84
|
async function refreshState() {
|
|
15
85
|
const state = await chrome.runtime.sendMessage({ type: "bak.getState" });
|
|
16
86
|
if (state.ok) {
|
|
87
|
+
latestState = state;
|
|
17
88
|
portInput.value = String(state.port);
|
|
18
89
|
debugRichTextInput.checked = Boolean(state.debugRichText);
|
|
90
|
+
renderConnectionDetails(state);
|
|
91
|
+
renderSessionBindings(state.sessionBindings);
|
|
19
92
|
if (state.connected) {
|
|
20
93
|
setStatus("Connected to bak CLI");
|
|
94
|
+
} else if (state.connectionState === "missing-token") {
|
|
95
|
+
setStatus("Pair token is required", true);
|
|
96
|
+
} else if (state.connectionState === "manual") {
|
|
97
|
+
setStatus("Disconnected manually");
|
|
98
|
+
} else if (state.connectionState === "reconnecting") {
|
|
99
|
+
setStatus("Reconnecting to bak CLI", true);
|
|
21
100
|
} else if (state.lastError) {
|
|
22
101
|
setStatus(`Disconnected: ${state.lastError}`, true);
|
|
23
102
|
} else {
|
|
@@ -28,7 +107,7 @@
|
|
|
28
107
|
saveBtn.addEventListener("click", async () => {
|
|
29
108
|
const token = tokenInput.value.trim();
|
|
30
109
|
const port = Number.parseInt(portInput.value.trim(), 10);
|
|
31
|
-
if (!token) {
|
|
110
|
+
if (!token && latestState?.hasToken !== true) {
|
|
32
111
|
setStatus("Pair token is required", true);
|
|
33
112
|
return;
|
|
34
113
|
}
|
|
@@ -38,10 +117,15 @@
|
|
|
38
117
|
}
|
|
39
118
|
await chrome.runtime.sendMessage({
|
|
40
119
|
type: "bak.updateConfig",
|
|
41
|
-
token,
|
|
120
|
+
...token ? { token } : {},
|
|
42
121
|
port,
|
|
43
122
|
debugRichText: debugRichTextInput.checked
|
|
44
123
|
});
|
|
124
|
+
tokenInput.value = "";
|
|
125
|
+
await refreshState();
|
|
126
|
+
});
|
|
127
|
+
reconnectBtn.addEventListener("click", async () => {
|
|
128
|
+
await chrome.runtime.sendMessage({ type: "bak.reconnectNow" });
|
|
45
129
|
await refreshState();
|
|
46
130
|
});
|
|
47
131
|
disconnectBtn.addEventListener("click", async () => {
|
|
@@ -49,4 +133,10 @@
|
|
|
49
133
|
await refreshState();
|
|
50
134
|
});
|
|
51
135
|
void refreshState();
|
|
136
|
+
var refreshInterval = window.setInterval(() => {
|
|
137
|
+
void refreshState();
|
|
138
|
+
}, 1e3);
|
|
139
|
+
window.addEventListener("unload", () => {
|
|
140
|
+
window.clearInterval(refreshInterval);
|
|
141
|
+
});
|
|
52
142
|
})();
|
package/dist/popup.html
CHANGED
|
@@ -70,9 +70,50 @@
|
|
|
70
70
|
background: #e2e8f0;
|
|
71
71
|
color: #0f172a;
|
|
72
72
|
}
|
|
73
|
+
#reconnect {
|
|
74
|
+
background: #dbeafe;
|
|
75
|
+
color: #1d4ed8;
|
|
76
|
+
}
|
|
73
77
|
#status {
|
|
74
78
|
margin-top: 10px;
|
|
75
79
|
font-size: 12px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
}
|
|
82
|
+
.panel {
|
|
83
|
+
margin-top: 12px;
|
|
84
|
+
padding: 10px;
|
|
85
|
+
border: 1px solid #cbd5e1;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
background: rgba(255, 255, 255, 0.8);
|
|
88
|
+
}
|
|
89
|
+
.panel h2 {
|
|
90
|
+
margin: 0 0 8px;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
}
|
|
93
|
+
.meta-grid {
|
|
94
|
+
display: grid;
|
|
95
|
+
grid-template-columns: auto 1fr;
|
|
96
|
+
gap: 6px 10px;
|
|
97
|
+
font-size: 11px;
|
|
98
|
+
}
|
|
99
|
+
.meta-grid dt {
|
|
100
|
+
color: #475569;
|
|
101
|
+
}
|
|
102
|
+
.meta-grid dd {
|
|
103
|
+
margin: 0;
|
|
104
|
+
color: #0f172a;
|
|
105
|
+
word-break: break-word;
|
|
106
|
+
}
|
|
107
|
+
#sessionList {
|
|
108
|
+
margin: 0;
|
|
109
|
+
padding-left: 16px;
|
|
110
|
+
font-size: 11px;
|
|
111
|
+
color: #0f172a;
|
|
112
|
+
}
|
|
113
|
+
#sessionList:empty::before {
|
|
114
|
+
content: "No tracked sessions";
|
|
115
|
+
color: #64748b;
|
|
116
|
+
margin-left: -16px;
|
|
76
117
|
}
|
|
77
118
|
.hint {
|
|
78
119
|
margin-top: 10px;
|
|
@@ -85,7 +126,7 @@
|
|
|
85
126
|
<h1>Browser Agent Kit</h1>
|
|
86
127
|
<label>
|
|
87
128
|
Pair token
|
|
88
|
-
<input id="token" placeholder="paste token from `bak pair`" />
|
|
129
|
+
<input id="token" placeholder="paste token from `bak pair` or leave blank to keep the saved token" />
|
|
89
130
|
</label>
|
|
90
131
|
<label>
|
|
91
132
|
CLI port
|
|
@@ -97,9 +138,39 @@
|
|
|
97
138
|
</label>
|
|
98
139
|
<div class="row">
|
|
99
140
|
<button id="save">Save & Connect</button>
|
|
141
|
+
<button id="reconnect">Reconnect</button>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="row">
|
|
100
144
|
<button id="disconnect">Disconnect</button>
|
|
101
145
|
</div>
|
|
102
146
|
<div id="status">Checking...</div>
|
|
147
|
+
<div class="panel">
|
|
148
|
+
<h2>Connection</h2>
|
|
149
|
+
<dl class="meta-grid">
|
|
150
|
+
<dt>State</dt>
|
|
151
|
+
<dd id="connectionState">-</dd>
|
|
152
|
+
<dt>Token</dt>
|
|
153
|
+
<dd id="tokenState">-</dd>
|
|
154
|
+
<dt>Reconnect</dt>
|
|
155
|
+
<dd id="reconnectState">-</dd>
|
|
156
|
+
<dt>CLI URL</dt>
|
|
157
|
+
<dd id="connectionUrl">-</dd>
|
|
158
|
+
<dt>Last error</dt>
|
|
159
|
+
<dd id="lastError">-</dd>
|
|
160
|
+
<dt>Last binding</dt>
|
|
161
|
+
<dd id="lastBindingUpdate">-</dd>
|
|
162
|
+
<dt>Extension</dt>
|
|
163
|
+
<dd id="extensionVersion">-</dd>
|
|
164
|
+
</dl>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="panel">
|
|
167
|
+
<h2>Sessions</h2>
|
|
168
|
+
<dl class="meta-grid">
|
|
169
|
+
<dt>Tracked</dt>
|
|
170
|
+
<dd id="sessionSummary">-</dd>
|
|
171
|
+
</dl>
|
|
172
|
+
<ul id="sessionList"></ul>
|
|
173
|
+
</div>
|
|
103
174
|
<div class="hint">Extension only connects to ws://127.0.0.1</div>
|
|
104
175
|
<script src="./popup.global.js"></script>
|
|
105
176
|
</body>
|
package/package.json
CHANGED