Refine Browser naming and managed MCP UX

- Rename Browser Use surfaces to Browser
- Register Browser MCP under the new server name
- Mark CloudCLI-managed MCP servers read-only
- Adjust MCP stdio framing and sidebar footer sizing
This commit is contained in:
Simos Mikelatos
2026-06-17 19:18:23 +00:00
parent 9881e5e366
commit 59194d1502
17 changed files with 166 additions and 132 deletions

View File

@@ -53,7 +53,7 @@ async function callBrowserUseApi(toolName: string, input: Record<string, unknown
}); });
const data = await response.json() as { success?: boolean; data?: unknown; error?: string }; const data = await response.json() as { success?: boolean; data?: unknown; error?: string };
if (!response.ok || data.success === false) { if (!response.ok || data.success === false) {
throw new Error(data.error || `Browser Use API request failed (${response.status})`); throw new Error(data.error || `Browser API request failed (${response.status})`);
} }
return data.data; return data.data;
} }
@@ -61,7 +61,7 @@ async function callBrowserUseApi(toolName: string, input: Record<string, unknown
const sessionIdSchema = { const sessionIdSchema = {
type: 'object', type: 'object',
properties: { properties: {
sessionId: { type: 'string', description: 'Browser Use session id.' }, sessionId: { type: 'string', description: 'Browser session id.' },
}, },
required: ['sessionId'], required: ['sessionId'],
}; };
@@ -69,7 +69,7 @@ const sessionIdSchema = {
const tools: ToolDefinition[] = [ const tools: ToolDefinition[] = [
{ {
name: 'browser_create_session', name: 'browser_create_session',
description: 'Create a temporary Browser Use session that the agent can control. Optionally provide a background profileName to reuse cookies and storage.', description: 'Create a temporary Browser session that the agent can control. Optionally provide a background profileName to reuse cookies and storage.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -79,22 +79,22 @@ const tools: ToolDefinition[] = [
}, },
{ {
name: 'browser_list_sessions', name: 'browser_list_sessions',
description: 'List Browser Use sessions currently available to agents.', description: 'List Browser sessions currently available to agents.',
inputSchema: { type: 'object', properties: {} }, inputSchema: { type: 'object', properties: {} },
}, },
{ {
name: 'browser_snapshot', name: 'browser_snapshot',
description: 'Capture current page metadata, screenshot data URL, and visible body text for a Browser Use session.', description: 'Capture current page metadata, screenshot data URL, and visible body text for a Browser session.',
inputSchema: sessionIdSchema, inputSchema: sessionIdSchema,
}, },
{ {
name: 'browser_take_screenshot', name: 'browser_take_screenshot',
description: 'Capture the latest screenshot for a Browser Use session.', description: 'Capture the latest screenshot for a Browser session.',
inputSchema: sessionIdSchema, inputSchema: sessionIdSchema,
}, },
{ {
name: 'browser_navigate', name: 'browser_navigate',
description: 'Navigate a Browser Use session to an HTTP or HTTPS URL.', description: 'Navigate a Browser session to an HTTP or HTTPS URL.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -196,7 +196,7 @@ const tools: ToolDefinition[] = [
}, },
{ {
name: 'browser_tabs', name: 'browser_tabs',
description: 'List, open, select, or close tabs in a Browser Use session.', description: 'List, open, select, or close tabs in a Browser session.',
inputSchema: { inputSchema: {
type: 'object', type: 'object',
properties: { properties: {
@@ -210,7 +210,7 @@ const tools: ToolDefinition[] = [
}, },
{ {
name: 'browser_close_session', name: 'browser_close_session',
description: 'Stop a Browser Use session controlled by agents.', description: 'Stop a Browser session controlled by agents.',
inputSchema: sessionIdSchema, inputSchema: sessionIdSchema,
}, },
]; ];
@@ -302,7 +302,7 @@ async function handleMessage(message: JsonRpcRequest) {
return { return {
protocolVersion: '2024-11-05', protocolVersion: '2024-11-05',
capabilities: { tools: {} }, capabilities: { tools: {} },
serverInfo: { name: 'cloudcli-browser-use', version: '1.0.0' }, serverInfo: { name: 'cloudcli-browser', version: '1.0.0' },
}; };
} }
@@ -327,8 +327,9 @@ async function handleMessage(message: JsonRpcRequest) {
} }
function writeMessage(message: Record<string, unknown>) { function writeMessage(message: Record<string, unknown>) {
const payload = JSON.stringify(message); // MCP stdio transport uses newline-delimited JSON (one JSON-RPC message per line,
process.stdout.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`); // no embedded newlines). This is NOT the LSP Content-Length framing.
process.stdout.write(`${JSON.stringify(message)}\n`);
} }
function sendResult(id: string | number | null | undefined, result: unknown) { function sendResult(id: string | number | null | undefined, result: unknown) {
@@ -352,33 +353,18 @@ function sendError(id: string | number | null | undefined, error: unknown) {
}); });
} }
let buffer = Buffer.alloc(0); let buffer = '';
process.stdin.on('data', (chunk) => { process.stdin.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]); buffer += chunk.toString('utf8');
while (true) { let newlineIndex: number;
const headerEnd = buffer.indexOf('\r\n\r\n'); while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
if (headerEnd === -1) { const rawMessage = buffer.slice(0, newlineIndex).trim();
return; buffer = buffer.slice(newlineIndex + 1);
} if (!rawMessage) {
const header = buffer.slice(0, headerEnd).toString('utf8');
const lengthMatch = /Content-Length:\s*(\d+)/i.exec(header);
if (!lengthMatch) {
buffer = buffer.slice(headerEnd + 4);
continue; continue;
} }
const length = Number.parseInt(lengthMatch[1], 10);
const messageStart = headerEnd + 4;
const messageEnd = messageStart + length;
if (buffer.length < messageEnd) {
return;
}
const rawMessage = buffer.slice(messageStart, messageEnd).toString('utf8');
buffer = buffer.slice(messageEnd);
void (async () => { void (async () => {
let request: JsonRpcRequest; let request: JsonRpcRequest;
try { try {

View File

@@ -8,7 +8,7 @@
* (no args) - Start the server (default) * (no args) - Start the server (default)
* start - Start the server * start - Start the server
* sandbox - Manage Docker sandbox environments * sandbox - Manage Docker sandbox environments
* browser-use-mcp - Run Browser Use MCP stdio server * browser-use-mcp - Run Browser MCP stdio server
* status - Show configuration and data locations * status - Show configuration and data locations
* help - Show help information * help - Show help information
* version - Show version information * version - Show version information
@@ -157,7 +157,7 @@ Usage:
Commands: Commands:
start Start the CloudCLI server (default) start Start the CloudCLI server (default)
sandbox Manage Docker sandbox environments sandbox Manage Docker sandbox environments
browser-use-mcp Run the Browser Use MCP stdio server browser-use-mcp Run the Browser MCP stdio server
status Show configuration and data locations status Show configuration and data locations
update Update to the latest version update Update to the latest version
help Show this help information help Show this help information

View File

@@ -196,10 +196,10 @@ app.use('/api/gemini', authenticateToken, geminiRoutes);
// Plugins API Routes (protected) // Plugins API Routes (protected)
app.use('/api/plugins', authenticateToken, pluginsRoutes); app.use('/api/plugins', authenticateToken, pluginsRoutes);
// Browser Use MCP bridge API (local token protected) // Browser MCP bridge API (local token protected)
app.use('/api/browser-use-mcp', browserUseMcpRoutes); app.use('/api/browser-use-mcp', browserUseMcpRoutes);
// Browser Use API Routes (protected) // Browser API Routes (protected)
app.use('/api/browser-use', authenticateToken, browserUseRoutes); app.use('/api/browser-use', authenticateToken, browserUseRoutes);
// Unified provider MCP routes (protected) // Unified provider MCP routes (protected)
@@ -1717,7 +1717,7 @@ async function startServer() {
try { try {
await browserUseService.stopAllSessions(); await browserUseService.stopAllSessions();
} catch (err) { } catch (err) {
console.error('[Browser Use] Error stopping sessions during shutdown:', err?.message || err); console.error('[Browser] Error stopping sessions during shutdown:', err?.message || err);
} }
try { try {
await stopAllPlugins(); await stopAllPlugins();

View File

@@ -16,7 +16,7 @@ router.use((req, res, next) => {
const expected = browserUseService.getMcpToken(); const expected = browserUseService.getMcpToken();
const token = readBearerToken(req.headers.authorization) || String(req.headers['x-browser-use-mcp-token'] || ''); const token = readBearerToken(req.headers.authorization) || String(req.headers['x-browser-use-mcp-token'] || '');
if (!token || token !== expected) { if (!token || token !== expected) {
res.status(401).json({ success: false, error: 'Invalid Browser Use MCP token.' }); res.status(401).json({ success: false, error: 'Invalid Browser MCP token.' });
return; return;
} }
next(); next();
@@ -104,7 +104,7 @@ router.post('/tools/:toolName', async (req, res) => {
result = await browserUseService.agentStopSession(sessionId); result = await browserUseService.agentStopSession(sessionId);
break; break;
default: default:
res.status(404).json({ success: false, error: `Unknown Browser Use MCP tool "${toolName}".` }); res.status(404).json({ success: false, error: `Unknown Browser MCP tool "${toolName}".` });
return; return;
} }
@@ -112,7 +112,7 @@ router.post('/tools/:toolName', async (req, res) => {
} catch (error) { } catch (error) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: error instanceof Error ? error.message : 'Browser Use MCP tool failed.', error: error instanceof Error ? error.message : 'Browser MCP tool failed.',
}); });
} }
}); });

View File

@@ -14,7 +14,7 @@ router.get('/status', async (_req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to load Browser Use status.', error: error instanceof Error ? error.message : 'Failed to load Browser status.',
}); });
} }
}); });
@@ -25,7 +25,7 @@ router.get('/settings', async (_req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to load Browser Use settings.', error: error instanceof Error ? error.message : 'Failed to load Browser settings.',
}); });
} }
}); });
@@ -37,7 +37,7 @@ router.put('/settings', async (req, res) => {
} catch (error) { } catch (error) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to save Browser Use settings.', error: error instanceof Error ? error.message : 'Failed to save Browser settings.',
}); });
} }
}); });
@@ -53,7 +53,7 @@ router.post('/runtime/install', async (_req, res) => {
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: error instanceof Error ? error.message : 'Failed to install Browser Use runtime.', error: error instanceof Error ? error.message : 'Failed to install Browser runtime.',
}); });
} }
}); });

View File

@@ -76,8 +76,8 @@ const DEFAULT_SETTINGS: BrowserUseSettings = {
}; };
const AGENT_OWNER_ID = 'agent'; const AGENT_OWNER_ID = 'agent';
const PROFILE_ROOT = path.join(os.homedir(), '.cloudcli', 'browser-use', 'profiles'); const PROFILE_ROOT = path.join(os.homedir(), '.cloudcli', 'browser-use', 'profiles');
const MCP_SERVER_NAME = 'cloudcli-browser-use'; const MCP_SERVER_NAME = 'cloudcli-browser';
const MCP_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini', 'opencode']; const LEGACY_MCP_SERVER_NAMES = ['cloudcli-browser-use'];
function getRuntime(): BrowserUseRuntime { function getRuntime(): BrowserUseRuntime {
return IS_PLATFORM ? 'cloud' : 'local'; return IS_PLATFORM ? 'cloud' : 'local';
@@ -95,7 +95,7 @@ function readSettings(): BrowserUseSettings {
enabled: parsed.enabled === true, enabled: parsed.enabled === true,
}; };
} catch (error: any) { } catch (error: any) {
console.warn('[Browser Use] Failed to read settings:', error?.message || error); console.warn('[Browser] Failed to read settings:', error?.message || error);
return DEFAULT_SETTINGS; return DEFAULT_SETTINGS;
} }
} }
@@ -121,7 +121,7 @@ function getOrCreateMcpToken(): string {
function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadiness): string { function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadiness): string {
if (!settings.enabled) { if (!settings.enabled) {
return 'Browser Use is disabled in settings.'; return 'Browser is disabled in settings.';
} }
if (!readiness.playwrightInstalled) { if (!readiness.playwrightInstalled) {
@@ -132,7 +132,7 @@ function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadine
return 'Playwright is installed, but Chromium is missing. Install the Chromium runtime to continue.'; return 'Playwright is installed, but Chromium is missing. Install the Chromium runtime to continue.';
} }
return readiness.installMessage || 'Browser Use runtime is not ready.'; return readiness.installMessage || 'Browser runtime is not ready.';
} }
function getPlaywright(): any | null { function getPlaywright(): any | null {
@@ -164,6 +164,14 @@ function getMcpApiUrl(): string {
return `http://127.0.0.1:${port}/api/browser-use-mcp`; return `http://127.0.0.1:${port}/api/browser-use-mcp`;
} }
async function removeMcpServerFromAllProviders(name: string) {
const results = await providerMcpService.removeMcpServerFromAllProviders({
name,
scope: 'user',
});
return results.map((result) => ({ ...result, name }));
}
function normalizeProfileName(profileName?: string | null): string | null { function normalizeProfileName(profileName?: string | null): string | null {
const normalized = String(profileName || '').trim(); const normalized = String(profileName || '').trim();
if (!normalized) { if (!normalized) {
@@ -259,7 +267,7 @@ function formatInstallError(error: unknown): string {
if (message.includes('sudo') && message.includes('password')) { if (message.includes('sudo') && message.includes('password')) {
return 'Installing Chromium system dependencies requires administrator privileges. Run `npx playwright install-deps chromium` on the machine where CloudCLI runs, then try again.'; return 'Installing Chromium system dependencies requires administrator privileges. Run `npx playwright install-deps chromium` on the machine where CloudCLI runs, then try again.';
} }
return message || 'Failed to install Browser Use runtime.'; return message || 'Failed to install Browser runtime.';
} }
async function installRuntime(): Promise<{ success: boolean; message: string }> { async function installRuntime(): Promise<{ success: boolean; message: string }> {
@@ -281,7 +289,7 @@ async function installRuntime(): Promise<{ success: boolean; message: string }>
lastInstallMessage = 'Installing Chromium runtime...'; lastInstallMessage = 'Installing Chromium runtime...';
await runCommand(npmCommand, ['exec', '--', 'playwright', 'install', 'chromium']); await runCommand(npmCommand, ['exec', '--', 'playwright', 'install', 'chromium']);
lastInstallMessage = 'Browser Use runtime installed.'; lastInstallMessage = 'Browser runtime installed.';
return { success: true, message: lastInstallMessage }; return { success: true, message: lastInstallMessage };
} catch (error) { } catch (error) {
lastInstallMessage = formatInstallError(error); lastInstallMessage = formatInstallError(error);
@@ -418,13 +426,14 @@ export const browserUseService = {
installInProgress: readiness.installInProgress, installInProgress: readiness.installInProgress,
sessionCount: sessions.size, sessionCount: sessions.size,
message: available message: available
? 'Browser Use runtime is available.' ? 'Browser runtime is available.'
: getSetupMessage(settings, readiness), : getSetupMessage(settings, readiness),
}; };
}, },
async registerAgentMcp() { async registerAgentMcp() {
const { command, args } = getMcpCommand(); const { command, args } = getMcpCommand();
await Promise.all(LEGACY_MCP_SERVER_NAMES.map((name) => removeMcpServerFromAllProviders(name)));
const results = await providerMcpService.addMcpServerToAllProviders({ const results = await providerMcpService.addMcpServerToAllProviders({
name: MCP_SERVER_NAME, name: MCP_SERVER_NAME,
scope: 'user', scope: 'user',
@@ -444,21 +453,9 @@ export const browserUseService = {
}, },
async unregisterAgentMcp() { async unregisterAgentMcp() {
const results = await Promise.all(MCP_PROVIDERS.map(async (provider) => { const results = (await Promise.all(
try { [MCP_SERVER_NAME, ...LEGACY_MCP_SERVER_NAMES].map((name) => removeMcpServerFromAllProviders(name)),
const result = await providerMcpService.removeProviderMcpServer(provider, { )).flat();
name: MCP_SERVER_NAME,
scope: 'user',
});
return { provider, removed: result.removed };
} catch (error) {
return {
provider,
removed: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}));
return { name: MCP_SERVER_NAME, results }; return { name: MCP_SERVER_NAME, results };
}, },
@@ -480,7 +477,7 @@ export const browserUseService = {
async createAgentSession(options?: { profileName?: string | null }) { async createAgentSession(options?: { profileName?: string | null }) {
const settings = readSettings(); const settings = readSettings();
if (!settings.enabled) { if (!settings.enabled) {
throw new Error('Browser Use agent tools are disabled.'); throw new Error('Browser agent tools are disabled.');
} }
await expireStaleSessions(); await expireStaleSessions();
@@ -507,7 +504,7 @@ export const browserUseService = {
const activeOwnerSessions = ownerSessions(AGENT_OWNER_ID).filter((item) => item.status === 'ready'); const activeOwnerSessions = ownerSessions(AGENT_OWNER_ID).filter((item) => item.status === 'ready');
if (activeOwnerSessions.length >= MAX_SESSIONS_PER_OWNER) { if (activeOwnerSessions.length >= MAX_SESSIONS_PER_OWNER) {
throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active agent sessions.`); throw new Error(`Browser is limited to ${MAX_SESSIONS_PER_OWNER} active agent sessions.`);
} }
const readiness = getRuntimeReadiness(); const readiness = getRuntimeReadiness();
@@ -563,7 +560,7 @@ export const browserUseService = {
async getAgentSession(sessionId: string) { async getAgentSession(sessionId: string) {
const settings = readSettings(); const settings = readSettings();
if (!settings.enabled) { if (!settings.enabled) {
throw new Error('Browser Use agent tools are disabled.'); throw new Error('Browser agent tools are disabled.');
} }
const session = sessions.get(sessionId); const session = sessions.get(sessionId);
if (!session || session.ownerId !== AGENT_OWNER_ID) { if (!session || session.ownerId !== AGENT_OWNER_ID) {

View File

@@ -3,7 +3,7 @@ import test from 'node:test';
import { browserUseService } from '@/modules/browser-use/browser-use.service.js'; import { browserUseService } from '@/modules/browser-use/browser-use.service.js';
test('browser use monitor list starts empty without agent sessions', async () => { test('browser monitor list starts empty without agent sessions', async () => {
const sessions = await browserUseService.listSessions(); const sessions = await browserUseService.listSessions();
assert.deepEqual(sessions, []); assert.deepEqual(sessions, []);

View File

@@ -80,4 +80,30 @@ export const providerMcpService = {
return results; return results;
}, },
/**
* Removes one MCP server from every provider. Mirrors `addMcpServerToAllProviders`
* by iterating the live provider registry, so callers stay in sync with which
* providers exist instead of maintaining their own provider list.
*/
async removeMcpServerFromAllProviders(
input: { name: string; scope?: McpScope; workspacePath?: string },
): Promise<Array<{ provider: LLMProvider; removed: boolean; error?: string }>> {
const results: Array<{ provider: LLMProvider; removed: boolean; error?: string }> = [];
const providers = providerRegistry.listProviders();
for (const provider of providers) {
try {
const result = await provider.mcp.removeServer(input);
results.push({ provider: provider.id, removed: result.removed });
} catch (error) {
results.push({
provider: provider.id,
removed: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return results;
},
}; };

View File

@@ -71,7 +71,6 @@ function AppContentInner() {
setActiveTab, setActiveTab,
setSidebarOpen, setSidebarOpen,
setIsInputFocused, setIsInputFocused,
setShowSettings,
openSettings, openSettings,
refreshProjectsSilently, refreshProjectsSilently,
registerOptimisticSession, registerOptimisticSession,
@@ -247,7 +246,7 @@ function AppContentInner() {
onSessionEstablished={(targetSessionId, context) => onSessionEstablished={(targetSessionId, context) =>
registerOptimisticSession({ sessionId: targetSessionId, ...context }) registerOptimisticSession({ sessionId: targetSessionId, ...context })
} }
onShowSettings={() => setShowSettings(true)} onShowSettings={openSettings}
externalMessageUpdate={externalMessageUpdate} externalMessageUpdate={externalMessageUpdate}
newSessionTrigger={newSessionTrigger} newSessionTrigger={newSessionTrigger}
/> />

View File

@@ -17,6 +17,7 @@ import {
import { cn } from '../../../lib/utils'; import { cn } from '../../../lib/utils';
import { Badge, Button } from '../../../shared/view/ui'; import { Badge, Button } from '../../../shared/view/ui';
import { authenticatedFetch } from '../../../utils/api'; import { authenticatedFetch } from '../../../utils/api';
import type { SettingsMainTab } from '../../settings/types/types';
type BrowserUseStatus = { type BrowserUseStatus = {
enabled: boolean; enabled: boolean;
@@ -53,7 +54,7 @@ type BrowserUseSession = {
type BrowserUsePanelProps = { type BrowserUsePanelProps = {
isVisible: boolean; isVisible: boolean;
onShowSettings?: () => void; onShowSettings?: (tab?: SettingsMainTab) => void;
}; };
async function readJson<T>(response: Response): Promise<T> { async function readJson<T>(response: Response): Promise<T> {
@@ -119,8 +120,8 @@ function getStatusDot(status: BrowserUseSession['status']): string {
} }
const PROMPTS = [ const PROMPTS = [
'Use Browser Use to inspect the checkout flow and report any broken UI states.', 'Use Browser to inspect the checkout flow and report any broken UI states.',
'Open <url> with Browser Use, interact with the page, and summarize what changed after each step.', 'Open <url> with Browser, interact with the page, and summarize what changed after each step.',
]; ];
export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUsePanelProps) { export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUsePanelProps) {
@@ -174,7 +175,7 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
)); ));
setError(null); setError(null);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load Browser Use'); setError(err instanceof Error ? err.message : 'Failed to load Browser');
} finally { } finally {
setIsRefreshing(false); setIsRefreshing(false);
} }
@@ -192,7 +193,7 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
await action(); await action();
await refresh(); await refresh();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Browser Use action failed'); setError(err instanceof Error ? err.message : 'Browser action failed');
} finally { } finally {
setIsBusy(false); setIsBusy(false);
} }
@@ -265,12 +266,12 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="text-sm font-semibold text-foreground"> <div className="text-sm font-semibold text-foreground">
{status?.enabled ? 'No browser sessions yet' : 'Browser Use is disabled'} {status?.enabled ? 'No browser sessions yet' : 'Browser is disabled'}
</div> </div>
<p className="mt-1 max-w-xl text-sm leading-6 text-muted-foreground"> <p className="mt-1 max-w-xl text-sm leading-6 text-muted-foreground">
{status?.enabled {status?.enabled
? 'Agent browser sessions appear here while an AI task is using Browser Use.' ? 'Agent browser sessions appear here while an AI task is using Browser.'
: 'Enable Browser Use in settings to let agents open monitored browser sessions.'} : 'Enable Browser in settings to let agents open monitored browser sessions.'}
</p> </p>
</div> </div>
</div> </div>
@@ -345,7 +346,7 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MonitorPlay className="h-4 w-4 text-primary" /> <MonitorPlay className="h-4 w-4 text-primary" />
<h3 className="text-sm font-semibold text-foreground">Browser Use</h3> <h3 className="text-sm font-semibold text-foreground">Browser</h3>
<Badge variant="outline" className={cn('text-[10px]', getRuntimeTone(status, isInstalling))}> <Badge variant="outline" className={cn('text-[10px]', getRuntimeTone(status, isInstalling))}>
{runtimeLabel} {runtimeLabel}
</Badge> </Badge>
@@ -358,9 +359,9 @@ export default function BrowserUsePanel({ isVisible, onShowSettings }: BrowserUs
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 w-7 p-0" className="h-7 w-7 p-0"
onClick={onShowSettings} onClick={() => onShowSettings('browser')}
title="Open Browser Use settings" title="Open Browser settings"
aria-label="Open Browser Use settings" aria-label="Open Browser settings"
> >
<Settings className="h-3.5 w-3.5" /> <Settings className="h-3.5 w-3.5" />
</Button> </Button>

View File

@@ -7,6 +7,7 @@ import type {
SessionActivityMap, SessionActivityMap,
} from '../../../hooks/useSessionProtection'; } from '../../../hooks/useSessionProtection';
import type { SessionEstablishedContext, SessionNavigationOptions } from '../../chat/types/types'; import type { SessionEstablishedContext, SessionNavigationOptions } from '../../chat/types/types';
import type { SettingsMainTab } from '../../settings/types/types';
export type TaskMasterTask = { export type TaskMasterTask = {
id: string | number; id: string | number;
@@ -53,7 +54,7 @@ export type MainContentProps = {
processingSessions: SessionActivityMap; processingSessions: SessionActivityMap;
onNavigateToSession: (targetSessionId: string, options?: SessionNavigationOptions) => void; onNavigateToSession: (targetSessionId: string, options?: SessionNavigationOptions) => void;
onSessionEstablished: (sessionId: string, context: SessionEstablishedContext) => void; onSessionEstablished: (sessionId: string, context: SessionEstablishedContext) => void;
onShowSettings: () => void; onShowSettings: (tab?: SettingsMainTab) => void;
externalMessageUpdate: number; externalMessageUpdate: number;
newSessionTrigger: number; newSessionTrigger: number;
}; };

View File

@@ -29,7 +29,7 @@ function getTabTitle(activeTab: AppTab, shouldShowTasksTab: boolean, t: (key: st
} }
if (activeTab === 'browser') { if (activeTab === 'browser') {
return 'Browser Use'; return 'Browser';
} }
return 'Project'; return 'Project';

View File

@@ -52,6 +52,11 @@ const getServerKey = (server: ProviderMcpServer): string => (
`${server.provider}:${server.scope}:${server.workspacePath || 'global'}:${server.name}` `${server.provider}:${server.scope}:${server.workspacePath || 'global'}:${server.name}`
); );
// Servers prefixed with `cloudcli-` are written and removed automatically by a
// CloudCLI feature toggle (e.g. the Browser tab), not added by the user. They are
// shown read-only so users don't edit/delete them out of sync with the feature.
const isManagedServer = (server: ProviderMcpServer): boolean => server.name.startsWith('cloudcli-');
function ConfigLine({ label, children }: { label: string; children: string }) { function ConfigLine({ label, children }: { label: string; children: string }) {
if (!children) { if (!children) {
return null; return null;
@@ -195,6 +200,12 @@ export default function McpServers({ selectedProvider, currentProjects }: McpSer
{server.projectDisplayName} {server.projectDisplayName}
</Badge> </Badge>
)} )}
{isManagedServer(server) && (
<Badge variant="outline" className="gap-1 text-xs text-muted-foreground">
<Lock className="h-3 w-3" />
{t('mcpServers.managed.badge', { defaultValue: 'Managed' })}
</Badge>
)}
</div> </div>
<div className="space-y-1 text-sm text-muted-foreground"> <div className="space-y-1 text-sm text-muted-foreground">
@@ -210,29 +221,38 @@ export default function McpServers({ selectedProvider, currentProjects }: McpSer
{server.envVars && server.envVars.length > 0 && ( {server.envVars && server.envVars.length > 0 && (
<ConfigLine label="Env Vars">{server.envVars.join(', ')}</ConfigLine> <ConfigLine label="Env Vars">{server.envVars.join(', ')}</ConfigLine>
)} )}
{isManagedServer(server) && (
<div className="pt-1 text-xs italic text-muted-foreground">
{t('mcpServers.managed.hint', {
defaultValue: 'Managed by CloudCLI — control it from the feature\'s settings toggle.',
})}
</div>
)}
</div> </div>
</div> </div>
<div className="ml-4 flex items-center gap-2"> {!isManagedServer(server) && (
<Button <div className="ml-4 flex items-center gap-2">
onClick={() => openForm(server)} <Button
variant="ghost" onClick={() => openForm(server)}
size="sm" variant="ghost"
className="text-muted-foreground hover:text-foreground" size="sm"
title={t('mcpServers.actions.edit')} className="text-muted-foreground hover:text-foreground"
> title={t('mcpServers.actions.edit')}
<Edit3 className="h-4 w-4" /> >
</Button> <Edit3 className="h-4 w-4" />
<Button </Button>
onClick={() => deleteServer(server)} <Button
variant="ghost" onClick={() => deleteServer(server)}
size="sm" variant="ghost"
className="text-red-600 hover:text-red-700" size="sm"
title={t('mcpServers.actions.delete')} className="text-red-600 hover:text-red-700"
> title={t('mcpServers.actions.delete')}
<Trash2 className="h-4 w-4" /> >
</Button> <Trash2 className="h-4 w-4" />
</div> </Button>
</div>
)}
</div> </div>
</div> </div>
))} ))}

View File

@@ -33,7 +33,7 @@ export const SETTINGS_MAIN_TABS: SettingsMainTabMeta[] = [
{ id: 'git', label: 'Git', keywords: 'git github commits', icon: GitBranch }, { id: 'git', label: 'Git', keywords: 'git github commits', icon: GitBranch },
{ id: 'api', label: 'API Tokens', keywords: 'api tokens auth keys', icon: KeyRound }, { id: 'api', label: 'API Tokens', keywords: 'api tokens auth keys', icon: KeyRound },
{ id: 'tasks', label: 'Tasks', keywords: 'tasks taskmaster', icon: ListChecks }, { id: 'tasks', label: 'Tasks', keywords: 'tasks taskmaster', icon: ListChecks },
{ id: 'browser', label: 'Browser Use', keywords: 'browser use playwright chromium automation', icon: MonitorPlay }, { id: 'browser', label: 'Browser', keywords: 'browser playwright chromium automation', icon: MonitorPlay },
{ id: 'notifications', label: 'Notifications', keywords: 'notifications alerts push', icon: Bell }, { id: 'notifications', label: 'Notifications', keywords: 'notifications alerts push', icon: Bell },
{ id: 'plugins', label: 'Plugins', keywords: 'plugins extensions integrations', icon: Plug }, { id: 'plugins', label: 'Plugins', keywords: 'plugins extensions integrations', icon: Plug },
{ id: 'about', label: 'About', keywords: 'about version info', icon: Info }, { id: 'about', label: 'About', keywords: 'about version info', icon: Info },

View File

@@ -52,7 +52,7 @@ export default function BrowserUseSettingsTab() {
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
void loadState() void loadState()
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser Use settings')) .catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser settings'))
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}, [loadState]); }, [loadState]);
@@ -69,7 +69,7 @@ export default function BrowserUseSettingsTab() {
window.dispatchEvent(new Event('browserUseSettingsChanged')); window.dispatchEvent(new Event('browserUseSettingsChanged'));
await loadState(); await loadState();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save Browser Use settings'); setError(err instanceof Error ? err.message : 'Failed to save Browser settings');
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
@@ -94,18 +94,18 @@ export default function BrowserUseSettingsTab() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<SettingsSection <SettingsSection
title="Browser Use" title="Browser"
description="Allow agents to create guarded Playwright browser sessions that you can monitor from the Browser Use tab." description="Allow agents to create guarded Playwright browser sessions that you can monitor from the Browser tab."
> >
<SettingsCard divided> <SettingsCard divided>
<SettingsRow <SettingsRow
label="Enable Browser Use" label="Enable Browser"
description="Registers Browser Use for supported agents. Agents can create browser sessions; you can watch, stop, and delete them." description="Registers Browser for supported agents. Agents can create browser sessions; you can watch, stop, and delete them."
> >
<SettingsToggle <SettingsToggle
checked={settings.enabled} checked={settings.enabled}
onChange={(value) => void updateSettings({ enabled: value })} onChange={(value) => void updateSettings({ enabled: value })}
ariaLabel="Enable Browser Use" ariaLabel="Enable Browser"
disabled={isLoading || isSaving} disabled={isLoading || isSaving}
/> />
</SettingsRow> </SettingsRow>
@@ -128,7 +128,7 @@ export default function BrowserUseSettingsTab() {
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-1">
<div className="text-sm font-medium text-foreground">Browser runtime required</div> <div className="text-sm font-medium text-foreground">Browser runtime required</div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{status?.message || 'Install the browser runtime before agents can create Browser Use sessions.'} {status?.message || 'Install the browser runtime before agents can create Browser sessions.'}
</p> </p>
</div> </div>

View File

@@ -69,7 +69,7 @@ export default function SidebarFooter({
onClick={onShowVersionModal} onClick={onShowVersionModal}
> >
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
<ArrowUpCircle className="w-4.5 h-4.5 text-blue-500 dark:text-blue-400" /> <ArrowUpCircle className="h-4 w-4 text-blue-500 dark:text-blue-400" />
<span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" /> <span className="absolute -right-0.5 -top-0.5 h-1.5 w-1.5 animate-pulse rounded-full bg-blue-500" />
</div> </div>
<div className="min-w-0 flex-1 text-left"> <div className="min-w-0 flex-1 text-left">
@@ -145,12 +145,12 @@ export default function SidebarFooter({
href={GITHUB_ISSUES_URL} href={GITHUB_ISSUES_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]" className="flex h-10 w-full items-center gap-3 rounded-xl bg-muted/40 px-3.5 transition-all hover:bg-muted/60 active:scale-[0.98]"
> >
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-background/80"> <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-background/80">
<Bug className="w-4.5 h-4.5 text-muted-foreground" /> <Bug className="h-4 w-4 text-muted-foreground" />
</div> </div>
<span className="text-base font-medium text-foreground">{t('actions.reportIssue')}</span> <span className="text-sm font-medium text-foreground">{t('actions.reportIssue')}</span>
</a> </a>
</div> </div>
@@ -160,25 +160,25 @@ export default function SidebarFooter({
href={DISCORD_INVITE_URL} href={DISCORD_INVITE_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]" className="flex h-10 w-full items-center gap-3 rounded-xl bg-muted/40 px-3.5 transition-all hover:bg-muted/60 active:scale-[0.98]"
> >
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-background/80"> <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-background/80">
<DiscordIcon className="w-4.5 h-4.5 text-muted-foreground" /> <DiscordIcon className="h-4 w-4 text-muted-foreground" />
</div> </div>
<span className="text-base font-medium text-foreground">{t('actions.joinCommunity')}</span> <span className="text-sm font-medium text-foreground">{t('actions.joinCommunity')}</span>
</a> </a>
</div> </div>
{/* Mobile settings */} {/* Mobile settings */}
<div className="px-3 pb-3 pt-2 md:hidden"> <div className="px-3 pb-3 pt-2 md:hidden">
<button <button
className="flex h-12 w-full items-center gap-3.5 rounded-xl bg-muted/40 px-4 transition-all hover:bg-muted/60 active:scale-[0.98]" className="flex h-10 w-full items-center gap-3 rounded-xl bg-muted/40 px-3.5 transition-all hover:bg-muted/60 active:scale-[0.98]"
onClick={onShowSettings} onClick={onShowSettings}
> >
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-background/80"> <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-background/80">
<Settings className="w-4.5 h-4.5 text-muted-foreground" /> <Settings className="h-4 w-4 text-muted-foreground" />
</div> </div>
<span className="text-base font-medium text-foreground">{t('actions.settings')}</span> <span className="text-sm font-medium text-foreground">{t('actions.settings')}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -94,7 +94,7 @@
"git": "Git", "git": "Git",
"apiTokens": "API & Tokens", "apiTokens": "API & Tokens",
"tasks": "Tasks", "tasks": "Tasks",
"browser": "Browser Use", "browser": "Browser",
"notifications": "Notifications", "notifications": "Notifications",
"plugins": "Plugins", "plugins": "Plugins",
"about": "About" "about": "About"
@@ -451,6 +451,10 @@
"edit": "Edit server", "edit": "Edit server",
"delete": "Delete server" "delete": "Delete server"
}, },
"managed": {
"badge": "Managed",
"hint": "Managed by CloudCLI — control it from the feature's settings toggle."
},
"help": { "help": {
"title": "About Codex MCP", "title": "About Codex MCP",
"description": "Codex supports stdio-based MCP servers. You can add servers that extend Codex's capabilities with additional tools and resources." "description": "Codex supports stdio-based MCP servers. You can add servers that extend Codex's capabilities with additional tools and resources."