import React from 'react'; import { isDesktopShell, requestFileAccess, startDesktopWindowDrag } from '@/lib/desktop'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Icon } from "@/components/icon/Icon"; import { updateDesktopSettings } from '@/lib/persistence'; import { copyTextToClipboard } from '@/lib/clipboard'; import { restartDesktopApp } from '@/lib/desktop'; import { useI18n } from '@/lib/i18n'; import { runtimeFetch } from '@/lib/runtime-fetch'; const INSTALL_COMMAND = 'curl -fsSL https://opencode.ai/install | bash'; const DOCS_URL = 'https://opencode.ai/docs'; type OnboardingPlatform = 'macos' | 'linux' | 'windows' | 'unknown'; type LocalSetupScreenProps = { /** Callback when user goes back */ onBack: () => void; /** Callback when CLI becomes available */ onCliAvailable?: () => void; /** Whether this screen was entered from recovery flow (shows "Connect to Remote" link) */ isFromRecovery?: boolean; /** Callback when user wants to switch to remote */ onSwitchToRemote?: () => void; }; function BashCommand({ onCopy, copyTitle }: { onCopy: () => void; copyTitle: string }) { return (
curl -fsSL https://opencode.ai/install | bash
); } const HINT_DELAY_MS = 30000; export function LocalSetupScreen({ onBack, onCliAvailable, isFromRecovery = false, onSwitchToRemote, }: LocalSetupScreenProps) { const { t } = useI18n(); const [copied, setCopied] = React.useState(false); const [showHint, setShowHint] = React.useState(false); const [isDesktopApp, setIsDesktopApp] = React.useState(false); const [isRetrying, setIsRetrying] = React.useState(false); const [isChecking, setIsChecking] = React.useState(false); const [checkError, setCheckError] = React.useState(null); const [opencodeBinary, setOpencodeBinary] = React.useState(''); const [platform, setPlatform] = React.useState('unknown'); React.useEffect(() => { const timer = setTimeout(() => setShowHint(true), HINT_DELAY_MS); return () => clearTimeout(timer); }, []); React.useEffect(() => { setIsDesktopApp(isDesktopShell()); }, []); React.useEffect(() => { if (typeof navigator === 'undefined') { setPlatform('unknown'); return; } const ua = navigator.userAgent || ''; if (/Windows/i.test(ua)) { setPlatform('windows'); return; } if (/Macintosh|Mac OS X/i.test(ua)) { setPlatform('macos'); return; } if (/Linux/i.test(ua)) { setPlatform('linux'); return; } setPlatform('unknown'); }, []); React.useEffect(() => { let cancelled = false; void (async () => { try { const response = await runtimeFetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' } }); if (!response.ok) return; const data = (await response.json().catch(() => null)) as null | { opencodeBinary?: unknown }; if (!data || cancelled) return; const value = typeof data.opencodeBinary === 'string' ? data.opencodeBinary.trim() : ''; if (value) { setOpencodeBinary(value); } } catch { // ignore } })(); return () => { cancelled = true; }; }, []); const handleDragStart = React.useCallback(async (e: React.MouseEvent) => { if ((e.target as HTMLElement).closest('button, a, input, select, textarea, code')) { return; } if (e.button !== 0) return; if (isDesktopApp) { await startDesktopWindowDrag(); } }, [isDesktopApp]); const checkCliAvailability = React.useCallback(async (): Promise => { try { const response = await runtimeFetch('/health'); if (!response.ok) return false; const data = await response.json(); return data.openCodeRunning === true || data.isOpenCodeReady === true; } catch { return false; } }, []); const handleBrowse = React.useCallback(async () => { if (typeof window === 'undefined') { return; } if (!isDesktopApp) { return; } try { const selected = await requestFileAccess(); if (selected.success && selected.path && selected.path.trim().length > 0) { setOpencodeBinary(selected.path.trim()); } } catch { // ignore } }, [isDesktopApp]); const handleApplyPath = React.useCallback(async () => { setIsRetrying(true); try { await updateDesktopSettings({ opencodeBinary: opencodeBinary.trim() }); // In desktop boot flow, restart the app so the native host can // re-evaluate the boot outcome with the updated binary path. if (isDesktopApp) { await restartDesktopApp(); return; } await runtimeFetch('/api/config/reload', { method: 'POST' }); } finally { setTimeout(() => setIsRetrying(false), 1000); } }, [isDesktopApp, opencodeBinary]); const handleCopy = React.useCallback(async () => { const result = await copyTextToClipboard(INSTALL_COMMAND); if (result.ok) { setCopied(true); setTimeout(() => setCopied(false), 2000); } else { console.error('Failed to copy:', result.error); } }, []); const handleCheckAndContinue = React.useCallback(async () => { setIsChecking(true); setCheckError(null); try { const available = await checkCliAvailability(); if (available) { // CLI is available, proceed to main screen onCliAvailable?.(); } else { setCheckError(t('onboarding.localSetup.errors.cliNotReady')); } } catch (err) { setCheckError(err instanceof Error ? err.message : t('onboarding.localSetup.errors.detectionFailed')); } finally { setIsChecking(false); } }, [checkCliAvailability, onCliAvailable, t]); const docsUrl = DOCS_URL; const binaryPlaceholder = platform === 'windows' ? 'C:\\Users\\you\\AppData\\Roaming\\npm\\opencode.cmd' : platform === 'linux' ? '/home/you/.bun/bin/opencode' : '/Users/you/.bun/bin/opencode'; return (

{t('onboarding.localSetup.title')}

{t('onboarding.localSetup.description')}

{platform === 'windows' && (
{t('onboarding.localSetup.windows.title')}
  1. {t('onboarding.localSetup.windows.stepRunInstallInWsl')}
  2. {t('onboarding.localSetup.windows.stepSetBinaryPath')}
)}
{copied ? (
{t('onboarding.common.status.copiedToClipboard')}
) : ( )}
{platform === 'windows' ? t('onboarding.localSetup.docs.windows') : t('onboarding.localSetup.docs.default')} {checkError && (
{checkError}
)}

{t('onboarding.localSetup.helper.checkAndContinue')}

{t('onboarding.localSetup.field.alreadyInstalled')}
setOpencodeBinary(e.target.value)} placeholder={binaryPlaceholder} disabled={isRetrying} className="flex-1 font-mono text-xs" />
{t('onboarding.localSetup.helper.saveAndReload')}
{isFromRecovery && onSwitchToRemote && (

{t('onboarding.localSetup.remotePreference')}

)}
{showHint && (
{platform === 'windows' ? ( <>

{t('onboarding.localSetup.windows.hintDetectionFailed')}

) : ( <>

{t('onboarding.localSetup.hint.ensurePath')}

{t('onboarding.localSetup.hint.setEnv')}

{t('onboarding.localSetup.hint.missingRuntime')}

)}
)}
); }