Add OpenCode binary selection with version detection

- Add binary selector component with dropdown and validation
- Support custom OpenCode binary paths and system PATH binary
- Async version checking for all binaries when selector opens
- Store recent binaries with timestamps and version info
- Enhanced instance creation to use selected binary
- Improved storage system for binary preferences
- Fixed validation to handle system PATH binaries properly
This commit is contained in:
Shantur Rathore
2025-10-26 10:26:32 +00:00
parent f4a664bfe7
commit f63a4b3754
11 changed files with 684 additions and 55 deletions

View File

@@ -183,7 +183,7 @@ const App: Component = () => {
return activeSessionId().get(instance.id) || null
})
async function handleSelectFolder(folderPath?: string) {
async function handleSelectFolder(folderPath?: string, binaryPath?: string) {
setIsSelectingFolder(true)
try {
let folder: string | null | undefined = folderPath
@@ -196,7 +196,7 @@ const App: Component = () => {
}
addRecentFolder(folder)
const instanceId = await createInstance(folder)
const instanceId = await createInstance(folder, binaryPath)
setHasInstances(true)
setShowFolderSelection(false)

View File

@@ -1,15 +1,18 @@
import { Component, createSignal, Show, For, onMount, onCleanup } from "solid-js"
import { Folder, Clock, Trash2, FolderPlus } from "lucide-solid"
import { recentFolders, removeRecentFolder } from "../stores/preferences"
import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronDown, ChevronUp } from "lucide-solid"
import { recentFolders, removeRecentFolder, preferences } from "../stores/preferences"
import OpenCodeBinarySelector from "./opencode-binary-selector"
interface FolderSelectionViewProps {
onSelectFolder: (folder?: string) => void
onSelectFolder: (folder?: string, binaryPath?: string) => void
isLoading?: boolean
}
const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
const [selectedIndex, setSelectedIndex] = createSignal(0)
const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent")
const [showAdvanced, setShowAdvanced] = createSignal(false)
const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode")
const folders = () => recentFolders()
@@ -111,11 +114,11 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
}
function handleFolderSelect(path: string) {
props.onSelectFolder(path)
props.onSelectFolder(path, selectedBinary())
}
function handleBrowse() {
props.onSelectFolder()
props.onSelectFolder(undefined, selectedBinary())
}
function handleRemove(path: string, e?: Event) {
@@ -146,7 +149,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
<p class="text-base text-gray-600 dark:text-gray-400">Select a folder to start coding with AI</p>
</div>
<div class="space-y-4">
<div class="space-y-4 overflow-visible">
<Show
when={folders().length > 0}
fallback={
@@ -159,7 +162,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
</div>
}
>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Recent Folders</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
@@ -221,11 +224,12 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
</div>
</Show>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<h2 class="text-base font-semibold text-gray-900 dark:text-gray-100">Browse for Folder</h2>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Select any folder on your computer</p>
</div>
<div class="p-4">
<button
onClick={handleBrowse}
@@ -242,6 +246,35 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
</kbd>
</button>
</div>
{/* Advanced settings section */}
<div class="border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => setShowAdvanced(!showAdvanced())}
class="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div class="flex items-center gap-2">
<Settings class="w-4 h-4 text-gray-500 dark:text-gray-400" />
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Advanced Settings</span>
</div>
{showAdvanced() ? (
<ChevronUp class="w-4 h-4 text-gray-400" />
) : (
<ChevronDown class="w-4 h-4 text-gray-400" />
)}
</button>
<Show when={showAdvanced()}>
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/50 overflow-visible">
<div class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">OpenCode Binary</div>
<OpenCodeBinarySelector
selectedBinary={selectedBinary()}
onBinaryChange={setSelectedBinary}
disabled={props.isLoading}
/>
</div>
</Show>
</div>
</div>
</div>

View File

@@ -0,0 +1,403 @@
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<OpenCodeBinarySelectorProps> = (props) => {
const [isOpen, setIsOpen] = createSignal(false)
const [customPath, setCustomPath] = createSignal("")
const [validating, setValidating] = createSignal(false)
const [validationError, setValidationError] = createSignal<string | null>(null)
const [versionInfo, setVersionInfo] = createSignal<Map<string, string>>(new Map())
const [validatingPaths, setValidatingPaths] = createSignal<Set<string>>(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 (
<div class="relative" style={{ position: "relative" }}>
{/* Main selector button */}
<button
type="button"
onClick={handleButtonClick}
disabled={props.disabled}
class="w-full px-3 py-2 text-left bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-sm hover:border-gray-400 dark:hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between"
ref={buttonRef}
>
<div class="flex items-center gap-2 flex-1 min-w-0">
<Show when={validating()} fallback={<FolderOpen class="w-4 h-4 text-gray-400 dark:text-gray-500" />}>
<Loader2 class="w-4 h-4 text-blue-500 animate-spin" />
</Show>
<span class="text-sm text-gray-900 dark:text-gray-100 truncate">
{getDisplayName(props.selectedBinary || "opencode")}
</span>
<Show when={versionInfo().get(props.selectedBinary)}>
<span class="text-xs text-gray-500 dark:text-gray-400">v{versionInfo().get(props.selectedBinary)}</span>
</Show>
</div>
<ChevronDown
class={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${isOpen() ? "rotate-180" : ""}`}
/>
</button>
{/* Dropdown */}
<Show when={isOpen()}>
<div
data-binary-dropdown
class="absolute top-full left-0 right-0 z-50 mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-96 overflow-y-auto"
style={{
position: "absolute",
"z-index": 500,
"max-height": "24rem",
}}
>
<div class="p-3 border-b border-gray-200 dark:border-gray-700">
<div class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">OpenCode Binary Selection</div>
{/* Custom path input */}
<div class="space-y-2">
<div class="flex gap-2">
<input
type="text"
value={customPath()}
onInput={(e) => 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"
/>
<button
onClick={handleCustomPathSubmit}
disabled={!customPath().trim() || validating()}
class="px-3 py-1.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
>
Add
</button>
</div>
{/* Browse button */}
<button
onClick={handleBrowseBinary}
disabled={validating()}
class="w-full px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
<FolderOpen class="w-4 h-4" />
Browse for Binary...
</button>
</div>
{/* Validation error */}
<Show when={validationError()}>
<div class="mt-2 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
<div class="flex items-start gap-2">
<AlertCircle class="w-4 h-4 text-red-500 mt-0.5 flex-shrink-0" />
<span class="text-xs text-red-700 dark:text-red-400">{validationError()}</span>
</div>
</div>
</Show>
</div>
{/* Recent binaries list */}
<div class="max-h-60 overflow-y-auto">
<Show
when={binaries().length > 0}
fallback={
<div class="p-4 text-center text-sm text-gray-500 dark:text-gray-400">
No recent binaries. Add one above or use system PATH.
</div>
}
>
<For each={binaries()}>
{(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 (
<div
class={`px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700 last:border-b-0 ${
isSelected() ? "bg-blue-50 dark:bg-blue-900/20" : ""
}`}
onClick={() => handleSelectBinary(binary.path)}
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 flex-1 min-w-0">
<Show
when={isSelected()}
fallback={<FolderOpen class="w-4 h-4 text-gray-400 dark:text-gray-500" />}
>
<Check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
</Show>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{getDisplayName(binary.path)}
</div>
<div class="flex items-center gap-2 mt-0.5">
<Show when={version()}>
<span class="text-xs text-gray-500 dark:text-gray-400">v{version()}</span>
</Show>
<span class="text-xs text-gray-400 dark:text-gray-500">
{formatRelativeTime(binary.lastUsed)}
</span>
</div>
</div>
</div>
<button
onClick={(e) => handleRemoveBinary(binary.path, e)}
class="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-all"
title="Remove binary"
>
<Trash2 class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400" />
</button>
</div>
</div>
)
}}
</For>
</Show>
</div>
{/* Default option */}
<div class="p-2 border-t border-gray-200 dark:border-gray-700">
<div
class={`px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer rounded ${
props.selectedBinary === "opencode" ? "bg-blue-50 dark:bg-blue-900/20" : ""
}`}
onClick={() => handleSelectBinary("opencode")}
>
<div class="flex items-center gap-2">
<Show
when={props.selectedBinary === "opencode"}
fallback={<FolderOpen class="w-4 h-4 text-gray-400 dark:text-gray-500" />}
>
<Check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
</Show>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">opencode (system PATH)</div>
<div class="flex items-center gap-2 mt-0.5">
<Show when={versionInfo().get("opencode")}>
<span class="text-xs text-gray-500 dark:text-gray-400">v{versionInfo().get("opencode")}</span>
</Show>
<Show when={!versionInfo().get("opencode") && validating()}>
<span class="text-xs text-gray-400 dark:text-gray-500">Checking...</span>
</Show>
<span class="text-xs text-gray-400 dark:text-gray-500">Use binary from system PATH</span>
</div>
</div>
</div>
</div>
</div>
</div>
</Show>
{/* Click outside to close */}
<Show when={isOpen()}>
<div class="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
</Show>
</div>
)
}
export default OpenCodeBinarySelector

View File

@@ -1,8 +1,9 @@
import type { Preferences, RecentFolder } from "../stores/preferences"
import type { Preferences, RecentFolder, OpenCodeBinary } from "../stores/preferences"
export interface ConfigData {
preferences: Preferences
recentFolders: RecentFolder[]
opencodeBinaries: OpenCodeBinary[]
}
export interface InstanceData {
@@ -10,22 +11,38 @@ export interface InstanceData {
}
export class FileStorage {
private configPath: string
private instancesDir: string
private configPath: string | undefined
private instancesDir: string | undefined
private configChangeListeners: Set<() => void> = new Set()
private initialized = false
constructor() {
this.configPath = window.electronAPI.getConfigPath()
this.instancesDir = window.electronAPI.getInstancesDir()
this.initialize()
}
private async initialize() {
if (this.initialized) return
this.configPath = await window.electronAPI.getConfigPath()
this.instancesDir = await window.electronAPI.getInstancesDir()
// Listen for config changes from other instances
window.electronAPI.onConfigChanged(() => {
this.configChangeListeners.forEach((listener) => listener())
})
this.initialized = true
}
private async ensureInitialized() {
if (!this.initialized) {
await this.initialize()
}
}
// Config operations
async loadConfig(): Promise<ConfigData> {
await this.ensureInitialized()
try {
const content = await window.electronAPI.readConfigFile()
return JSON.parse(content)
@@ -36,11 +53,13 @@ export class FileStorage {
showThinkingBlocks: false,
},
recentFolders: [],
opencodeBinaries: [],
}
}
}
async saveConfig(config: ConfigData): Promise<void> {
await this.ensureInitialized()
try {
await window.electronAPI.writeConfigFile(JSON.stringify(config, null, 2))
} catch (error) {
@@ -51,6 +70,7 @@ export class FileStorage {
// Instance operations
async loadInstanceData(instanceId: string): Promise<InstanceData> {
await this.ensureInitialized()
try {
const filename = this.instanceIdToFilename(instanceId)
const content = await window.electronAPI.readInstanceFile(filename)
@@ -64,6 +84,7 @@ export class FileStorage {
}
async saveInstanceData(instanceId: string, data: InstanceData): Promise<void> {
await this.ensureInitialized()
try {
const filename = this.instanceIdToFilename(instanceId)
await window.electronAPI.writeInstanceFile(filename, JSON.stringify(data, null, 2))
@@ -74,6 +95,7 @@ export class FileStorage {
}
async deleteInstanceData(instanceId: string): Promise<void> {
await this.ensureInitialized()
try {
const filename = this.instanceIdToFilename(instanceId)
await window.electronAPI.deleteInstanceFile(filename)

View File

@@ -40,7 +40,7 @@ function removeInstance(id: string) {
}
}
async function createInstance(folder: string): Promise<string> {
async function createInstance(folder: string, binaryPath?: string): Promise<string> {
const id = `instance-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
const instance: Instance = {
@@ -56,7 +56,12 @@ async function createInstance(folder: string): Promise<string> {
addInstance(instance)
try {
const { id: returnedId, port, pid, binaryPath } = await window.electronAPI.createInstance(id, folder)
const {
id: returnedId,
port,
pid,
binaryPath: actualBinaryPath,
} = await window.electronAPI.createInstance(id, folder, binaryPath)
const client = sdkManager.createClient(port)
@@ -65,7 +70,7 @@ async function createInstance(folder: string): Promise<string> {
pid,
client,
status: "ready",
binaryPath,
binaryPath: actualBinaryPath,
})
setActiveInstanceId(id)

View File

@@ -3,6 +3,13 @@ import { storage, type ConfigData } from "../lib/storage"
export interface Preferences {
showThinkingBlocks: boolean
lastUsedBinary?: string
}
export interface OpenCodeBinary {
path: string
version?: string
lastUsed: number
}
export interface RecentFolder {
@@ -18,12 +25,14 @@ const defaultPreferences: Preferences = {
const [preferences, setPreferences] = createSignal<Preferences>(defaultPreferences)
const [recentFolders, setRecentFolders] = createSignal<RecentFolder[]>([])
const [opencodeBinaries, setOpenCodeBinaries] = createSignal<OpenCodeBinary[]>([])
async function loadConfig(): Promise<void> {
try {
const config = await storage.loadConfig()
setPreferences({ ...defaultPreferences, ...config.preferences })
setRecentFolders(config.recentFolders)
setOpenCodeBinaries(config.opencodeBinaries || [])
} catch (error) {
console.error("Failed to load config:", error)
}
@@ -34,6 +43,7 @@ async function saveConfig(): Promise<void> {
const config: ConfigData = {
preferences: preferences(),
recentFolders: recentFolders(),
opencodeBinaries: opencodeBinaries(),
}
await storage.saveConfig(config)
} catch (error) {
@@ -66,6 +76,35 @@ function removeRecentFolder(path: string): void {
saveConfig().catch(console.error)
}
function addOpenCodeBinary(path: string, version?: string): void {
const binaries = opencodeBinaries().filter((b) => b.path !== path)
binaries.unshift({ path, version, lastUsed: Date.now() })
const trimmed = binaries.slice(0, 10) // Keep max 10 binaries
setOpenCodeBinaries(trimmed)
saveConfig().catch(console.error)
}
function removeOpenCodeBinary(path: string): void {
const binaries = opencodeBinaries().filter((b) => b.path !== path)
setOpenCodeBinaries(binaries)
saveConfig().catch(console.error)
}
function updateLastUsedBinary(path: string): void {
updatePreferences({ lastUsedBinary: path })
const binaries = opencodeBinaries()
const binary = binaries.find((b) => b.path === path)
if (binary) {
binary.lastUsed = Date.now()
// Move to front
const sorted = [binary, ...binaries.filter((b) => b.path !== path)]
setOpenCodeBinaries(sorted)
saveConfig().catch(console.error)
}
}
// Load config on mount and listen for changes from other instances
onMount(() => {
loadConfig()
@@ -79,4 +118,15 @@ onMount(() => {
return unsubscribe
})
export { preferences, updatePreferences, toggleShowThinkingBlocks, recentFolders, addRecentFolder, removeRecentFolder }
export {
preferences,
updatePreferences,
toggleShowThinkingBlocks,
recentFolders,
addRecentFolder,
removeRecentFolder,
opencodeBinaries,
addOpenCodeBinary,
removeOpenCodeBinary,
updateLastUsedBinary,
}