@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
package/public/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/src/background.ts
CHANGED
|
@@ -59,11 +59,47 @@ interface ExtensionConfig {
|
|
|
59
59
|
debugRichText: boolean;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
interface RuntimeErrorDetails {
|
|
63
|
-
message: string;
|
|
64
|
-
context: 'config' | 'socket' | 'request' | 'parse';
|
|
65
|
-
at: number;
|
|
66
|
-
}
|
|
62
|
+
interface RuntimeErrorDetails {
|
|
63
|
+
message: string;
|
|
64
|
+
context: 'config' | 'socket' | 'request' | 'parse';
|
|
65
|
+
at: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface PopupSessionBindingSummary {
|
|
69
|
+
id: string;
|
|
70
|
+
label: string;
|
|
71
|
+
tabCount: number;
|
|
72
|
+
activeTabId: number | null;
|
|
73
|
+
windowId: number | null;
|
|
74
|
+
groupId: number | null;
|
|
75
|
+
detached: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface PopupState {
|
|
79
|
+
ok: true;
|
|
80
|
+
connected: boolean;
|
|
81
|
+
connectionState: 'connected' | 'connecting' | 'reconnecting' | 'disconnected' | 'manual' | 'missing-token';
|
|
82
|
+
hasToken: boolean;
|
|
83
|
+
port: number;
|
|
84
|
+
wsUrl: string;
|
|
85
|
+
debugRichText: boolean;
|
|
86
|
+
lastError: string | null;
|
|
87
|
+
lastErrorAt: number | null;
|
|
88
|
+
lastErrorContext: RuntimeErrorDetails['context'] | null;
|
|
89
|
+
reconnectAttempt: number;
|
|
90
|
+
nextReconnectInMs: number | null;
|
|
91
|
+
manualDisconnect: boolean;
|
|
92
|
+
extensionVersion: string;
|
|
93
|
+
lastBindingUpdateAt: number | null;
|
|
94
|
+
lastBindingUpdateReason: string | null;
|
|
95
|
+
sessionBindings: {
|
|
96
|
+
count: number;
|
|
97
|
+
attachedCount: number;
|
|
98
|
+
detachedCount: number;
|
|
99
|
+
tabCount: number;
|
|
100
|
+
items: PopupSessionBindingSummary[];
|
|
101
|
+
};
|
|
102
|
+
}
|
|
67
103
|
|
|
68
104
|
const DEFAULT_PORT = 17373;
|
|
69
105
|
const STORAGE_KEY_TOKEN = 'pairToken';
|
|
@@ -112,11 +148,14 @@ const REPLAY_FORBIDDEN_HEADER_NAMES = new Set([
|
|
|
112
148
|
let ws: WebSocket | null = null;
|
|
113
149
|
let reconnectTimer: number | null = null;
|
|
114
150
|
let nextReconnectInMs: number | null = null;
|
|
151
|
+
let nextReconnectAt: number | null = null;
|
|
115
152
|
let reconnectAttempt = 0;
|
|
116
153
|
let lastError: RuntimeErrorDetails | null = null;
|
|
117
154
|
let manualDisconnect = false;
|
|
118
155
|
let sessionBindingStateMutationQueue: Promise<void> = Promise.resolve();
|
|
119
156
|
let preserveHumanFocusDepth = 0;
|
|
157
|
+
let lastBindingUpdateAt: number | null = null;
|
|
158
|
+
let lastBindingUpdateReason: string | null = null;
|
|
120
159
|
|
|
121
160
|
async function getConfig(): Promise<ExtensionConfig> {
|
|
122
161
|
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
@@ -151,13 +190,14 @@ function setRuntimeError(message: string, context: RuntimeErrorDetails['context'
|
|
|
151
190
|
};
|
|
152
191
|
}
|
|
153
192
|
|
|
154
|
-
function clearReconnectTimer(): void {
|
|
155
|
-
if (reconnectTimer !== null) {
|
|
156
|
-
clearTimeout(reconnectTimer);
|
|
157
|
-
reconnectTimer = null;
|
|
158
|
-
}
|
|
159
|
-
nextReconnectInMs = null;
|
|
160
|
-
|
|
193
|
+
function clearReconnectTimer(): void {
|
|
194
|
+
if (reconnectTimer !== null) {
|
|
195
|
+
clearTimeout(reconnectTimer);
|
|
196
|
+
reconnectTimer = null;
|
|
197
|
+
}
|
|
198
|
+
nextReconnectInMs = null;
|
|
199
|
+
nextReconnectAt = null;
|
|
200
|
+
}
|
|
161
201
|
|
|
162
202
|
function sendResponse(payload: CliResponse): void {
|
|
163
203
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -264,9 +304,71 @@ async function loadSessionBindingState(bindingId: string): Promise<SessionBindin
|
|
|
264
304
|
return stateMap[bindingId] ?? null;
|
|
265
305
|
}
|
|
266
306
|
|
|
267
|
-
async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
268
|
-
return Object.values(await loadSessionBindingStateMap());
|
|
269
|
-
}
|
|
307
|
+
async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
308
|
+
return Object.values(await loadSessionBindingStateMap());
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function summarizeSessionBindings(states: SessionBindingRecord[]): PopupState['sessionBindings'] {
|
|
312
|
+
const items = states.map((state) => {
|
|
313
|
+
const detached = state.windowId === null || state.tabIds.length === 0;
|
|
314
|
+
return {
|
|
315
|
+
id: state.id,
|
|
316
|
+
label: state.label,
|
|
317
|
+
tabCount: state.tabIds.length,
|
|
318
|
+
activeTabId: state.activeTabId,
|
|
319
|
+
windowId: state.windowId,
|
|
320
|
+
groupId: state.groupId,
|
|
321
|
+
detached
|
|
322
|
+
} satisfies PopupSessionBindingSummary;
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
count: items.length,
|
|
326
|
+
attachedCount: items.filter((item) => !item.detached).length,
|
|
327
|
+
detachedCount: items.filter((item) => item.detached).length,
|
|
328
|
+
tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
|
|
329
|
+
items
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function buildPopupState(): Promise<PopupState> {
|
|
334
|
+
const config = await getConfig();
|
|
335
|
+
const sessionBindings = summarizeSessionBindings(await listSessionBindingStates());
|
|
336
|
+
const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
|
|
337
|
+
let connectionState: PopupState['connectionState'];
|
|
338
|
+
if (!config.token) {
|
|
339
|
+
connectionState = 'missing-token';
|
|
340
|
+
} else if (ws?.readyState === WebSocket.OPEN) {
|
|
341
|
+
connectionState = 'connected';
|
|
342
|
+
} else if (ws?.readyState === WebSocket.CONNECTING) {
|
|
343
|
+
connectionState = 'connecting';
|
|
344
|
+
} else if (manualDisconnect) {
|
|
345
|
+
connectionState = 'manual';
|
|
346
|
+
} else if (nextReconnectInMs !== null) {
|
|
347
|
+
connectionState = 'reconnecting';
|
|
348
|
+
} else {
|
|
349
|
+
connectionState = 'disconnected';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
connected: ws?.readyState === WebSocket.OPEN,
|
|
355
|
+
connectionState,
|
|
356
|
+
hasToken: Boolean(config.token),
|
|
357
|
+
port: config.port,
|
|
358
|
+
wsUrl: `ws://127.0.0.1:${config.port}/extension`,
|
|
359
|
+
debugRichText: config.debugRichText,
|
|
360
|
+
lastError: lastError?.message ?? null,
|
|
361
|
+
lastErrorAt: lastError?.at ?? null,
|
|
362
|
+
lastErrorContext: lastError?.context ?? null,
|
|
363
|
+
reconnectAttempt,
|
|
364
|
+
nextReconnectInMs: reconnectRemainingMs,
|
|
365
|
+
manualDisconnect,
|
|
366
|
+
extensionVersion: EXTENSION_VERSION,
|
|
367
|
+
lastBindingUpdateAt,
|
|
368
|
+
lastBindingUpdateReason,
|
|
369
|
+
sessionBindings
|
|
370
|
+
};
|
|
371
|
+
}
|
|
270
372
|
|
|
271
373
|
async function saveSessionBindingState(state: SessionBindingRecord): Promise<void> {
|
|
272
374
|
await mutateSessionBindingStateMap((stateMap) => {
|
|
@@ -299,6 +401,8 @@ function emitSessionBindingUpdated(
|
|
|
299
401
|
state: SessionBindingRecord | null,
|
|
300
402
|
extras: Record<string, unknown> = {}
|
|
301
403
|
): void {
|
|
404
|
+
lastBindingUpdateAt = Date.now();
|
|
405
|
+
lastBindingUpdateReason = reason;
|
|
302
406
|
sendEvent('sessionBinding.updated', {
|
|
303
407
|
bindingId,
|
|
304
408
|
reason,
|
|
@@ -307,7 +411,7 @@ function emitSessionBindingUpdated(
|
|
|
307
411
|
});
|
|
308
412
|
}
|
|
309
413
|
|
|
310
|
-
const sessionBindingBrowser: SessionBindingBrowser = {
|
|
414
|
+
const sessionBindingBrowser: SessionBindingBrowser = {
|
|
311
415
|
async getTab(tabId) {
|
|
312
416
|
try {
|
|
313
417
|
return toTabInfo(await chrome.tabs.get(tabId));
|
|
@@ -364,34 +468,71 @@ const sessionBindingBrowser: SessionBindingBrowser = {
|
|
|
364
468
|
return null;
|
|
365
469
|
}
|
|
366
470
|
},
|
|
367
|
-
async createWindow(options) {
|
|
368
|
-
const previouslyFocusedWindow =
|
|
369
|
-
options.focused === true
|
|
370
|
-
? null
|
|
371
|
-
: (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === 'number') ?? null;
|
|
372
|
-
const
|
|
373
|
-
previouslyFocusedWindow?.id !== undefined
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
471
|
+
async createWindow(options) {
|
|
472
|
+
const previouslyFocusedWindow =
|
|
473
|
+
options.focused === true
|
|
474
|
+
? null
|
|
475
|
+
: (await chrome.windows.getAll()).find((window) => window.focused === true && typeof window.id === 'number') ?? null;
|
|
476
|
+
const previouslyFocusedTabs =
|
|
477
|
+
previouslyFocusedWindow?.id !== undefined ? await chrome.tabs.query({ windowId: previouslyFocusedWindow.id }) : [];
|
|
478
|
+
const previouslyFocusedTabIds = new Set(
|
|
479
|
+
previouslyFocusedTabs.flatMap((tab) => (typeof tab.id === 'number' ? [tab.id] : []))
|
|
480
|
+
);
|
|
481
|
+
const previouslyFocusedTab =
|
|
482
|
+
previouslyFocusedTabs.find((tab) => tab.active === true && typeof tab.id === 'number') ?? null;
|
|
483
|
+
const desiredUrl = options.url ?? 'about:blank';
|
|
484
|
+
let created = await chrome.windows.create({
|
|
485
|
+
url: desiredUrl,
|
|
486
|
+
focused: true
|
|
487
|
+
});
|
|
488
|
+
if (!created || typeof created.id !== 'number') {
|
|
489
|
+
throw new Error('Window missing id');
|
|
490
|
+
}
|
|
491
|
+
const pickSeedTab = async (windowId: number): Promise<chrome.tabs.Tab | null> => {
|
|
492
|
+
const tabs = await chrome.tabs.query({ windowId });
|
|
493
|
+
const newlyCreatedTab =
|
|
494
|
+
windowId === previouslyFocusedWindow?.id
|
|
495
|
+
? tabs.find((tab) => typeof tab.id === 'number' && !previouslyFocusedTabIds.has(tab.id))
|
|
496
|
+
: null;
|
|
497
|
+
const normalizedDesiredUrl = normalizeComparableTabUrl(desiredUrl);
|
|
498
|
+
return (
|
|
499
|
+
newlyCreatedTab ??
|
|
500
|
+
tabs.find((tab) => {
|
|
501
|
+
const pendingUrl = 'pendingUrl' in tab && typeof tab.pendingUrl === 'string' ? tab.pendingUrl : '';
|
|
502
|
+
return normalizeComparableTabUrl(tab.url ?? pendingUrl) === normalizedDesiredUrl;
|
|
503
|
+
}) ??
|
|
504
|
+
tabs.find((tab) => tab.active === true && typeof tab.id === 'number') ??
|
|
505
|
+
tabs.find((tab) => typeof tab.id === 'number') ??
|
|
506
|
+
null
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
let seedTab = await pickSeedTab(created.id);
|
|
510
|
+
const createdWindowTabs = await chrome.tabs.query({ windowId: created.id });
|
|
511
|
+
const createdWindowReusedFocusedWindow = previouslyFocusedWindow?.id === created.id;
|
|
512
|
+
const createdWindowLooksDirty = createdWindowTabs.length > 1;
|
|
513
|
+
if ((createdWindowReusedFocusedWindow || createdWindowLooksDirty) && typeof seedTab?.id === 'number') {
|
|
514
|
+
created = await chrome.windows.create({
|
|
515
|
+
tabId: seedTab.id,
|
|
516
|
+
focused: true
|
|
517
|
+
});
|
|
518
|
+
if (!created || typeof created.id !== 'number') {
|
|
519
|
+
throw new Error('Lifted window missing id');
|
|
520
|
+
}
|
|
521
|
+
seedTab = await pickSeedTab(created.id);
|
|
522
|
+
}
|
|
523
|
+
if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
|
|
524
|
+
await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
|
|
525
|
+
if (typeof previouslyFocusedTab?.id === 'number') {
|
|
526
|
+
await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
|
|
527
|
+
}
|
|
388
528
|
}
|
|
389
529
|
const finalWindow = await chrome.windows.get(created.id);
|
|
390
|
-
return {
|
|
391
|
-
id: finalWindow.id!,
|
|
392
|
-
focused: Boolean(finalWindow.focused)
|
|
393
|
-
|
|
394
|
-
|
|
530
|
+
return {
|
|
531
|
+
id: finalWindow.id!,
|
|
532
|
+
focused: Boolean(finalWindow.focused),
|
|
533
|
+
initialTabId: seedTab?.id ?? null
|
|
534
|
+
};
|
|
535
|
+
},
|
|
395
536
|
async updateWindow(windowId, options) {
|
|
396
537
|
const updated = await chrome.windows.update(windowId, {
|
|
397
538
|
focused: options.focused
|
|
@@ -2336,14 +2477,16 @@ function scheduleReconnect(reason: string): void {
|
|
|
2336
2477
|
return;
|
|
2337
2478
|
}
|
|
2338
2479
|
|
|
2339
|
-
const delayMs = computeReconnectDelayMs(reconnectAttempt);
|
|
2340
|
-
reconnectAttempt += 1;
|
|
2341
|
-
nextReconnectInMs = delayMs;
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2480
|
+
const delayMs = computeReconnectDelayMs(reconnectAttempt);
|
|
2481
|
+
reconnectAttempt += 1;
|
|
2482
|
+
nextReconnectInMs = delayMs;
|
|
2483
|
+
nextReconnectAt = Date.now() + delayMs;
|
|
2484
|
+
reconnectTimer = setTimeout(() => {
|
|
2485
|
+
reconnectTimer = null;
|
|
2486
|
+
nextReconnectInMs = null;
|
|
2487
|
+
nextReconnectAt = null;
|
|
2488
|
+
void connectWebSocket();
|
|
2489
|
+
}, delayMs) as unknown as number;
|
|
2347
2490
|
|
|
2348
2491
|
if (!lastError) {
|
|
2349
2492
|
setRuntimeError(`Reconnect scheduled: ${reason}`, 'socket');
|
|
@@ -2366,26 +2509,30 @@ async function connectWebSocket(): Promise<void> {
|
|
|
2366
2509
|
return;
|
|
2367
2510
|
}
|
|
2368
2511
|
|
|
2369
|
-
const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2512
|
+
const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
|
|
2513
|
+
const socket = new WebSocket(url);
|
|
2514
|
+
ws = socket;
|
|
2515
|
+
|
|
2516
|
+
socket.addEventListener('open', () => {
|
|
2517
|
+
if (ws !== socket) {
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
manualDisconnect = false;
|
|
2521
|
+
reconnectAttempt = 0;
|
|
2522
|
+
lastError = null;
|
|
2523
|
+
socket.send(JSON.stringify({
|
|
2377
2524
|
type: 'hello',
|
|
2378
2525
|
role: 'extension',
|
|
2379
2526
|
version: EXTENSION_VERSION,
|
|
2380
2527
|
ts: Date.now()
|
|
2381
2528
|
}));
|
|
2382
2529
|
});
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
try {
|
|
2386
|
-
const request = JSON.parse(String(event.data)) as CliRequest;
|
|
2387
|
-
if (!request.id || !request.method) {
|
|
2388
|
-
return;
|
|
2530
|
+
|
|
2531
|
+
socket.addEventListener('message', (event) => {
|
|
2532
|
+
try {
|
|
2533
|
+
const request = JSON.parse(String(event.data)) as CliRequest;
|
|
2534
|
+
if (!request.id || !request.method) {
|
|
2535
|
+
return;
|
|
2389
2536
|
}
|
|
2390
2537
|
void handleRequest(request)
|
|
2391
2538
|
.then((result) => {
|
|
@@ -2402,29 +2549,40 @@ async function connectWebSocket(): Promise<void> {
|
|
|
2402
2549
|
ok: false,
|
|
2403
2550
|
error: toError('E_INTERNAL', error instanceof Error ? error.message : String(error))
|
|
2404
2551
|
});
|
|
2405
|
-
}
|
|
2406
|
-
});
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
ws
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
socket.addEventListener('close', () => {
|
|
2556
|
+
if (ws !== socket) {
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
ws = null;
|
|
2560
|
+
scheduleReconnect('socket-closed');
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2563
|
+
socket.addEventListener('error', () => {
|
|
2564
|
+
if (ws !== socket) {
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
setRuntimeError('Cannot connect to bak cli', 'socket');
|
|
2568
|
+
socket.close();
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2418
2571
|
|
|
2419
2572
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
2420
2573
|
dropNetworkCapture(tabId);
|
|
2421
2574
|
void mutateSessionBindingStateMap((stateMap) => {
|
|
2422
|
-
const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
|
|
2575
|
+
const updates: Array<{ bindingId: string; state: SessionBindingRecord | null }> = [];
|
|
2423
2576
|
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2424
2577
|
if (!state.tabIds.includes(tabId)) {
|
|
2425
2578
|
continue;
|
|
2426
2579
|
}
|
|
2427
2580
|
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
2581
|
+
if (nextTabIds.length === 0) {
|
|
2582
|
+
delete stateMap[bindingId];
|
|
2583
|
+
updates.push({ bindingId, state: null });
|
|
2584
|
+
continue;
|
|
2585
|
+
}
|
|
2428
2586
|
const fallbackTabId = nextTabIds[0] ?? null;
|
|
2429
2587
|
const nextState: SessionBindingRecord = {
|
|
2430
2588
|
...state,
|
|
@@ -2482,21 +2640,13 @@ chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
|
2482
2640
|
|
|
2483
2641
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
2484
2642
|
void mutateSessionBindingStateMap((stateMap) => {
|
|
2485
|
-
const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
|
|
2643
|
+
const updates: Array<{ bindingId: string; state: SessionBindingRecord | null }> = [];
|
|
2486
2644
|
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2487
2645
|
if (state.windowId !== windowId) {
|
|
2488
2646
|
continue;
|
|
2489
2647
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
windowId: null,
|
|
2493
|
-
groupId: null,
|
|
2494
|
-
tabIds: [],
|
|
2495
|
-
activeTabId: null,
|
|
2496
|
-
primaryTabId: null
|
|
2497
|
-
};
|
|
2498
|
-
stateMap[bindingId] = nextState;
|
|
2499
|
-
updates.push({ bindingId, state: nextState });
|
|
2648
|
+
delete stateMap[bindingId];
|
|
2649
|
+
updates.push({ bindingId, state: null });
|
|
2500
2650
|
}
|
|
2501
2651
|
return updates;
|
|
2502
2652
|
}).then((updates) => {
|
|
@@ -2517,48 +2667,49 @@ chrome.runtime.onStartup.addListener(() => {
|
|
|
2517
2667
|
void connectWebSocket();
|
|
2518
2668
|
|
|
2519
2669
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
2520
|
-
if (message?.type === 'bak.updateConfig') {
|
|
2521
|
-
manualDisconnect = false;
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
if (message?.type === 'bak.getState') {
|
|
2534
|
-
void getConfig().then((config) => {
|
|
2535
|
-
sendResponse({
|
|
2536
|
-
ok: true,
|
|
2537
|
-
connected: ws?.readyState === WebSocket.OPEN,
|
|
2538
|
-
hasToken: Boolean(config.token),
|
|
2539
|
-
port: config.port,
|
|
2540
|
-
debugRichText: config.debugRichText,
|
|
2541
|
-
lastError: lastError?.message ?? null,
|
|
2542
|
-
lastErrorAt: lastError?.at ?? null,
|
|
2543
|
-
lastErrorContext: lastError?.context ?? null,
|
|
2544
|
-
reconnectAttempt,
|
|
2545
|
-
nextReconnectInMs
|
|
2546
|
-
});
|
|
2547
|
-
});
|
|
2548
|
-
return true;
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
if (message?.type === 'bak.disconnect') {
|
|
2552
|
-
manualDisconnect = true;
|
|
2553
|
-
clearReconnectTimer();
|
|
2554
|
-
reconnectAttempt = 0;
|
|
2555
|
-
ws?.close();
|
|
2556
|
-
ws = null;
|
|
2557
|
-
sendResponse({ ok: true });
|
|
2558
|
-
return false;
|
|
2559
|
-
}
|
|
2670
|
+
if (message?.type === 'bak.updateConfig') {
|
|
2671
|
+
manualDisconnect = false;
|
|
2672
|
+
const token = typeof message.token === 'string' ? message.token.trim() : '';
|
|
2673
|
+
void setConfig({
|
|
2674
|
+
...(token ? { token } : {}),
|
|
2675
|
+
port: Number(message.port ?? DEFAULT_PORT),
|
|
2676
|
+
debugRichText: message.debugRichText === true
|
|
2677
|
+
}).then(() => {
|
|
2678
|
+
ws?.close();
|
|
2679
|
+
void connectWebSocket().then(() => sendResponse({ ok: true }));
|
|
2680
|
+
});
|
|
2681
|
+
return true;
|
|
2682
|
+
}
|
|
2560
2683
|
|
|
2561
|
-
|
|
2562
|
-
|
|
2684
|
+
if (message?.type === 'bak.getState') {
|
|
2685
|
+
void buildPopupState().then((state) => {
|
|
2686
|
+
sendResponse(state);
|
|
2687
|
+
});
|
|
2688
|
+
return true;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
if (message?.type === 'bak.disconnect') {
|
|
2692
|
+
manualDisconnect = true;
|
|
2693
|
+
clearReconnectTimer();
|
|
2694
|
+
reconnectAttempt = 0;
|
|
2695
|
+
lastError = null;
|
|
2696
|
+
ws?.close();
|
|
2697
|
+
ws = null;
|
|
2698
|
+
sendResponse({ ok: true });
|
|
2699
|
+
return false;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
if (message?.type === 'bak.reconnectNow') {
|
|
2703
|
+
manualDisconnect = false;
|
|
2704
|
+
clearReconnectTimer();
|
|
2705
|
+
reconnectAttempt = 0;
|
|
2706
|
+
ws?.close();
|
|
2707
|
+
ws = null;
|
|
2708
|
+
void connectWebSocket().then(() => sendResponse({ ok: true }));
|
|
2709
|
+
return true;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
return false;
|
|
2713
|
+
});
|
|
2563
2714
|
|
|
2564
2715
|
|