import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { computerUseApi, type ComputerUseStatus, type SetupResult, type InstalledApp, type AuthorizedApp } from '../api/computerUse' import { useTranslation } from '../i18n' import { getDesktopHost } from '../lib/desktopHost' type CheckState = 'loading' | 'ready' | 'error' const PYTHON_DOWNLOAD_URLS: Record = { darwin: 'https://www.python.org/downloads/macos/', win32: 'https://www.python.org/downloads/windows/', } function StatusIcon({ ok }: { ok: boolean | null }) { if (ok === null) { return help } return ok ? ( check_circle ) : ( cancel ) } function StatusRow({ label, ok, detail }: { label: string; ok: boolean | null; detail: string }) { return (
{label} {detail}
) } async function openSystemSettings(pane: 'Privacy_ScreenCapture' | 'Privacy_Accessibility') { await computerUseApi.openSettings(pane) } async function openExternalUrl(url: string) { const host = getDesktopHost() try { await host.shell.open(url) } catch { window.open(url, '_blank', 'noopener,noreferrer') } } export function ComputerUseSettings() { const t = useTranslation() const [status, setStatus] = useState(null) const [checkState, setCheckState] = useState('loading') const [setupRunning, setSetupRunning] = useState(false) const [setupResult, setSetupResult] = useState(null) // App authorization state const [installedApps, setInstalledApps] = useState([]) const [authorizedBundleIds, setAuthorizedBundleIds] = useState>(new Set()) const [authorizedApps, setAuthorizedApps] = useState([]) const [appsLoading, setAppsLoading] = useState(false) const [appsSaved, setAppsSaved] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [computerUseEnabled, setComputerUseEnabled] = useState(true) const [clipboardAccess, setClipboardAccess] = useState(true) const [systemKeys, setSystemKeys] = useState(true) const [pythonPathDraft, setPythonPathDraft] = useState('') const [pythonPathSaved, setPythonPathSaved] = useState('') const [pythonPathSaving, setPythonPathSaving] = useState(false) const [pythonPathMessage, setPythonPathMessage] = useState(null) const configMutationSeqRef = useRef(0) const fetchStatus = useCallback(async () => { setCheckState('loading') try { const s = await computerUseApi.getStatus() setStatus(s) setCheckState('ready') } catch { setCheckState('error') } }, []) const applyConfig = useCallback(( configResult: Awaited>, requestSeq = configMutationSeqRef.current, ) => { if (requestSeq !== configMutationSeqRef.current) return setComputerUseEnabled(configResult.enabled) setAuthorizedApps(configResult.authorizedApps) setAuthorizedBundleIds(new Set(configResult.authorizedApps.map(a => a.bundleId))) setClipboardAccess(configResult.grantFlags.clipboardRead) setSystemKeys(configResult.grantFlags.systemKeyCombos) setPythonPathDraft(configResult.pythonPath ?? '') setPythonPathSaved(configResult.pythonPath ?? '') }, []) const fetchConfig = useCallback(async () => { const requestSeq = configMutationSeqRef.current try { applyConfig(await computerUseApi.getAuthorizedApps(), requestSeq) } catch { // API not ready } }, [applyConfig]) const fetchApps = useCallback(async () => { const requestSeq = configMutationSeqRef.current setAppsLoading(true) try { const [appsResult, configResult] = await Promise.all([ computerUseApi.getInstalledApps(), computerUseApi.getAuthorizedApps(), ]) setInstalledApps(appsResult.apps) applyConfig(configResult, requestSeq) } catch { // API not ready } finally { setAppsLoading(false) } }, [applyConfig]) useEffect(() => { fetchStatus() fetchConfig() }, [fetchStatus, fetchConfig]) // Load apps when environment is ready const envReady = status?.venv.created && status?.dependencies.installed useEffect(() => { if (envReady) fetchApps() }, [envReady, fetchApps]) const handleSetup = async () => { setSetupRunning(true) setSetupResult(null) try { const result = await computerUseApi.runSetup() setSetupResult(result) await fetchStatus() if (result.success) await fetchApps() } catch { setSetupResult({ success: false, steps: [{ name: 'error', ok: false, message: 'Request failed' }] }) } finally { setSetupRunning(false) } } const toggleApp = (app: InstalledApp) => { configMutationSeqRef.current += 1 const newSet = new Set(authorizedBundleIds) let newAuthorized = [...authorizedApps] if (newSet.has(app.bundleId)) { newSet.delete(app.bundleId) newAuthorized = newAuthorized.filter(a => a.bundleId !== app.bundleId) } else { newSet.add(app.bundleId) newAuthorized.push({ bundleId: app.bundleId, displayName: app.displayName, authorizedAt: new Date().toISOString(), }) } setAuthorizedBundleIds(newSet) setAuthorizedApps(newAuthorized) // Auto-save computerUseApi.setAuthorizedApps({ authorizedApps: newAuthorized, grantFlags: { clipboardRead: clipboardAccess, clipboardWrite: clipboardAccess, systemKeyCombos: systemKeys }, }).then(() => { setAppsSaved(true) setTimeout(() => setAppsSaved(false), 1500) }) } const toggleFlag = (flag: 'clipboard' | 'systemKeys', value: boolean) => { configMutationSeqRef.current += 1 if (flag === 'clipboard') setClipboardAccess(value) else setSystemKeys(value) computerUseApi.setAuthorizedApps({ authorizedApps, grantFlags: { clipboardRead: flag === 'clipboard' ? value : clipboardAccess, clipboardWrite: flag === 'clipboard' ? value : clipboardAccess, systemKeyCombos: flag === 'systemKeys' ? value : systemKeys, }, }) } const toggleComputerUseEnabled = (value: boolean) => { configMutationSeqRef.current += 1 setComputerUseEnabled(value) computerUseApi.setAuthorizedApps({ enabled: value }).then(() => { setAppsSaved(true) setTimeout(() => setAppsSaved(false), 1500) }) } const savePythonPath = async (value = pythonPathDraft) => { configMutationSeqRef.current += 1 const normalized = value.trim() setPythonPathSaving(true) setPythonPathMessage(null) try { await computerUseApi.setAuthorizedApps({ pythonPath: normalized || null }) setPythonPathDraft(normalized) setPythonPathSaved(normalized) setPythonPathMessage(t('settings.computerUse.pythonPathSaved')) await fetchStatus() } catch { setPythonPathMessage(t('settings.computerUse.pythonPathSaveFailed')) } finally { setPythonPathSaving(false) } } const choosePythonPath = async () => { const host = getDesktopHost() if (!host.capabilities.dialogs) { setPythonPathMessage(t('settings.computerUse.pythonPathDialogFailed')) return } try { const selected = await host.dialogs.open({ multiple: false, directory: false, title: t('settings.computerUse.pythonPathDialogTitle'), }) const selectedPath = Array.isArray(selected) ? selected[0] : selected if (typeof selectedPath === 'string' && selectedPath.trim()) { setPythonPathDraft(selectedPath) await savePythonPath(selectedPath) } } catch { setPythonPathMessage(t('settings.computerUse.pythonPathDialogFailed')) } } const allReady = status?.supported && status.python.installed && status.venv.created && status.dependencies.installed const accessibilityNeedsAttention = status?.permissions.accessibility === false const screenRecordingNeedsAttention = status?.permissions.screenRecording === false const screenRecordingReady = status ? status.permissions.screenRecording !== false : null const pythonDownloadUrl = status ? PYTHON_DOWNLOAD_URLS[status.platform] ?? 'https://www.python.org/downloads/' : 'https://www.python.org/downloads/' const pythonPathDirty = pythonPathDraft.trim() !== pythonPathSaved const pythonDetail = status?.python.installed ? `${t('settings.computerUse.pythonFound')} — ${status.python.version} (${status.python.path})` : status?.python.source === 'custom' ? `${t('settings.computerUse.pythonCustomInvalid')} — ${status.python.path}${status.python.error ? `: ${status.python.error}` : ''}` : t('settings.computerUse.pythonNotFound') // Filter apps by search query const filteredApps = useMemo(() => { if (!searchQuery) return installedApps const q = searchQuery.toLowerCase() return installedApps.filter( a => a.displayName.toLowerCase().includes(q) || a.bundleId.toLowerCase().includes(q) ) }, [installedApps, searchQuery]) // Sort: authorized apps first, then alphabetical const sortedApps = useMemo(() => { return [...filteredApps].sort((a, b) => { const aAuth = authorizedBundleIds.has(a.bundleId) ? 0 : 1 const bAuth = authorizedBundleIds.has(b.bundleId) ? 0 : 1 if (aAuth !== bAuth) return aAuth - bAuth return a.displayName.localeCompare(b.displayName) }) }, [filteredApps, authorizedBundleIds]) return (
{/* Title */}

{t('settings.computerUse.title')}

{t('settings.computerUse.description')}

{!computerUseEnabled && (
{t('settings.computerUse.disabledHint')}
)} {checkState === 'loading' ? (
{t('common.loading')}
) : checkState === 'error' ? (
Failed to check status.
) : status ? ( <> {!status.supported && (
{t('settings.computerUse.notSupported')}
)} {/* Status checks */}
{ setPythonPathDraft(e.target.value) setPythonPathMessage(null) }} placeholder={t('settings.computerUse.pythonPathPlaceholder')} className="min-w-[220px] flex-1 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-container)] px-3 py-2 font-mono text-xs text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:border-[var(--color-brand)] focus:outline-none" /> {pythonPathSaved && ( )}

{pythonPathMessage ?? t('settings.computerUse.pythonPathHint')}

{/* macOS Permissions — only shown on macOS (darwin) */} {envReady && status.platform === 'darwin' && ( <> {(accessibilityNeedsAttention || screenRecordingNeedsAttention) && (

{t('settings.computerUse.permRestartHint')}

{accessibilityNeedsAttention && ( )} {screenRecordingNeedsAttention && ( )}
)} )} {allReady && (status.platform !== 'darwin' || (status.permissions.accessibility && screenRecordingReady)) && (
verified {t('settings.computerUse.allReady')}
)} {setupResult && (
{setupResult.success ? t('settings.computerUse.setupSuccess') : t('settings.computerUse.setupFail')}
{setupResult.steps.map((step, i) => (
{step.message}
))}
)} {/* Action buttons */}
{!status.python.installed && ( )} {!envReady && status.python.installed && ( )}
{/* ─── App Authorization Section ─── */} {envReady && (

{t('settings.computerUse.appsTitle')} {appsSaved && ( check {t('settings.computerUse.appsSaved')} )}

{t('settings.computerUse.appsDescription')}

{/* Grant flags */}
{/* Search */}
search setSearchQuery(e.target.value)} placeholder={t('settings.computerUse.appsSearch')} className="w-full pl-9 pr-4 py-2 text-sm bg-[var(--color-surface-container-low)] border border-[var(--color-border)] rounded-lg text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:border-[var(--color-brand)]" />
{/* App list */} {appsLoading ? (
{t('settings.computerUse.appsLoading')}
) : installedApps.length === 0 ? (
{t('settings.computerUse.appsEmpty')}
) : (
{sortedApps.map(app => { const isAuthorized = authorizedBundleIds.has(app.bundleId) return ( ) })}
)}
)} ) : null}
) }