rechrome 1.13.0 → 1.15.0
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/lib/background.mjs +42 -25
- package/extension/lib/ui/connect.js +8 -7
- package/package.json +1 -1
- package/serve.js +20 -1
- package/serve.ts +20 -1
|
@@ -366,16 +366,32 @@ async function openRelayConnection(mcpRelayUrl, protocolVersion) {
|
|
|
366
366
|
throw new Error(message);
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
-
const PLAYWRIGHT_GROUP_TITLE = "
|
|
369
|
+
const PLAYWRIGHT_GROUP_TITLE = "pw";
|
|
370
370
|
const PLAYWRIGHT_GROUP_COLOR = "green";
|
|
371
|
+
const MAX_GROUP_TITLE_LEN = 7;
|
|
371
372
|
const NON_DEBUGGABLE_SCHEMES = ["chrome:", "edge:", "devtools:"];
|
|
372
373
|
const CONNECTED_BADGE = { text: "✓", color: "#4CAF50", title: "Connected to Playwright client" };
|
|
373
374
|
function isNonDebuggableUrl(url) {
|
|
374
375
|
return !!url && NON_DEBUGGABLE_SCHEMES.some((s) => url.startsWith(s));
|
|
375
376
|
}
|
|
377
|
+
function urlDomain(url) {
|
|
378
|
+
if (!url)
|
|
379
|
+
return void 0;
|
|
380
|
+
try {
|
|
381
|
+
const u = new URL(url);
|
|
382
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
383
|
+
return void 0;
|
|
384
|
+
return u.hostname.replace(/^www\./, "").split(".")[0];
|
|
385
|
+
} catch {
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function groupTitle(clientName, seedUrl) {
|
|
390
|
+
return (clientName || urlDomain(seedUrl) || PLAYWRIGHT_GROUP_TITLE).slice(0, MAX_GROUP_TITLE_LEN);
|
|
391
|
+
}
|
|
376
392
|
async function cleanupStalePlaywrightGroups() {
|
|
377
393
|
try {
|
|
378
|
-
const groups = await chrome.tabGroups.query({
|
|
394
|
+
const groups = await chrome.tabGroups.query({ color: PLAYWRIGHT_GROUP_COLOR });
|
|
379
395
|
const tabsPerGroup = await Promise.all(groups.map((g) => chrome.tabs.query({ groupId: g.id })));
|
|
380
396
|
const tabIds = tabsPerGroup.flat().map((t) => t.id).filter((id) => id !== void 0);
|
|
381
397
|
if (tabIds.length)
|
|
@@ -385,14 +401,16 @@ async function cleanupStalePlaywrightGroups() {
|
|
|
385
401
|
}
|
|
386
402
|
}
|
|
387
403
|
class ConnectedTabGroup {
|
|
388
|
-
constructor(connection, selectedTab) {
|
|
404
|
+
constructor(connection, selectedTab, clientName) {
|
|
389
405
|
__publicField(this, "_connection");
|
|
390
406
|
__publicField(this, "_groupId", null);
|
|
391
407
|
__publicField(this, "_groupTabIds", /* @__PURE__ */ new Set());
|
|
392
408
|
__publicField(this, "_onTabUpdatedListener");
|
|
393
409
|
__publicField(this, "_onTabRemovedListener");
|
|
410
|
+
__publicField(this, "_groupTitle");
|
|
394
411
|
__publicField(this, "onclose");
|
|
395
412
|
this._connection = connection;
|
|
413
|
+
this._groupTitle = groupTitle(clientName, selectedTab.url);
|
|
396
414
|
this._connection.onclose = () => this._onConnectionClose();
|
|
397
415
|
this._connection.ontabattached = (tabId) => this._onTabAttached(tabId);
|
|
398
416
|
this._connection.ontabdetached = (tabId) => this._onTabDetached(tabId);
|
|
@@ -485,7 +503,7 @@ class ConnectedTabGroup {
|
|
|
485
503
|
await this._retryOnDrag(async () => {
|
|
486
504
|
if (this._groupId === null) {
|
|
487
505
|
this._groupId = await chrome.tabs.group({ tabIds: [tabId] });
|
|
488
|
-
await chrome.tabGroups.update(this._groupId, { color: PLAYWRIGHT_GROUP_COLOR, title:
|
|
506
|
+
await chrome.tabGroups.update(this._groupId, { color: PLAYWRIGHT_GROUP_COLOR, title: this._groupTitle });
|
|
489
507
|
} else {
|
|
490
508
|
await chrome.tabs.group({ groupId: this._groupId, tabIds: [tabId] });
|
|
491
509
|
}
|
|
@@ -518,8 +536,11 @@ class ConnectedTabGroup {
|
|
|
518
536
|
}
|
|
519
537
|
class PlaywrightExtension {
|
|
520
538
|
constructor() {
|
|
521
|
-
|
|
522
|
-
|
|
539
|
+
// Multiple concurrent clients can share one Chrome profile — each connection gets
|
|
540
|
+
// its own ConnectedTabGroup (its own Chrome tab group), so a new client no longer
|
|
541
|
+
// evicts existing ones. Tabs stay isolated by per-group _groupId / attachedTabs.
|
|
542
|
+
__publicField(this, "_activeGroups", /* @__PURE__ */ new Set());
|
|
543
|
+
__publicField(this, "_clientNames", /* @__PURE__ */ new Map());
|
|
523
544
|
__publicField(this, "_pendingConnections", new PendingConnections());
|
|
524
545
|
// Service worker restarts lose all connection state, so any existing
|
|
525
546
|
// Playwright groups are stale. Connections wait on this before reconciling.
|
|
@@ -530,7 +551,6 @@ class PlaywrightExtension {
|
|
|
530
551
|
}
|
|
531
552
|
// Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031
|
|
532
553
|
_onMessage(message, sender, sendResponse) {
|
|
533
|
-
var _a;
|
|
534
554
|
switch (message.type) {
|
|
535
555
|
case "connectionRequested":
|
|
536
556
|
this._pendingConnections.create(sender.tab.id, message.mcpRelayUrl, message.protocolVersion).then(
|
|
@@ -541,8 +561,8 @@ class PlaywrightExtension {
|
|
|
541
561
|
case "getTabs":
|
|
542
562
|
this._getTabs().then(
|
|
543
563
|
(tabs) => {
|
|
544
|
-
var
|
|
545
|
-
return sendResponse({ success: true, tabs, currentTabId: (
|
|
564
|
+
var _a;
|
|
565
|
+
return sendResponse({ success: true, tabs, currentTabId: (_a = sender.tab) == null ? void 0 : _a.id });
|
|
546
566
|
},
|
|
547
567
|
(error) => sendResponse({ success: false, error: error.message })
|
|
548
568
|
);
|
|
@@ -557,8 +577,8 @@ class PlaywrightExtension {
|
|
|
557
577
|
}
|
|
558
578
|
case "getConnectionStatus":
|
|
559
579
|
sendResponse({
|
|
560
|
-
connectedTabIds: ((
|
|
561
|
-
clientName: this.
|
|
580
|
+
connectedTabIds: [...this._activeGroups].flatMap((group) => group.connectedTabIds()),
|
|
581
|
+
clientName: [...this._clientNames.values()].filter(Boolean).join(", ") || void 0
|
|
562
582
|
});
|
|
563
583
|
return false;
|
|
564
584
|
case "disconnect":
|
|
@@ -576,19 +596,16 @@ class PlaywrightExtension {
|
|
|
576
596
|
async _connectTab(selectorTabId, tab, clientName) {
|
|
577
597
|
try {
|
|
578
598
|
await this._cleanupPromise;
|
|
579
|
-
this._disconnect("Another connection is requested");
|
|
580
599
|
const connection = await this._pendingConnections.take(selectorTabId);
|
|
581
600
|
if (!connection)
|
|
582
601
|
throw new Error("Pending client connection closed");
|
|
583
|
-
const group = new ConnectedTabGroup(connection, tab);
|
|
602
|
+
const group = new ConnectedTabGroup(connection, tab, clientName);
|
|
584
603
|
group.onclose = () => {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
this._activeClientName = void 0;
|
|
588
|
-
}
|
|
604
|
+
this._activeGroups.delete(group);
|
|
605
|
+
this._clientNames.delete(group);
|
|
589
606
|
};
|
|
590
|
-
this.
|
|
591
|
-
this.
|
|
607
|
+
this._activeGroups.add(group);
|
|
608
|
+
this._clientNames.set(group, clientName);
|
|
592
609
|
await Promise.all([
|
|
593
610
|
chrome.tabs.update(tab.id, { active: true }),
|
|
594
611
|
chrome.windows.update(tab.windowId, { focused: true })
|
|
@@ -612,13 +629,13 @@ class PlaywrightExtension {
|
|
|
612
629
|
active: true
|
|
613
630
|
});
|
|
614
631
|
}
|
|
615
|
-
// Closes
|
|
616
|
-
//
|
|
632
|
+
// Closes every active group's connection. ConnectedTabGroup's onclose handles
|
|
633
|
+
// per-group state cleanup (connectedTabIds, badges, reconcile).
|
|
617
634
|
_disconnect(reason) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
this.
|
|
621
|
-
this.
|
|
635
|
+
for (const group of this._activeGroups)
|
|
636
|
+
group.close(reason);
|
|
637
|
+
this._activeGroups.clear();
|
|
638
|
+
this._clientNames.clear();
|
|
622
639
|
}
|
|
623
640
|
}
|
|
624
641
|
new PlaywrightExtension();
|
|
@@ -27,9 +27,10 @@ const ConnectApp = () => {
|
|
|
27
27
|
setError(`Invalid mcpRelayUrl parameter in URL: ${relayUrl}. ${e}`);
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
+
let info = "unknown";
|
|
30
31
|
try {
|
|
31
32
|
const client = JSON.parse(params.get("client") || "{}");
|
|
32
|
-
|
|
33
|
+
info = `${client.name || "unknown"}`;
|
|
33
34
|
setClientInfo(info);
|
|
34
35
|
setStatus({
|
|
35
36
|
type: "connecting",
|
|
@@ -60,7 +61,7 @@ const ConnectApp = () => {
|
|
|
60
61
|
const expectedToken = getOrCreateAuthToken();
|
|
61
62
|
const token = params.get("token");
|
|
62
63
|
if (token === expectedToken) {
|
|
63
|
-
await handleConnectToTab();
|
|
64
|
+
await handleConnectToTab(void 0, info);
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
67
|
if (token) {
|
|
@@ -86,26 +87,26 @@ const ConnectApp = () => {
|
|
|
86
87
|
else
|
|
87
88
|
setStatus({ type: "error", message: "Failed to load tabs: " + response.error });
|
|
88
89
|
}, []);
|
|
89
|
-
const handleConnectToTab = reactExports.useCallback(async (tab) => {
|
|
90
|
+
const handleConnectToTab = reactExports.useCallback(async (tab, clientName = clientInfo) => {
|
|
90
91
|
setShowTabList(false);
|
|
91
92
|
try {
|
|
92
93
|
const response = await chrome.runtime.sendMessage({
|
|
93
94
|
type: "connectToTab",
|
|
94
95
|
tab,
|
|
95
|
-
clientName
|
|
96
|
+
clientName
|
|
96
97
|
});
|
|
97
98
|
if (response == null ? void 0 : response.success) {
|
|
98
|
-
setStatus({ type: "connected", message: `"${
|
|
99
|
+
setStatus({ type: "connected", message: `"${clientName}" connected.` });
|
|
99
100
|
} else {
|
|
100
101
|
setStatus({
|
|
101
102
|
type: "error",
|
|
102
|
-
message: (response == null ? void 0 : response.error) || `"${
|
|
103
|
+
message: (response == null ? void 0 : response.error) || `"${clientName}" failed to connect.`
|
|
103
104
|
});
|
|
104
105
|
}
|
|
105
106
|
} catch (e) {
|
|
106
107
|
setStatus({
|
|
107
108
|
type: "error",
|
|
108
|
-
message: `"${
|
|
109
|
+
message: `"${clientName}" failed to connect: ${e}`
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
}, [clientInfo]);
|
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -14,6 +14,25 @@ import {
|
|
|
14
14
|
const TAILSCALE_BIN = process.env.TAILSCALE_BIN || "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
15
15
|
const CERT_RENEW_THRESHOLD_DAYS = 7;
|
|
16
16
|
|
|
17
|
+
// Short label for a client identity, used as the Chrome tab-group name (the tab
|
|
18
|
+
// strip is space-constrained, so cap at 7 chars). gitUrl ".../owner/repo/tree/branch"
|
|
19
|
+
// -> "rep:bra" (3+3); "host:/path/to/dir" -> "dir"; bare host/IP -> as-is. Strips a
|
|
20
|
+
// trailing "@profile" suffix first.
|
|
21
|
+
const MAX_GROUP_LABEL_LEN = 7;
|
|
22
|
+
function shortClientLabel(raw: string): string {
|
|
23
|
+
if (!raw) return raw;
|
|
24
|
+
const baseId = raw.includes("@") ? raw.slice(0, raw.indexOf("@")) : raw;
|
|
25
|
+
const git = baseId.match(/^https?:\/\/[^/]+\/[^/]+\/([^/]+?)(?:\/tree\/(.+))?$/);
|
|
26
|
+
let label: string;
|
|
27
|
+
if (git)
|
|
28
|
+
label = git[2] ? `${git[1].slice(0, 3)}:${git[2].slice(0, 3)}` : git[1];
|
|
29
|
+
else {
|
|
30
|
+
const hostCwd = baseId.match(/^[^:]+:(.+)$/);
|
|
31
|
+
label = hostCwd ? (hostCwd[1].split("/").filter(Boolean).pop() || baseId) : baseId;
|
|
32
|
+
}
|
|
33
|
+
return label.slice(0, MAX_GROUP_LABEL_LEN);
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
async function renewCertIfNeeded(certPath: string, keyPath: string): Promise<boolean> {
|
|
18
37
|
const certContent = await file(certPath).text().catch(() => null);
|
|
19
38
|
if (!certContent) return false;
|
|
@@ -233,7 +252,7 @@ export async function serve() {
|
|
|
233
252
|
TMPDIR: process.env.TMPDIR,
|
|
234
253
|
DISPLAY: process.env.DISPLAY,
|
|
235
254
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
236
|
-
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
|
|
255
|
+
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: shortClientLabel(clientName) } : {}),
|
|
237
256
|
...passthroughEnv,
|
|
238
257
|
// Enable extension bridge when credentials are present
|
|
239
258
|
...(passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_ID && passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_TOKEN
|
package/serve.ts
CHANGED
|
@@ -14,6 +14,25 @@ import {
|
|
|
14
14
|
const TAILSCALE_BIN = process.env.TAILSCALE_BIN || "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
15
15
|
const CERT_RENEW_THRESHOLD_DAYS = 7;
|
|
16
16
|
|
|
17
|
+
// Short label for a client identity, used as the Chrome tab-group name (the tab
|
|
18
|
+
// strip is space-constrained, so cap at 7 chars). gitUrl ".../owner/repo/tree/branch"
|
|
19
|
+
// -> "rep:bra" (3+3); "host:/path/to/dir" -> "dir"; bare host/IP -> as-is. Strips a
|
|
20
|
+
// trailing "@profile" suffix first.
|
|
21
|
+
const MAX_GROUP_LABEL_LEN = 7;
|
|
22
|
+
function shortClientLabel(raw: string): string {
|
|
23
|
+
if (!raw) return raw;
|
|
24
|
+
const baseId = raw.includes("@") ? raw.slice(0, raw.indexOf("@")) : raw;
|
|
25
|
+
const git = baseId.match(/^https?:\/\/[^/]+\/[^/]+\/([^/]+?)(?:\/tree\/(.+))?$/);
|
|
26
|
+
let label: string;
|
|
27
|
+
if (git)
|
|
28
|
+
label = git[2] ? `${git[1].slice(0, 3)}:${git[2].slice(0, 3)}` : git[1];
|
|
29
|
+
else {
|
|
30
|
+
const hostCwd = baseId.match(/^[^:]+:(.+)$/);
|
|
31
|
+
label = hostCwd ? (hostCwd[1].split("/").filter(Boolean).pop() || baseId) : baseId;
|
|
32
|
+
}
|
|
33
|
+
return label.slice(0, MAX_GROUP_LABEL_LEN);
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
async function renewCertIfNeeded(certPath: string, keyPath: string): Promise<boolean> {
|
|
18
37
|
const certContent = await file(certPath).text().catch(() => null);
|
|
19
38
|
if (!certContent) return false;
|
|
@@ -233,7 +252,7 @@ export async function serve() {
|
|
|
233
252
|
TMPDIR: process.env.TMPDIR,
|
|
234
253
|
DISPLAY: process.env.DISPLAY,
|
|
235
254
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
236
|
-
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
|
|
255
|
+
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: shortClientLabel(clientName) } : {}),
|
|
237
256
|
...passthroughEnv,
|
|
238
257
|
// Enable extension bridge when credentials are present
|
|
239
258
|
...(passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_ID && passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_TOKEN
|