mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-19 15:32:05 +08:00
Improve Browser settings load and managed MCP display
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user