rechrome 1.13.0 → 1.14.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.
@@ -368,14 +368,33 @@ async function openRelayConnection(mcpRelayUrl, protocolVersion) {
368
368
  }
369
369
  const PLAYWRIGHT_GROUP_TITLE = "Playwright";
370
370
  const PLAYWRIGHT_GROUP_COLOR = "green";
371
+ const PLAYWRIGHT_GROUP_MARK = "🎭";
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\./, "");
385
+ } catch {
386
+ return void 0;
387
+ }
388
+ }
389
+ function groupTitle(clientName, seedUrl) {
390
+ return `${PLAYWRIGHT_GROUP_MARK} ${clientName || urlDomain(seedUrl) || PLAYWRIGHT_GROUP_TITLE}`;
391
+ }
376
392
  async function cleanupStalePlaywrightGroups() {
377
393
  try {
378
- const groups = await chrome.tabGroups.query({ title: PLAYWRIGHT_GROUP_TITLE });
394
+ const groups = (await chrome.tabGroups.query({})).filter((g) => {
395
+ var _a;
396
+ return (_a = g.title) == null ? void 0 : _a.startsWith(PLAYWRIGHT_GROUP_MARK);
397
+ });
379
398
  const tabsPerGroup = await Promise.all(groups.map((g) => chrome.tabs.query({ groupId: g.id })));
380
399
  const tabIds = tabsPerGroup.flat().map((t) => t.id).filter((id) => id !== void 0);
381
400
  if (tabIds.length)
@@ -385,14 +404,16 @@ async function cleanupStalePlaywrightGroups() {
385
404
  }
386
405
  }
387
406
  class ConnectedTabGroup {
388
- constructor(connection, selectedTab) {
407
+ constructor(connection, selectedTab, clientName) {
389
408
  __publicField(this, "_connection");
390
409
  __publicField(this, "_groupId", null);
391
410
  __publicField(this, "_groupTabIds", /* @__PURE__ */ new Set());
392
411
  __publicField(this, "_onTabUpdatedListener");
393
412
  __publicField(this, "_onTabRemovedListener");
413
+ __publicField(this, "_groupTitle");
394
414
  __publicField(this, "onclose");
395
415
  this._connection = connection;
416
+ this._groupTitle = groupTitle(clientName, selectedTab.url);
396
417
  this._connection.onclose = () => this._onConnectionClose();
397
418
  this._connection.ontabattached = (tabId) => this._onTabAttached(tabId);
398
419
  this._connection.ontabdetached = (tabId) => this._onTabDetached(tabId);
@@ -485,7 +506,7 @@ class ConnectedTabGroup {
485
506
  await this._retryOnDrag(async () => {
486
507
  if (this._groupId === null) {
487
508
  this._groupId = await chrome.tabs.group({ tabIds: [tabId] });
488
- await chrome.tabGroups.update(this._groupId, { color: PLAYWRIGHT_GROUP_COLOR, title: PLAYWRIGHT_GROUP_TITLE });
509
+ await chrome.tabGroups.update(this._groupId, { color: PLAYWRIGHT_GROUP_COLOR, title: this._groupTitle });
489
510
  } else {
490
511
  await chrome.tabs.group({ groupId: this._groupId, tabIds: [tabId] });
491
512
  }
@@ -518,8 +539,11 @@ class ConnectedTabGroup {
518
539
  }
519
540
  class PlaywrightExtension {
520
541
  constructor() {
521
- __publicField(this, "_activeGroup");
522
- __publicField(this, "_activeClientName");
542
+ // Multiple concurrent clients can share one Chrome profile — each connection gets
543
+ // its own ConnectedTabGroup (its own Chrome tab group), so a new client no longer
544
+ // evicts existing ones. Tabs stay isolated by per-group _groupId / attachedTabs.
545
+ __publicField(this, "_activeGroups", /* @__PURE__ */ new Set());
546
+ __publicField(this, "_clientNames", /* @__PURE__ */ new Map());
523
547
  __publicField(this, "_pendingConnections", new PendingConnections());
524
548
  // Service worker restarts lose all connection state, so any existing
525
549
  // Playwright groups are stale. Connections wait on this before reconciling.
@@ -530,7 +554,6 @@ class PlaywrightExtension {
530
554
  }
531
555
  // Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031
532
556
  _onMessage(message, sender, sendResponse) {
533
- var _a;
534
557
  switch (message.type) {
535
558
  case "connectionRequested":
536
559
  this._pendingConnections.create(sender.tab.id, message.mcpRelayUrl, message.protocolVersion).then(
@@ -541,8 +564,8 @@ class PlaywrightExtension {
541
564
  case "getTabs":
542
565
  this._getTabs().then(
543
566
  (tabs) => {
544
- var _a2;
545
- return sendResponse({ success: true, tabs, currentTabId: (_a2 = sender.tab) == null ? void 0 : _a2.id });
567
+ var _a;
568
+ return sendResponse({ success: true, tabs, currentTabId: (_a = sender.tab) == null ? void 0 : _a.id });
546
569
  },
547
570
  (error) => sendResponse({ success: false, error: error.message })
548
571
  );
@@ -557,8 +580,8 @@ class PlaywrightExtension {
557
580
  }
558
581
  case "getConnectionStatus":
559
582
  sendResponse({
560
- connectedTabIds: ((_a = this._activeGroup) == null ? void 0 : _a.connectedTabIds()) ?? [],
561
- clientName: this._activeClientName
583
+ connectedTabIds: [...this._activeGroups].flatMap((group) => group.connectedTabIds()),
584
+ clientName: [...this._clientNames.values()].filter(Boolean).join(", ") || void 0
562
585
  });
563
586
  return false;
564
587
  case "disconnect":
@@ -576,19 +599,16 @@ class PlaywrightExtension {
576
599
  async _connectTab(selectorTabId, tab, clientName) {
577
600
  try {
578
601
  await this._cleanupPromise;
579
- this._disconnect("Another connection is requested");
580
602
  const connection = await this._pendingConnections.take(selectorTabId);
581
603
  if (!connection)
582
604
  throw new Error("Pending client connection closed");
583
- const group = new ConnectedTabGroup(connection, tab);
605
+ const group = new ConnectedTabGroup(connection, tab, clientName);
584
606
  group.onclose = () => {
585
- if (this._activeGroup === group) {
586
- this._activeGroup = void 0;
587
- this._activeClientName = void 0;
588
- }
607
+ this._activeGroups.delete(group);
608
+ this._clientNames.delete(group);
589
609
  };
590
- this._activeGroup = group;
591
- this._activeClientName = clientName;
610
+ this._activeGroups.add(group);
611
+ this._clientNames.set(group, clientName);
592
612
  await Promise.all([
593
613
  chrome.tabs.update(tab.id, { active: true }),
594
614
  chrome.windows.update(tab.windowId, { focused: true })
@@ -612,13 +632,13 @@ class PlaywrightExtension {
612
632
  active: true
613
633
  });
614
634
  }
615
- // Closes the active group's connection if any. ConnectedTabGroup's onclose
616
- // handles state cleanup (connectedTabIds, badges, reconcile).
635
+ // Closes every active group's connection. ConnectedTabGroup's onclose handles
636
+ // per-group state cleanup (connectedTabIds, badges, reconcile).
617
637
  _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;
638
+ for (const group of this._activeGroups)
639
+ group.close(reason);
640
+ this._activeGroups.clear();
641
+ this._clientNames.clear();
622
642
  }
623
643
  }
624
644
  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
- const info = `${client.name || "unknown"}`;
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: clientInfo
96
+ clientName
96
97
  });
97
98
  if (response == null ? void 0 : response.success) {
98
- setStatus({ type: "connected", message: `"${clientInfo}" connected.` });
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) || `"${clientInfo}" failed to connect.`
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: `"${clientInfo}" failed to connect: ${e}`
109
+ message: `"${clientName}" failed to connect: ${e}`
109
110
  });
110
111
  }
111
112
  }, [clientInfo]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/serve.js CHANGED
@@ -14,6 +14,20 @@ 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, human-friendly label for a client identity, used as the Chrome tab-group
18
+ // name so concurrent sessions on one profile are distinguishable.
19
+ // gitUrl ".../owner/repo/tree/branch" -> "repo:branch"; "host:/path/to/dir" -> "dir";
20
+ // bare host/IP -> as-is. Strips a trailing "@profile" (email) suffix first.
21
+ function shortClientLabel(raw: string): string {
22
+ if (!raw) return raw;
23
+ const baseId = raw.includes("@") ? raw.slice(0, raw.indexOf("@")) : raw;
24
+ const git = baseId.match(/^https?:\/\/[^/]+\/[^/]+\/([^/]+?)(?:\/tree\/(.+))?$/);
25
+ if (git) return git[2] ? `${git[1]}:${git[2]}` : git[1];
26
+ const hostCwd = baseId.match(/^[^:]+:(.+)$/);
27
+ if (hostCwd) return hostCwd[1].split("/").filter(Boolean).pop() || baseId;
28
+ return baseId;
29
+ }
30
+
17
31
  async function renewCertIfNeeded(certPath: string, keyPath: string): Promise<boolean> {
18
32
  const certContent = await file(certPath).text().catch(() => null);
19
33
  if (!certContent) return false;
@@ -233,7 +247,7 @@ export async function serve() {
233
247
  TMPDIR: process.env.TMPDIR,
234
248
  DISPLAY: process.env.DISPLAY,
235
249
  XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
236
- ...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
250
+ ...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: shortClientLabel(clientName) } : {}),
237
251
  ...passthroughEnv,
238
252
  // Enable extension bridge when credentials are present
239
253
  ...(passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_ID && passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_TOKEN
package/serve.ts CHANGED
@@ -14,6 +14,20 @@ 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, human-friendly label for a client identity, used as the Chrome tab-group
18
+ // name so concurrent sessions on one profile are distinguishable.
19
+ // gitUrl ".../owner/repo/tree/branch" -> "repo:branch"; "host:/path/to/dir" -> "dir";
20
+ // bare host/IP -> as-is. Strips a trailing "@profile" (email) suffix first.
21
+ function shortClientLabel(raw: string): string {
22
+ if (!raw) return raw;
23
+ const baseId = raw.includes("@") ? raw.slice(0, raw.indexOf("@")) : raw;
24
+ const git = baseId.match(/^https?:\/\/[^/]+\/[^/]+\/([^/]+?)(?:\/tree\/(.+))?$/);
25
+ if (git) return git[2] ? `${git[1]}:${git[2]}` : git[1];
26
+ const hostCwd = baseId.match(/^[^:]+:(.+)$/);
27
+ if (hostCwd) return hostCwd[1].split("/").filter(Boolean).pop() || baseId;
28
+ return baseId;
29
+ }
30
+
17
31
  async function renewCertIfNeeded(certPath: string, keyPath: string): Promise<boolean> {
18
32
  const certContent = await file(certPath).text().catch(() => null);
19
33
  if (!certContent) return false;
@@ -233,7 +247,7 @@ export async function serve() {
233
247
  TMPDIR: process.env.TMPDIR,
234
248
  DISPLAY: process.env.DISPLAY,
235
249
  XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
236
- ...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
250
+ ...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: shortClientLabel(clientName) } : {}),
237
251
  ...passthroughEnv,
238
252
  // Enable extension bridge when credentials are present
239
253
  ...(passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_ID && passthroughEnv.PLAYWRIGHT_MCP_EXTENSION_TOKEN