@flrande/bak-extension 0.6.3 → 0.6.5

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/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,10 +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();
156
+ let preserveHumanFocusDepth = 0;
157
+ let lastBindingUpdateAt: number | null = null;
158
+ let lastBindingUpdateReason: string | null = null;
119
159
 
120
160
  async function getConfig(): Promise<ExtensionConfig> {
121
161
  const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
@@ -150,19 +190,33 @@ function setRuntimeError(message: string, context: RuntimeErrorDetails['context'
150
190
  };
151
191
  }
152
192
 
153
- function clearReconnectTimer(): void {
154
- if (reconnectTimer !== null) {
155
- clearTimeout(reconnectTimer);
156
- reconnectTimer = null;
157
- }
158
- nextReconnectInMs = null;
159
- }
193
+ function clearReconnectTimer(): void {
194
+ if (reconnectTimer !== null) {
195
+ clearTimeout(reconnectTimer);
196
+ reconnectTimer = null;
197
+ }
198
+ nextReconnectInMs = null;
199
+ nextReconnectAt = null;
200
+ }
160
201
 
161
- function sendResponse(payload: CliResponse): void {
162
- if (ws && ws.readyState === WebSocket.OPEN) {
163
- ws.send(JSON.stringify(payload));
164
- }
165
- }
202
+ function sendResponse(payload: CliResponse): void {
203
+ if (ws && ws.readyState === WebSocket.OPEN) {
204
+ ws.send(JSON.stringify(payload));
205
+ }
206
+ }
207
+
208
+ function sendEvent(event: string, data: Record<string, unknown>): void {
209
+ if (ws && ws.readyState === WebSocket.OPEN) {
210
+ ws.send(
211
+ JSON.stringify({
212
+ type: 'event',
213
+ event,
214
+ data,
215
+ ts: Date.now()
216
+ })
217
+ );
218
+ }
219
+ }
166
220
 
167
221
  function toError(code: string, message: string, data?: Record<string, unknown>): CliResponse['error'] {
168
222
  return { code, message, data };
@@ -250,9 +304,71 @@ async function loadSessionBindingState(bindingId: string): Promise<SessionBindin
250
304
  return stateMap[bindingId] ?? null;
251
305
  }
252
306
 
253
- async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
254
- return Object.values(await loadSessionBindingStateMap());
255
- }
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
+ }
256
372
 
257
373
  async function saveSessionBindingState(state: SessionBindingRecord): Promise<void> {
258
374
  await mutateSessionBindingStateMap((stateMap) => {
@@ -265,6 +381,35 @@ async function deleteSessionBindingState(bindingId: string): Promise<void> {
265
381
  delete stateMap[bindingId];
266
382
  });
267
383
  }
384
+
385
+ function toSessionBindingEventBrowser(state: SessionBindingRecord | null): Record<string, unknown> | null {
386
+ if (!state) {
387
+ return null;
388
+ }
389
+ return {
390
+ windowId: state.windowId,
391
+ groupId: state.groupId,
392
+ tabIds: [...state.tabIds],
393
+ activeTabId: state.activeTabId,
394
+ primaryTabId: state.primaryTabId
395
+ };
396
+ }
397
+
398
+ function emitSessionBindingUpdated(
399
+ bindingId: string,
400
+ reason: string,
401
+ state: SessionBindingRecord | null,
402
+ extras: Record<string, unknown> = {}
403
+ ): void {
404
+ lastBindingUpdateAt = Date.now();
405
+ lastBindingUpdateReason = reason;
406
+ sendEvent('sessionBinding.updated', {
407
+ bindingId,
408
+ reason,
409
+ browser: toSessionBindingEventBrowser(state),
410
+ ...extras
411
+ });
412
+ }
268
413
 
269
414
  const sessionBindingBrowser: SessionBindingBrowser = {
270
415
  async getTab(tabId) {
@@ -673,18 +818,23 @@ async function restoreFocusContext(context: FocusContext): Promise<void> {
673
818
  }
674
819
  }
675
820
 
676
- async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
677
- if (!enabled) {
678
- return action();
679
- }
680
-
681
- const focusContext = await captureFocusContext();
682
- try {
683
- return await action();
684
- } finally {
685
- await restoreFocusContext(focusContext);
686
- }
687
- }
821
+ async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
822
+ if (!enabled) {
823
+ return action();
824
+ }
825
+
826
+ const focusContext = await captureFocusContext();
827
+ preserveHumanFocusDepth += 1;
828
+ try {
829
+ return await action();
830
+ } finally {
831
+ try {
832
+ await restoreFocusContext(focusContext);
833
+ } finally {
834
+ preserveHumanFocusDepth = Math.max(0, preserveHumanFocusDepth - 1);
835
+ }
836
+ }
837
+ }
688
838
 
689
839
  function requireRpcEnvelope(
690
840
  method: string,
@@ -1686,11 +1836,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1686
1836
  const result = await bindingManager.ensureBinding({
1687
1837
  bindingId: String(params.bindingId ?? ''),
1688
1838
  focus: params.focus === true,
1689
- initialUrl: typeof params.url === 'string' ? params.url : undefined
1839
+ initialUrl: typeof params.url === 'string' ? params.url : undefined,
1840
+ label: typeof params.label === 'string' ? params.label : undefined
1690
1841
  });
1691
1842
  for (const tab of result.binding.tabs) {
1692
1843
  void ensureNetworkDebugger(tab.id).catch(() => undefined);
1693
1844
  }
1845
+ emitSessionBindingUpdated(result.binding.id, 'ensure', result.binding);
1694
1846
  return {
1695
1847
  browser: result.binding,
1696
1848
  created: result.created,
@@ -1711,11 +1863,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1711
1863
  bindingId: String(params.bindingId ?? ''),
1712
1864
  url: expectedUrl,
1713
1865
  active: params.active === true,
1714
- focus: params.focus === true
1866
+ focus: params.focus === true,
1867
+ label: typeof params.label === 'string' ? params.label : undefined
1715
1868
  });
1716
1869
  });
1717
1870
  const finalized = await finalizeOpenedSessionBindingTab(opened, expectedUrl);
1718
1871
  void ensureNetworkDebugger(finalized.tab.id).catch(() => undefined);
1872
+ emitSessionBindingUpdated(finalized.binding.id, 'open-tab', finalized.binding);
1719
1873
  return {
1720
1874
  browser: finalized.binding,
1721
1875
  tab: finalized.tab
@@ -1738,6 +1892,7 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1738
1892
  case 'sessionBinding.setActiveTab': {
1739
1893
  const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.bindingId ?? ''));
1740
1894
  void ensureNetworkDebugger(result.tab.id).catch(() => undefined);
1895
+ emitSessionBindingUpdated(result.binding.id, 'set-active-tab', result.binding);
1741
1896
  return {
1742
1897
  browser: result.binding,
1743
1898
  tab: result.tab
@@ -1755,8 +1910,10 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1755
1910
  const result = await bindingManager.reset({
1756
1911
  bindingId: String(params.bindingId ?? ''),
1757
1912
  focus: params.focus === true,
1758
- initialUrl: typeof params.url === 'string' ? params.url : undefined
1913
+ initialUrl: typeof params.url === 'string' ? params.url : undefined,
1914
+ label: typeof params.label === 'string' ? params.label : undefined
1759
1915
  });
1916
+ emitSessionBindingUpdated(result.binding.id, 'reset', result.binding);
1760
1917
  return {
1761
1918
  browser: result.binding,
1762
1919
  created: result.created,
@@ -1765,8 +1922,22 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
1765
1922
  };
1766
1923
  });
1767
1924
  }
1925
+ case 'sessionBinding.closeTab': {
1926
+ const bindingId = String(params.bindingId ?? '');
1927
+ const result = await bindingManager.closeTab(bindingId, typeof params.tabId === 'number' ? params.tabId : undefined);
1928
+ emitSessionBindingUpdated(bindingId, 'close-tab', result.binding, {
1929
+ closedTabId: result.closedTabId
1930
+ });
1931
+ return {
1932
+ browser: result.binding,
1933
+ closedTabId: result.closedTabId
1934
+ };
1935
+ }
1768
1936
  case 'sessionBinding.close': {
1769
- return await bindingManager.close(String(params.bindingId ?? ''));
1937
+ const bindingId = String(params.bindingId ?? '');
1938
+ const result = await bindingManager.close(bindingId);
1939
+ emitSessionBindingUpdated(bindingId, 'close', null);
1940
+ return result;
1770
1941
  }
1771
1942
  case 'page.goto': {
1772
1943
  return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
@@ -2269,14 +2440,16 @@ function scheduleReconnect(reason: string): void {
2269
2440
  return;
2270
2441
  }
2271
2442
 
2272
- const delayMs = computeReconnectDelayMs(reconnectAttempt);
2273
- reconnectAttempt += 1;
2274
- nextReconnectInMs = delayMs;
2275
- reconnectTimer = setTimeout(() => {
2276
- reconnectTimer = null;
2277
- nextReconnectInMs = null;
2278
- void connectWebSocket();
2279
- }, delayMs) as unknown as number;
2443
+ const delayMs = computeReconnectDelayMs(reconnectAttempt);
2444
+ reconnectAttempt += 1;
2445
+ nextReconnectInMs = delayMs;
2446
+ nextReconnectAt = Date.now() + delayMs;
2447
+ reconnectTimer = setTimeout(() => {
2448
+ reconnectTimer = null;
2449
+ nextReconnectInMs = null;
2450
+ nextReconnectAt = null;
2451
+ void connectWebSocket();
2452
+ }, delayMs) as unknown as number;
2280
2453
 
2281
2454
  if (!lastError) {
2282
2455
  setRuntimeError(`Reconnect scheduled: ${reason}`, 'socket');
@@ -2299,26 +2472,30 @@ async function connectWebSocket(): Promise<void> {
2299
2472
  return;
2300
2473
  }
2301
2474
 
2302
- const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
2303
- ws = new WebSocket(url);
2304
-
2305
- ws.addEventListener('open', () => {
2306
- manualDisconnect = false;
2307
- reconnectAttempt = 0;
2308
- lastError = null;
2309
- ws?.send(JSON.stringify({
2475
+ const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
2476
+ const socket = new WebSocket(url);
2477
+ ws = socket;
2478
+
2479
+ socket.addEventListener('open', () => {
2480
+ if (ws !== socket) {
2481
+ return;
2482
+ }
2483
+ manualDisconnect = false;
2484
+ reconnectAttempt = 0;
2485
+ lastError = null;
2486
+ socket.send(JSON.stringify({
2310
2487
  type: 'hello',
2311
2488
  role: 'extension',
2312
2489
  version: EXTENSION_VERSION,
2313
2490
  ts: Date.now()
2314
2491
  }));
2315
2492
  });
2316
-
2317
- ws.addEventListener('message', (event) => {
2318
- try {
2319
- const request = JSON.parse(String(event.data)) as CliRequest;
2320
- if (!request.id || !request.method) {
2321
- return;
2493
+
2494
+ socket.addEventListener('message', (event) => {
2495
+ try {
2496
+ const request = JSON.parse(String(event.data)) as CliRequest;
2497
+ if (!request.id || !request.method) {
2498
+ return;
2322
2499
  }
2323
2500
  void handleRequest(request)
2324
2501
  .then((result) => {
@@ -2335,66 +2512,109 @@ async function connectWebSocket(): Promise<void> {
2335
2512
  ok: false,
2336
2513
  error: toError('E_INTERNAL', error instanceof Error ? error.message : String(error))
2337
2514
  });
2338
- }
2339
- });
2340
-
2341
- ws.addEventListener('close', () => {
2342
- ws = null;
2343
- scheduleReconnect('socket-closed');
2344
- });
2345
-
2346
- ws.addEventListener('error', () => {
2347
- setRuntimeError('Cannot connect to bak cli', 'socket');
2348
- ws?.close();
2349
- });
2350
- }
2515
+ }
2516
+ });
2517
+
2518
+ socket.addEventListener('close', () => {
2519
+ if (ws !== socket) {
2520
+ return;
2521
+ }
2522
+ ws = null;
2523
+ scheduleReconnect('socket-closed');
2524
+ });
2525
+
2526
+ socket.addEventListener('error', () => {
2527
+ if (ws !== socket) {
2528
+ return;
2529
+ }
2530
+ setRuntimeError('Cannot connect to bak cli', 'socket');
2531
+ socket.close();
2532
+ });
2533
+ }
2351
2534
 
2352
2535
  chrome.tabs.onRemoved.addListener((tabId) => {
2353
2536
  dropNetworkCapture(tabId);
2354
2537
  void mutateSessionBindingStateMap((stateMap) => {
2538
+ const updates: Array<{ bindingId: string; state: SessionBindingRecord | null }> = [];
2355
2539
  for (const [bindingId, state] of Object.entries(stateMap)) {
2356
2540
  if (!state.tabIds.includes(tabId)) {
2357
2541
  continue;
2358
2542
  }
2359
2543
  const nextTabIds = state.tabIds.filter((id) => id !== tabId);
2360
- stateMap[bindingId] = {
2544
+ if (nextTabIds.length === 0) {
2545
+ delete stateMap[bindingId];
2546
+ updates.push({ bindingId, state: null });
2547
+ continue;
2548
+ }
2549
+ const fallbackTabId = nextTabIds[0] ?? null;
2550
+ const nextState: SessionBindingRecord = {
2361
2551
  ...state,
2362
2552
  tabIds: nextTabIds,
2363
- activeTabId: state.activeTabId === tabId ? null : state.activeTabId,
2364
- primaryTabId: state.primaryTabId === tabId ? null : state.primaryTabId
2553
+ activeTabId: state.activeTabId === tabId ? fallbackTabId : state.activeTabId,
2554
+ primaryTabId: state.primaryTabId === tabId ? fallbackTabId : state.primaryTabId
2365
2555
  };
2556
+ stateMap[bindingId] = nextState;
2557
+ updates.push({ bindingId, state: nextState });
2558
+ }
2559
+ return updates;
2560
+ }).then((updates) => {
2561
+ for (const update of updates) {
2562
+ emitSessionBindingUpdated(update.bindingId, 'tab-removed', update.state, {
2563
+ closedTabId: tabId
2564
+ });
2366
2565
  }
2367
2566
  });
2368
2567
  });
2369
2568
 
2370
2569
  chrome.tabs.onActivated.addListener((activeInfo) => {
2371
- void mutateSessionBindingStateMap((stateMap) => {
2372
- for (const [bindingId, state] of Object.entries(stateMap)) {
2373
- if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
2374
- continue;
2570
+ if (preserveHumanFocusDepth > 0) {
2571
+ return;
2572
+ }
2573
+ void chrome.windows
2574
+ .get(activeInfo.windowId)
2575
+ .then((window) => window.focused === true)
2576
+ .catch(() => false)
2577
+ .then((windowFocused) => {
2578
+ if (!windowFocused) {
2579
+ return [] as Array<{ bindingId: string; state: SessionBindingRecord }>;
2375
2580
  }
2376
- stateMap[bindingId] = {
2377
- ...state,
2378
- activeTabId: activeInfo.tabId
2379
- };
2380
- }
2381
- });
2581
+ return mutateSessionBindingStateMap((stateMap) => {
2582
+ const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
2583
+ for (const [bindingId, state] of Object.entries(stateMap)) {
2584
+ if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
2585
+ continue;
2586
+ }
2587
+ const nextState: SessionBindingRecord = {
2588
+ ...state,
2589
+ activeTabId: activeInfo.tabId
2590
+ };
2591
+ stateMap[bindingId] = nextState;
2592
+ updates.push({ bindingId, state: nextState });
2593
+ }
2594
+ return updates;
2595
+ });
2596
+ })
2597
+ .then((updates) => {
2598
+ for (const update of updates) {
2599
+ emitSessionBindingUpdated(update.bindingId, 'tab-activated', update.state);
2600
+ }
2601
+ });
2382
2602
  });
2383
2603
 
2384
2604
  chrome.windows.onRemoved.addListener((windowId) => {
2385
2605
  void mutateSessionBindingStateMap((stateMap) => {
2606
+ const updates: Array<{ bindingId: string; state: SessionBindingRecord | null }> = [];
2386
2607
  for (const [bindingId, state] of Object.entries(stateMap)) {
2387
2608
  if (state.windowId !== windowId) {
2388
2609
  continue;
2389
2610
  }
2390
- stateMap[bindingId] = {
2391
- ...state,
2392
- windowId: null,
2393
- groupId: null,
2394
- tabIds: [],
2395
- activeTabId: null,
2396
- primaryTabId: null
2397
- };
2611
+ delete stateMap[bindingId];
2612
+ updates.push({ bindingId, state: null });
2613
+ }
2614
+ return updates;
2615
+ }).then((updates) => {
2616
+ for (const update of updates) {
2617
+ emitSessionBindingUpdated(update.bindingId, 'window-removed', update.state);
2398
2618
  }
2399
2619
  });
2400
2620
  });
@@ -2410,48 +2630,49 @@ chrome.runtime.onStartup.addListener(() => {
2410
2630
  void connectWebSocket();
2411
2631
 
2412
2632
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
2413
- if (message?.type === 'bak.updateConfig') {
2414
- manualDisconnect = false;
2415
- void setConfig({
2416
- token: message.token,
2417
- port: Number(message.port ?? DEFAULT_PORT),
2418
- debugRichText: message.debugRichText === true
2419
- }).then(() => {
2420
- ws?.close();
2421
- void connectWebSocket().then(() => sendResponse({ ok: true }));
2422
- });
2423
- return true;
2424
- }
2425
-
2426
- if (message?.type === 'bak.getState') {
2427
- void getConfig().then((config) => {
2428
- sendResponse({
2429
- ok: true,
2430
- connected: ws?.readyState === WebSocket.OPEN,
2431
- hasToken: Boolean(config.token),
2432
- port: config.port,
2433
- debugRichText: config.debugRichText,
2434
- lastError: lastError?.message ?? null,
2435
- lastErrorAt: lastError?.at ?? null,
2436
- lastErrorContext: lastError?.context ?? null,
2437
- reconnectAttempt,
2438
- nextReconnectInMs
2439
- });
2440
- });
2441
- return true;
2442
- }
2443
-
2444
- if (message?.type === 'bak.disconnect') {
2445
- manualDisconnect = true;
2446
- clearReconnectTimer();
2447
- reconnectAttempt = 0;
2448
- ws?.close();
2449
- ws = null;
2450
- sendResponse({ ok: true });
2451
- return false;
2452
- }
2633
+ if (message?.type === 'bak.updateConfig') {
2634
+ manualDisconnect = false;
2635
+ const token = typeof message.token === 'string' ? message.token.trim() : '';
2636
+ void setConfig({
2637
+ ...(token ? { token } : {}),
2638
+ port: Number(message.port ?? DEFAULT_PORT),
2639
+ debugRichText: message.debugRichText === true
2640
+ }).then(() => {
2641
+ ws?.close();
2642
+ void connectWebSocket().then(() => sendResponse({ ok: true }));
2643
+ });
2644
+ return true;
2645
+ }
2453
2646
 
2454
- return false;
2455
- });
2647
+ if (message?.type === 'bak.getState') {
2648
+ void buildPopupState().then((state) => {
2649
+ sendResponse(state);
2650
+ });
2651
+ return true;
2652
+ }
2653
+
2654
+ if (message?.type === 'bak.disconnect') {
2655
+ manualDisconnect = true;
2656
+ clearReconnectTimer();
2657
+ reconnectAttempt = 0;
2658
+ lastError = null;
2659
+ ws?.close();
2660
+ ws = null;
2661
+ sendResponse({ ok: true });
2662
+ return false;
2663
+ }
2664
+
2665
+ if (message?.type === 'bak.reconnectNow') {
2666
+ manualDisconnect = false;
2667
+ clearReconnectTimer();
2668
+ reconnectAttempt = 0;
2669
+ ws?.close();
2670
+ ws = null;
2671
+ void connectWebSocket().then(() => sendResponse({ ok: true }));
2672
+ return true;
2673
+ }
2674
+
2675
+ return false;
2676
+ });
2456
2677
 
2457
2678