import { Component, For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { FolderOpen, Trash2, Check, AlertCircle, Loader2, Plus } from "lucide-solid" import { useConfig } from "../stores/preferences" import { serverApi } from "../lib/api-client" import FileSystemBrowserDialog from "./filesystem-browser-dialog" interface BinaryOption { path: string version?: string lastUsed?: number isDefault?: boolean } interface OpenCodeBinarySelectorProps { selectedBinary: string onBinaryChange: (binary: string) => void disabled?: boolean isVisible?: boolean } const OpenCodeBinarySelector: Component = (props) => { const { opencodeBinaries, addOpenCodeBinary, removeOpenCodeBinary, preferences, updatePreferences, } = useConfig() 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()) const [isBinaryBrowserOpen, setIsBinaryBrowserOpen] = createSignal(false) const binaries = () => opencodeBinaries() const lastUsedBinary = () => preferences().lastUsedBinary const customBinaries = createMemo(() => binaries().filter((binary) => binary.path !== "opencode")) const binaryOptions = createMemo(() => [{ path: "opencode", isDefault: true }, ...customBinaries()]) const currentSelectionPath = () => props.selectedBinary || "opencode" createEffect(() => { if (!props.selectedBinary && lastUsedBinary()) { props.onBinaryChange(lastUsedBinary()!) } else if (!props.selectedBinary) { const firstBinary = binaries()[0] if (firstBinary) { props.onBinaryChange(firstBinary.path) } } }) createEffect(() => { const cache = new Map(versionInfo()) let updated = false binaries().forEach((binary) => { if (binary.version && !cache.has(binary.path)) { cache.set(binary.path, binary.version) updated = true } }) if (updated) { setVersionInfo(cache) } }) createEffect(() => { if (!props.isVisible) return const cache = versionInfo() const pathsToValidate = ["opencode", ...customBinaries().map((binary) => binary.path)].filter( (path) => !cache.has(path), ) if (pathsToValidate.length === 0) return setTimeout(() => { pathsToValidate.forEach((path) => { validateBinary(path).catch(console.error) }) }, 0) }) onCleanup(() => { setValidatingPaths(new Set()) setValidating(false) }) async function validateBinary(path: string): Promise<{ valid: boolean; version?: string; error?: string }> { if (versionInfo().has(path)) { const cachedVersion = versionInfo().get(path) return cachedVersion ? { valid: true, version: cachedVersion } : { valid: true } } if (validatingPaths().has(path)) { return { valid: false, error: "Already validating" } } try { setValidatingPaths((prev) => new Set(prev).add(path)) setValidating(true) setValidationError(null) const result = await serverApi.validateBinary(path) if (result.valid && result.version) { const updatedVersionInfo = new Map(versionInfo()) updatedVersionInfo.set(path, result.version) setVersionInfo(updatedVersionInfo) } return result } catch (error) { return { valid: false, error: error instanceof Error ? error.message : String(error) } } finally { setValidatingPaths((prev) => { const next = new Set(prev) next.delete(path) if (next.size === 0) { setValidating(false) } return next }) } } function handleBrowseBinary() { if (props.disabled) return setValidationError(null) setIsBinaryBrowserOpen(true) } async function handleValidateAndAdd(path: string) { const validation = await validateBinary(path) if (validation.valid) { addOpenCodeBinary(path, validation.version) props.onBinaryChange(path) updatePreferences({ lastUsedBinary: path }) setCustomPath("") setValidationError(null) } else { setValidationError(validation.error || "Invalid OpenCode binary") } } function handleBinaryBrowserSelect(path: string) { setIsBinaryBrowserOpen(false) setCustomPath(path) void handleValidateAndAdd(path) } async function handleCustomPathSubmit() { const path = customPath().trim() if (!path) return await handleValidateAndAdd(path) } function handleSelectBinary(path: string) { if (props.disabled) return if (path === props.selectedBinary) return props.onBinaryChange(path) updatePreferences({ lastUsedBinary: path }) } function handleRemoveBinary(path: string, event: Event) { event.stopPropagation() if (props.disabled) return removeOpenCodeBinary(path) if (props.selectedBinary === path) { props.onBinaryChange("opencode") updatePreferences({ lastUsedBinary: "opencode" }) } } function formatRelativeTime(timestamp?: number): string { if (!timestamp) return "" 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)" const parts = path.split(/[/\\]/) return parts[parts.length - 1] ?? path } const isPathValidating = (path: string) => validatingPaths().has(path) return ( <>

OpenCode Binary

Choose which executable OpenCode should run

Checking versions…
setCustomPath(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() handleCustomPathSubmit() } }} disabled={props.disabled} placeholder="Enter path to opencode binary…" class="selector-input" />
{validationError()}
{(binary) => { const isDefault = binary.isDefault const versionLabel = () => versionInfo().get(binary.path) ?? binary.version return (
) }}
setIsBinaryBrowserOpen(false)} onSelect={handleBinaryBrowserSelect} /> ) } export default OpenCodeBinarySelector