feat: add browser use runtime setup settings

This commit is contained in:
Simos Mikelatos
2026-06-15 17:52:27 +00:00
parent 828d1a2302
commit 260070bae0
11 changed files with 450 additions and 54 deletions

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ExternalLink, Globe, MonitorPlay, Navigation, Pause, RefreshCw, Square } from 'lucide-react';
import { Download, ExternalLink, Globe, Loader2, MonitorPlay, Navigation, Pause, RefreshCw, Square } from 'lucide-react';
import { Badge, Button } from '../../../shared/view/ui';
import { authenticatedFetch } from '../../../utils/api';
@@ -8,6 +8,9 @@ type BrowserUseStatus = {
enabled: boolean;
available: boolean;
runtime: 'cloud' | 'local';
playwrightInstalled: boolean;
chromiumInstalled: boolean;
installInProgress: boolean;
sessionCount: number;
mcpRecommended: boolean;
message: string;
@@ -44,6 +47,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
const [targetUrl, setTargetUrl] = useState('https://example.com');
const [isBusy, setIsBusy] = useState(false);
const [isInstalling, setIsInstalling] = useState(false);
const [error, setError] = useState<string | null>(null);
const selectedSession = useMemo(
@@ -108,6 +112,18 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
await readJson(response);
});
const installRuntime = () => runAction(async () => {
setIsInstalling(true);
try {
const response = await authenticatedFetch('/api/browser-use/runtime/install', { method: 'POST' });
await readJson(response);
} finally {
setIsInstalling(false);
}
});
const canInstallRuntime = Boolean(status?.enabled && (!status.playwrightInstalled || !status.chromiumInstalled));
return (
<div className="flex h-full min-h-0 flex-col bg-background">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border/60 px-4 py-3">
@@ -130,7 +146,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
<Button size="sm" onClick={createSession} disabled={isBusy}>
<Button size="sm" onClick={createSession} disabled={isBusy || !status?.available}>
<Globe className="h-4 w-4" />
New Session
</Button>
@@ -143,6 +159,22 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Runtime</div>
<div className="mt-2 text-sm text-foreground">{status?.available ? 'Available' : 'Setup required'}</div>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">{status?.message || 'Loading Browser Use status...'}</p>
{canInstallRuntime && (
<Button
type="button"
size="sm"
className="mt-3 w-full"
onClick={installRuntime}
disabled={isBusy || isInstalling || status?.installInProgress}
>
{isInstalling || status?.installInProgress ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
Install Runtime
</Button>
)}
</div>
<div className="mt-3 space-y-2">
@@ -219,7 +251,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
{selectedSession?.message || 'Create a browser session to start.'}
</div>
<p className="mt-2 text-xs leading-relaxed text-neutral-400">
This panel shows captured browser screenshots. Interactive agent control should use the guarded Browser Use API.
Install the Browser Use runtime from this panel or enable it from Settings.
</p>
</div>
)}

View File

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

View File

@@ -54,7 +54,7 @@ type NotificationPreferencesResponse = {
type ActiveLoginProvider = AgentProvider | '';
const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'notifications', 'plugins'];
const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'browser', 'notifications', 'plugins', 'about'];
const normalizeMainTab = (tab: string): SettingsMainTab => {
// Keep backwards compatibility with older callers that still pass "tools".

View File

@@ -3,7 +3,7 @@ import type { Dispatch, SetStateAction } from 'react';
import type { LLMProvider } from '../../../types/app';
import type { ProviderAuthStatus } from '../../provider-auth/types';
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins' | 'about';
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'browser' | 'notifications' | 'plugins' | 'about';
export type AgentProvider = LLMProvider;
export type AgentCategory = 'account' | 'permissions' | 'mcp';
export type ProjectSortOrder = 'name' | 'date';

View File

@@ -7,6 +7,7 @@ import AgentsSettingsTab from '../view/tabs/agents-settings/AgentsSettingsTab';
import AppearanceSettingsTab from '../view/tabs/AppearanceSettingsTab';
import CredentialsSettingsTab from '../view/tabs/api-settings/CredentialsSettingsTab';
import GitSettingsTab from '../view/tabs/git-settings/GitSettingsTab';
import BrowserUseSettingsTab from '../view/tabs/browser-use-settings/BrowserUseSettingsTab';
import NotificationsSettingsTab from '../view/tabs/NotificationsSettingsTab';
import TasksSettingsTab from '../view/tabs/tasks-settings/TasksSettingsTab';
import PluginSettingsTab from '../../plugins/view/PluginSettingsTab';
@@ -139,17 +140,19 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
{activeTab === 'tasks' && <TasksSettingsTab />}
{activeTab === 'notifications' && (
<NotificationsSettingsTab
notificationPreferences={notificationPreferences}
onNotificationPreferencesChange={setNotificationPreferences}
pushPermission={pushPermission}
isPushSubscribed={isPushSubscribed}
isPushLoading={isPushLoading}
onEnablePush={handleEnablePush}
onDisablePush={handleDisablePush}
/>
)}
{activeTab === 'browser' && <BrowserUseSettingsTab />}
{activeTab === 'notifications' && (
<NotificationsSettingsTab
notificationPreferences={notificationPreferences}
onNotificationPreferencesChange={setNotificationPreferences}
pushPermission={pushPermission}
isPushSubscribed={isPushSubscribed}
isPushLoading={isPushLoading}
onEnablePush={handleEnablePush}
onDisablePush={handleDisablePush}
/>
)}
{activeTab === 'api' && <CredentialsSettingsTab />}

View File

@@ -1,4 +1,4 @@
import { Bell, Bot, GitBranch, Info, Key, ListChecks, Palette, Puzzle } from 'lucide-react';
import { Bell, Bot, GitBranch, Info, Key, ListChecks, MonitorPlay, Palette, Puzzle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { cn } from '../../../lib/utils';
import { PillBar, Pill } from '../../../shared/view/ui';
@@ -21,6 +21,7 @@ const NAV_ITEMS: NavItem[] = [
{ id: 'git', labelKey: 'mainTabs.git', icon: GitBranch },
{ id: 'api', labelKey: 'mainTabs.apiTokens', icon: Key },
{ id: 'tasks', labelKey: 'mainTabs.tasks', icon: ListChecks },
{ id: 'browser', labelKey: 'mainTabs.browser', icon: MonitorPlay },
{ id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle },
{ id: 'notifications', labelKey: 'mainTabs.notifications', icon: Bell },
{ id: 'about', labelKey: 'mainTabs.about', icon: Info },

View File

@@ -0,0 +1,164 @@
import { useCallback, useEffect, useState } from 'react';
import { Download, Loader2, MonitorPlay, RefreshCw } from 'lucide-react';
import { Button } from '../../../../../shared/view/ui';
import { authenticatedFetch } from '../../../../../utils/api';
import SettingsCard from '../../SettingsCard';
import SettingsRow from '../../SettingsRow';
import SettingsSection from '../../SettingsSection';
import SettingsToggle from '../../SettingsToggle';
type BrowserUseSettings = {
enabled: boolean;
};
type BrowserUseStatus = {
enabled: boolean;
available: boolean;
runtime: 'cloud' | 'local';
playwrightInstalled: boolean;
chromiumInstalled: boolean;
installInProgress: boolean;
message: string;
};
async function readJson<T>(response: Response): Promise<T> {
const data = await response.json();
if (!response.ok || data.success === false) {
throw new Error(data.error || data.details || `Request failed (${response.status})`);
}
return data as T;
}
export default function BrowserUseSettingsTab() {
const [settings, setSettings] = useState<BrowserUseSettings>({ enabled: true });
const [status, setStatus] = useState<BrowserUseStatus | null>(null);
const [isLoading, setIsLoading] = 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 settingsData = await readJson<{ data: { settings: BrowserUseSettings } }>(settingsResponse);
const statusData = await readJson<{ data: BrowserUseStatus }>(statusResponse);
setSettings(settingsData.data.settings);
setStatus(statusData.data);
}, []);
useEffect(() => {
setIsLoading(true);
void loadState()
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load Browser Use settings'))
.finally(() => setIsLoading(false));
}, [loadState]);
const updateEnabled = async (enabled: boolean) => {
setIsSaving(true);
setError(null);
try {
const response = await authenticatedFetch('/api/browser-use/settings', {
method: 'PUT',
body: JSON.stringify({ enabled }),
});
const data = await readJson<{ data: { settings: BrowserUseSettings } }>(response);
setSettings(data.data.settings);
await loadState();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save Browser Use settings');
} finally {
setIsSaving(false);
}
};
const installRuntime = async () => {
setIsInstalling(true);
setError(null);
try {
const response = await authenticatedFetch('/api/browser-use/runtime/install', { method: 'POST' });
await readJson(response);
await loadState();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to install Browser Use runtime');
} finally {
setIsInstalling(false);
}
};
const needsRuntime = Boolean(settings.enabled && status && (!status.playwrightInstalled || !status.chromiumInstalled));
return (
<div className="space-y-8">
<SettingsSection
title="Browser Use"
description="Manage local Playwright browser sessions used for captured browser screenshots and guarded navigation."
>
<SettingsCard divided>
<SettingsRow
label="Enable Browser Use"
description="Allow CloudCLI to create owner-scoped Playwright browser sessions."
>
<SettingsToggle
checked={settings.enabled}
onChange={(value) => void updateEnabled(value)}
ariaLabel="Enable Browser Use"
disabled={isLoading || isSaving}
/>
</SettingsRow>
<div className="space-y-4 px-4 py-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 space-y-1">
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
<MonitorPlay className="h-4 w-4 text-primary" />
Runtime
</div>
<p className="text-sm text-muted-foreground">
{status?.message || (isLoading ? 'Checking Browser Use runtime...' : 'Runtime status unavailable.')}
</p>
{status && (
<div className="flex flex-wrap gap-2 pt-1 text-xs text-muted-foreground">
<span className="rounded-md border border-border px-2 py-1">Mode: {status.runtime}</span>
<span className="rounded-md border border-border px-2 py-1">
Playwright: {status.playwrightInstalled ? 'installed' : 'missing'}
</span>
<span className="rounded-md border border-border px-2 py-1">
Chromium: {status.chromiumInstalled ? 'installed' : 'missing'}
</span>
</div>
)}
</div>
<div className="flex flex-shrink-0 gap-2">
<Button type="button" variant="outline" size="sm" onClick={() => void loadState()} disabled={isLoading || isInstalling}>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
{needsRuntime && (
<Button type="button" size="sm" onClick={() => void installRuntime()} disabled={isInstalling || status?.installInProgress}>
{isInstalling || status?.installInProgress ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
Install Runtime
</Button>
)}
</div>
</div>
{error && (
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-200">
{error}
</div>
)}
</div>
</SettingsCard>
</SettingsSection>
</div>
);
}