rechrome 1.12.1 → 1.12.3

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.
@@ -1,83 +1,249 @@
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
+ class ProtocolV1Handler {
5
+ constructor(context) {
6
+ __publicField(this, "_context");
7
+ __publicField(this, "_selectedTabPromise");
8
+ __publicField(this, "_selectedTabResolve");
9
+ this._context = context;
10
+ this._selectedTabPromise = new Promise((resolve) => this._selectedTabResolve = resolve);
11
+ }
12
+ async handleCommand(message) {
13
+ if (message.method === "attachToTab") {
14
+ const tabId = await this._selectedTabPromise;
15
+ const debuggee = { tabId };
16
+ await chrome.debugger.attach(debuggee, "1.3");
17
+ this._context.notifyTabAttached(tabId);
18
+ const result = await chrome.debugger.sendCommand(debuggee, "Target.getTargetInfo");
19
+ return { targetInfo: result == null ? void 0 : result.targetInfo };
20
+ }
21
+ if (message.method === "forwardCDPCommand") {
22
+ const { sessionId, method, params } = message.params;
23
+ if (method === "Target.createTarget")
24
+ throw new Error("Tab creation is not supported yet. Update Playwright MCP or CLI to the latest version.");
25
+ const tabId = [...this._context.attachedTabs][0];
26
+ if (tabId === void 0)
27
+ throw new Error("No tab is connected");
28
+ const debuggerSession = { tabId, sessionId };
29
+ return await chrome.debugger.sendCommand(debuggerSession, method, params);
30
+ }
31
+ throw new Error(`Unknown method: ${message.method}`);
32
+ }
33
+ forwardChromeEvent(fullMethod, args) {
34
+ if (fullMethod !== "chrome.debugger.onEvent")
35
+ return;
36
+ const [source, method, params] = args;
37
+ this._context.sendMessage({
38
+ method: "forwardCDPEvent",
39
+ params: { sessionId: source.sessionId, method, params }
40
+ });
41
+ }
42
+ onUserAttachRequest(tab) {
43
+ if (tab.id !== void 0)
44
+ this._selectedTabResolve(tab.id);
45
+ }
46
+ onUserDetachRequest(_tabId) {
47
+ }
48
+ didInitialize() {
49
+ }
50
+ }
51
+ const ALLOWED_CHROME_COMMANDS = /* @__PURE__ */ new Set([
52
+ "chrome.debugger.attach",
53
+ "chrome.debugger.detach",
54
+ "chrome.debugger.sendCommand",
55
+ "chrome.tabs.create",
56
+ "chrome.tabs.remove"
57
+ ]);
58
+ class ProtocolV2Handler {
59
+ constructor(context) {
60
+ __publicField(this, "_context");
61
+ this._context = context;
62
+ }
63
+ async handleCommand(message) {
64
+ if (ALLOWED_CHROME_COMMANDS.has(message.method)) {
65
+ const args = message.params ?? [];
66
+ const result = await invokeChromeMethod(message.method, args);
67
+ if (message.method === "chrome.debugger.attach") {
68
+ const target = args[0];
69
+ if ((target == null ? void 0 : target.tabId) !== void 0)
70
+ this._context.notifyTabAttached(target.tabId);
71
+ }
72
+ return result ?? {};
73
+ }
74
+ throw new Error(`Unknown method: ${message.method}`);
75
+ }
76
+ forwardChromeEvent(fullMethod, args) {
77
+ this._context.sendMessage({ method: fullMethod, params: args });
78
+ }
79
+ onUserAttachRequest(tab) {
80
+ this._context.sendMessage({ method: "chrome.tabs.onCreated", params: [tab] });
81
+ }
82
+ didInitialize() {
83
+ this._context.sendMessage({ method: "extension.initialized", params: [] });
84
+ }
85
+ onUserDetachRequest(tabId) {
86
+ this._context.sendMessage({
87
+ method: "chrome.debugger.onDetach",
88
+ params: [{ tabId }, "target_closed"]
89
+ });
90
+ }
91
+ }
92
+ function resolveChromeMember(fullMethod) {
93
+ const parts = fullMethod.split(".");
94
+ if (parts[0] !== "chrome" || parts.length < 3)
95
+ throw new Error(`Invalid chrome method: ${fullMethod}`);
96
+ let obj = chrome;
97
+ for (let i = 1; i < parts.length - 1; i++) {
98
+ obj = obj == null ? void 0 : obj[parts[i]];
99
+ if (obj === void 0)
100
+ throw new Error(`Unknown chrome path: ${parts.slice(0, i + 1).join(".")}, calling ${fullMethod}`);
101
+ }
102
+ return { obj, name: parts[parts.length - 1] };
103
+ }
104
+ async function invokeChromeMethod(fullMethod, args) {
105
+ const { obj, name } = resolveChromeMember(fullMethod);
106
+ const fn = obj[name];
107
+ if (typeof fn !== "function")
108
+ throw new Error(`Not a function: ${fullMethod}`);
109
+ return await fn.apply(obj, args);
110
+ }
1
111
  function debugLog(...args) {
2
112
  {
3
113
  console.log("[Extension]", ...args);
4
114
  }
5
115
  }
116
+ const CHROME_EVENT_METHODS = [
117
+ "chrome.debugger.onEvent",
118
+ "chrome.debugger.onDetach",
119
+ "chrome.tabs.onCreated",
120
+ "chrome.tabs.onRemoved"
121
+ ];
6
122
  class RelayConnection {
7
- _debuggee;
8
- _ws;
9
- _eventListener;
10
- _detachListener;
11
- _tabPromise;
12
- _tabPromiseResolve;
13
- _closed = false;
14
- _playwrightTabIds = /* @__PURE__ */ new Set();
15
- onclose;
16
- onPlaywrightTabCreated;
17
- onPlaywrightTabRemoved;
18
- constructor(ws) {
19
- this._debuggee = {};
20
- this._tabPromise = new Promise((resolve) => this._tabPromiseResolve = resolve);
123
+ constructor(ws, protocolVersion) {
124
+ __publicField(this, "_ws");
125
+ __publicField(this, "_handler");
126
+ // Tabs whose debugger we have explicitly attached for this connection.
127
+ __publicField(this, "_attachedTabs", /* @__PURE__ */ new Set());
128
+ // Once we've attached at least one tab, detaching the last one closes the connection.
129
+ __publicField(this, "_hasEverAttached", false);
130
+ __publicField(this, "_eventListeners", []);
131
+ __publicField(this, "_closed", false);
132
+ __publicField(this, "onclose");
133
+ __publicField(this, "ontabattached");
134
+ __publicField(this, "ontabdetached");
21
135
  this._ws = ws;
136
+ const context = {
137
+ attachedTabs: this._attachedTabs,
138
+ sendMessage: (msg) => this._sendMessage(msg),
139
+ notifyTabAttached: (tabId) => this._notifyTabAttached(tabId),
140
+ notifyTabDetached: (tabId) => this._notifyTabDetached(tabId)
141
+ };
142
+ this._handler = protocolVersion === 1 ? new ProtocolV1Handler(context) : new ProtocolV2Handler(context);
143
+ this._installEventForwarders();
22
144
  this._ws.onmessage = this._onMessage.bind(this);
23
145
  this._ws.onclose = () => this._onClose();
24
- this._eventListener = this._onDebuggerEvent.bind(this);
25
- this._detachListener = this._onDebuggerDetach.bind(this);
26
- chrome.debugger.onEvent.addListener(this._eventListener);
27
- chrome.debugger.onDetach.addListener(this._detachListener);
28
146
  }
29
- // Either setTabId or close is called after creating the connection.
30
- setTabId(tabId) {
31
- this._debuggee = { tabId };
32
- this._tabPromiseResolve();
147
+ get attachedTabs() {
148
+ return this._attachedTabs;
149
+ }
150
+ // Signals the end of the initial-tab handshake — call after the initial
151
+ // round of `attachTab` invocations. For v2 this sends `extension.initialized`
152
+ // so the relay can unblock Playwright CDP traffic; v1 has no handshake.
153
+ didInitialize() {
154
+ this._handler.didInitialize();
33
155
  }
34
156
  close(message) {
35
157
  this._ws.close(1e3, message);
36
158
  this._onClose();
37
159
  }
160
+ // Called when the UI adds a tab to the Playwright group. The handler asks
161
+ // the relay to attach; the normal command path fires ontabattached.
162
+ attachTab(tab) {
163
+ if (this._closed || this._attachedTabs.has(tab.id))
164
+ return;
165
+ this._handler.onUserAttachRequest(tab);
166
+ }
167
+ // Called when the UI removes a tab from the Playwright group. We detach the
168
+ // debugger and update bookkeeping; the handler emits the wire-level detach
169
+ // notification for protocols that have one.
170
+ detachTab(tabId) {
171
+ if (this._closed || !this._attachedTabs.has(tabId))
172
+ return;
173
+ chrome.debugger.detach({ tabId }).catch((error) => {
174
+ debugLog("Error detaching tab:", error);
175
+ });
176
+ this._notifyTabDetached(tabId);
177
+ this._handler.onUserDetachRequest(tabId);
178
+ this._checkLastTabDetached();
179
+ }
180
+ _notifyTabAttached(tabId) {
181
+ var _a;
182
+ this._attachedTabs.add(tabId);
183
+ this._hasEverAttached = true;
184
+ (_a = this.ontabattached) == null ? void 0 : _a.call(this, tabId);
185
+ }
186
+ _notifyTabDetached(tabId) {
187
+ var _a;
188
+ this._attachedTabs.delete(tabId);
189
+ (_a = this.ontabdetached) == null ? void 0 : _a.call(this, tabId);
190
+ }
191
+ _installEventForwarders() {
192
+ for (const fullMethod of CHROME_EVENT_METHODS) {
193
+ const target = resolveChromeMember(fullMethod);
194
+ const listener = (...args) => this._onChromeEvent(fullMethod, args);
195
+ target.obj[target.name].addListener(listener);
196
+ this._eventListeners.push({
197
+ remove: () => target.obj[target.name].removeListener(listener)
198
+ });
199
+ }
200
+ }
38
201
  _onClose() {
202
+ var _a;
39
203
  if (this._closed)
40
204
  return;
41
205
  this._closed = true;
42
- chrome.debugger.onEvent.removeListener(this._eventListener);
43
- chrome.debugger.onDetach.removeListener(this._detachListener);
44
- chrome.debugger.detach(this._debuggee).catch(() => {
45
- });
46
- for (const tabId of this._playwrightTabIds)
206
+ for (const l of this._eventListeners)
207
+ l.remove();
208
+ this._eventListeners = [];
209
+ for (const tabId of [...this._attachedTabs]) {
47
210
  chrome.debugger.detach({ tabId }).catch(() => {
48
211
  });
49
- this._playwrightTabIds.clear();
50
- this.onclose?.();
212
+ this._notifyTabDetached(tabId);
213
+ }
214
+ (_a = this.onclose) == null ? void 0 : _a.call(this);
51
215
  }
52
- _onDebuggerEvent(source, method, params) {
53
- const isInitialTab = source.tabId === this._debuggee.tabId;
54
- const isPlaywrightTab = source.tabId !== void 0 && this._playwrightTabIds.has(source.tabId);
55
- if (!isInitialTab && !isPlaywrightTab)
56
- return;
57
- debugLog("Forwarding CDP event:", method, params);
58
- const sessionId = source.sessionId;
59
- const tabId = isPlaywrightTab ? source.tabId : void 0;
60
- this._sendMessage({
61
- method: "forwardCDPEvent",
62
- params: {
63
- sessionId,
64
- method,
65
- params,
66
- tabId
67
- }
68
- });
216
+ _checkLastTabDetached() {
217
+ if (this._hasEverAttached && this._attachedTabs.size === 0)
218
+ this.close("All controlled tabs detached");
69
219
  }
70
- _onDebuggerDetach(source, reason) {
71
- if (source.tabId !== void 0 && this._playwrightTabIds.has(source.tabId)) {
72
- debugLog("Playwright tab detached:", source.tabId, reason);
73
- this._playwrightTabIds.delete(source.tabId);
74
- this.onPlaywrightTabRemoved?.(source.tabId);
220
+ // Filters chrome.* events to attached tabs, delegates wire formatting to the
221
+ // handler, then runs shared detach bookkeeping.
222
+ _onChromeEvent(fullMethod, args) {
223
+ const tabId = this._tabIdForEventArgs(fullMethod, args);
224
+ if (tabId === void 0 || !this._attachedTabs.has(tabId))
75
225
  return;
226
+ this._handler.forwardChromeEvent(fullMethod, args);
227
+ if (fullMethod === "chrome.debugger.onDetach") {
228
+ this._notifyTabDetached(tabId);
229
+ this._checkLastTabDetached();
76
230
  }
77
- if (source.tabId !== this._debuggee.tabId)
78
- return;
79
- this.close(`Debugger detached: ${reason}`);
80
- this._debuggee = {};
231
+ }
232
+ // Returns the tabId an event refers to, for filtering by _attachedTabs.
233
+ _tabIdForEventArgs(fullMethod, args) {
234
+ var _a;
235
+ switch (fullMethod) {
236
+ case "chrome.debugger.onEvent":
237
+ case "chrome.debugger.onDetach":
238
+ return (_a = args[0]) == null ? void 0 : _a.tabId;
239
+ case "chrome.tabs.onCreated": {
240
+ const tab = args[0];
241
+ return tab.openerTabId;
242
+ }
243
+ case "chrome.tabs.onRemoved":
244
+ return args[0];
245
+ }
246
+ return void 0;
81
247
  }
82
248
  _onMessage(event) {
83
249
  this._onMessageAsync(event).catch((e) => debugLog("Error handling message:", e));
@@ -87,64 +253,21 @@ class RelayConnection {
87
253
  try {
88
254
  message = JSON.parse(event.data);
89
255
  } catch (error) {
90
- debugLog("Error parsing message:", error);
256
+ debugLog(`Error parsing message ${event.data}:`, error);
91
257
  this._sendError(-32700, `Error parsing message: ${error.message}`);
92
258
  return;
93
259
  }
94
- debugLog("Received message:", message);
95
260
  const response = {
96
261
  id: message.id
97
262
  };
98
263
  try {
99
- response.result = await this._handleCommand(message);
264
+ response.result = await this._handler.handleCommand(message);
100
265
  } catch (error) {
101
- debugLog("Error handling command:", error);
266
+ debugLog(`Error handling command ${JSON.stringify(message)}:`, error);
102
267
  response.error = error.message;
103
268
  }
104
- debugLog("Sending response:", response);
105
269
  this._sendMessage(response);
106
270
  }
107
- async _handleCommand(message) {
108
- if (message.method === "attachToTab") {
109
- await this._tabPromise;
110
- debugLog("Attaching debugger to tab:", this._debuggee);
111
- await chrome.debugger.attach(this._debuggee, "1.3");
112
- const result = await chrome.debugger.sendCommand(this._debuggee, "Target.getTargetInfo");
113
- return {
114
- targetInfo: result?.targetInfo,
115
- tabId: this._debuggee.tabId
116
- };
117
- }
118
- if (message.method === "createTab") {
119
- const url = message.params?.url ?? "about:blank";
120
- debugLog("Creating new tab:", url);
121
- const tab = await chrome.tabs.create({ url, active: true });
122
- const tabId = tab.id;
123
- await new Promise((resolve) => setTimeout(resolve, 300));
124
- await chrome.debugger.attach({ tabId }, "1.3");
125
- const result = await chrome.debugger.sendCommand({ tabId }, "Target.getTargetInfo");
126
- const targetInfo = result?.targetInfo || {
127
- targetId: String(tabId),
128
- type: "page",
129
- title: "",
130
- url: tab.url || url,
131
- attached: false,
132
- canAccessOpener: false
133
- };
134
- this._playwrightTabIds.add(tabId);
135
- this.onPlaywrightTabCreated?.(tabId);
136
- debugLog("Created playwright tab:", tabId, targetInfo);
137
- return { tabId, targetInfo };
138
- }
139
- if (!this._debuggee.tabId)
140
- throw new Error("No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.");
141
- if (message.method === "forwardCDPCommand") {
142
- const { sessionId, method, params, tabId } = message.params;
143
- debugLog("CDP command:", method, params, "tabId:", tabId);
144
- const debuggee = tabId !== void 0 ? { tabId, sessionId } : { ...this._debuggee, sessionId };
145
- return await chrome.debugger.sendCommand(debuggee, method, params);
146
- }
147
- }
148
271
  _sendError(code, message) {
149
272
  this._sendMessage({
150
273
  error: {
@@ -158,192 +281,330 @@ class RelayConnection {
158
281
  this._ws.send(JSON.stringify(message));
159
282
  }
160
283
  }
161
- class TabShareExtension {
162
- _connections = /* @__PURE__ */ new Map();
163
- _pendingTabSelection = /* @__PURE__ */ new Map();
284
+ class EagerPending {
285
+ constructor(connection) {
286
+ __publicField(this, "_connection");
287
+ __publicField(this, "onclose");
288
+ this._connection = connection;
289
+ this._connection.onclose = () => {
290
+ var _a;
291
+ return (_a = this.onclose) == null ? void 0 : _a.call(this);
292
+ };
293
+ }
294
+ static async create(mcpRelayUrl, protocolVersion) {
295
+ const connection = await openRelayConnection(mcpRelayUrl, protocolVersion);
296
+ return new EagerPending(connection);
297
+ }
298
+ async connect() {
299
+ return this._connection;
300
+ }
301
+ close(reason) {
302
+ this._connection.close(reason);
303
+ }
304
+ }
305
+ class DeferredPending {
306
+ constructor(_mcpRelayUrl, _protocolVersion) {
307
+ this._mcpRelayUrl = _mcpRelayUrl;
308
+ this._protocolVersion = _protocolVersion;
309
+ }
310
+ async connect() {
311
+ return openRelayConnection(this._mcpRelayUrl, this._protocolVersion);
312
+ }
313
+ close(_reason) {
314
+ }
315
+ }
316
+ class PendingConnections {
164
317
  constructor() {
318
+ __publicField(this, "_map", /* @__PURE__ */ new Map());
165
319
  chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this));
166
- chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this));
167
- chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this));
320
+ }
321
+ // v1 opens the relay WS eagerly — the daemon expects a prompt connection.
322
+ // v2 records only the descriptor; the WS opens lazily in `take` once the
323
+ // user clicks Allow.
324
+ async create(selectorTabId, mcpRelayUrl, protocolVersion) {
325
+ if (protocolVersion !== 1) {
326
+ this._map.set(selectorTabId, new DeferredPending(mcpRelayUrl, protocolVersion));
327
+ return;
328
+ }
329
+ const entry = await EagerPending.create(mcpRelayUrl, protocolVersion);
330
+ entry.onclose = () => {
331
+ if (this._map.get(selectorTabId) !== entry)
332
+ return;
333
+ this._map.delete(selectorTabId);
334
+ chrome.tabs.sendMessage(selectorTabId, { type: "pendingConnectionClosed" }).catch(() => {
335
+ });
336
+ };
337
+ this._map.set(selectorTabId, entry);
338
+ }
339
+ async take(selectorTabId) {
340
+ const entry = this._map.get(selectorTabId);
341
+ if (!entry)
342
+ return void 0;
343
+ this._map.delete(selectorTabId);
344
+ return entry.connect();
345
+ }
346
+ _onTabRemoved(tabId) {
347
+ const entry = this._map.get(tabId);
348
+ if (!entry)
349
+ return;
350
+ this._map.delete(tabId);
351
+ entry.close("Browser tab closed");
352
+ }
353
+ }
354
+ async function openRelayConnection(mcpRelayUrl, protocolVersion) {
355
+ try {
356
+ const socket = new WebSocket(mcpRelayUrl);
357
+ await new Promise((resolve, reject) => {
358
+ socket.onopen = () => resolve();
359
+ socket.onerror = () => reject(new Error("WebSocket error"));
360
+ setTimeout(() => reject(new Error("Connection timeout")), 5e3);
361
+ });
362
+ return new RelayConnection(socket, protocolVersion);
363
+ } catch (error) {
364
+ const message = `Failed to connect to MCP relay: ${error.message}`;
365
+ debugLog(message);
366
+ throw new Error(message);
367
+ }
368
+ }
369
+ const PLAYWRIGHT_GROUP_TITLE = "Playwright";
370
+ const PLAYWRIGHT_GROUP_COLOR = "green";
371
+ const NON_DEBUGGABLE_SCHEMES = ["chrome:", "edge:", "devtools:"];
372
+ const CONNECTED_BADGE = { text: "✓", color: "#4CAF50", title: "Connected to Playwright client" };
373
+ function isNonDebuggableUrl(url) {
374
+ return !!url && NON_DEBUGGABLE_SCHEMES.some((s) => url.startsWith(s));
375
+ }
376
+ async function cleanupStalePlaywrightGroups() {
377
+ try {
378
+ const groups = await chrome.tabGroups.query({ title: PLAYWRIGHT_GROUP_TITLE });
379
+ const tabsPerGroup = await Promise.all(groups.map((g) => chrome.tabs.query({ groupId: g.id })));
380
+ const tabIds = tabsPerGroup.flat().map((t) => t.id).filter((id) => id !== void 0);
381
+ if (tabIds.length)
382
+ await chrome.tabs.ungroup(tabIds);
383
+ } catch (error) {
384
+ debugLog("Error cleaning up stale groups:", error);
385
+ }
386
+ }
387
+ class ConnectedTabGroup {
388
+ constructor(connection, selectedTab) {
389
+ __publicField(this, "_connection");
390
+ __publicField(this, "_groupId", null);
391
+ __publicField(this, "_groupTabIds", /* @__PURE__ */ new Set());
392
+ __publicField(this, "_onTabUpdatedListener");
393
+ __publicField(this, "_onTabRemovedListener");
394
+ __publicField(this, "onclose");
395
+ this._connection = connection;
396
+ this._connection.onclose = () => this._onConnectionClose();
397
+ this._connection.ontabattached = (tabId) => this._onTabAttached(tabId);
398
+ this._connection.ontabdetached = (tabId) => this._onTabDetached(tabId);
399
+ this._onTabUpdatedListener = this._onTabUpdated.bind(this);
400
+ this._onTabRemovedListener = this._onTabRemoved.bind(this);
401
+ chrome.tabs.onUpdated.addListener(this._onTabUpdatedListener);
402
+ chrome.tabs.onRemoved.addListener(this._onTabRemovedListener);
403
+ this._connection.attachTab(selectedTab);
404
+ this._connection.didInitialize();
405
+ }
406
+ connectedTabIds() {
407
+ return [...this._groupTabIds];
408
+ }
409
+ close(reason) {
410
+ this._connection.close(reason);
411
+ }
412
+ _onTabUpdated(tabId, changeInfo, tab) {
413
+ if (changeInfo.groupId !== void 0)
414
+ this._onTabGroupChanged(tabId, tab);
415
+ if (changeInfo.url === void 0)
416
+ return;
417
+ if (this._connection.attachedTabs.has(tabId))
418
+ void this._updateBadge(tabId, CONNECTED_BADGE);
419
+ else if (this._groupTabIds.has(tabId) && !isNonDebuggableUrl(changeInfo.url))
420
+ this._connection.attachTab(tab);
421
+ }
422
+ // Single entry point for group membership changes, whether the user dragged
423
+ // or we grouped the tab ourselves. Attaches on entry (if debuggable) and
424
+ // detaches on exit; a chrome:// tab stays in the group until it navigates
425
+ // (handled in _onTabUpdated).
426
+ _onTabGroupChanged(tabId, tab) {
427
+ const inOurGroup = this._groupId !== null && tab.groupId === this._groupId;
428
+ const wasInGroup = this._groupTabIds.has(tabId);
429
+ if (inOurGroup === wasInGroup)
430
+ return;
431
+ if (inOurGroup) {
432
+ this._groupTabIds.add(tabId);
433
+ if (!isNonDebuggableUrl(tab.url))
434
+ this._connection.attachTab(tab);
435
+ } else {
436
+ this._groupTabIds.delete(tabId);
437
+ if (this._connection.attachedTabs.has(tabId))
438
+ this._connection.detachTab(tabId);
439
+ }
440
+ }
441
+ _onTabRemoved(tabId) {
442
+ this._groupTabIds.delete(tabId);
443
+ }
444
+ _onTabAttached(tabId) {
445
+ void this._updateBadge(tabId, CONNECTED_BADGE);
446
+ void this._addTabToGroup(tabId);
447
+ }
448
+ // The debugger detached (drag-out, tab close, or external action). Clear the
449
+ // badge but leave the tab in the group — the user's intent is still there,
450
+ // and a subsequent navigation will re-attach via _onTabUpdated.
451
+ _onTabDetached(tabId) {
452
+ void this._updateBadge(tabId, { text: "" });
453
+ }
454
+ _onConnectionClose() {
455
+ var _a;
456
+ chrome.tabs.onUpdated.removeListener(this._onTabUpdatedListener);
457
+ chrome.tabs.onRemoved.removeListener(this._onTabRemovedListener);
458
+ const groupTabs = [...this._groupTabIds];
459
+ this._groupTabIds.clear();
460
+ if (groupTabs.length) {
461
+ this._retryOnDrag(() => chrome.tabs.ungroup(groupTabs)).catch((error) => {
462
+ debugLog("Error ungrouping tabs on close:", error);
463
+ });
464
+ }
465
+ (_a = this.onclose) == null ? void 0 : _a.call(this);
466
+ }
467
+ async _updateBadge(tabId, { text, color, title }) {
468
+ try {
469
+ await Promise.all([
470
+ chrome.action.setBadgeText({ tabId, text }),
471
+ chrome.action.setTitle({ tabId, title: title || "" }),
472
+ color ? chrome.action.setBadgeBackgroundColor({ tabId, color }) : Promise.resolve()
473
+ ]);
474
+ } catch (error) {
475
+ }
476
+ }
477
+ // Moves an already-attached tab into our Chrome tab group, creating it on
478
+ // first use. `_groupTabIds` is updated after the await so an onUpdated event
479
+ // that arrives concurrently (`_groupId` still null, wasInGroup still false)
480
+ // becomes a harmless no-op rather than taking the drag-out branch.
481
+ async _addTabToGroup(tabId) {
482
+ if (this._groupTabIds.has(tabId))
483
+ return;
484
+ try {
485
+ await this._retryOnDrag(async () => {
486
+ if (this._groupId === null) {
487
+ this._groupId = await chrome.tabs.group({ tabIds: [tabId] });
488
+ await chrome.tabGroups.update(this._groupId, { color: PLAYWRIGHT_GROUP_COLOR, title: PLAYWRIGHT_GROUP_TITLE });
489
+ } else {
490
+ await chrome.tabs.group({ groupId: this._groupId, tabIds: [tabId] });
491
+ }
492
+ });
493
+ this._groupTabIds.add(tabId);
494
+ } catch (error) {
495
+ debugLog("Error adding tab to group:", error);
496
+ }
497
+ }
498
+ // Chrome throws "user may be dragging a tab" while a drag is in progress.
499
+ // Retry with backoff until it clears (or we give up).
500
+ async _retryOnDrag(fn) {
501
+ var _a;
502
+ const delays = [0, 100, 200, 400, 800];
503
+ let lastError;
504
+ for (const delay of delays) {
505
+ if (delay)
506
+ await new Promise((resolve) => setTimeout(resolve, delay));
507
+ try {
508
+ await fn();
509
+ return;
510
+ } catch (error) {
511
+ if (!((_a = error == null ? void 0 : error.message) == null ? void 0 : _a.includes("user may be dragging a tab")))
512
+ throw error;
513
+ lastError = error;
514
+ }
515
+ }
516
+ throw lastError;
517
+ }
518
+ }
519
+ class PlaywrightExtension {
520
+ constructor() {
521
+ __publicField(this, "_activeGroup");
522
+ __publicField(this, "_activeClientName");
523
+ __publicField(this, "_pendingConnections", new PendingConnections());
524
+ // Service worker restarts lose all connection state, so any existing
525
+ // Playwright groups are stale. Connections wait on this before reconciling.
526
+ __publicField(this, "_cleanupPromise");
168
527
  chrome.runtime.onMessage.addListener(this._onMessage.bind(this));
169
528
  chrome.action.onClicked.addListener(this._onActionClicked.bind(this));
529
+ this._cleanupPromise = cleanupStalePlaywrightGroups();
170
530
  }
171
531
  // Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031
172
532
  _onMessage(message, sender, sendResponse) {
533
+ var _a;
173
534
  switch (message.type) {
174
- case "connectToMCPRelay":
175
- this._connectToRelay(sender.tab.id, message.mcpRelayUrl).then(
535
+ case "connectionRequested":
536
+ this._pendingConnections.create(sender.tab.id, message.mcpRelayUrl, message.protocolVersion).then(
176
537
  () => sendResponse({ success: true }),
177
538
  (error) => sendResponse({ success: false, error: error.message })
178
539
  );
179
540
  return true;
180
541
  case "getTabs":
181
542
  this._getTabs().then(
182
- (tabs) => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }),
543
+ (tabs) => {
544
+ var _a2;
545
+ return sendResponse({ success: true, tabs, currentTabId: (_a2 = sender.tab) == null ? void 0 : _a2.id });
546
+ },
183
547
  (error) => sendResponse({ success: false, error: error.message })
184
548
  );
185
549
  return true;
186
- case "connectToTab":
187
- const tabId = message.tabId || sender.tab?.id;
188
- const windowId = message.windowId || sender.tab?.windowId;
189
- this._connectTab(sender.tab.id, tabId, windowId, message.mcpRelayUrl).then(
550
+ case "connectToTab": {
551
+ const selectedTab = message.tab ?? sender.tab;
552
+ this._connectTab(sender.tab.id, selectedTab, message.clientName).then(
190
553
  () => sendResponse({ success: true }),
191
554
  (error) => sendResponse({ success: false, error: error.message })
192
555
  );
193
556
  return true;
194
- // Return true to indicate that the response will be sent asynchronously
557
+ }
195
558
  case "getConnectionStatus":
196
559
  sendResponse({
197
- connections: [...this._connections.values()].map((s) => ({
198
- mcpRelayUrl: s.mcpRelayUrl,
199
- connectedTabId: s.connectedTabId,
200
- playwrightTabIds: [...s.playwrightTabIds]
201
- })),
202
- // Legacy fields for backward compat: first connection's tabId
203
- connectedTabId: [...this._connections.values()][0]?.connectedTabId ?? null,
204
- playwrightTabIds: [...this._connections.values()].flatMap((s) => [...s.playwrightTabIds])
560
+ connectedTabIds: ((_a = this._activeGroup) == null ? void 0 : _a.connectedTabIds()) ?? [],
561
+ clientName: this._activeClientName
205
562
  });
206
563
  return false;
207
564
  case "disconnect":
208
- this._disconnect(message.mcpRelayUrl).then(
209
- () => sendResponse({ success: true }),
210
- (error) => sendResponse({ success: false, error: error.message })
211
- );
565
+ try {
566
+ this._disconnect("User disconnected");
567
+ sendResponse({ success: true });
568
+ } catch (error) {
569
+ sendResponse({ success: false, error: error.message });
570
+ }
212
571
  return true;
213
- }
214
- return false;
215
- }
216
- async _connectToRelay(selectorTabId, mcpRelayUrl) {
217
- try {
218
- debugLog(`Connecting to relay at ${mcpRelayUrl}`);
219
- const socket = new WebSocket(mcpRelayUrl);
220
- await new Promise((resolve, reject) => {
221
- socket.onopen = () => resolve();
222
- socket.onerror = () => reject(new Error("WebSocket error"));
223
- setTimeout(() => reject(new Error("Connection timeout")), 5e3);
224
- });
225
- const connection = new RelayConnection(socket);
226
- connection.onclose = () => {
227
- debugLog("Connection closed");
228
- this._pendingTabSelection.delete(selectorTabId);
229
- };
230
- this._pendingTabSelection.set(selectorTabId, { connection, mcpRelayUrl });
231
- debugLog(`Connected to MCP relay`);
232
- } catch (error) {
233
- const message = `Failed to connect to MCP relay: ${error.message}`;
234
- debugLog(message);
235
- throw new Error(message);
572
+ case "keepalive":
573
+ return false;
236
574
  }
237
575
  }
238
- async _connectTab(selectorTabId, tabId, windowId, mcpRelayUrl) {
576
+ async _connectTab(selectorTabId, tab, clientName) {
239
577
  try {
240
- debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`);
241
- const pending = this._pendingTabSelection.get(selectorTabId);
242
- if (!pending)
243
- throw new Error("No active MCP relay connection");
244
- this._pendingTabSelection.delete(selectorTabId);
245
- const connection = pending.connection;
246
- const relayUrl = pending.mcpRelayUrl;
247
- const existing = this._connections.get(relayUrl);
248
- if (existing) {
249
- existing.connection.close("Another connection is requested");
250
- this._connections.delete(relayUrl);
251
- }
252
- const state = {
253
- connection,
254
- connectedTabId: tabId,
255
- playwrightTabIds: /* @__PURE__ */ new Set(),
256
- mcpRelayUrl: relayUrl
257
- };
258
- this._connections.set(relayUrl, state);
259
- connection.setTabId(tabId);
260
- connection.onclose = () => {
261
- debugLog("MCP connection closed");
262
- if (this._connections.get(relayUrl)?.connection === connection)
263
- this._connections.delete(relayUrl);
264
- void this._updateBadge(state.connectedTabId, { text: "" });
265
- for (const pwTabId of state.playwrightTabIds)
266
- void this._updateBadge(pwTabId, { text: "" });
267
- state.playwrightTabIds.clear();
268
- };
269
- connection.onPlaywrightTabCreated = (pwTabId) => {
270
- state.playwrightTabIds.add(pwTabId);
271
- void this._updateBadge(pwTabId, { text: "✓", color: "#1976D2", title: "Playwright managed tab" });
272
- };
273
- connection.onPlaywrightTabRemoved = (pwTabId) => {
274
- state.playwrightTabIds.delete(pwTabId);
275
- void this._updateBadge(pwTabId, { text: "" });
578
+ await this._cleanupPromise;
579
+ this._disconnect("Another connection is requested");
580
+ const connection = await this._pendingConnections.take(selectorTabId);
581
+ if (!connection)
582
+ throw new Error("Pending client connection closed");
583
+ const group = new ConnectedTabGroup(connection, tab);
584
+ group.onclose = () => {
585
+ if (this._activeGroup === group) {
586
+ this._activeGroup = void 0;
587
+ this._activeClientName = void 0;
588
+ }
276
589
  };
590
+ this._activeGroup = group;
591
+ this._activeClientName = clientName;
277
592
  await Promise.all([
278
- this._updateBadge(tabId, { text: "✓", color: "#4CAF50", title: "Connected to MCP client" }),
279
- chrome.tabs.update(tabId, { active: true }),
280
- chrome.windows.update(windowId, { focused: true })
281
- ]);
282
- debugLog(`Connected to MCP bridge`);
593
+ chrome.tabs.update(tab.id, { active: true }),
594
+ chrome.windows.update(tab.windowId, { focused: true })
595
+ ]).catch(() => {
596
+ });
597
+ if (tab.id !== selectorTabId)
598
+ await chrome.tabs.remove(selectorTabId).catch(() => {
599
+ });
283
600
  } catch (error) {
284
- debugLog(`Failed to connect tab ${tabId}:`, error.message);
601
+ debugLog(`Failed to connect tab ${tab.id}:`, error.message);
285
602
  throw error;
286
603
  }
287
604
  }
288
- async _updateBadge(tabId, { text, color, title }) {
289
- try {
290
- await chrome.action.setBadgeText({ tabId, text });
291
- await chrome.action.setTitle({ tabId, title: title || "" });
292
- if (color)
293
- await chrome.action.setBadgeBackgroundColor({ tabId, color });
294
- } catch (error) {
295
- }
296
- }
297
- async _onTabRemoved(tabId) {
298
- const pendingConnection = [...this._pendingTabSelection.entries()].find(([k]) => k === tabId)?.[1];
299
- if (pendingConnection) {
300
- this._pendingTabSelection.delete(tabId);
301
- pendingConnection.connection.close("Browser tab closed");
302
- return;
303
- }
304
- for (const [relayUrl, state] of this._connections) {
305
- if (state.playwrightTabIds.has(tabId)) {
306
- state.playwrightTabIds.delete(tabId);
307
- return;
308
- }
309
- if (state.connectedTabId === tabId) {
310
- state.connection.close("Browser tab closed");
311
- this._connections.delete(relayUrl);
312
- return;
313
- }
314
- }
315
- }
316
- _onTabActivated(activeInfo) {
317
- for (const [tabId, pending] of this._pendingTabSelection) {
318
- if (tabId === activeInfo.tabId) {
319
- if (pending.timerId) {
320
- clearTimeout(pending.timerId);
321
- pending.timerId = void 0;
322
- }
323
- continue;
324
- }
325
- if (!pending.timerId) {
326
- pending.timerId = setTimeout(() => {
327
- const existed = this._pendingTabSelection.delete(tabId);
328
- if (existed) {
329
- pending.connection.close("Tab has been inactive for 5 seconds");
330
- chrome.tabs.sendMessage(tabId, { type: "connectionTimeout" });
331
- }
332
- }, 5e3);
333
- }
334
- }
335
- }
336
- _onTabUpdated(tabId, changeInfo, tab) {
337
- for (const state of this._connections.values()) {
338
- if (state.connectedTabId === tabId)
339
- void this._updateBadge(tabId, { text: "✓", color: "#4CAF50", title: "Connected to MCP client" });
340
- if (state.playwrightTabIds.has(tabId))
341
- void this._updateBadge(tabId, { text: "✓", color: "#1976D2", title: "Playwright managed tab" });
342
- }
343
- }
344
605
  async _getTabs() {
345
606
  const tabs = await chrome.tabs.query({});
346
- return tabs.filter((tab) => tab.url && !["chrome:", "edge:", "devtools:"].some((scheme) => tab.url.startsWith(scheme)));
607
+ return tabs.filter((tab) => !isNonDebuggableUrl(tab.url));
347
608
  }
348
609
  async _onActionClicked() {
349
610
  await chrome.tabs.create({
@@ -351,18 +612,13 @@ class TabShareExtension {
351
612
  active: true
352
613
  });
353
614
  }
354
- async _disconnect(mcpRelayUrl) {
355
- if (mcpRelayUrl) {
356
- const state = this._connections.get(mcpRelayUrl);
357
- if (state) {
358
- state.connection.close("User disconnected");
359
- this._connections.delete(mcpRelayUrl);
360
- }
361
- } else {
362
- for (const state of this._connections.values())
363
- state.connection.close("User disconnected");
364
- this._connections.clear();
365
- }
615
+ // Closes the active group's connection if any. ConnectedTabGroup's onclose
616
+ // handles state cleanup (connectedTabIds, badges, reconcile).
617
+ _disconnect(reason) {
618
+ var _a;
619
+ (_a = this._activeGroup) == null ? void 0 : _a.close(reason);
620
+ this._activeGroup = void 0;
621
+ this._activeClientName = void 0;
366
622
  }
367
623
  }
368
- new TabShareExtension();
624
+ new PlaywrightExtension();