diff --git a/extension/src/background.ts b/extension/src/background.ts index 2c6eecb..dcc3ce0 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -44,7 +44,7 @@ class TabShareExtension { private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) { switch (message.type) { case 'connectToMCPRelay': - this._connectToRelay(message.mcpRelayUrl!, sender.tab!.id!).then( + this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl!).then( () => sendResponse({ success: true }), (error: any) => sendResponse({ success: false, error: error.message })); return true; @@ -54,7 +54,7 @@ class TabShareExtension { (error: any) => sendResponse({ success: false, error: error.message })); return true; case 'connectToTab': - this._connectTab(message.tabId, message.windowId, message.mcpRelayUrl!).then( + this._connectTab(sender.tab!.id!, message.tabId, message.windowId, message.mcpRelayUrl!).then( () => sendResponse({ success: true }), (error: any) => sendResponse({ success: false, error: error.message })); return true; // Return true to indicate that the response will be sent asynchronously @@ -62,7 +62,7 @@ class TabShareExtension { return false; } - private async _connectToRelay(mcpRelayUrl: string, selectorTabId: number): Promise { + private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise { try { debugLog(`Connecting to relay at ${mcpRelayUrl}`); const socket = new WebSocket(mcpRelayUrl); @@ -86,7 +86,7 @@ class TabShareExtension { } } - private async _connectTab(tabId: number, windowId: number, mcpRelayUrl: string): Promise { + private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise { try { debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`); try { @@ -96,10 +96,10 @@ class TabShareExtension { } await this._setConnectedTabId(null); - this._activeConnection = this._pendingTabSelection.get(tabId)?.connection; + this._activeConnection = this._pendingTabSelection.get(selectorTabId)?.connection; if (!this._activeConnection) throw new Error('No active MCP relay connection'); - this._pendingTabSelection.delete(tabId); + this._pendingTabSelection.delete(selectorTabId); this._activeConnection.setTabId(tabId); this._activeConnection.onclose = () => { diff --git a/extension/src/ui/connect.css b/extension/src/ui/connect.css index 788be0f..e86a20b 100644 --- a/extension/src/ui/connect.css +++ b/extension/src/ui/connect.css @@ -25,10 +25,9 @@ body { background-color: #ffffff; color: #1f2328; margin: 0; - padding: 24px; + padding: 16px; min-height: 100vh; font-size: 14px; - line-height: 1.5; } .content-wrapper { @@ -36,50 +35,55 @@ body { margin: 0 auto; } -.main-title { - font-size: 32px; - font-weight: 600; - margin-bottom: 8px; - color: #1f2328; +/* Status Banner */ +.status-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding-right: 12px; } -/* Status Banner */ .status-banner { - padding: 16px; - margin-bottom: 24px; - border-radius: 6px; - border: 1px solid; + padding: 12px; font-size: 14px; font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + flex: 1; } .status-banner.connected { - background-color: #dafbe1; - border-color: #1a7f37; - color: #0d5a23; + color: #1f2328; +} + +.status-banner.connected::before { + content: "\2705"; + margin-right: 8px; } .status-banner.error { - background-color: #ffebe9; - border-color: #da3633; - color: #a40e26; + color: #1f2328; } -.status-banner.connecting { - background-color: #fff8c5; - border-color: #d1b500; - color: #7a5c00; +.status-banner.error::before { + content: "\274C"; + margin-right: 8px; } /* Buttons */ .button-container { - margin-bottom: 24px; + margin-bottom: 16px; + display: flex; + justify-content: flex-end; + padding-right: 12px; } .button { padding: 8px 16px; border-radius: 6px; - border: 1px solid; + border: none; font-size: 14px; font-weight: 500; cursor: pointer; @@ -88,46 +92,63 @@ body { justify-content: center; text-decoration: none; margin-right: 8px; + min-width: 90px; } .button.primary { - background-color: #2da44e; - border-color: #2da44e; - color: #ffffff; + background-color: #f8f9fa; + color: #3c4043; + border: 1px solid #dadce0; } .button.primary:hover { - background-color: #2c974b; + background-color: #f1f3f4; + border-color: #dadce0; + box-shadow: 0 1px 2px 0 rgba(60,64,67,.1); } .button.default { background-color: #f6f8fa; - border-color: #d1d9e0; color: #24292f; } .button.default:hover { background-color: #f3f4f6; - border-color: #c7d2da; +} + +.button.reject { + background-color: #da3633; + color: #ffffff; + border: 1px solid #da3633; +} + +.button.reject:hover { + background-color: #c73836; + border-color: #c73836; } /* Tab selection */ .tab-section-title { - font-size: 20px; - font-weight: 600; - margin-bottom: 16px; - color: #1f2328; + padding-left: 12px; + font-size: 12px; + font-weight: 400; + margin-bottom: 12px; + color: #656d76; } .tab-item { display: flex; align-items: center; padding: 12px; - border: 1px solid #d1d9e0; - border-radius: 6px; margin-bottom: 8px; background-color: #ffffff; cursor: pointer; + border-radius: 6px; + transition: background-color 0.2s ease; +} + +.tab-item:hover { + background-color: #f8f9fa; } .tab-item.selected { diff --git a/extension/src/ui/connect.tsx b/extension/src/ui/connect.tsx index a06ae8b..fcbcc4d 100644 --- a/extension/src/ui/connect.tsx +++ b/extension/src/ui/connect.tsx @@ -29,7 +29,6 @@ type StatusType = 'connected' | 'error' | 'connecting'; const ConnectApp: React.FC = () => { const [tabs, setTabs] = useState([]); - const [selectedTab, setSelectedTab] = useState(); const [status, setStatus] = useState<{ type: StatusType; message: string } | null>(null); const [showButtons, setShowButtons] = useState(true); const [showTabList, setShowTabList] = useState(true); @@ -54,7 +53,7 @@ const ConnectApp: React.FC = () => { setClientInfo(info); setStatus({ type: 'connecting', - message: `MCP client "${info}" is trying to connect. Do you want to continue?` + message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?` }); } catch (e) { setStatus({ type: 'error', message: 'Failed to parse client version.' }); @@ -73,30 +72,22 @@ const ConnectApp: React.FC = () => { const loadTabs = useCallback(async () => { const response = await chrome.runtime.sendMessage({ type: 'getTabs' }); - if (response.success) { + if (response.success) setTabs(response.tabs); - const currentTab = response.tabs.find((tab: TabInfo) => tab.id === response.currentTabId); - setSelectedTab(currentTab); - } else { + else setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error }); - } }, []); - const handleContinue = useCallback(async () => { + const handleConnectToTab = useCallback(async (tab: TabInfo) => { setShowButtons(false); setShowTabList(false); - if (!selectedTab) { - setStatus({ type: 'error', message: 'Tab not selected.' }); - return; - } - try { const response = await chrome.runtime.sendMessage({ type: 'connectToTab', mcpRelayUrl, - tabId: selectedTab.id, - windowId: selectedTab.windowId, + tabId: tab.id, + windowId: tab.windowId, }); if (response?.success) { @@ -113,7 +104,7 @@ const ConnectApp: React.FC = () => { message: `MCP client "${clientInfo}" failed to connect: ${e}` }); } - }, [selectedTab, clientInfo, mcpRelayUrl]); + }, [clientInfo, mcpRelayUrl]); const handleReject = useCallback(() => { setShowButtons(false); @@ -122,45 +113,41 @@ const ConnectApp: React.FC = () => { }, []); useEffect(() => { - chrome.runtime.onMessage.addListener(message => { + const listener = (message: any) => { if (message.type === 'connectionTimeout') handleReject(); - }); + }; + chrome.runtime.onMessage.addListener(listener); + return () => { + chrome.runtime.onMessage.removeListener(listener); + }; }, []); return (
-

- Playwright MCP Extension -

- - {status && } - - {showButtons && ( -
- - + {status && ( +
+ + {showButtons && ( + + )}
)} - {showTabList && (
-

+
Select page to expose to MCP server: -

+
{tabs.map(tab => ( setSelectedTab(tab)} + onConnect={() => handleConnectToTab(tab)} /> ))}
@@ -175,7 +162,7 @@ const StatusBanner: React.FC<{ type: StatusType; message: string }> = ({ type, m return
{message}
; }; -const Button: React.FC<{ variant: 'primary' | 'default'; onClick: () => void; children: React.ReactNode }> = ({ +const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({ variant, onClick, children @@ -187,20 +174,12 @@ const Button: React.FC<{ variant: 'primary' | 'default'; onClick: () => void; ch ); }; -const TabItem: React.FC<{ tab: TabInfo; isSelected: boolean; onSelect: () => void }> = ({ +const TabItem: React.FC<{ tab: TabInfo; onConnect: () => void }> = ({ tab, - isSelected, - onSelect + onConnect }) => { - const className = `tab-item ${isSelected ? 'selected' : ''}`.trim(); - return ( -
- +
'} alt='' @@ -210,6 +189,9 @@ const TabItem: React.FC<{ tab: TabInfo; isSelected: boolean; onSelect: () => voi
{tab.title || 'Untitled'}
{tab.url}
+
); }; diff --git a/extension/tests/extension.spec.ts b/extension/tests/extension.spec.ts index 683d1d6..017b35c 100644 --- a/extension/tests/extension.spec.ts +++ b/extension/tests/extension.spec.ts @@ -94,7 +94,7 @@ test('navigate with extension', async ({ browserWithExtension, startClient, serv }); const selectorPage = await confirmationPagePromise; - await selectorPage.getByRole('button', { name: 'Continue' }).click(); + await selectorPage.locator('.tab-item', { hasText: 'Playwright MCP Extension' }).getByRole('button', { name: 'Connect' }).click(); expect(await navigateResponse).toHaveResponse({ pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),