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:
147
src/components/environment-variables-editor.tsx
Normal file
147
src/components/environment-variables-editor.tsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -51,6 +51,7 @@ export class FileStorage {
|
||||
return {
|
||||
preferences: {
|
||||
showThinkingBlocks: false,
|
||||
environmentVariables: {},
|
||||
},
|
||||
recentFolders: [],
|
||||
opencodeBinaries: [],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user