e2e-pilot 0.0.69 → 0.0.70

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.
@@ -0,0 +1,1126 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ const createStoreImpl = (createState) => {
5
+ let state;
6
+ const listeners = /* @__PURE__ */ new Set();
7
+ const setState = (partial, replace) => {
8
+ const nextState = typeof partial === "function" ? partial(state) : partial;
9
+ if (!Object.is(nextState, state)) {
10
+ const previousState = state;
11
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
12
+ listeners.forEach((listener) => listener(state, previousState));
13
+ }
14
+ };
15
+ const getState = () => state;
16
+ const getInitialState = () => initialState;
17
+ const subscribe = (listener) => {
18
+ listeners.add(listener);
19
+ return () => listeners.delete(listener);
20
+ };
21
+ const api = { setState, getState, getInitialState, subscribe };
22
+ const initialState = state = createState(setState, getState, api);
23
+ return api;
24
+ };
25
+ const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
26
+ const RELAY_PORT = "19988";
27
+ const RELAY_URL = `ws://localhost:${RELAY_PORT}/extension`;
28
+ function sleep(ms) {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+ let childSessions = /* @__PURE__ */ new Map();
32
+ let nextSessionId = 1;
33
+ let tabGroupQueue = Promise.resolve();
34
+ class ConnectionManager {
35
+ constructor() {
36
+ __publicField(this, "ws", null);
37
+ __publicField(this, "connectionPromise", null);
38
+ }
39
+ async ensureConnection() {
40
+ var _a;
41
+ if (((_a = this.ws) == null ? void 0 : _a.readyState) === WebSocket.OPEN) {
42
+ return;
43
+ }
44
+ if (store.getState().connectionState === "extension-replaced") {
45
+ throw new Error("Connection replaced by another extension");
46
+ }
47
+ if (this.connectionPromise) {
48
+ return this.connectionPromise;
49
+ }
50
+ this.connectionPromise = this.connect();
51
+ try {
52
+ await this.connectionPromise;
53
+ } finally {
54
+ this.connectionPromise = null;
55
+ }
56
+ }
57
+ async connect() {
58
+ logger.debug(`Waiting for server at http://localhost:${RELAY_PORT}...`);
59
+ const maxAttempts = 30;
60
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
61
+ try {
62
+ await fetch(`http://localhost:${RELAY_PORT}`, { method: "HEAD", signal: AbortSignal.timeout(2e3) });
63
+ logger.debug("Server is available");
64
+ break;
65
+ } catch {
66
+ if (attempt === maxAttempts - 1) {
67
+ throw new Error("Server not available");
68
+ }
69
+ if (attempt % 5 === 0) {
70
+ logger.debug(`Server not available, retrying... (attempt ${attempt + 1}/${maxAttempts})`);
71
+ }
72
+ await sleep(1e3);
73
+ }
74
+ }
75
+ logger.debug("Creating WebSocket connection to:", RELAY_URL);
76
+ const socket = new WebSocket(RELAY_URL);
77
+ await new Promise((resolve, reject) => {
78
+ let timeoutFired = false;
79
+ const timeout = setTimeout(() => {
80
+ timeoutFired = true;
81
+ logger.debug("WebSocket connection TIMEOUT after 5 seconds");
82
+ reject(new Error("Connection timeout"));
83
+ }, 5e3);
84
+ socket.onopen = () => {
85
+ if (timeoutFired) {
86
+ return;
87
+ }
88
+ logger.debug("WebSocket connected");
89
+ clearTimeout(timeout);
90
+ resolve();
91
+ };
92
+ socket.onerror = (error) => {
93
+ logger.debug("WebSocket error during connection:", error);
94
+ if (!timeoutFired) {
95
+ clearTimeout(timeout);
96
+ reject(new Error("WebSocket connection failed"));
97
+ }
98
+ };
99
+ socket.onclose = (event) => {
100
+ logger.debug("WebSocket closed during connection:", { code: event.code, reason: event.reason });
101
+ if (!timeoutFired) {
102
+ clearTimeout(timeout);
103
+ reject(new Error(`WebSocket closed: ${event.reason || event.code}`));
104
+ }
105
+ };
106
+ });
107
+ this.ws = socket;
108
+ this.ws.onmessage = async (event) => {
109
+ let message;
110
+ try {
111
+ message = JSON.parse(event.data);
112
+ } catch (error) {
113
+ logger.debug("Error parsing message:", error);
114
+ sendMessage({ error: { code: -32700, message: `Error parsing message: ${error.message}` } });
115
+ return;
116
+ }
117
+ if (message.method === "ping") {
118
+ sendMessage({ method: "pong" });
119
+ return;
120
+ }
121
+ if (message.method === "createInitialTab") {
122
+ try {
123
+ logger.debug("Creating initial tab for Playwright client");
124
+ const tab = await chrome.tabs.create({ url: "about:blank", active: false });
125
+ if (tab.id) {
126
+ const { targetInfo, sessionId } = await attachTab(tab.id, { skipAttachedEvent: true });
127
+ logger.debug("Initial tab created and connected:", tab.id, "sessionId:", sessionId);
128
+ sendMessage({
129
+ id: message.id,
130
+ result: {
131
+ success: true,
132
+ tabId: tab.id,
133
+ sessionId,
134
+ targetInfo
135
+ }
136
+ });
137
+ } else {
138
+ throw new Error("Failed to create tab - no tab ID returned");
139
+ }
140
+ } catch (error) {
141
+ logger.debug("Failed to create initial tab:", error);
142
+ sendMessage({ id: message.id, error: error.message });
143
+ }
144
+ return;
145
+ }
146
+ const response = { id: message.id };
147
+ try {
148
+ response.result = await handleCommand(message);
149
+ } catch (error) {
150
+ logger.debug("Error handling command:", error);
151
+ response.error = error.message;
152
+ }
153
+ logger.debug("Sending response:", response);
154
+ sendMessage(response);
155
+ };
156
+ this.ws.onclose = (event) => {
157
+ this.handleClose(event.reason, event.code);
158
+ };
159
+ this.ws.onerror = (event) => {
160
+ logger.debug("WebSocket error:", event);
161
+ };
162
+ chrome.debugger.onEvent.addListener(onDebuggerEvent);
163
+ chrome.debugger.onDetach.addListener(onDebuggerDetach);
164
+ logger.debug("Connection established");
165
+ }
166
+ handleClose(reason, code) {
167
+ try {
168
+ const mem = performance.memory;
169
+ if (mem) {
170
+ const formatMB = (b) => (b / 1024 / 1024).toFixed(2) + "MB";
171
+ logger.warn(
172
+ `DISCONNECT MEMORY: used=${formatMB(mem.usedJSHeapSize)} total=${formatMB(mem.totalJSHeapSize)} limit=${formatMB(mem.jsHeapSizeLimit)}`
173
+ );
174
+ }
175
+ } catch {
176
+ }
177
+ logger.warn(`DISCONNECT: WS closed code=${code} reason=${reason || "none"} stack=${getCallStack()}`);
178
+ chrome.debugger.onEvent.removeListener(onDebuggerEvent);
179
+ chrome.debugger.onDetach.removeListener(onDebuggerDetach);
180
+ const { tabs } = store.getState();
181
+ for (const [tabId] of tabs) {
182
+ chrome.debugger.detach({ tabId }).catch((err) => {
183
+ logger.debug("Error detaching from tab:", tabId, err.message);
184
+ });
185
+ }
186
+ childSessions.clear();
187
+ this.ws = null;
188
+ if (reason === "Extension Replaced" || code === 4001) {
189
+ logger.debug("Connection replaced by another extension instance");
190
+ store.setState({
191
+ tabs: /* @__PURE__ */ new Map(),
192
+ connectionState: "extension-replaced",
193
+ errorText: "Disconnected: Replaced by another extension"
194
+ });
195
+ return;
196
+ }
197
+ store.setState((state) => {
198
+ const newTabs = new Map(state.tabs);
199
+ for (const [tabId, tab] of newTabs) {
200
+ newTabs.set(tabId, { ...tab, state: "connecting" });
201
+ }
202
+ return { tabs: newTabs, connectionState: "idle", errorText: void 0 };
203
+ });
204
+ }
205
+ async maintainLoop() {
206
+ var _a;
207
+ while (true) {
208
+ if (((_a = this.ws) == null ? void 0 : _a.readyState) === WebSocket.OPEN) {
209
+ await sleep(1e3);
210
+ continue;
211
+ }
212
+ if (store.getState().connectionState === "extension-replaced") {
213
+ try {
214
+ const response = await fetch(`http://localhost:${RELAY_PORT}/extension/status`, {
215
+ method: "GET",
216
+ signal: AbortSignal.timeout(2e3)
217
+ });
218
+ const data = await response.json();
219
+ if (!data.connected) {
220
+ store.setState({ connectionState: "idle", errorText: void 0 });
221
+ logger.debug("Extension slot is free, cleared error state");
222
+ } else {
223
+ logger.debug("Extension slot still taken, will retry...");
224
+ }
225
+ } catch {
226
+ logger.debug("Server not available, will retry...");
227
+ }
228
+ await sleep(3e3);
229
+ continue;
230
+ }
231
+ const currentTabs = store.getState().tabs;
232
+ const hasConnectedTabs = Array.from(currentTabs.values()).some((t) => t.state === "connected");
233
+ if (hasConnectedTabs) {
234
+ store.setState((state) => {
235
+ const newTabs = new Map(state.tabs);
236
+ for (const [tabId, tab] of newTabs) {
237
+ if (tab.state === "connected") {
238
+ newTabs.set(tabId, { ...tab, state: "connecting" });
239
+ }
240
+ }
241
+ return { tabs: newTabs };
242
+ });
243
+ }
244
+ try {
245
+ await this.ensureConnection();
246
+ store.setState({ connectionState: "connected" });
247
+ const tabsToReattach = Array.from(store.getState().tabs.entries()).filter(([_, tab]) => tab.state === "connecting").map(([tabId]) => tabId);
248
+ for (const tabId of tabsToReattach) {
249
+ const currentTab = store.getState().tabs.get(tabId);
250
+ if (!currentTab || currentTab.state !== "connecting") {
251
+ logger.debug("Skipping reattach, tab state changed:", tabId, currentTab == null ? void 0 : currentTab.state);
252
+ continue;
253
+ }
254
+ try {
255
+ await chrome.tabs.get(tabId);
256
+ await attachTab(tabId);
257
+ logger.debug("Successfully re-attached tab:", tabId);
258
+ } catch (error) {
259
+ logger.debug("Failed to re-attach tab:", tabId, error.message);
260
+ store.setState((state) => {
261
+ const newTabs = new Map(state.tabs);
262
+ newTabs.delete(tabId);
263
+ return { tabs: newTabs };
264
+ });
265
+ }
266
+ }
267
+ } catch (error) {
268
+ logger.debug("Connection attempt failed:", error.message);
269
+ store.setState({ connectionState: "idle" });
270
+ }
271
+ await sleep(3e3);
272
+ }
273
+ }
274
+ }
275
+ const connectionManager = new ConnectionManager();
276
+ const store = createStore(() => ({
277
+ tabs: /* @__PURE__ */ new Map(),
278
+ connectionState: "idle",
279
+ currentTabId: void 0,
280
+ errorText: void 0
281
+ }));
282
+ globalThis.toggleExtensionForActiveTab = toggleExtensionForActiveTab;
283
+ globalThis.disconnectEverything = disconnectEverything;
284
+ globalThis.getExtensionState = () => store.getState();
285
+ function safeSerialize(arg) {
286
+ if (arg === void 0) return "undefined";
287
+ if (arg === null) return "null";
288
+ if (typeof arg === "function") return `[Function: ${arg.name || "anonymous"}]`;
289
+ if (typeof arg === "symbol") return String(arg);
290
+ if (arg instanceof Error) return arg.stack || arg.message || String(arg);
291
+ if (typeof arg === "object") {
292
+ try {
293
+ const seen = /* @__PURE__ */ new WeakSet();
294
+ return JSON.stringify(arg, (key, value) => {
295
+ if (typeof value === "object" && value !== null) {
296
+ if (seen.has(value)) return "[Circular]";
297
+ seen.add(value);
298
+ if (value instanceof Map) return { dataType: "Map", value: Array.from(value.entries()) };
299
+ if (value instanceof Set) return { dataType: "Set", value: Array.from(value.values()) };
300
+ }
301
+ return value;
302
+ });
303
+ } catch {
304
+ return String(arg);
305
+ }
306
+ }
307
+ return String(arg);
308
+ }
309
+ function sendLog(level, args) {
310
+ sendMessage({
311
+ method: "log",
312
+ params: { level, args: args.map(safeSerialize) }
313
+ });
314
+ }
315
+ const logger = {
316
+ log: (...args) => {
317
+ console.log(...args);
318
+ sendLog("log", args);
319
+ },
320
+ debug: (...args) => {
321
+ console.debug(...args);
322
+ sendLog("debug", args);
323
+ },
324
+ info: (...args) => {
325
+ console.info(...args);
326
+ sendLog("info", args);
327
+ },
328
+ warn: (...args) => {
329
+ console.warn(...args);
330
+ sendLog("warn", args);
331
+ },
332
+ error: (...args) => {
333
+ console.error(...args);
334
+ sendLog("error", args);
335
+ }
336
+ };
337
+ function getCallStack() {
338
+ const stack = new Error().stack || "";
339
+ return stack.split("\n").slice(2, 6).join(" <- ").replace(/\s+/g, " ");
340
+ }
341
+ self.addEventListener("error", (event) => {
342
+ const error = event.error;
343
+ const stack = (error == null ? void 0 : error.stack) || `${event.message} at ${event.filename}:${event.lineno}:${event.colno}`;
344
+ logger.error("Uncaught error:", stack);
345
+ });
346
+ self.addEventListener("unhandledrejection", (event) => {
347
+ const reason = event.reason;
348
+ const stack = (reason == null ? void 0 : reason.stack) || String(reason);
349
+ logger.error("Unhandled promise rejection:", stack);
350
+ });
351
+ let messageCount = 0;
352
+ function sendMessage(message) {
353
+ var _a;
354
+ if (((_a = connectionManager.ws) == null ? void 0 : _a.readyState) === WebSocket.OPEN) {
355
+ try {
356
+ connectionManager.ws.send(JSON.stringify(message));
357
+ if (++messageCount % 100 === 0) {
358
+ checkMemory();
359
+ }
360
+ } catch (error) {
361
+ console.debug("ERROR sending message:", error, "message type:", message.method || "response");
362
+ }
363
+ }
364
+ }
365
+ async function syncTabGroup() {
366
+ var _a;
367
+ try {
368
+ const connectedTabIds = Array.from(store.getState().tabs.entries()).filter(([_, info]) => info.state === "connected").map(([tabId]) => tabId);
369
+ const existingGroups = await chrome.tabGroups.query({ title: "e2e-pilot" });
370
+ if (connectedTabIds.length === 0) {
371
+ for (const group of existingGroups) {
372
+ const tabsInGroup2 = await chrome.tabs.query({ groupId: group.id });
373
+ const tabIdsToUngroup = tabsInGroup2.map((t) => t.id).filter((id) => id !== void 0);
374
+ if (tabIdsToUngroup.length > 0) {
375
+ await chrome.tabs.ungroup(tabIdsToUngroup);
376
+ }
377
+ logger.debug("Cleared e2e-pilot group:", group.id);
378
+ }
379
+ return;
380
+ }
381
+ let groupId = (_a = existingGroups[0]) == null ? void 0 : _a.id;
382
+ if (existingGroups.length > 1) {
383
+ const [keep, ...duplicates] = existingGroups;
384
+ groupId = keep.id;
385
+ for (const group of duplicates) {
386
+ const tabsInDupe = await chrome.tabs.query({ groupId: group.id });
387
+ const tabIdsToUngroup = tabsInDupe.map((t) => t.id).filter((id) => id !== void 0);
388
+ if (tabIdsToUngroup.length > 0) {
389
+ await chrome.tabs.ungroup(tabIdsToUngroup);
390
+ }
391
+ logger.debug("Removed duplicate e2e-pilot group:", group.id);
392
+ }
393
+ }
394
+ const allTabs = await chrome.tabs.query({});
395
+ const tabsInGroup = allTabs.filter((t) => t.groupId === groupId && t.id !== void 0);
396
+ const tabIdsInGroup = new Set(tabsInGroup.map((t) => t.id));
397
+ const tabsToAdd = connectedTabIds.filter((id) => !tabIdsInGroup.has(id));
398
+ const tabsToRemove = Array.from(tabIdsInGroup).filter((id) => !connectedTabIds.includes(id));
399
+ if (tabsToRemove.length > 0) {
400
+ try {
401
+ await chrome.tabs.ungroup(tabsToRemove);
402
+ logger.debug("Removed tabs from group:", tabsToRemove);
403
+ } catch (e) {
404
+ logger.debug("Failed to ungroup tabs:", tabsToRemove, e.message);
405
+ }
406
+ }
407
+ if (tabsToAdd.length > 0) {
408
+ if (groupId === void 0) {
409
+ const newGroupId = await chrome.tabs.group({ tabIds: tabsToAdd });
410
+ await chrome.tabGroups.update(newGroupId, { title: "e2e-pilot", color: "green" });
411
+ logger.debug("Created tab group:", newGroupId, "with tabs:", tabsToAdd);
412
+ } else {
413
+ await chrome.tabs.group({ tabIds: tabsToAdd, groupId });
414
+ logger.debug("Added tabs to existing group:", tabsToAdd);
415
+ }
416
+ }
417
+ } catch (error) {
418
+ logger.debug("Failed to sync tab group:", error.message);
419
+ }
420
+ }
421
+ function getTabBySessionId(sessionId) {
422
+ for (const [tabId, tab] of store.getState().tabs) {
423
+ if (tab.sessionId === sessionId) {
424
+ return { tabId, tab };
425
+ }
426
+ }
427
+ return void 0;
428
+ }
429
+ function getTabByTargetId(targetId) {
430
+ for (const [tabId, tab] of store.getState().tabs) {
431
+ if (tab.targetId === targetId) {
432
+ return { tabId, tab };
433
+ }
434
+ }
435
+ return void 0;
436
+ }
437
+ async function handleCommand(msg) {
438
+ var _a, _b;
439
+ if (msg.method !== "forwardCDPCommand") return;
440
+ let targetTabId;
441
+ let targetTab;
442
+ if (msg.params.sessionId) {
443
+ const found = getTabBySessionId(msg.params.sessionId);
444
+ if (found) {
445
+ targetTabId = found.tabId;
446
+ targetTab = found.tab;
447
+ }
448
+ }
449
+ if (!targetTab && msg.params.sessionId) {
450
+ const parentTabId = childSessions.get(msg.params.sessionId);
451
+ if (parentTabId) {
452
+ targetTabId = parentTabId;
453
+ targetTab = store.getState().tabs.get(parentTabId);
454
+ logger.debug("Found parent tab for child session:", msg.params.sessionId, "tabId:", parentTabId);
455
+ }
456
+ }
457
+ if (!targetTab && msg.params.params && "targetId" in msg.params.params && msg.params.params.targetId) {
458
+ const found = getTabByTargetId(msg.params.params.targetId);
459
+ if (found) {
460
+ targetTabId = found.tabId;
461
+ targetTab = found.tab;
462
+ logger.debug("Found tab for targetId:", msg.params.params.targetId, "tabId:", targetTabId);
463
+ }
464
+ }
465
+ const debuggee = targetTabId ? { tabId: targetTabId } : void 0;
466
+ switch (msg.params.method) {
467
+ case "Runtime.enable": {
468
+ if (!debuggee) {
469
+ throw new Error(`No debuggee found for Runtime.enable (sessionId: ${msg.params.sessionId})`);
470
+ }
471
+ try {
472
+ await chrome.debugger.sendCommand(debuggee, "Runtime.disable");
473
+ await sleep(50);
474
+ } catch (e) {
475
+ logger.debug("Error disabling Runtime (ignoring):", e);
476
+ }
477
+ return await chrome.debugger.sendCommand(debuggee, "Runtime.enable", msg.params.params);
478
+ }
479
+ case "Target.createTarget": {
480
+ const url = ((_a = msg.params.params) == null ? void 0 : _a.url) || "about:blank";
481
+ logger.debug("Creating new tab with URL:", url);
482
+ const tab = await chrome.tabs.create({ url, active: false });
483
+ if (!tab.id) throw new Error("Failed to create tab");
484
+ logger.debug("Created tab:", tab.id, "waiting for it to load...");
485
+ await sleep(100);
486
+ const { targetInfo } = await attachTab(tab.id);
487
+ return { targetId: targetInfo.targetId };
488
+ }
489
+ case "Target.closeTarget": {
490
+ if (!targetTabId) {
491
+ logger.log(`Target not found: ${(_b = msg.params.params) == null ? void 0 : _b.targetId}`);
492
+ return { success: false };
493
+ }
494
+ await chrome.tabs.remove(targetTabId);
495
+ return { success: true };
496
+ }
497
+ }
498
+ if (!debuggee || !targetTab) {
499
+ throw new Error(
500
+ `No tab found for method ${msg.params.method} sessionId: ${msg.params.sessionId} params: ${JSON.stringify(msg.params.params || null)}`
501
+ );
502
+ }
503
+ logger.debug("CDP command:", msg.params.method, "for tab:", targetTabId);
504
+ const debuggerSession = {
505
+ ...debuggee,
506
+ sessionId: msg.params.sessionId !== targetTab.sessionId ? msg.params.sessionId : void 0
507
+ };
508
+ return await chrome.debugger.sendCommand(debuggerSession, msg.params.method, msg.params.params);
509
+ }
510
+ function onDebuggerEvent(source, method, params) {
511
+ const tab = source.tabId ? store.getState().tabs.get(source.tabId) : void 0;
512
+ if (!tab) return;
513
+ logger.debug("Forwarding CDP event:", method, "from tab:", source.tabId);
514
+ if (method === "Target.attachedToTarget" && (params == null ? void 0 : params.sessionId)) {
515
+ logger.debug("Child target attached:", params.sessionId, "for tab:", source.tabId);
516
+ childSessions.set(params.sessionId, source.tabId);
517
+ }
518
+ if (method === "Target.detachedFromTarget" && (params == null ? void 0 : params.sessionId)) {
519
+ const mainTab = getTabBySessionId(params.sessionId);
520
+ if (mainTab) {
521
+ logger.debug("Main tab detached via CDP event:", mainTab.tabId, "sessionId:", params.sessionId);
522
+ store.setState((state) => {
523
+ const newTabs = new Map(state.tabs);
524
+ newTabs.delete(mainTab.tabId);
525
+ return { tabs: newTabs };
526
+ });
527
+ } else {
528
+ logger.debug("Child target detached:", params.sessionId);
529
+ childSessions.delete(params.sessionId);
530
+ }
531
+ }
532
+ sendMessage({
533
+ method: "forwardCDPEvent",
534
+ params: {
535
+ sessionId: source.sessionId || tab.sessionId,
536
+ method,
537
+ params
538
+ }
539
+ });
540
+ }
541
+ function onDebuggerDetach(source, reason) {
542
+ const tabId = source.tabId;
543
+ if (!tabId || !store.getState().tabs.has(tabId)) {
544
+ logger.debug("Ignoring debugger detach event for untracked tab:", tabId);
545
+ return;
546
+ }
547
+ logger.warn(`DISCONNECT: onDebuggerDetach tabId=${tabId} reason=${reason}`);
548
+ const tab = store.getState().tabs.get(tabId);
549
+ if (tab) {
550
+ sendMessage({
551
+ method: "forwardCDPEvent",
552
+ params: {
553
+ method: "Target.detachedFromTarget",
554
+ params: { sessionId: tab.sessionId, targetId: tab.targetId }
555
+ }
556
+ });
557
+ }
558
+ for (const [childSessionId, parentTabId] of childSessions.entries()) {
559
+ if (parentTabId === tabId) {
560
+ logger.debug("Cleaning up child session:", childSessionId, "for tab:", tabId);
561
+ childSessions.delete(childSessionId);
562
+ }
563
+ }
564
+ store.setState((state) => {
565
+ const newTabs = new Map(state.tabs);
566
+ newTabs.delete(tabId);
567
+ return { tabs: newTabs };
568
+ });
569
+ if (reason === chrome.debugger.DetachReason.CANCELED_BY_USER) {
570
+ store.setState({ connectionState: "idle", errorText: void 0 });
571
+ }
572
+ }
573
+ async function attachTab(tabId, { skipAttachedEvent = false } = {}) {
574
+ const debuggee = { tabId };
575
+ let debuggerAttached = false;
576
+ try {
577
+ logger.debug("Attaching debugger to tab:", tabId);
578
+ await chrome.debugger.attach(debuggee, "1.3");
579
+ debuggerAttached = true;
580
+ logger.debug("Debugger attached successfully to tab:", tabId);
581
+ await chrome.debugger.sendCommand(debuggee, "Page.enable");
582
+ const contextMenuScript = `
583
+ document.addEventListener('contextmenu', (e) => {
584
+ window.__e2e_pilot_lastRightClicked = e.target;
585
+ }, true);
586
+ `;
587
+ await chrome.debugger.sendCommand(debuggee, "Page.addScriptToEvaluateOnNewDocument", { source: contextMenuScript });
588
+ await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", { expression: contextMenuScript });
589
+ const result = await chrome.debugger.sendCommand(
590
+ debuggee,
591
+ "Target.getTargetInfo"
592
+ );
593
+ const targetInfo = result.targetInfo;
594
+ if (!targetInfo.url || targetInfo.url === "" || targetInfo.url === ":") {
595
+ logger.error(
596
+ "WARNING: Target.attachedToTarget will be sent with empty URL! tabId:",
597
+ tabId,
598
+ "targetInfo:",
599
+ JSON.stringify(targetInfo)
600
+ );
601
+ }
602
+ const attachOrder = nextSessionId;
603
+ const sessionId = `pw-tab-${nextSessionId++}`;
604
+ store.setState((state) => {
605
+ const newTabs = new Map(state.tabs);
606
+ newTabs.set(tabId, {
607
+ sessionId,
608
+ targetId: targetInfo.targetId,
609
+ state: "connected",
610
+ attachOrder
611
+ });
612
+ return { tabs: newTabs, connectionState: "connected", errorText: void 0 };
613
+ });
614
+ if (!skipAttachedEvent) {
615
+ sendMessage({
616
+ method: "forwardCDPEvent",
617
+ params: {
618
+ method: "Target.attachedToTarget",
619
+ params: {
620
+ sessionId,
621
+ targetInfo: { ...targetInfo, attached: true },
622
+ waitingForDebugger: false
623
+ }
624
+ }
625
+ });
626
+ }
627
+ logger.debug(
628
+ "Tab attached successfully:",
629
+ tabId,
630
+ "sessionId:",
631
+ sessionId,
632
+ "targetId:",
633
+ targetInfo.targetId,
634
+ "url:",
635
+ targetInfo.url,
636
+ "skipAttachedEvent:",
637
+ skipAttachedEvent
638
+ );
639
+ return { targetInfo, sessionId };
640
+ } catch (error) {
641
+ if (debuggerAttached) {
642
+ logger.debug("Cleaning up debugger after partial attach failure:", tabId);
643
+ chrome.debugger.detach(debuggee).catch(() => {
644
+ });
645
+ }
646
+ throw error;
647
+ }
648
+ }
649
+ function detachTab(tabId, shouldDetachDebugger) {
650
+ const tab = store.getState().tabs.get(tabId);
651
+ if (!tab) {
652
+ logger.debug("detachTab: tab not found in map:", tabId);
653
+ return;
654
+ }
655
+ logger.warn(`DISCONNECT: detachTab tabId=${tabId} shouldDetach=${shouldDetachDebugger} stack=${getCallStack()}`);
656
+ if (tab.sessionId && tab.targetId) {
657
+ sendMessage({
658
+ method: "forwardCDPEvent",
659
+ params: {
660
+ method: "Target.detachedFromTarget",
661
+ params: { sessionId: tab.sessionId, targetId: tab.targetId }
662
+ }
663
+ });
664
+ }
665
+ store.setState((state) => {
666
+ const newTabs = new Map(state.tabs);
667
+ newTabs.delete(tabId);
668
+ return { tabs: newTabs };
669
+ });
670
+ for (const [childSessionId, parentTabId] of childSessions.entries()) {
671
+ if (parentTabId === tabId) {
672
+ logger.debug("Cleaning up child session:", childSessionId, "for tab:", tabId);
673
+ childSessions.delete(childSessionId);
674
+ }
675
+ }
676
+ {
677
+ chrome.debugger.detach({ tabId }).catch((err) => {
678
+ logger.debug("Error detaching debugger from tab:", tabId, err.message);
679
+ });
680
+ }
681
+ }
682
+ async function connectTab(tabId) {
683
+ try {
684
+ logger.debug(`Starting connection to tab ${tabId}`);
685
+ store.setState((state) => {
686
+ const newTabs = new Map(state.tabs);
687
+ newTabs.set(tabId, { state: "connecting" });
688
+ return { tabs: newTabs };
689
+ });
690
+ await connectionManager.ensureConnection();
691
+ await attachTab(tabId);
692
+ logger.debug(`Successfully connected to tab ${tabId}`);
693
+ } catch (error) {
694
+ logger.debug(`Failed to connect to tab ${tabId}:`, error);
695
+ const isWsError = error.message === "Server not available" || error.message === "Connection timeout" || error.message === "Connection replaced by another extension" || error.message.startsWith("WebSocket");
696
+ if (isWsError) {
697
+ logger.debug(`WS connection failed, keeping tab ${tabId} in connecting state for retry`);
698
+ } else {
699
+ store.setState((state) => {
700
+ const newTabs = new Map(state.tabs);
701
+ newTabs.set(tabId, { state: "error", errorText: `Error: ${error.message}` });
702
+ return { tabs: newTabs };
703
+ });
704
+ }
705
+ }
706
+ }
707
+ async function disconnectTab(tabId) {
708
+ logger.debug(`Disconnecting tab ${tabId}`);
709
+ const { tabs } = store.getState();
710
+ if (!tabs.has(tabId)) {
711
+ logger.debug("Tab not in tabs map, ignoring disconnect");
712
+ return;
713
+ }
714
+ detachTab(tabId, true);
715
+ }
716
+ async function toggleExtensionForActiveTab() {
717
+ var _a;
718
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
719
+ const tab = tabs[0];
720
+ if (!(tab == null ? void 0 : tab.id)) throw new Error("No active tab found");
721
+ await onActionClicked(tab);
722
+ await new Promise((resolve) => {
723
+ const check = () => {
724
+ const state2 = store.getState();
725
+ const tabInfo = state2.tabs.get(tab.id);
726
+ if ((tabInfo == null ? void 0 : tabInfo.state) === "connecting") {
727
+ setTimeout(check, 100);
728
+ return;
729
+ }
730
+ resolve();
731
+ };
732
+ check();
733
+ });
734
+ const state = store.getState();
735
+ const isConnected = state.tabs.has(tab.id) && ((_a = state.tabs.get(tab.id)) == null ? void 0 : _a.state) === "connected";
736
+ return { isConnected, state };
737
+ }
738
+ async function disconnectEverything() {
739
+ tabGroupQueue = tabGroupQueue.then(async () => {
740
+ const { tabs } = store.getState();
741
+ for (const tabId of tabs.keys()) {
742
+ await disconnectTab(tabId);
743
+ }
744
+ });
745
+ await tabGroupQueue;
746
+ }
747
+ async function resetDebugger() {
748
+ let targets = await chrome.debugger.getTargets();
749
+ targets = targets.filter((x) => x.tabId && x.attached);
750
+ logger.log(`found ${targets.length} existing debugger targets. detaching them before background script starts`);
751
+ for (const target of targets) {
752
+ await chrome.debugger.detach({ tabId: target.tabId });
753
+ }
754
+ }
755
+ function isRestrictedUrl(url) {
756
+ if (!url) return false;
757
+ const restrictedPrefixes = [
758
+ "chrome://",
759
+ "chrome-extension://",
760
+ "devtools://",
761
+ "edge://",
762
+ "arc://",
763
+ "https://chrome.google.com/",
764
+ "https://chromewebstore.google.com/"
765
+ ];
766
+ return restrictedPrefixes.some((prefix) => url.startsWith(prefix));
767
+ }
768
+ const icons = {
769
+ connected: {
770
+ path: {
771
+ "16": "/icons/icon-green-16.png",
772
+ "32": "/icons/icon-green-32.png",
773
+ "48": "/icons/icon-green-48.png",
774
+ "128": "/icons/icon-green-128.png"
775
+ },
776
+ title: "Connected - Click to disconnect",
777
+ badgeText: "",
778
+ badgeColor: [64, 64, 64, 255]
779
+ },
780
+ connecting: {
781
+ path: {
782
+ "16": "/icons/icon-gray-16.png",
783
+ "32": "/icons/icon-gray-32.png",
784
+ "48": "/icons/icon-gray-48.png",
785
+ "128": "/icons/icon-gray-128.png"
786
+ },
787
+ title: "Waiting for MCP WS server...",
788
+ badgeText: "...",
789
+ badgeColor: [64, 64, 64, 255]
790
+ },
791
+ idle: {
792
+ path: {
793
+ "16": "/icons/icon-black-16.png",
794
+ "32": "/icons/icon-black-32.png",
795
+ "48": "/icons/icon-black-48.png",
796
+ "128": "/icons/icon-black-128.png"
797
+ },
798
+ title: "Click to attach debugger",
799
+ badgeText: "",
800
+ badgeColor: [64, 64, 64, 255]
801
+ },
802
+ restricted: {
803
+ path: {
804
+ "16": "/icons/icon-gray-16.png",
805
+ "32": "/icons/icon-gray-32.png",
806
+ "48": "/icons/icon-gray-48.png",
807
+ "128": "/icons/icon-gray-128.png"
808
+ },
809
+ title: "Cannot attach to this page",
810
+ badgeText: "",
811
+ badgeColor: [64, 64, 64, 255]
812
+ },
813
+ extensionReplaced: {
814
+ path: {
815
+ "16": "/icons/icon-gray-16.png",
816
+ "32": "/icons/icon-gray-32.png",
817
+ "48": "/icons/icon-gray-48.png",
818
+ "128": "/icons/icon-gray-128.png"
819
+ },
820
+ title: "Replaced by another extension - Click to retry",
821
+ badgeText: "!",
822
+ badgeColor: [220, 38, 38, 255]
823
+ },
824
+ tabError: {
825
+ path: {
826
+ "16": "/icons/icon-gray-16.png",
827
+ "32": "/icons/icon-gray-32.png",
828
+ "48": "/icons/icon-gray-48.png",
829
+ "128": "/icons/icon-gray-128.png"
830
+ },
831
+ title: "Error",
832
+ badgeText: "!",
833
+ badgeColor: [220, 38, 38, 255]
834
+ }
835
+ };
836
+ async function updateIcons() {
837
+ const state = store.getState();
838
+ const { connectionState, tabs, errorText } = state;
839
+ const connectedCount = Array.from(tabs.values()).filter((t) => t.state === "connected").length;
840
+ const allTabs = await chrome.tabs.query({});
841
+ const tabUrlMap = new Map(allTabs.map((tab) => [tab.id, tab.url]));
842
+ const allTabIds = [void 0, ...allTabs.map((tab) => tab.id).filter((id) => id !== void 0)];
843
+ for (const tabId of allTabIds) {
844
+ const tabInfo = tabId !== void 0 ? tabs.get(tabId) : void 0;
845
+ const tabUrl = tabId !== void 0 ? tabUrlMap.get(tabId) : void 0;
846
+ const iconConfig = (() => {
847
+ if (connectionState === "extension-replaced") return icons.extensionReplaced;
848
+ if (tabId !== void 0 && isRestrictedUrl(tabUrl)) return icons.restricted;
849
+ if ((tabInfo == null ? void 0 : tabInfo.state) === "error") return icons.tabError;
850
+ if ((tabInfo == null ? void 0 : tabInfo.state) === "connecting") return icons.connecting;
851
+ if ((tabInfo == null ? void 0 : tabInfo.state) === "connected") return icons.connected;
852
+ return icons.idle;
853
+ })();
854
+ const title = (() => {
855
+ if (connectionState === "extension-replaced" && errorText) return errorText;
856
+ if (tabInfo == null ? void 0 : tabInfo.errorText) return tabInfo.errorText;
857
+ return iconConfig.title;
858
+ })();
859
+ const badgeText = (() => {
860
+ if (iconConfig === icons.connected || iconConfig === icons.idle || iconConfig === icons.restricted) {
861
+ return connectedCount > 0 ? String(connectedCount) : "";
862
+ }
863
+ return iconConfig.badgeText;
864
+ })();
865
+ void chrome.action.setIcon({ tabId, path: iconConfig.path });
866
+ void chrome.action.setTitle({ tabId, title });
867
+ if (iconConfig.badgeColor) void chrome.action.setBadgeBackgroundColor({ tabId, color: iconConfig.badgeColor });
868
+ void chrome.action.setBadgeText({ tabId, text: badgeText });
869
+ }
870
+ }
871
+ async function onTabRemoved(tabId) {
872
+ const { tabs } = store.getState();
873
+ if (!tabs.has(tabId)) return;
874
+ logger.debug(`Connected tab ${tabId} was closed, disconnecting`);
875
+ await disconnectTab(tabId);
876
+ }
877
+ async function onTabActivated(activeInfo) {
878
+ store.setState({ currentTabId: activeInfo.tabId });
879
+ }
880
+ async function onActionClicked(tab) {
881
+ if (!tab.id) {
882
+ logger.debug("No tab ID available");
883
+ return;
884
+ }
885
+ if (isRestrictedUrl(tab.url)) {
886
+ logger.debug("Cannot attach to restricted URL:", tab.url);
887
+ return;
888
+ }
889
+ const { tabs, connectionState } = store.getState();
890
+ const tabInfo = tabs.get(tab.id);
891
+ if (connectionState === "extension-replaced") {
892
+ logger.debug("Clearing extension-replaced state, connecting clicked tab");
893
+ store.setState({ connectionState: "idle", errorText: void 0 });
894
+ await connectTab(tab.id);
895
+ return;
896
+ }
897
+ if ((tabInfo == null ? void 0 : tabInfo.state) === "error") {
898
+ logger.debug("Tab has error - disconnecting to clear state");
899
+ await disconnectTab(tab.id);
900
+ return;
901
+ }
902
+ if ((tabInfo == null ? void 0 : tabInfo.state) === "connecting") {
903
+ logger.debug("Tab is already connecting, ignoring click");
904
+ return;
905
+ }
906
+ if ((tabInfo == null ? void 0 : tabInfo.state) === "connected") {
907
+ await disconnectTab(tab.id);
908
+ } else {
909
+ await connectTab(tab.id);
910
+ }
911
+ }
912
+ resetDebugger();
913
+ connectionManager.maintainLoop();
914
+ chrome.contextMenus.remove("e2e-pilot-pin-element").catch(() => {
915
+ }).finally(() => {
916
+ chrome.contextMenus.create({
917
+ id: "e2e-pilot-pin-element",
918
+ title: "Copy E2E Pilot Element Reference",
919
+ contexts: ["all"],
920
+ visible: false
921
+ });
922
+ });
923
+ function updateContextMenuVisibility() {
924
+ var _a;
925
+ const { currentTabId, tabs } = store.getState();
926
+ const isConnected = currentTabId !== void 0 && ((_a = tabs.get(currentTabId)) == null ? void 0 : _a.state) === "connected";
927
+ chrome.contextMenus.update("e2e-pilot-pin-element", { visible: isConnected });
928
+ }
929
+ chrome.runtime.onInstalled.addListener((details) => {
930
+ if (details.reason === "install") {
931
+ void chrome.tabs.create({ url: "welcome.html" });
932
+ }
933
+ });
934
+ function serializeTabs(tabs) {
935
+ return JSON.stringify(Array.from(tabs.entries()));
936
+ }
937
+ store.subscribe((state, prevState) => {
938
+ logger.log(state);
939
+ void updateIcons();
940
+ updateContextMenuVisibility();
941
+ const tabsChanged = serializeTabs(state.tabs) !== serializeTabs(prevState.tabs);
942
+ if (tabsChanged) {
943
+ tabGroupQueue = tabGroupQueue.then(syncTabGroup).catch((e) => {
944
+ logger.debug("syncTabGroup error:", e);
945
+ });
946
+ }
947
+ });
948
+ logger.debug(`Using relay URL: ${RELAY_URL}`);
949
+ let lastMemoryUsage = 0;
950
+ let lastMemoryCheck = Date.now();
951
+ const MEMORY_WARNING_THRESHOLD = 50 * 1024 * 1024;
952
+ const MEMORY_CRITICAL_THRESHOLD = 100 * 1024 * 1024;
953
+ const MEMORY_GROWTH_THRESHOLD = 10 * 1024 * 1024;
954
+ function checkMemory() {
955
+ try {
956
+ const memory = performance.memory;
957
+ if (!memory) {
958
+ return;
959
+ }
960
+ const used = memory.usedJSHeapSize;
961
+ const total = memory.totalJSHeapSize;
962
+ const limit = memory.jsHeapSizeLimit;
963
+ const now = Date.now();
964
+ const timeDelta = now - lastMemoryCheck;
965
+ const memoryDelta = used - lastMemoryUsage;
966
+ const formatMB = (bytes) => (bytes / 1024 / 1024).toFixed(2) + "MB";
967
+ const growthRate = timeDelta > 0 ? memoryDelta / timeDelta * 1e3 : 0;
968
+ if (used > MEMORY_CRITICAL_THRESHOLD) {
969
+ logger.error(
970
+ `MEMORY CRITICAL: used=${formatMB(used)} total=${formatMB(total)} limit=${formatMB(limit)} growth=${formatMB(memoryDelta)} rate=${formatMB(growthRate)}/s`
971
+ );
972
+ } else if (used > MEMORY_WARNING_THRESHOLD) {
973
+ logger.warn(
974
+ `MEMORY WARNING: used=${formatMB(used)} total=${formatMB(total)} limit=${formatMB(limit)} growth=${formatMB(memoryDelta)} rate=${formatMB(growthRate)}/s`
975
+ );
976
+ } else if (memoryDelta > MEMORY_GROWTH_THRESHOLD && timeDelta < 6e4) {
977
+ logger.warn(
978
+ `MEMORY SPIKE: grew ${formatMB(memoryDelta)} in ${(timeDelta / 1e3).toFixed(1)}s (used=${formatMB(used)})`
979
+ );
980
+ }
981
+ lastMemoryUsage = used;
982
+ lastMemoryCheck = now;
983
+ } catch (e) {
984
+ }
985
+ }
986
+ setInterval(checkMemory, 5e3);
987
+ checkMemory();
988
+ chrome.tabs.onRemoved.addListener(onTabRemoved);
989
+ chrome.tabs.onActivated.addListener(onTabActivated);
990
+ chrome.action.onClicked.addListener(onActionClicked);
991
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
992
+ void updateIcons();
993
+ if (changeInfo.groupId !== void 0) {
994
+ tabGroupQueue = tabGroupQueue.then(async () => {
995
+ var _a;
996
+ const existingGroups = await chrome.tabGroups.query({ title: "e2e-pilot" });
997
+ const groupId = (_a = existingGroups[0]) == null ? void 0 : _a.id;
998
+ if (groupId === void 0) {
999
+ return;
1000
+ }
1001
+ const { tabs } = store.getState();
1002
+ if (changeInfo.groupId === groupId) {
1003
+ if (!tabs.has(tabId) && !isRestrictedUrl(tab.url)) {
1004
+ logger.debug("Tab manually added to e2e-pilot group:", tabId);
1005
+ await connectTab(tabId);
1006
+ }
1007
+ } else if (tabs.has(tabId)) {
1008
+ const tabInfo = tabs.get(tabId);
1009
+ if ((tabInfo == null ? void 0 : tabInfo.state) === "connecting") {
1010
+ logger.debug("Tab removed from group while connecting, ignoring:", tabId);
1011
+ return;
1012
+ }
1013
+ logger.debug("Tab manually removed from e2e-pilot group:", tabId);
1014
+ await disconnectTab(tabId);
1015
+ }
1016
+ }).catch((e) => {
1017
+ logger.debug("onTabUpdated handler error:", e);
1018
+ });
1019
+ }
1020
+ });
1021
+ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
1022
+ if (info.menuItemId !== "e2e-pilot-pin-element" || !(tab == null ? void 0 : tab.id)) return;
1023
+ const tabInfo = store.getState().tabs.get(tab.id);
1024
+ if (!tabInfo || tabInfo.state !== "connected") {
1025
+ logger.debug("Tab not connected, ignoring");
1026
+ return;
1027
+ }
1028
+ const debuggee = { tabId: tab.id };
1029
+ const count = (tabInfo.pinnedCount || 0) + 1;
1030
+ store.setState((state) => {
1031
+ const newTabs = new Map(state.tabs);
1032
+ const existing = newTabs.get(tab.id);
1033
+ if (existing) {
1034
+ newTabs.set(tab.id, { ...existing, pinnedCount: count });
1035
+ }
1036
+ return { tabs: newTabs };
1037
+ });
1038
+ const name = `e2ePilotPinnedElem${count}`;
1039
+ const connectedTabs = Array.from(store.getState().tabs.entries()).filter(([_, t]) => t.state === "connected").sort((a, b) => (a[1].attachOrder ?? 0) - (b[1].attachOrder ?? 0));
1040
+ const pageIndex = connectedTabs.findIndex(([id]) => id === tab.id);
1041
+ const hasMultiplePages = connectedTabs.length > 1;
1042
+ try {
1043
+ const result = await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
1044
+ expression: `
1045
+ if (window.__e2e_pilot_lastRightClicked) {
1046
+ window.${name} = window.__e2e_pilot_lastRightClicked;
1047
+ '${name}';
1048
+ } else {
1049
+ throw new Error('No element was right-clicked');
1050
+ }
1051
+ `,
1052
+ returnByValue: true
1053
+ });
1054
+ if (result.exceptionDetails) {
1055
+ logger.error("Failed to pin element:", result.exceptionDetails.text);
1056
+ return;
1057
+ }
1058
+ const clipboardText = hasMultiplePages ? `globalThis.${name} (page ${pageIndex}, ${tab.url || "unknown url"})` : `globalThis.${name}`;
1059
+ await chrome.debugger.sendCommand(debuggee, "Runtime.evaluate", {
1060
+ expression: `
1061
+ (() => {
1062
+ const el = window.${name};
1063
+ if (!el) return;
1064
+ const orig = el.getAttribute('style') || '';
1065
+ el.setAttribute('style', orig + '; outline: 3px solid #22c55e !important; outline-offset: 2px !important; box-shadow: 0 0 0 3px #22c55e !important;');
1066
+ setTimeout(() => el.setAttribute('style', orig), 300);
1067
+ return navigator.clipboard.writeText(${JSON.stringify(clipboardText)});
1068
+ })()
1069
+ `,
1070
+ awaitPromise: true
1071
+ });
1072
+ logger.debug("Pinned element as:", name);
1073
+ } catch (error) {
1074
+ logger.error("Failed to pin element:", error.message);
1075
+ }
1076
+ });
1077
+ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
1078
+ const handleMessage = async () => {
1079
+ switch (message.type) {
1080
+ case "getState": {
1081
+ const state = store.getState();
1082
+ const chromeTabs = await chrome.tabs.query({});
1083
+ const tabsWithInfo = Array.from(state.tabs.entries()).map(([tabId, info]) => {
1084
+ const chromeTab = chromeTabs.find((t) => t.id === tabId);
1085
+ return {
1086
+ tabId,
1087
+ info,
1088
+ title: chromeTab == null ? void 0 : chromeTab.title,
1089
+ url: chromeTab == null ? void 0 : chromeTab.url
1090
+ };
1091
+ });
1092
+ return {
1093
+ connectionState: state.connectionState,
1094
+ tabs: tabsWithInfo,
1095
+ currentTabId: state.currentTabId,
1096
+ errorText: state.errorText
1097
+ };
1098
+ }
1099
+ case "connectActiveTab": {
1100
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
1101
+ const tab = tabs[0];
1102
+ if (tab == null ? void 0 : tab.id) {
1103
+ await onActionClicked(tab);
1104
+ }
1105
+ return { success: true };
1106
+ }
1107
+ case "disconnectTab": {
1108
+ if (message.tabId) {
1109
+ await disconnectTab(message.tabId);
1110
+ }
1111
+ return { success: true };
1112
+ }
1113
+ case "disconnectAll": {
1114
+ await disconnectEverything();
1115
+ return { success: true };
1116
+ }
1117
+ default:
1118
+ return { error: "Unknown message type" };
1119
+ }
1120
+ };
1121
+ handleMessage().then(sendResponse).catch((error) => {
1122
+ sendResponse({ error: error.message });
1123
+ });
1124
+ return true;
1125
+ });
1126
+ void updateIcons();