From 0b26ffd97d1a02688300693eb15f0a85137de276 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Sun, 26 Oct 2025 10:48:47 +0000 Subject: [PATCH] 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 --- electron/main/ipc.ts | 81 +++++----- electron/main/process-manager.ts | 16 +- electron/preload/index.ts | 5 +- .../environment-variables-editor.tsx | 147 ++++++++++++++++++ src/components/folder-selection-view.tsx | 21 ++- src/lib/storage.ts | 1 + src/stores/instances.ts | 3 +- src/stores/preferences.ts | 20 +++ 8 files changed, 245 insertions(+), 49 deletions(-) create mode 100644 src/components/environment-variables-editor.tsx diff --git a/electron/main/ipc.ts b/electron/main/ipc.ts index ad71b1a9..2a1c73f3 100644 --- a/electron/main/ipc.ts +++ b/electron/main/ipc.ts @@ -37,47 +37,54 @@ export function setupInstanceIPC(mainWindow: BrowserWindow) { return result.filePaths[0] }) - ipcMain.handle("instance:create", async (event, id: string, folder: string, binaryPath?: string) => { - const instance: Instance = { - id, - folder, - port: 0, - pid: 0, - status: "starting", - } - - instances.set(id, instance) - - try { - const { pid, port, binaryPath: actualBinaryPath } = await processManager.spawn(folder, id, binaryPath) - - instance.port = port - instance.pid = pid - instance.status = "ready" - - mainWindow.webContents.send("instance:started", { id, port, pid, binaryPath: actualBinaryPath }) - - const meta = processManager.getAllProcesses().get(pid) - if (meta) { - meta.childProcess.on("exit", (code, signal) => { - instance.status = "stopped" - mainWindow.webContents.send("instance:stopped", { id }) - }) + ipcMain.handle( + "instance:create", + async (event, id: string, folder: string, binaryPath?: string, environmentVariables?: Record) => { + const instance: Instance = { + id, + folder, + port: 0, + pid: 0, + status: "starting", } - return { id, port, pid, binaryPath: actualBinaryPath } - } catch (error) { - instance.status = "error" - instance.error = error instanceof Error ? error.message : String(error) + instances.set(id, instance) - mainWindow.webContents.send("instance:error", { - id, - error: instance.error, - }) + try { + const { + pid, + port, + binaryPath: actualBinaryPath, + } = await processManager.spawn(folder, id, binaryPath, environmentVariables) - throw error - } - }) + instance.port = port + instance.pid = pid + instance.status = "ready" + + mainWindow.webContents.send("instance:started", { id, port, pid, binaryPath: actualBinaryPath }) + + const meta = processManager.getAllProcesses().get(pid) + if (meta) { + meta.childProcess.on("exit", (code, signal) => { + instance.status = "stopped" + mainWindow.webContents.send("instance:stopped", { id }) + }) + } + + return { id, port, pid, binaryPath: actualBinaryPath } + } catch (error) { + instance.status = "error" + instance.error = error instanceof Error ? error.message : String(error) + + mainWindow.webContents.send("instance:error", { + id, + error: instance.error, + }) + + throw error + } + }, + ) ipcMain.handle("instance:stop", async (event, pid: number) => { await processManager.kill(pid) diff --git a/electron/main/process-manager.ts b/electron/main/process-manager.ts index 5b41e832..fdacc31d 100644 --- a/electron/main/process-manager.ts +++ b/electron/main/process-manager.ts @@ -50,17 +50,29 @@ class ProcessManager { } } - async spawn(folder: string, instanceId: string, binaryPath?: string): Promise { + async spawn( + folder: string, + instanceId: string, + binaryPath?: string, + environmentVariables?: Record, + ): Promise { this.validateFolder(folder) const actualBinaryPath = binaryPath ? this.validateCustomBinary(binaryPath) : this.validateOpenCodeBinary() this.sendLog(instanceId, "info", `Starting OpenCode server for ${folder} using ${actualBinaryPath}...`) + // Merge environment variables with process environment + const env = { ...process.env } + if (environmentVariables) { + Object.assign(env, environmentVariables) + this.sendLog(instanceId, "info", `Using ${Object.keys(environmentVariables).length} custom environment variables`) + } + return new Promise((resolve, reject) => { const child = spawn(actualBinaryPath, ["serve", "--port", "0", "--print-logs", "--log-level", "DEBUG"], { cwd: folder, stdio: ["ignore", "pipe", "pipe"], - env: process.env, + env, shell: false, }) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 28c1b4a3..082b1a0a 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -6,6 +6,7 @@ export interface ElectronAPI { id: string, folder: string, binaryPath?: string, + environmentVariables?: Record, ) => Promise<{ id: string; port: number; pid: number; binaryPath: string }> stopInstance: (pid: number) => Promise onInstanceStarted: (callback: (data: { id: string; port: number; pid: number; binaryPath: string }) => void) => void @@ -35,8 +36,8 @@ export interface ElectronAPI { const electronAPI: ElectronAPI = { selectFolder: () => ipcRenderer.invoke("dialog:selectFolder"), - createInstance: (id: string, folder: string, binaryPath?: string) => - ipcRenderer.invoke("instance:create", id, folder, binaryPath), + createInstance: (id: string, folder: string, binaryPath?: string, environmentVariables?: Record) => + ipcRenderer.invoke("instance:create", id, folder, binaryPath, environmentVariables), stopInstance: (pid: number) => ipcRenderer.invoke("instance:stop", pid), onInstanceStarted: (callback) => { ipcRenderer.on("instance:started", (_, data) => callback(data)) diff --git a/src/components/environment-variables-editor.tsx b/src/components/environment-variables-editor.tsx new file mode 100644 index 00000000..725f4710 --- /dev/null +++ b/src/components/environment-variables-editor.tsx @@ -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 = (props) => { + const [envVars, setEnvVars] = createSignal>(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 ( +
+
+ + Environment Variables + + ({entries().length} variable{entries().length !== 1 ? "s" : ""}) + +
+ + {/* Existing variables */} + 0}> +
+ + {([key, value]) => ( +
+
+ + + 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" + /> +
+ +
+ )} +
+
+
+ + {/* Add new variable */} +
+
+ + 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" + /> + 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" + /> +
+ +
+ + +
+ No environment variables configured. Add variables above to customize the OpenCode environment. +
+
+ +
+ These variables will be available in the OpenCode environment when starting instances. +
+
+ ) +} + +export default EnvironmentVariablesEditor diff --git a/src/components/folder-selection-view.tsx b/src/components/folder-selection-view.tsx index e2e86b85..dbb00ee8 100644 --- a/src/components/folder-selection-view.tsx +++ b/src/components/folder-selection-view.tsx @@ -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 = (props) => { -
-
OpenCode Binary
- +
+
+
OpenCode Binary
+ +
+ +
+ +
diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 4a2f0cef..0dd5c16e 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -51,6 +51,7 @@ export class FileStorage { return { preferences: { showThinkingBlocks: false, + environmentVariables: {}, }, recentFolders: [], opencodeBinaries: [], diff --git a/src/stores/instances.ts b/src/stores/instances.ts index 522a04a2..d31df2ef 100644 --- a/src/stores/instances.ts +++ b/src/stores/instances.ts @@ -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>(new Map()) const [activeInstanceId, setActiveInstanceId] = createSignal(null) @@ -61,7 +62,7 @@ async function createInstance(folder: string, binaryPath?: string): Promise } export interface OpenCodeBinary { @@ -105,6 +106,22 @@ function updateLastUsedBinary(path: string): void { } } +function updateEnvironmentVariables(envVars: Record): 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, }