chore(extension): connect button for each page, style tweaks (#848)

<img width="643" height="709" alt="image"
src="https://github.com/user-attachments/assets/850f2455-b853-4c0f-8047-a7f2ced16b7b"
/>
This commit is contained in:
Yury Semikhatsky
2025-08-07 17:24:48 -07:00
committed by GitHub
parent 636f1956cc
commit 3b6ecf0a43
4 changed files with 95 additions and 92 deletions

View File

@@ -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<void> {
private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise<void> {
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<void> {
private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise<void> {
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 = () => {

View File

@@ -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 {

View File

@@ -29,7 +29,6 @@ type StatusType = 'connected' | 'error' | 'connecting';
const ConnectApp: React.FC = () => {
const [tabs, setTabs] = useState<TabInfo[]>([]);
const [selectedTab, setSelectedTab] = useState<TabInfo | undefined>();
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 (
<div className='app-container'>
<div className='content-wrapper'>
<h1 className='main-title'>
Playwright MCP Extension
</h1>
{status && <StatusBanner type={status.type} message={status.message} />}
{showButtons && (
<div className='button-container'>
<Button variant='primary' onClick={handleContinue}>
Continue
</Button>
<Button variant='default' onClick={handleReject}>
Reject
</Button>
{status && (
<div className='status-container'>
<StatusBanner type={status.type} message={status.message} />
{showButtons && (
<Button variant='reject' onClick={handleReject}>
Reject
</Button>
)}
</div>
)}
{showTabList && (
<div>
<h2 className='tab-section-title'>
<div className='tab-section-title'>
Select page to expose to MCP server:
</h2>
</div>
<div>
{tabs.map(tab => (
<TabItem
key={tab.id}
tab={tab}
isSelected={selectedTab?.id === tab.id}
onSelect={() => setSelectedTab(tab)}
onConnect={() => handleConnectToTab(tab)}
/>
))}
</div>
@@ -175,7 +162,7 @@ const StatusBanner: React.FC<{ type: StatusType; message: string }> = ({ type, m
return <div className={`status-banner ${type}`}>{message}</div>;
};
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 (
<div className={className} onClick={onSelect}>
<input
type='radio'
className='tab-radio'
checked={isSelected}
/>
<div className='tab-item'>
<img
src={tab.favIconUrl || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect width="16" height="16" fill="%23f6f8fa"/></svg>'}
alt=''
@@ -210,6 +189,9 @@ const TabItem: React.FC<{ tab: TabInfo; isSelected: boolean; onSelect: () => voi
<div className='tab-title'>{tab.title || 'Untitled'}</div>
<div className='tab-url'>{tab.url}</div>
</div>
<Button variant='primary' onClick={onConnect}>
Connect
</Button>
</div>
);
};

View File

@@ -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!`),