import { Component, createSignal, Show, For, onMount, createEffect, onCleanup } from "solid-js" import { ChevronDown, ChevronUp, FolderOpen, Trash2, Check, AlertCircle, Loader2 } from "lucide-solid" import { opencodeBinaries, addOpenCodeBinary, removeOpenCodeBinary, preferences, updateLastUsedBinary, } from "../stores/preferences" interface OpenCodeBinarySelectorProps { selectedBinary: string onBinaryChange: (binary: string) => void disabled?: boolean } const OpenCodeBinarySelector: Component = (props) => { const [isOpen, setIsOpen] = createSignal(false) const [customPath, setCustomPath] = createSignal("") const [validating, setValidating] = createSignal(false) const [validationError, setValidationError] = createSignal(null) const [versionInfo, setVersionInfo] = createSignal>(new Map()) const [validatingPaths, setValidatingPaths] = createSignal>(new Set()) let buttonRef: HTMLButtonElement | undefined const binaries = () => opencodeBinaries() const lastUsedBinary = () => preferences().lastUsedBinary // Set initial selected binary createEffect(() => { console.log( `[BinarySelector] Component effect - selectedBinary: ${props.selectedBinary}, lastUsed: ${lastUsedBinary()}, binaries count: ${binaries().length}`, ) if (!props.selectedBinary && lastUsedBinary()) { props.onBinaryChange(lastUsedBinary()!) } else if (!props.selectedBinary && binaries().length > 0) { props.onBinaryChange(binaries()[0].path) } }) // Validate all binaries when selector opens (only once) createEffect(() => { if (isOpen()) { const pathsToValidate = ["opencode", ...binaries().map((b) => b.path)] // Use setTimeout to break the reactive cycle and validate once setTimeout(() => { pathsToValidate.forEach((path) => { validateBinary(path).catch(console.error) }) }, 0) } }) // Click outside handler onMount(() => { const handleClickOutside = (event: MouseEvent) => { if (buttonRef && !buttonRef.contains(event.target as Node)) { const dropdown = document.querySelector("[data-binary-dropdown]") if (dropdown && !dropdown.contains(event.target as Node)) { setIsOpen(false) } } } document.addEventListener("click", handleClickOutside) onCleanup(() => { document.removeEventListener("click", handleClickOutside) // Clean up validating state on unmount setValidatingPaths(new Set()) setValidating(false) }) }) async function validateBinary(path: string): Promise<{ valid: boolean; version?: string; error?: string }> { // Prevent duplicate validation calls if (validatingPaths().has(path)) { console.log(`[BinarySelector] Already validating ${path}, skipping...`) return { valid: false, error: "Already validating" } } try { // Add to validating set setValidatingPaths((prev) => new Set(prev).add(path)) setValidating(true) setValidationError(null) console.log(`[BinarySelector] Starting validation for: ${path}`) const result = await window.electronAPI.validateOpenCodeBinary(path) console.log(`[BinarySelector] Validation result:`, result) if (result.valid && result.version) { const updatedVersionInfo = new Map(versionInfo()) updatedVersionInfo.set(path, result.version) setVersionInfo(updatedVersionInfo) console.log(`[BinarySelector] Updated version info for ${path}: ${result.version}`) } else { console.log(`[BinarySelector] No valid version returned for ${path}`) } return result } catch (error) { console.error(`[BinarySelector] Validation error for ${path}:`, error) return { valid: false, error: error instanceof Error ? error.message : String(error) } } finally { // Remove from validating set setValidatingPaths((prev) => { const newSet = new Set(prev) newSet.delete(path) return newSet }) // Only set validating to false if no other paths are being validated if (validatingPaths().size <= 1) { setValidating(false) } } } async function handleBrowseBinary() { try { const path = await window.electronAPI.selectOpenCodeBinary() if (path) { setCustomPath(path) const validation = await validateBinary(path) if (validation.valid) { addOpenCodeBinary(path, validation.version) props.onBinaryChange(path) updateLastUsedBinary(path) setCustomPath("") } else { setValidationError(validation.error || "Invalid OpenCode binary") } } } catch (error) { setValidationError(error instanceof Error ? error.message : "Failed to select binary") } } async function handleCustomPathSubmit() { const path = customPath().trim() if (!path) return const validation = await validateBinary(path) if (validation.valid) { addOpenCodeBinary(path, validation.version) props.onBinaryChange(path) updateLastUsedBinary(path) setCustomPath("") setValidationError(null) } else { setValidationError(validation.error || "Invalid OpenCode binary") } } function handleSelectBinary(path: string) { props.onBinaryChange(path) updateLastUsedBinary(path) setIsOpen(false) } function handleRemoveBinary(path: string, e: Event) { e.stopPropagation() removeOpenCodeBinary(path) if (props.selectedBinary === path) { const remaining = binaries().filter((b) => b.path !== path) if (remaining.length > 0) { handleSelectBinary(remaining[0].path) } else { props.onBinaryChange("opencode") // Default to system PATH } } } function formatRelativeTime(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) if (days > 0) return `${days}d ago` if (hours > 0) return `${hours}h ago` if (minutes > 0) return `${minutes}m ago` return "just now" } function getDisplayName(path: string): string { if (path === "opencode") return "opencode (system PATH)" // Extract just the binary name from path const parts = path.split(/[/\\]/) const name = parts[parts.length - 1] // If it's the same as default, show full path if (name === "opencode") { return path } return name } function handleButtonClick() { setIsOpen(!isOpen()) } return (
{/* Main selector button */} {/* Dropdown */}
OpenCode Binary Selection
{/* Custom path input */}
setCustomPath(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleCustomPathSubmit() } else if (e.key === "Escape") { setCustomPath("") setValidationError(null) } }} placeholder="Enter path to opencode binary..." class="flex-1 px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500" />
{/* Browse button */}
{/* Validation error */}
{validationError()}
{/* Recent binaries list */}
0} fallback={
No recent binaries. Add one above or use system PATH.
} > {(binary) => { const isSelected = () => props.selectedBinary === binary.path const version = () => { const ver = versionInfo().get(binary.path) console.log(`[BinarySelector] Rendering version for ${binary.path}: ${ver || "undefined"}`) return ver } return (
handleSelectBinary(binary.path)} >
} >
{getDisplayName(binary.path)}
v{version()} {formatRelativeTime(binary.lastUsed)}
) }}
{/* Default option */}
handleSelectBinary("opencode")} >
} >
opencode (system PATH)
v{versionInfo().get("opencode")} Checking... Use binary from system PATH
{/* Click outside to close */}
setIsOpen(false)} />
) } export default OpenCodeBinarySelector