mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-28 23:35:27 +08:00
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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, []);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
Reference in New Issue
Block a user