mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-18 06:12:08 +08:00
Improve Browser settings load and managed MCP display
This commit is contained in:
@@ -66,10 +66,13 @@ type RuntimeReadiness = {
|
||||
installMessage: string | null;
|
||||
};
|
||||
|
||||
type RuntimeProbe = Omit<RuntimeReadiness, 'installInProgress' | 'installMessage'>;
|
||||
|
||||
const sessions = new Map<string, BrowserUseSession>();
|
||||
const handles = new Map<string, RuntimeHandle>();
|
||||
let installPromise: Promise<{ success: boolean; message: string }> | null = null;
|
||||
let lastInstallMessage: string | null = null;
|
||||
let runtimeProbeCache: { value: RuntimeProbe; updatedAt: number } | null = null;
|
||||
|
||||
const DEFAULT_SETTINGS: BrowserUseSettings = {
|
||||
enabled: false,
|
||||
@@ -78,6 +81,7 @@ const AGENT_OWNER_ID = 'agent';
|
||||
const PROFILE_ROOT = path.join(os.homedir(), '.cloudcli', 'browser-use', 'profiles');
|
||||
const MCP_SERVER_NAME = 'cloudcli-browser';
|
||||
const LEGACY_MCP_SERVER_NAMES = ['cloudcli-browser-use'];
|
||||
const RUNTIME_READINESS_CACHE_TTL_MS = 30_000;
|
||||
|
||||
function getRuntime(): BrowserUseRuntime {
|
||||
return IS_PLATFORM ? 'cloud' : 'local';
|
||||
@@ -190,15 +194,13 @@ function getProfilePath(profileName: string): string {
|
||||
return path.join(PROFILE_ROOT, safeName);
|
||||
}
|
||||
|
||||
function getRuntimeReadiness(): RuntimeReadiness {
|
||||
function probeRuntime(): RuntimeProbe {
|
||||
const playwright = getPlaywright();
|
||||
const readiness: RuntimeReadiness = {
|
||||
const readiness: RuntimeProbe = {
|
||||
playwright,
|
||||
playwrightInstalled: Boolean(playwright),
|
||||
chromiumInstalled: false,
|
||||
chromiumExecutablePath: null,
|
||||
installInProgress: Boolean(installPromise),
|
||||
installMessage: lastInstallMessage,
|
||||
};
|
||||
|
||||
if (!playwright) {
|
||||
@@ -216,6 +218,26 @@ function getRuntimeReadiness(): RuntimeReadiness {
|
||||
return readiness;
|
||||
}
|
||||
|
||||
function getRuntimeReadiness(options: { force?: boolean } = {}): RuntimeReadiness {
|
||||
const now = Date.now();
|
||||
const cachedProbe = runtimeProbeCache;
|
||||
const canUseCache = !options.force
|
||||
&& !installPromise
|
||||
&& cachedProbe
|
||||
&& now - cachedProbe.updatedAt < RUNTIME_READINESS_CACHE_TTL_MS;
|
||||
const probe = canUseCache ? cachedProbe.value : probeRuntime();
|
||||
|
||||
if (!canUseCache && !installPromise) {
|
||||
runtimeProbeCache = { value: probe, updatedAt: now };
|
||||
}
|
||||
|
||||
return {
|
||||
...probe,
|
||||
installInProgress: Boolean(installPromise),
|
||||
installMessage: lastInstallMessage,
|
||||
};
|
||||
}
|
||||
|
||||
const INSTALL_COMMAND_TIMEOUT_MS = Number.parseInt(
|
||||
process.env.CLOUDCLI_BROWSER_USE_INSTALL_TIMEOUT_MS || String(10 * 60 * 1000),
|
||||
10,
|
||||
@@ -276,6 +298,7 @@ async function installRuntime(): Promise<{ success: boolean; message: string }>
|
||||
}
|
||||
|
||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
runtimeProbeCache = null;
|
||||
installPromise = (async () => {
|
||||
try {
|
||||
lastInstallMessage = 'Installing Playwright package...';
|
||||
@@ -301,6 +324,7 @@ async function installRuntime(): Promise<{ success: boolean; message: string }>
|
||||
return await installPromise;
|
||||
} finally {
|
||||
installPromise = null;
|
||||
runtimeProbeCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,80 +182,92 @@ export default function McpServers({ selectedProvider, currentProjects }: McpSer
|
||||
<div className="py-8 text-center text-muted-foreground">Loading MCP servers...</div>
|
||||
)}
|
||||
|
||||
{servers.map((server) => (
|
||||
<div key={getServerKey(server)} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
{getTransportIcon(server.transport)}
|
||||
<span className="font-medium text-foreground">{server.name}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{server.transport || 'stdio'}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getScopeLabel(server.scope)}
|
||||
</Badge>
|
||||
{server.projectDisplayName && (
|
||||
<Badge variant="outline" className="max-w-full truncate text-xs">
|
||||
{server.projectDisplayName}
|
||||
</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>
|
||||
)}
|
||||
{servers.map((server) => {
|
||||
const managed = isManagedServer(server);
|
||||
|
||||
return (
|
||||
<div key={getServerKey(server)} className="rounded-lg border border-border bg-card/50 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||
{!managed && getTransportIcon(server.transport)}
|
||||
<span className="font-medium text-foreground">{server.name}</span>
|
||||
{!managed && (
|
||||
<>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{server.transport || 'stdio'}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getScopeLabel(server.scope)}
|
||||
</Badge>
|
||||
{server.projectDisplayName && (
|
||||
<Badge variant="outline" className="max-w-full truncate text-xs">
|
||||
{server.projectDisplayName}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{managed && (
|
||||
<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 className="space-y-1 text-sm text-muted-foreground">
|
||||
{!managed && (
|
||||
<>
|
||||
<ConfigLine label={t('mcpServers.config.command')}>{server.command || ''}</ConfigLine>
|
||||
<ConfigLine label={t('mcpServers.config.url')}>{server.url || ''}</ConfigLine>
|
||||
<ConfigLine label={t('mcpServers.config.args')}>{(server.args || []).join(' ')}</ConfigLine>
|
||||
<ConfigLine label="Cwd">{server.cwd || ''}</ConfigLine>
|
||||
{server.env && Object.keys(server.env).length > 0 && (
|
||||
<ConfigLine label={t('mcpServers.config.environment')}>
|
||||
{Object.entries(server.env).map(([key, value]) => `${key}=${maskSecret(value)}`).join(', ')}
|
||||
</ConfigLine>
|
||||
)}
|
||||
{server.envVars && server.envVars.length > 0 && (
|
||||
<ConfigLine label="Env Vars">{server.envVars.join(', ')}</ConfigLine>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{managed && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('mcpServers.managed.hint', {
|
||||
defaultValue: 'Managed by CloudCLI.',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-sm text-muted-foreground">
|
||||
<ConfigLine label={t('mcpServers.config.command')}>{server.command || ''}</ConfigLine>
|
||||
<ConfigLine label={t('mcpServers.config.url')}>{server.url || ''}</ConfigLine>
|
||||
<ConfigLine label={t('mcpServers.config.args')}>{(server.args || []).join(' ')}</ConfigLine>
|
||||
<ConfigLine label="Cwd">{server.cwd || ''}</ConfigLine>
|
||||
{server.env && Object.keys(server.env).length > 0 && (
|
||||
<ConfigLine label={t('mcpServers.config.environment')}>
|
||||
{Object.entries(server.env).map(([key, value]) => `${key}=${maskSecret(value)}`).join(', ')}
|
||||
</ConfigLine>
|
||||
)}
|
||||
{server.envVars && server.envVars.length > 0 && (
|
||||
<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>
|
||||
{!managed && (
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => openForm(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t('mcpServers.actions.edit')}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => deleteServer(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700"
|
||||
title={t('mcpServers.actions.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isManagedServer(server) && (
|
||||
<div className="ml-4 flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => openForm(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
title={t('mcpServers.actions.edit')}
|
||||
>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => deleteServer(server)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700"
|
||||
title={t('mcpServers.actions.delete')}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{!isLoading && !isLoadingProjectScopes && servers.length === 0 && (
|
||||
<div className="py-8 text-center text-muted-foreground">{t('mcpServers.empty')}</div>
|
||||
|
||||
@@ -30,31 +30,39 @@ async function readJson<T>(response: Response): Promise<T> {
|
||||
}
|
||||
|
||||
export default function BrowserUseSettingsTab() {
|
||||
const [settings, setSettings] = useState<BrowserUseSettings>({ enabled: false });
|
||||
const [settings, setSettings] = useState<BrowserUseSettings | null>(null);
|
||||
const [status, setStatus] = useState<BrowserUseStatus | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSettingsLoading, setIsSettingsLoading] = useState(true);
|
||||
const [isStatusLoading, setIsStatusLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadState = useCallback(async () => {
|
||||
setError(null);
|
||||
const [settingsResponse, statusResponse] = await Promise.all([
|
||||
authenticatedFetch('/api/browser-use/settings'),
|
||||
authenticatedFetch('/api/browser-use/status'),
|
||||
]);
|
||||
const loadSettings = useCallback(async () => {
|
||||
const settingsResponse = await authenticatedFetch('/api/browser-use/settings');
|
||||
const settingsData = await readJson<{ data: { settings: BrowserUseSettings } }>(settingsResponse);
|
||||
const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse);
|
||||
setSettings(settingsData.data.settings);
|
||||
}, []);
|
||||
|
||||
const loadStatus = useCallback(async () => {
|
||||
const statusResponse = await authenticatedFetch('/api/browser-use/status');
|
||||
const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse);
|
||||
setStatus(statusData.data);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
void loadState()
|
||||
setError(null);
|
||||
setIsSettingsLoading(true);
|
||||
setIsStatusLoading(true);
|
||||
|
||||
void loadSettings()
|
||||
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser settings'))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [loadState]);
|
||||
.finally(() => setIsSettingsLoading(false));
|
||||
|
||||
void loadStatus()
|
||||
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser status'))
|
||||
.finally(() => setIsStatusLoading(false));
|
||||
}, [loadSettings, loadStatus]);
|
||||
|
||||
const updateSettings = async (nextSettings: Partial<BrowserUseSettings>) => {
|
||||
setIsSaving(true);
|
||||
@@ -67,10 +75,12 @@ export default function BrowserUseSettingsTab() {
|
||||
const data = await readJson<{ data: { settings: BrowserUseSettings } }>(response);
|
||||
setSettings(data.data.settings);
|
||||
window.dispatchEvent(new Event('browserUseSettingsChanged'));
|
||||
await loadState();
|
||||
setIsStatusLoading(true);
|
||||
await loadStatus();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save Browser settings');
|
||||
} finally {
|
||||
setIsStatusLoading(false);
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
@@ -81,15 +91,24 @@ export default function BrowserUseSettingsTab() {
|
||||
try {
|
||||
const response = await authenticatedFetch('/api/browser-use/runtime/install', { method: 'POST' });
|
||||
await readJson(response);
|
||||
await loadState();
|
||||
setIsStatusLoading(true);
|
||||
await loadStatus();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to install browser runtime');
|
||||
} finally {
|
||||
setIsStatusLoading(false);
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const needsBrowserBinaries = Boolean(settings.enabled && status && (!status.playwrightInstalled || !status.chromiumInstalled));
|
||||
const browserEnabled = settings?.enabled === true;
|
||||
const needsBrowserBinaries = Boolean(browserEnabled && status && (!status.playwrightInstalled || !status.chromiumInstalled));
|
||||
const runtimeLabel = (installed?: boolean) => {
|
||||
if (isStatusLoading && !status) {
|
||||
return 'checking...';
|
||||
}
|
||||
return installed ? 'installed' : 'missing';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -102,24 +121,28 @@ export default function BrowserUseSettingsTab() {
|
||||
label="Enable Browser"
|
||||
description="Registers Browser for supported agents. Agents can create browser sessions; you can watch, stop, and delete them."
|
||||
>
|
||||
<SettingsToggle
|
||||
checked={settings.enabled}
|
||||
onChange={(value) => void updateSettings({ enabled: value })}
|
||||
ariaLabel="Enable Browser"
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
{isSettingsLoading && !settings ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<SettingsToggle
|
||||
checked={browserEnabled}
|
||||
onChange={(value) => void updateSettings({ enabled: value })}
|
||||
ariaLabel="Enable Browser"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
)}
|
||||
</SettingsRow>
|
||||
|
||||
<div className="space-y-4 px-4 py-4">
|
||||
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||
<span className="rounded-md border border-border px-2 py-1">
|
||||
Playwright: {status?.playwrightInstalled ? 'installed' : 'missing'}
|
||||
Playwright: {runtimeLabel(status?.playwrightInstalled)}
|
||||
</span>
|
||||
<span className="rounded-md border border-border px-2 py-1">
|
||||
Chromium: {status?.chromiumInstalled ? 'installed' : 'missing'}
|
||||
Chromium: {runtimeLabel(status?.chromiumInstalled)}
|
||||
</span>
|
||||
<span className="rounded-md border border-border px-2 py-1">
|
||||
Status: {status?.available ? 'ready' : settings.enabled ? 'setup required' : 'disabled'}
|
||||
Status: {isStatusLoading && !status ? 'checking...' : status?.available ? 'ready' : browserEnabled ? 'setup required' : 'disabled'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -453,7 +453,7 @@
|
||||
},
|
||||
"managed": {
|
||||
"badge": "Managed",
|
||||
"hint": "Managed by CloudCLI — control it from the feature's settings toggle."
|
||||
"hint": "Managed by CloudCLI."
|
||||
},
|
||||
"help": {
|
||||
"title": "About Codex MCP",
|
||||
|
||||
Reference in New Issue
Block a user