Add environment variables configuration to OpenCode instances

- Add environment variables editor in advanced settings
- Store environment variables in global preferences
- Pass environment variables to OpenCode processes when spawning
- Display environment variables in instance information and server logs
- Fix system PATH binary handling for 'opencode' command
- Show detailed environment variable values in startup logs
- Save and restore last used binary across app restarts
- Add system PATH binary to recent binaries list when used

Features:
- Environment variables editor with add/remove functionality
- Persistent storage of environment variables across sessions
- Real-time display of active environment variables in logs
- Instance information shows configured environment variables
- Proper handling of system PATH vs custom binary paths
- Last used binary persistence and automatic restoration
This commit is contained in:
Shantur Rathore
2025-10-26 10:48:47 +00:00
parent f63a4b3754
commit 0b26ffd97d
8 changed files with 245 additions and 49 deletions

View File

@@ -0,0 +1,147 @@
import { Component, createSignal, For, Show } from "solid-js"
import { Plus, Trash2, Key, Globe } from "lucide-solid"
import {
preferences,
addEnvironmentVariable,
removeEnvironmentVariable,
updateEnvironmentVariables,
} from "../stores/preferences"
interface EnvironmentVariablesEditorProps {
disabled?: boolean
}
const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (props) => {
const [envVars, setEnvVars] = createSignal<Record<string, string>>(preferences().environmentVariables || {})
const [newKey, setNewKey] = createSignal("")
const [newValue, setNewValue] = createSignal("")
const entries = () => Object.entries(envVars())
function handleAddVariable() {
const key = newKey().trim()
const value = newValue().trim()
if (!key) return
addEnvironmentVariable(key, value)
setEnvVars({ ...envVars(), [key]: value })
setNewKey("")
setNewValue("")
}
function handleRemoveVariable(key: string) {
removeEnvironmentVariable(key)
const { [key]: removed, ...rest } = envVars()
setEnvVars(rest)
}
function handleUpdateVariable(key: string, value: string) {
const updated = { ...envVars(), [key]: value }
setEnvVars(updated)
updateEnvironmentVariables(updated)
}
function handleKeyPress(e: KeyboardEvent) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleAddVariable()
}
}
return (
<div class="space-y-3">
<div class="flex items-center gap-2 mb-3">
<Globe 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">Environment Variables</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
({entries().length} variable{entries().length !== 1 ? "s" : ""})
</span>
</div>
{/* Existing variables */}
<Show when={entries().length > 0}>
<div class="space-y-2">
<For each={entries()}>
{([key, value]) => (
<div class="flex items-center gap-2">
<div class="flex-1 flex items-center gap-2">
<Key class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" />
<input
type="text"
value={key}
disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded text-gray-500 dark:text-gray-400 cursor-not-allowed"
placeholder="Variable name"
title="Variable name (read-only)"
/>
<input
type="text"
value={value}
disabled={props.disabled}
onInput={(e) => handleUpdateVariable(key, e.currentTarget.value)}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable value"
/>
</div>
<button
onClick={() => handleRemoveVariable(key)}
disabled={props.disabled}
class="p-1.5 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Remove variable"
>
<Trash2 class="w-3.5 h-3.5" />
</button>
</div>
)}
</For>
</div>
</Show>
{/* Add new variable */}
<div class="flex items-center gap-2 pt-2 border-t border-gray-200 dark:border-gray-600">
<div class="flex-1 flex items-center gap-2">
<Key class="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 flex-shrink-0" />
<input
type="text"
value={newKey()}
onInput={(e) => setNewKey(e.currentTarget.value)}
onKeyPress={handleKeyPress}
disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable name"
/>
<input
type="text"
value={newValue()}
onInput={(e) => setNewValue(e.currentTarget.value)}
onKeyPress={handleKeyPress}
disabled={props.disabled}
class="flex-1 px-2.5 py-1.5 text-sm bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Variable value"
/>
</div>
<button
onClick={handleAddVariable}
disabled={props.disabled || !newKey().trim()}
class="p-1.5 text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Add variable"
>
<Plus class="w-3.5 h-3.5" />
</button>
</div>
<Show when={entries().length === 0}>
<div class="text-xs text-gray-500 dark:text-gray-400 text-center py-2">
No environment variables configured. Add variables above to customize the OpenCode environment.
</div>
</Show>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
These variables will be available in the OpenCode environment when starting instances.
</div>
</div>
)
}
export default EnvironmentVariablesEditor

View File

@@ -2,6 +2,7 @@ import { Component, createSignal, Show, For, onMount, onCleanup } from "solid-js
import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronDown, ChevronUp } from "lucide-solid"
import { recentFolders, removeRecentFolder, preferences } from "../stores/preferences"
import OpenCodeBinarySelector from "./opencode-binary-selector"
import EnvironmentVariablesEditor from "./environment-variables-editor"
interface FolderSelectionViewProps {
onSelectFolder: (folder?: string, binaryPath?: string) => void
@@ -265,13 +266,19 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
</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 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 space-y-4">
<div>
<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>
<div>
<EnvironmentVariablesEditor disabled={props.isLoading} />
</div>
</div>
</Show>
</div>

View File

@@ -51,6 +51,7 @@ export class FileStorage {
return {
preferences: {
showThinkingBlocks: false,
environmentVariables: {},
},
recentFolders: [],
opencodeBinaries: [],

View File

@@ -3,6 +3,7 @@ import type { Instance, LogEntry } from "../types/instance"
import { sdkManager } from "../lib/sdk-manager"
import { sseManager } from "../lib/sse-manager"
import { fetchSessions, fetchAgents, fetchProviders } from "./sessions"
import { preferences } from "./preferences"
const [instances, setInstances] = createSignal<Map<string, Instance>>(new Map())
const [activeInstanceId, setActiveInstanceId] = createSignal<string | null>(null)
@@ -61,7 +62,7 @@ async function createInstance(folder: string, binaryPath?: string): Promise<stri
port,
pid,
binaryPath: actualBinaryPath,
} = await window.electronAPI.createInstance(id, folder, binaryPath)
} = await window.electronAPI.createInstance(id, folder, binaryPath, preferences().environmentVariables)
const client = sdkManager.createClient(port)

View File

@@ -4,6 +4,7 @@ import { storage, type ConfigData } from "../lib/storage"
export interface Preferences {
showThinkingBlocks: boolean
lastUsedBinary?: string
environmentVariables?: Record<string, string>
}
export interface OpenCodeBinary {
@@ -105,6 +106,22 @@ function updateLastUsedBinary(path: string): void {
}
}
function updateEnvironmentVariables(envVars: Record<string, string>): void {
updatePreferences({ environmentVariables: envVars })
}
function addEnvironmentVariable(key: string, value: string): void {
const current = preferences().environmentVariables || {}
const updated = { ...current, [key]: value }
updateEnvironmentVariables(updated)
}
function removeEnvironmentVariable(key: string): void {
const current = preferences().environmentVariables || {}
const { [key]: removed, ...rest } = current
updateEnvironmentVariables(rest)
}
// Load config on mount and listen for changes from other instances
onMount(() => {
loadConfig()
@@ -129,4 +146,7 @@ export {
addOpenCodeBinary,
removeOpenCodeBinary,
updateLastUsedBinary,
updateEnvironmentVariables,
addEnvironmentVariable,
removeEnvironmentVariable,
}