import React from 'react'; import { RiFileCopyLine, RiCheckLine, RiExternalLinkLine } from '@remixicon/react'; import { isDesktopShell, isTauriShell } from '@/lib/desktop'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { updateDesktopSettings } from '@/lib/persistence'; import { copyTextToClipboard } from '@/lib/clipboard'; import { restartDesktopApp } from '@/lib/desktop'; 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'; 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 }: { onCopy: () => void }) { return (
curl -fsSL https://opencode.ai/install | bash
); } const HINT_DELAY_MS = 30000; export function LocalSetupScreen({ onBack, onCliAvailable, isFromRecovery = false, onSwitchToRemote, }: LocalSetupScreenProps) { 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 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) => { if ((e.target as HTMLElement).closest('button, a, input, select, textarea, code')) { return; } if (e.button !== 0) return; if (isDesktopApp && isTauriShell()) { try { const { getCurrentWindow } = await import('@tauri-apps/api/window'); const window = getCurrentWindow(); await window.startDragging(); } catch (error) { console.error('Failed to start window dragging:', error); } } }, [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 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: 'Select opencode binary', multiple: false, directory: false, }); if (typeof selected === 'string' && selected.trim().length > 0) { setOpencodeBinary(selected.trim()); } } catch { // ignore } }, [isDesktopApp]); const handleApplyPath = React.useCallback(async () => { setIsRetrying(true); try { await updateDesktopSettings({ opencodeBinary: opencodeBinary.trim() }); // In desktop boot flow, always restart the entire Tauri app so Rust // can re-evaluate the boot outcome with the updated binary path. if (isTauriShell()) { await restartDesktopApp(); return; } await fetch('/api/config/reload', { method: 'POST' }); } finally { setTimeout(() => setIsRetrying(false), 1000); } }, [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('OpenCode CLI is not ready yet. Please confirm installation is complete and try again.'); } } catch (err) { setCheckError(err instanceof Error ? err.message : 'Detection failed'); } finally { setIsChecking(false); } }, [checkCliAvailability, onCliAvailable]); 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'; return (

Setting Up OpenCode

Install OpenCode CLI to continue.

{platform === 'windows' && (
Windows setup (WSL recommended)
  1. Install WSL (if needed) with wsl --install in PowerShell.
  2. Run the install command below inside your WSL terminal.
  3. If OpenChamber does not detect OpenCode automatically, set the binary path below.
)}
{copied ? (
Copied to clipboard
) : ( )}
{platform === 'windows' ? 'View Windows + WSL documentation' : 'View documentation'} {checkError && (
{checkError}
)}

Click to check if OpenCode CLI is available. If successful, you'll automatically enter the main screen.

Already installed? Set the OpenCode CLI path:
setOpencodeBinary(e.target.value)} placeholder={binaryPlaceholder} disabled={isRetrying} className="flex-1 font-mono text-xs" />
Saves to OpenChamber settings and reloads OpenCode configuration.
{isFromRecovery && onSwitchToRemote && (

Prefer to use a remote server?

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

On Windows, install and run OpenCode in WSL for best compatibility.

If detection fails, set a native path (opencode.cmd/opencode.exe), wsl.exe, or wsl:/usr/local/bin/opencode.

) : ( <>

Already installed? Make sure opencode is in your PATH

or set OPENCODE_BINARY environment variable.

If you see env: node: No such file or directory or env: bun: No such file or directory, install that runtime or ensure it is on PATH.

)}
)}
); }