import React from 'react'; import { isDesktopShell, isTauriShell, 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 { cn } from '@/lib/utils'; import { RemoteConnectionForm } from './RemoteConnectionForm'; import { desktopHostsGet, desktopHostsSet } from '@/lib/desktopHosts'; import { useI18n } from '@/lib/i18n'; const INSTALL_COMMAND = 'curl -fsSL https://opencode.ai/install | bash'; const DOCS_URL = 'https://opencode.ai/docs'; const WINDOWS_WSL_DOCS_URL = 'https://opencode.ai/docs/windows-wsl'; const POLL_INTERVAL_MS = 2500; type OnboardingPlatform = 'macos' | 'linux' | 'windows' | 'unknown'; type ChooserScreenProps = { /** Callback when CLI becomes available */ onCliAvailable?: () => void; }; function BashCommand({ onCopy, copyTitle }: { onCopy: () => void; copyTitle: string }) { return (
curl -fsSL https://opencode.ai/install | bash
); } export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) { const { t } = useI18n(); const [copied, setCopied] = React.useState(false); const [isDesktopApp, setIsDesktopApp] = React.useState(false); const [isApplyingPath, setIsApplyingPath] = React.useState(false); const [isManualChecking, setIsManualChecking] = React.useState(false); const [opencodeBinary, setOpencodeBinary] = React.useState(''); const [platform, setPlatform] = React.useState('unknown'); const [activeTab, setActiveTab] = React.useState<'local' | 'remote'>('local'); const [advancedOpen, setAdvancedOpen] = React.useState(false); const [troubleOpen, setTroubleOpen] = React.useState(false); React.useEffect(() => { setIsDesktopApp(isDesktopShell()); }, []); React.useEffect(() => { if (typeof navigator === 'undefined') { setPlatform('unknown'); return; } const ua = navigator.userAgent || ''; if (/Windows/i.test(ua)) setPlatform('windows'); else if (/Macintosh|Mac OS X/i.test(ua)) setPlatform('macos'); else if (/Linux/i.test(ua)) setPlatform('linux'); else setPlatform('unknown'); }, []); React.useEffect(() => { let cancelled = false; void (async () => { try { const response = await fetch('/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) => { const target = e.target as HTMLElement; if (target.closest('.app-region-no-drag')) return; if (target.closest('button, a, input, select, textarea, code, summary, details')) return; if (e.button !== 0) return; if (isDesktopApp) { await startDesktopWindowDrag(); } }, [isDesktopApp]); const checkCliAvailability = React.useCallback(async (): Promise => { try { const response = await fetch('/health'); if (!response.ok) return false; const data = await response.json(); return data.openCodeRunning === true || data.isOpenCodeReady === true; } catch { return false; } }, []); const persistFirstChoice = React.useCallback(async (choice: 'local' | 'remote') => { if (!isTauriShell()) return; const config = await desktopHostsGet(); await desktopHostsSet({ ...config, ...(choice === 'local' ? { defaultHostId: 'local' } : {}), initialHostChoiceCompleted: true, }); }, []); const announceAvailable = React.useCallback(async () => { if (isTauriShell()) { await persistFirstChoice('local'); } onCliAvailable?.(); }, [onCliAvailable, persistFirstChoice]); // Background polling: while the local tab is visible, periodically check // whether the OpenCode CLI is reachable. As soon as it is, transition // automatically — the user doesn't have to click anything. React.useEffect(() => { if (activeTab !== 'local') return; let cancelled = false; let timer: ReturnType | null = null; const tick = async () => { if (cancelled) return; try { const available = await checkCliAvailability(); if (cancelled) return; if (available) { await announceAvailable(); return; } } catch { // ignore } if (!cancelled) { timer = setTimeout(tick, POLL_INTERVAL_MS); } }; timer = setTimeout(tick, POLL_INTERVAL_MS); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [activeTab, checkCliAvailability, announceAvailable]); const handleManualCheck = React.useCallback(async () => { setIsManualChecking(true); try { const available = await checkCliAvailability(); if (available) await announceAvailable(); } finally { setIsManualChecking(false); } }, [checkCliAvailability, announceAvailable]); const handleBrowse = React.useCallback(async () => { if (typeof window === 'undefined') return; if (!isDesktopApp || !isTauriShell()) return; const tauri = (window as unknown as { __TAURI__?: { dialog?: { open?: (opts: Record) => Promise } } }).__TAURI__; if (!tauri?.dialog?.open) return; try { const selected = await tauri.dialog.open({ title: t('onboarding.localSetup.dialog.selectOpencodeBinary'), multiple: false, directory: false, }); if (typeof selected === 'string' && selected.trim().length > 0) { setOpencodeBinary(selected.trim()); } } catch { // ignore } }, [isDesktopApp, t]); const handleApplyPath = React.useCallback(async () => { setIsApplyingPath(true); try { await updateDesktopSettings({ opencodeBinary: opencodeBinary.trim() }); if (isTauriShell()) { await persistFirstChoice('local'); await restartDesktopApp(); return; } await fetch('/api/config/reload', { method: 'POST' }); } finally { setTimeout(() => setIsApplyingPath(false), 1000); } }, [opencodeBinary, persistFirstChoice]); 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 docsUrl = platform === 'windows' ? WINDOWS_WSL_DOCS_URL : 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'; const showLocal = !isDesktopApp || !isTauriShell() || activeTab === 'local'; return (

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

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

{isDesktopApp && isTauriShell() && (
)} {isDesktopApp && isTauriShell() && activeTab === 'remote' ? (
setActiveTab('local')} showBackButton={false} onSwitchToLocal={() => setActiveTab('local')} />
) : null} {showLocal && (
{platform === 'windows' && (
{t('onboarding.localSetup.windows.title')}
  1. {t('onboarding.localSetup.windows.stepInstallWsl')} wsl --install {t('onboarding.localSetup.windows.stepInstallWslSuffix')}
  2. {t('onboarding.localSetup.windows.stepRunInstallInWsl')}
  3. {t('onboarding.localSetup.windows.stepSetBinaryPath')}
)}

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

{copied ? (
{t('onboarding.common.status.copiedToClipboard')}
) : ( )}
{platform === 'windows' ? t('onboarding.localSetup.docs.windows') : t('onboarding.localSetup.docs.default')}
{t('onboarding.localSetup.status.watching')}
{t('onboarding.localSetup.status.autoContinue')}
setAdvancedOpen((e.currentTarget as HTMLDetailsElement).open)} > {t('onboarding.localSetup.advanced.title')}
setOpencodeBinary(e.target.value)} placeholder={binaryPlaceholder} disabled={isApplyingPath} className="flex-1 font-mono text-xs" />

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

setTroubleOpen((e.currentTarget as HTMLDetailsElement).open)} > {t('onboarding.localSetup.troubleshoot.title')}
    {platform === 'windows' ? ( <>
  • {t('onboarding.localSetup.windows.hintInstallInWsl')}
  • {t('onboarding.localSetup.windows.hintDetectionFailed')}
  • ) : ( <>
  • {t('onboarding.localSetup.hint.ensurePath')}
  • {t('onboarding.localSetup.hint.setEnv')}
  • {t('onboarding.localSetup.hint.missingRuntime')}
  • )}
)}
); }