Skip to content

Commit 21ced70

Browse files
authored
chore(extension): status page (#856)
1 parent d3bf2ee commit 21ced70

File tree

8 files changed

+282
-49
lines changed

8 files changed

+282
-49
lines changed

extension/src/background.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ type PageMessage = {
2626
tabId: number;
2727
windowId: number;
2828
mcpRelayUrl: string;
29+
} | {
30+
type: 'getConnectionStatus';
31+
} | {
32+
type: 'disconnect';
2933
};
3034

3135
class TabShareExtension {
@@ -38,6 +42,7 @@ class TabShareExtension {
3842
chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this));
3943
chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this));
4044
chrome.runtime.onMessage.addListener(this._onMessage.bind(this));
45+
chrome.action.onClicked.addListener(this._onActionClicked.bind(this));
4146
}
4247

4348
// Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031
@@ -58,6 +63,16 @@ class TabShareExtension {
5863
() => sendResponse({ success: true }),
5964
(error: any) => sendResponse({ success: false, error: error.message }));
6065
return true; // Return true to indicate that the response will be sent asynchronously
66+
case 'getConnectionStatus':
67+
sendResponse({
68+
connectedTabId: this._connectedTabId
69+
});
70+
return false;
71+
case 'disconnect':
72+
this._disconnect().then(
73+
() => sendResponse({ success: true }),
74+
(error: any) => sendResponse({ success: false, error: error.message }));
75+
return true;
6176
}
6277
return false;
6378
}
@@ -125,14 +140,15 @@ class TabShareExtension {
125140
const oldTabId = this._connectedTabId;
126141
this._connectedTabId = tabId;
127142
if (oldTabId && oldTabId !== tabId)
128-
await this._updateBadge(oldTabId, { text: '', color: null });
143+
await this._updateBadge(oldTabId, { text: '' });
129144
if (tabId)
130-
await this._updateBadge(tabId, { text: '', color: '#4CAF50' });
145+
await this._updateBadge(tabId, { text: '', color: '#4CAF50', title: 'Connected to MCP client' });
131146
}
132147

133-
private async _updateBadge(tabId: number, { text, color }: { text: string; color: string | null }): Promise<void> {
148+
private async _updateBadge(tabId: number, { text, color, title }: { text: string; color?: string, title?: string }): Promise<void> {
134149
try {
135150
await chrome.action.setBadgeText({ tabId, text });
151+
await chrome.action.setTitle({ tabId, title: title || '' });
136152
if (color)
137153
await chrome.action.setBadgeBackgroundColor({ tabId, color });
138154
} catch (error: any) {
@@ -185,6 +201,19 @@ class TabShareExtension {
185201
const tabs = await chrome.tabs.query({});
186202
return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme)));
187203
}
204+
205+
private async _onActionClicked(): Promise<void> {
206+
await chrome.tabs.create({
207+
url: chrome.runtime.getURL('status.html'),
208+
active: true
209+
});
210+
}
211+
212+
private async _disconnect(): Promise<void> {
213+
this._activeConnection?.close('User disconnected');
214+
this._activeConnection = undefined;
215+
await this._setConnectedTabId(null);
216+
}
188217
}
189218

190219
new TabShareExtension();

extension/src/ui/connect.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<head>
1919
<title>Playwright MCP extension</title>
2020
<meta name="viewport" content="width=device-width, initial-scale=1">
21+
<link rel="icon" type="image/png" sizes="32x32" href="../../icons/icon-32.png">
22+
<link rel="icon" type="image/png" sizes="16x16" href="../../icons/icon-16.png">
2123
<link rel="stylesheet" href="connect.css">
2224
</head>
2325
<body>

extension/src/ui/connect.tsx

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,8 @@
1616

1717
import React, { useState, useEffect, useCallback } from 'react';
1818
import { createRoot } from 'react-dom/client';
19-
20-
interface TabInfo {
21-
id: number;
22-
windowId: number;
23-
title: string;
24-
url: string;
25-
favIconUrl?: string;
26-
}
19+
import { Button, TabItem } from './tabItem.js';
20+
import type { TabInfo } from './tabItem.js';
2721

2822
type StatusType = 'connected' | 'error' | 'connecting';
2923

@@ -147,7 +141,11 @@ const ConnectApp: React.FC = () => {
147141
<TabItem
148142
key={tab.id}
149143
tab={tab}
150-
onConnect={() => handleConnectToTab(tab)}
144+
button={
145+
<Button variant='primary' onClick={() => handleConnectToTab(tab)}>
146+
Connect
147+
</Button>
148+
}
151149
/>
152150
))}
153151
</div>
@@ -162,41 +160,6 @@ const StatusBanner: React.FC<{ type: StatusType; message: string }> = ({ type, m
162160
return <div className={`status-banner ${type}`}>{message}</div>;
163161
};
164162

165-
const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({
166-
variant,
167-
onClick,
168-
children
169-
}) => {
170-
return (
171-
<button className={`button ${variant}`} onClick={onClick}>
172-
{children}
173-
</button>
174-
);
175-
};
176-
177-
const TabItem: React.FC<{ tab: TabInfo; onConnect: () => void }> = ({
178-
tab,
179-
onConnect
180-
}) => {
181-
return (
182-
<div className='tab-item'>
183-
<img
184-
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>'}
185-
alt=''
186-
className='tab-favicon'
187-
/>
188-
<div className='tab-content'>
189-
<div className='tab-title'>{tab.title || 'Untitled'}</div>
190-
<div className='tab-url'>{tab.url}</div>
191-
</div>
192-
<Button variant='primary' onClick={onConnect}>
193-
Connect
194-
</Button>
195-
</div>
196-
);
197-
};
198-
199-
200163
// Initialize the React app
201164
const container = document.getElementById('root');
202165
if (container) {

extension/src/ui/status.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Playwright MCP Bridge Status</title>
7+
<link rel="stylesheet" href="connect.css">
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script src="status.tsx" type="module"></script>
12+
</body>
13+
</html>

extension/src/ui/status.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React, { useState, useEffect } from 'react';
18+
import { createRoot } from 'react-dom/client';
19+
import { Button, TabItem } from './tabItem.js';
20+
21+
import type { TabInfo } from './tabItem.js';
22+
23+
interface ConnectionStatus {
24+
isConnected: boolean;
25+
connectedTabId: number | null;
26+
connectedTab?: TabInfo;
27+
}
28+
29+
const StatusApp: React.FC = () => {
30+
const [status, setStatus] = useState<ConnectionStatus>({
31+
isConnected: false,
32+
connectedTabId: null
33+
});
34+
35+
useEffect(() => {
36+
void loadStatus();
37+
}, []);
38+
39+
const loadStatus = async () => {
40+
// Get current connection status from background script
41+
const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });
42+
if (connectedTabId) {
43+
const tab = await chrome.tabs.get(connectedTabId);
44+
setStatus({
45+
isConnected: true,
46+
connectedTabId,
47+
connectedTab: {
48+
id: tab.id!,
49+
windowId: tab.windowId!,
50+
title: tab.title!,
51+
url: tab.url!,
52+
favIconUrl: tab.favIconUrl
53+
}
54+
});
55+
} else {
56+
setStatus({
57+
isConnected: false,
58+
connectedTabId: null
59+
});
60+
}
61+
};
62+
63+
const openConnectedTab = async () => {
64+
if (!status.connectedTabId)
65+
return;
66+
await chrome.tabs.update(status.connectedTabId, { active: true });
67+
window.close();
68+
};
69+
70+
const disconnect = async () => {
71+
await chrome.runtime.sendMessage({ type: 'disconnect' });
72+
window.close();
73+
};
74+
75+
return (
76+
<div className='app-container'>
77+
<div className='content-wrapper'>
78+
{status.isConnected && status.connectedTab ? (
79+
<div>
80+
<div className='tab-section-title'>
81+
Page with connected MCP client:
82+
</div>
83+
<div>
84+
<TabItem
85+
tab={status.connectedTab}
86+
button={
87+
<Button variant='primary' onClick={disconnect}>
88+
Disconnect
89+
</Button>
90+
}
91+
onClick={openConnectedTab}
92+
/>
93+
</div>
94+
</div>
95+
) : (
96+
<div className='status-banner'>
97+
No MCP clients are currently connected.
98+
</div>
99+
)}
100+
</div>
101+
</div>
102+
);
103+
};
104+
105+
// Initialize the React app
106+
const container = document.getElementById('root');
107+
if (container) {
108+
const root = createRoot(container);
109+
root.render(<StatusApp />);
110+
}

extension/src/ui/tabItem.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
19+
export interface TabInfo {
20+
id: number;
21+
windowId: number;
22+
title: string;
23+
url: string;
24+
favIconUrl?: string;
25+
}
26+
27+
export const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({
28+
variant,
29+
onClick,
30+
children
31+
}) => {
32+
return (
33+
<button className={`button ${variant}`} onClick={onClick}>
34+
{children}
35+
</button>
36+
);
37+
};
38+
39+
40+
export interface TabItemProps {
41+
tab: TabInfo;
42+
onClick?: () => void;
43+
button?: React.ReactNode;
44+
}
45+
46+
export const TabItem: React.FC<TabItemProps> = ({
47+
tab,
48+
onClick,
49+
button
50+
}) => {
51+
return (
52+
<div className='tab-item' onClick={onClick} style={onClick ? { cursor: 'pointer' } : undefined}>
53+
<img
54+
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>'}
55+
alt=''
56+
className='tab-favicon'
57+
/>
58+
<div className='tab-content'>
59+
<div className='tab-title'>
60+
{tab.title || 'Untitled'}
61+
</div>
62+
<div className='tab-url'>{tab.url}</div>
63+
</div>
64+
{button}
65+
</div>
66+
);
67+
};

0 commit comments

Comments
 (0)