@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/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 previouslyFocusedTab =
373
- previouslyFocusedWindow?.id !== undefined
374
- ? (await chrome.tabs.query({ windowId: previouslyFocusedWindow.id, active: true })).find((tab) => typeof tab.id === 'number') ?? null
375
- : null;
376
- const created = await chrome.windows.create({
377
- url: options.url ?? 'about:blank',
378
- focused: true
379
- });
380
- if (!created || typeof created.id !== 'number') {
381
- throw new Error('Window missing id');
382
- }
383
- if (options.focused !== true && previouslyFocusedWindow?.id && previouslyFocusedWindow.id !== created.id) {
384
- await chrome.windows.update(previouslyFocusedWindow.id, { focused: true });
385
- if (typeof previouslyFocusedTab?.id === 'number') {
386
- await chrome.tabs.update(previouslyFocusedTab.id, { active: true });
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
- reconnectTimer = setTimeout(() => {
2343
- reconnectTimer = null;
2344
- nextReconnectInMs = null;
2345
- void connectWebSocket();
2346
- }, delayMs) as unknown as number;
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
- ws = new WebSocket(url);
2371
-
2372
- ws.addEventListener('open', () => {
2373
- manualDisconnect = false;
2374
- reconnectAttempt = 0;
2375
- lastError = null;
2376
- ws?.send(JSON.stringify({
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
- ws.addEventListener('message', (event) => {
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
- ws.addEventListener('close', () => {
2409
- ws = null;
2410
- scheduleReconnect('socket-closed');
2411
- });
2412
-
2413
- ws.addEventListener('error', () => {
2414
- setRuntimeError('Cannot connect to bak cli', 'socket');
2415
- ws?.close();
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
- const nextState: SessionBindingRecord = {
2491
- ...state,
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
- void setConfig({
2523
- token: message.token,
2524
- port: Number(message.port ?? DEFAULT_PORT),
2525
- debugRichText: message.debugRichText === true
2526
- }).then(() => {
2527
- ws?.close();
2528
- void connectWebSocket().then(() => sendResponse({ ok: true }));
2529
- });
2530
- return true;
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
- return false;
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