From 260070bae0f6288c01d263038fc66b780e9b891d Mon Sep 17 00:00:00 2001 From: Simos Mikelatos Date: Mon, 15 Jun 2026 17:52:27 +0000 Subject: [PATCH] feat: add browser use runtime setup settings --- .../modules/browser-use/browser-use.routes.ts | 50 ++++- .../browser-use/browser-use.service.ts | 192 ++++++++++++++++-- .../tests/browser-use.service.test.ts | 25 +-- .../browser-use/view/BrowserUsePanel.tsx | 38 +++- .../settings/constants/constants.ts | 2 + .../settings/hooks/useSettingsController.ts | 2 +- src/components/settings/types/types.ts | 2 +- src/components/settings/view/Settings.tsx | 25 ++- .../settings/view/SettingsSidebar.tsx | 3 +- .../BrowserUseSettingsTab.tsx | 164 +++++++++++++++ src/i18n/locales/en/settings.json | 1 + 11 files changed, 450 insertions(+), 54 deletions(-) create mode 100644 src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx diff --git a/server/modules/browser-use/browser-use.routes.ts b/server/modules/browser-use/browser-use.routes.ts index f5dc563b..c730dd53 100644 --- a/server/modules/browser-use/browser-use.routes.ts +++ b/server/modules/browser-use/browser-use.routes.ts @@ -22,8 +22,54 @@ function readParam(value: string | string[] | undefined): string { return Array.isArray(value) ? value[0] || '' : value || ''; } -router.get('/status', (_req, res) => { - res.json({ success: true, data: browserUseService.getStatus() }); +router.get('/status', async (_req, res) => { + try { + res.json({ success: true, data: await browserUseService.getStatus() }); + } catch (error) { + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to load Browser Use status.', + }); + } +}); + +router.get('/settings', async (_req, res) => { + try { + res.json({ success: true, data: { settings: await browserUseService.getSettings() } }); + } catch (error) { + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to load Browser Use settings.', + }); + } +}); + +router.put('/settings', async (req, res) => { + try { + const settings = await browserUseService.updateSettings(req.body || {}); + res.json({ success: true, data: { settings } }); + } catch (error) { + res.status(400).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to save Browser Use settings.', + }); + } +}); + +router.post('/runtime/install', async (_req, res) => { + try { + const result = await browserUseService.installRuntime(); + res.status(result.success ? 200 : 500).json({ + success: result.success, + data: result, + error: result.success ? undefined : result.message, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to install Browser Use runtime.', + }); + } }); router.get('/sessions', async (req: AuthenticatedRequest, res) => { diff --git a/server/modules/browser-use/browser-use.service.ts b/server/modules/browser-use/browser-use.service.ts index 4ca96695..5abcfbf3 100644 --- a/server/modules/browser-use/browser-use.service.ts +++ b/server/modules/browser-use/browser-use.service.ts @@ -1,13 +1,19 @@ import { createRequire } from 'node:module'; import { randomUUID } from 'node:crypto'; +import { spawn } from 'node:child_process'; import dns from 'node:dns/promises'; +import fs from 'node:fs'; +import fsPromises from 'node:fs/promises'; import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; const require = createRequire(import.meta.url); const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true'; const MAX_SESSIONS_PER_OWNER = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_MAX_SESSIONS_PER_OWNER || '3', 10); const SESSION_TTL_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_SESSION_TTL_MS || String(30 * 60 * 1000), 10); const ALLOW_PRIVATE_NETWORKS = process.env.CLOUDCLI_BROWSER_USE_ALLOW_PRIVATE_NETWORKS === '1'; +const SETTINGS_PATH = path.join(os.homedir(), '.cloudcli', 'browser-use-settings.json'); type BrowserUseRuntime = 'cloud' | 'local'; type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable'; @@ -37,23 +43,71 @@ type BrowserUseOwner = { id: string | number; }; +type BrowserUseSettings = { + enabled: boolean; +}; + +type RuntimeReadiness = { + playwright: any | null; + playwrightInstalled: boolean; + chromiumInstalled: boolean; + chromiumExecutablePath: string | null; + installInProgress: boolean; + installMessage: string | null; +}; + const sessions = new Map(); const handles = new Map(); +let installPromise: Promise<{ success: boolean; message: string }> | null = null; +let lastInstallMessage: string | null = null; + +const DEFAULT_SETTINGS: BrowserUseSettings = { + enabled: true, +}; function getRuntime(): BrowserUseRuntime { return IS_PLATFORM ? 'cloud' : 'local'; } -function isBrowserUseEnabled(): boolean { - return process.env.CLOUDCLI_BROWSER_USE_ENABLED === '1'; +async function readSettings(): Promise { + try { + const raw = await fsPromises.readFile(SETTINGS_PATH, 'utf8'); + const parsed = JSON.parse(raw) as Partial; + return { + enabled: parsed.enabled !== false, + }; + } catch (error: any) { + if (error?.code !== 'ENOENT') { + console.warn('[Browser Use] Failed to read settings:', error?.message || error); + } + return DEFAULT_SETTINGS; + } } -function getSetupMessage(): string { - if (!isBrowserUseEnabled()) { - return 'Browser Use is disabled. Set CLOUDCLI_BROWSER_USE_ENABLED=1 after provisioning a Playwright/Chromium runtime.'; +async function writeSettings(settings: BrowserUseSettings): Promise { + const normalized = { + enabled: settings.enabled !== false, + }; + + await fsPromises.mkdir(path.dirname(SETTINGS_PATH), { recursive: true }); + await fsPromises.writeFile(SETTINGS_PATH, JSON.stringify(normalized, null, 2), 'utf8'); + return normalized; +} + +function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadiness): string { + if (!settings.enabled) { + return 'Browser Use is disabled in settings.'; } - return 'Playwright is not available in this runtime. Install/provision Playwright or point CloudCLI at a managed browser worker.'; + if (!readiness.playwrightInstalled) { + return 'Install Playwright and Chromium to use browser sessions.'; + } + + if (!readiness.chromiumInstalled) { + return 'Playwright is installed, but Chromium is missing. Install the Chromium runtime to continue.'; + } + + return readiness.installMessage || 'Browser Use runtime is not ready.'; } function getPlaywright(): any | null { @@ -64,6 +118,85 @@ function getPlaywright(): any | null { } } +function getRuntimeReadiness(): RuntimeReadiness { + const playwright = getPlaywright(); + const readiness: RuntimeReadiness = { + playwright, + playwrightInstalled: Boolean(playwright), + chromiumInstalled: false, + chromiumExecutablePath: null, + installInProgress: Boolean(installPromise), + installMessage: lastInstallMessage, + }; + + if (!playwright) { + return readiness; + } + + try { + const executablePath = playwright.chromium.executablePath(); + readiness.chromiumExecutablePath = executablePath; + readiness.chromiumInstalled = Boolean(executablePath && fs.existsSync(executablePath)); + } catch { + readiness.chromiumInstalled = false; + } + + return readiness; +} + +function runCommand(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: process.cwd(), + env: process.env, + shell: false, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const output: string[] = []; + + child.stdout.on('data', (chunk) => output.push(String(chunk))); + child.stderr.on('data', (chunk) => output.push(String(chunk))); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + + reject(new Error(output.join('').trim() || `${command} ${args.join(' ')} exited with code ${code}`)); + }); + }); +} + +async function installRuntime(): Promise<{ success: boolean; message: string }> { + if (installPromise) { + return installPromise; + } + + const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + installPromise = (async () => { + try { + lastInstallMessage = 'Installing Playwright package...'; + await runCommand(npmCommand, ['install', '--no-save', '--no-package-lock', 'playwright']); + + lastInstallMessage = 'Installing Chromium runtime...'; + await runCommand(npmCommand, ['exec', '--', 'playwright', 'install', 'chromium']); + + lastInstallMessage = 'Browser Use runtime installed.'; + return { success: true, message: lastInstallMessage }; + } catch (error) { + lastInstallMessage = error instanceof Error ? error.message : 'Failed to install Browser Use runtime.'; + return { success: false, message: lastInstallMessage }; + } + })(); + + try { + return await installPromise; + } finally { + installPromise = null; + } +} + function getOwnerId(owner: BrowserUseOwner): string { if (owner.id === undefined || owner.id === null || String(owner.id).trim() === '') { throw new Error('Authenticated user is required.'); @@ -218,19 +351,43 @@ async function captureSession(session: BrowserUseSession, page: any): Promise) { + const current = await readSettings(); + return writeSettings({ + ...current, + enabled: settings.enabled ?? current.enabled, + }); + }, + + async getStatus() { + const settings = await readSettings(); + const readiness = getRuntimeReadiness(); + const available = settings.enabled && readiness.playwrightInstalled && readiness.chromiumInstalled; return { - enabled, + enabled: settings.enabled, runtime: getRuntime(), - available: enabled, + available, + playwrightInstalled: readiness.playwrightInstalled, + chromiumInstalled: readiness.chromiumInstalled, + installInProgress: readiness.installInProgress, sessionCount: sessions.size, mcpRecommended: true, - message: enabled + message: available ? 'Browser Use runtime is available.' - : getSetupMessage(), + : getSetupMessage(settings, readiness), + }; + }, + + async installRuntime() { + const result = await installRuntime(); + return { + ...result, + status: await this.getStatus(), }; }, @@ -264,14 +421,15 @@ export const browserUseService = { throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active sessions per user.`); } - const playwright = getPlaywright(); - if (!isBrowserUseEnabled() || !playwright) { - session.message = getSetupMessage(); + const settings = await readSettings(); + const readiness = getRuntimeReadiness(); + if (!settings.enabled || !readiness.playwrightInstalled || !readiness.chromiumInstalled || !readiness.playwright) { + session.message = getSetupMessage(settings, readiness); sessions.set(session.id, session); return publicSession(session); } - const browser = await playwright.chromium.launch({ + const browser = await readiness.playwright.chromium.launch({ headless: true, args: ['--disable-dev-shm-usage'], }); diff --git a/server/modules/browser-use/tests/browser-use.service.test.ts b/server/modules/browser-use/tests/browser-use.service.test.ts index 3a291682..162e9439 100644 --- a/server/modules/browser-use/tests/browser-use.service.test.ts +++ b/server/modules/browser-use/tests/browser-use.service.test.ts @@ -15,27 +15,16 @@ test('browser use blocks private and local network addresses by default', () => }); test('browser use sessions are listed only for their owner', async () => { - const originalEnabled = process.env.CLOUDCLI_BROWSER_USE_ENABLED; - process.env.CLOUDCLI_BROWSER_USE_ENABLED = '0'; - const ownerA = { id: `owner-a-${Date.now()}-${Math.random()}` }; const ownerB = { id: `owner-b-${Date.now()}-${Math.random()}` }; - try { - const ownerASession = await browserUseService.createSession(ownerA); - await browserUseService.createSession(ownerB); + const ownerASession = await browserUseService.createSession(ownerA); + await browserUseService.createSession(ownerB); - const ownerASessions = await browserUseService.listSessions(ownerA); - const ownerBSessions = await browserUseService.listSessions(ownerB); + const ownerASessions = await browserUseService.listSessions(ownerA); + const ownerBSessions = await browserUseService.listSessions(ownerB); - assert.equal(ownerASessions.some((session) => session.id === ownerASession.id), true); - assert.equal(ownerBSessions.some((session) => session.id === ownerASession.id), false); - assert.equal(Object.hasOwn(ownerASession, 'ownerId'), false); - } finally { - if (originalEnabled === undefined) { - delete process.env.CLOUDCLI_BROWSER_USE_ENABLED; - } else { - process.env.CLOUDCLI_BROWSER_USE_ENABLED = originalEnabled; - } - } + assert.equal(ownerASessions.some((session) => session.id === ownerASession.id), true); + assert.equal(ownerBSessions.some((session) => session.id === ownerASession.id), false); + assert.equal(Object.hasOwn(ownerASession, 'ownerId'), false); }); diff --git a/src/components/browser-use/view/BrowserUsePanel.tsx b/src/components/browser-use/view/BrowserUsePanel.tsx index d2494a2a..22d1153b 100644 --- a/src/components/browser-use/view/BrowserUsePanel.tsx +++ b/src/components/browser-use/view/BrowserUsePanel.tsx @@ -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(null); const [targetUrl, setTargetUrl] = useState('https://example.com'); const [isBusy, setIsBusy] = useState(false); + const [isInstalling, setIsInstalling] = useState(false); const [error, setError] = useState(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 (
@@ -130,7 +146,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { Refresh - @@ -143,6 +159,22 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
Runtime
{status?.available ? 'Available' : 'Setup required'}

{status?.message || 'Loading Browser Use status...'}

+ {canInstallRuntime && ( + + )}
@@ -219,7 +251,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) { {selectedSession?.message || 'Create a browser session to start.'}

- 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.

)} diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts index 7d4c353d..37fa9df3 100644 --- a/src/components/settings/constants/constants.ts +++ b/src/components/settings/constants/constants.ts @@ -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 }, diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index 70e9ed1c..a172b831 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -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". diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts index f68cacc4..672be1ee 100644 --- a/src/components/settings/types/types.ts +++ b/src/components/settings/types/types.ts @@ -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'; diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx index 8340a547..800440e0 100644 --- a/src/components/settings/view/Settings.tsx +++ b/src/components/settings/view/Settings.tsx @@ -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' && } - {activeTab === 'notifications' && ( - - )} + {activeTab === 'browser' && } + + {activeTab === 'notifications' && ( + + )} {activeTab === 'api' && } diff --git a/src/components/settings/view/SettingsSidebar.tsx b/src/components/settings/view/SettingsSidebar.tsx index 149c1492..dde32a9e 100644 --- a/src/components/settings/view/SettingsSidebar.tsx +++ b/src/components/settings/view/SettingsSidebar.tsx @@ -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 }, diff --git a/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx new file mode 100644 index 00000000..4cc0f86b --- /dev/null +++ b/src/components/settings/view/tabs/browser-use-settings/BrowserUseSettingsTab.tsx @@ -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(response: Response): Promise { + 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({ enabled: true }); + const [status, setStatus] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isInstalling, setIsInstalling] = useState(false); + const [error, setError] = useState(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 ( +
+ + + + void updateEnabled(value)} + ariaLabel="Enable Browser Use" + disabled={isLoading || isSaving} + /> + + +
+
+
+
+ + Runtime +
+

+ {status?.message || (isLoading ? 'Checking Browser Use runtime...' : 'Runtime status unavailable.')} +

+ {status && ( +
+ Mode: {status.runtime} + + Playwright: {status.playwrightInstalled ? 'installed' : 'missing'} + + + Chromium: {status.chromiumInstalled ? 'installed' : 'missing'} + +
+ )} +
+ +
+ + {needsRuntime && ( + + )} +
+
+ + {error && ( +
+ {error} +
+ )} +
+
+
+
+ ); +} diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index d5bc7900..bae8db89 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -94,6 +94,7 @@ "git": "Git", "apiTokens": "API & Tokens", "tasks": "Tasks", + "browser": "Browser Use", "notifications": "Notifications", "plugins": "Plugins", "about": "About"