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.
@@ -366,16 +366,32 @@ async function openRelayConnection(mcpRelayUrl, protocolVersion) {
366
366
  throw new Error(message);
367
367
  }
368
368
  }
369
- const PLAYWRIGHT_GROUP_TITLE = "Playwright";
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({ title: PLAYWRIGHT_GROUP_TITLE });
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: PLAYWRIGHT_GROUP_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
- __publicField(this, "_activeGroup");
522
- __publicField(this, "_activeClientName");
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 _a2;
545
- return sendResponse({ success: true, tabs, currentTabId: (_a2 = sender.tab) == null ? void 0 : _a2.id });
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: ((_a = this._activeGroup) == null ? void 0 : _a.connectedTabIds()) ?? [],
561
- clientName: this._activeClientName
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
- if (this._activeGroup === group) {
586
- this._activeGroup = void 0;
587
- this._activeClientName = void 0;
588
- }
604
+ this._activeGroups.delete(group);
605
+ this._clientNames.delete(group);
589
606
  };
590
- this._activeGroup = group;
591
- this._activeClientName = clientName;
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 the active group's connection if any. ConnectedTabGroup's onclose
616
- // handles state cleanup (connectedTabIds, badges, reconcile).
632
+ // Closes every active group's connection. ConnectedTabGroup's onclose handles
633
+ // per-group state cleanup (connectedTabIds, badges, reconcile).
617
634
  _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;
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
- 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.15.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
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