import { useState, useEffect, useCallback, useMemo } from 'react' import { computerUseApi, type ComputerUseStatus, type SetupResult, type InstalledApp, type AuthorizedApp } from '../api/computerUse' import { useTranslation } from '../i18n' 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) { try { const { open } = await import('@tauri-apps/plugin-shell') await 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 [clipboardAccess, setClipboardAccess] = useState(true) const [systemKeys, setSystemKeys] = useState(true) const fetchStatus = useCallback(async () => { setCheckState('loading') try { const s = await computerUseApi.getStatus() setStatus(s) setCheckState('ready') } catch { setCheckState('error') } }, []) const fetchApps = useCallback(async () => { setAppsLoading(true) try { const [appsResult, configResult] = await Promise.all([ computerUseApi.getInstalledApps(), computerUseApi.getAuthorizedApps(), ]) setInstalledApps(appsResult.apps) setAuthorizedApps(configResult.authorizedApps) setAuthorizedBundleIds(new Set(configResult.authorizedApps.map(a => a.bundleId))) setClipboardAccess(configResult.grantFlags.clipboardRead) setSystemKeys(configResult.grantFlags.systemKeyCombos) } catch { // API not ready } finally { setAppsLoading(false) } }, []) useEffect(() => { fetchStatus() }, [fetchStatus]) // 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) => { 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) => { 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 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/' // 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')}

{checkState === 'loading' ? (
{t('common.loading')}
) : checkState === 'error' ? (
Failed to check status.
) : status ? ( <> {!status.supported && (
{t('settings.computerUse.notSupported')}
)} {/* Status checks */}
{/* 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}
) }