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" import { openNativeFileDialog, supportsNativeDialogsInCurrentWindow } from "../lib/native/native-functions" import { useI18n } from "../lib/i18n" import { getLogger } from "../lib/logger" const log = getLogger("actions") 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 { t } = useI18n() const { opencodeBinaries, addOpenCodeBinary, removeOpenCodeBinary, serverSettings, updateLastUsedBinary, } = 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 = () => serverSettings().opencodeBinary 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((error) => log.error("Failed to validate binary", { path, 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: t("opencodeBinarySelector.validation.alreadyValidating") } } 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 }) } } async function handleBrowseBinary() { if (props.disabled) return setValidationError(null) if (supportsNativeDialogsInCurrentWindow()) { const selected = await openNativeFileDialog({ title: t("opencodeBinarySelector.dialog.title"), }) if (selected) { setCustomPath(selected) void handleValidateAndAdd(selected) } return } setIsBinaryBrowserOpen(true) } async function handleValidateAndAdd(path: string) { const validation = await validateBinary(path) if (validation.valid) { addOpenCodeBinary(path, validation.version) props.onBinaryChange(path) updateLastUsedBinary(path) setCustomPath("") setValidationError(null) } else { setValidationError(validation.error || t("opencodeBinarySelector.validation.invalidBinary")) } } 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) updateLastUsedBinary(path) } function handleRemoveBinary(path: string, event: Event) { event.stopPropagation() if (props.disabled) return removeOpenCodeBinary(path) if (props.selectedBinary === path) { props.onBinaryChange("opencode") updateLastUsedBinary("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 t("time.relative.daysAgoShort", { count: days }) if (hours > 0) return t("time.relative.hoursAgoShort", { count: hours }) if (minutes > 0) return t("time.relative.minutesAgoShort", { count: minutes }) return t("time.relative.justNow") } function getDisplayName(path: string): string { if (path === "opencode") return t("opencodeBinarySelector.display.systemPath", { name: "opencode" }) const parts = path.split(/[/\\]/) return parts[parts.length - 1] ?? path } const isPathValidating = (path: string) => validatingPaths().has(path) return ( <>

{t("opencodeBinarySelector.title")}

{t("opencodeBinarySelector.subtitle")}

{t("opencodeBinarySelector.status.checkingVersions")}
setCustomPath(e.currentTarget.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault() handleCustomPathSubmit() } }} disabled={props.disabled} placeholder={t("opencodeBinarySelector.customPath.placeholder")} 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