rechrome 1.12.2 → 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.
- package/extension/connect.html +1 -1
- package/extension/lib/background.mjs +518 -262
- package/extension/lib/ui/authToken.css +34 -6
- package/extension/lib/ui/authToken.js +11638 -6158
- package/extension/lib/ui/connect.js +52 -60
- package/extension/lib/ui/status.js +31 -50
- package/extension/manifest.json +8 -7
- package/extension/status.html +1 -1
- package/package.json +1 -1
- package/rech.js +25 -12
- package/rech.ts +25 -12
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
50
|
-
|
|
212
|
+
this._notifyTabDetached(tabId);
|
|
213
|
+
}
|
|
214
|
+
(_a = this.onclose) == null ? void 0 : _a.call(this);
|
|
51
215
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
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.
|
|
264
|
+
response.result = await this._handler.handleCommand(message);
|
|
100
265
|
} catch (error) {
|
|
101
|
-
debugLog(
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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 "
|
|
175
|
-
this.
|
|
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) =>
|
|
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
|
|
188
|
-
|
|
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
|
-
|
|
557
|
+
}
|
|
195
558
|
case "getConnectionStatus":
|
|
196
559
|
sendResponse({
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
209
|
-
(
|
|
210
|
-
|
|
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
|
-
|
|
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,
|
|
576
|
+
async _connectTab(selectorTabId, tab, clientName) {
|
|
239
577
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
279
|
-
chrome.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 ${
|
|
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) =>
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
624
|
+
new PlaywrightExtension();
|