From 4e1560a10ed0cdfbd3677a83135f3353f1e8fe0c Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Thu, 4 Jun 2026 20:15:56 +0300 Subject: [PATCH] fix: improve auto-accept and Windows opencode upgrades --- packages/ui/src/sync/sync-context.tsx | 1 + .../web/server/lib/opencode/env-runtime.js | 22 +++++- .../lib/opencode/server-utils-runtime.js | 68 ++++++++++++++++++- .../lib/opencode/server-utils-runtime.test.js | 59 +++++++++++++++- 4 files changed, 141 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/sync/sync-context.tsx b/packages/ui/src/sync/sync-context.tsx index 225af12f..17ebe696 100644 --- a/packages/ui/src/sync/sync-context.tsx +++ b/packages/ui/src/sync/sync-context.tsx @@ -1335,6 +1335,7 @@ function handleEvent( if (permissionStore.isSessionAutoAccepting(permission.sessionID)) { updateRoutingIndexFromEvent(routingIndex, resolvedDirectory, payload) void sessionActions.respondToPermission(permission.sessionID, permission.id, "once").catch(() => undefined) + return } const toastKey = getPermissionToastKey(permission.sessionID, permission.id) diff --git a/packages/web/server/lib/opencode/env-runtime.js b/packages/web/server/lib/opencode/env-runtime.js index 736f9292..e9f3ed82 100644 --- a/packages/web/server/lib/opencode/env-runtime.js +++ b/packages/web/server/lib/opencode/env-runtime.js @@ -32,7 +32,18 @@ export const createOpenCodeEnvRuntime = (deps) => { result[key] = value; } - return Object.keys(result).length > 0 ? result : null; + if (Object.keys(result).length === 0) { + return null; + } + + if (process.platform === 'win32' && typeof result.PATH !== 'string') { + const pathEntry = Object.entries(result).find(([key]) => key.toLowerCase() === 'path'); + if (pathEntry && typeof pathEntry[1] === 'string') { + result.PATH = pathEntry[1]; + } + } + + return result; }; const isExecutable = (filePath) => { @@ -122,8 +133,13 @@ export const createOpenCodeEnvRuntime = (deps) => { const getWindowsShellEnvSnapshot = () => { const parseResult = (stdout) => parseNullSeparatedEnvSnapshot(typeof stdout === 'string' ? stdout : ''); - const psScript = - "Get-ChildItem Env: | ForEach-Object { [Console]::Out.Write($_.Name); [Console]::Out.Write('='); [Console]::Out.Write($_.Value); [Console]::Out.Write([char]0) }"; + const psScript = [ + '$entries = [ordered]@{}', + 'Get-ChildItem Env: | ForEach-Object { $entries[$_.Name] = $_.Value }', + "$pathValues = @([Environment]::GetEnvironmentVariable('Path', 'Machine'), [Environment]::GetEnvironmentVariable('Path', 'User'), [Environment]::GetEnvironmentVariable('Path', 'Process')) | Where-Object { $_ }", + "if ($pathValues.Count -gt 0) { $entries['Path'] = ($pathValues -join ';') }", + "$entries.GetEnumerator() | ForEach-Object { [Console]::Out.Write($_.Name); [Console]::Out.Write('='); [Console]::Out.Write($_.Value); [Console]::Out.Write([char]0) }", + ].join('; '); const powershellCandidates = [ 'pwsh.exe', diff --git a/packages/web/server/lib/opencode/server-utils-runtime.js b/packages/web/server/lib/opencode/server-utils-runtime.js index dc4584ec..bf835849 100644 --- a/packages/web/server/lib/opencode/server-utils-runtime.js +++ b/packages/web/server/lib/opencode/server-utils-runtime.js @@ -62,8 +62,69 @@ export const createServerUtilsRuntime = (dependencies) => { throw new Error('Timed out waiting for OpenCode port'); }; + const getEnvValue = (name) => { + const env = process.env || {}; + if (typeof env[name] === 'string') return env[name]; + const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase()); + return key && typeof env[key] === 'string' ? env[key] : ''; + }; + + const buildWindowsManagedToolchainPath = () => { + if (process.platform !== 'win32') return ''; + + const home = os.homedir(); + const userProfile = getEnvValue('USERPROFILE') || home; + const appData = getEnvValue('APPDATA') || (userProfile ? path.join(userProfile, 'AppData', 'Roaming') : ''); + const localAppData = getEnvValue('LOCALAPPDATA') || (userProfile ? path.join(userProfile, 'AppData', 'Local') : ''); + const programFiles = getEnvValue('ProgramFiles') || 'C:\\Program Files'; + const programFilesX86 = getEnvValue('ProgramFiles(x86)'); + const programData = getEnvValue('ProgramData') || 'C:\\ProgramData'; + const bunInstall = getEnvValue('BUN_INSTALL'); + const voltaHome = getEnvValue('VOLTA_HOME'); + const scoop = getEnvValue('SCOOP'); + const scoopGlobal = getEnvValue('SCOOP_GLOBAL'); + + const candidates = [ + path.join(appData, 'npm'), + path.join(programFiles, 'nodejs'), + programFilesX86 ? path.join(programFilesX86, 'nodejs') : '', + path.join(localAppData, 'Programs', 'nodejs'), + getEnvValue('PNPM_HOME'), + path.join(localAppData, 'pnpm'), + bunInstall ? path.join(bunInstall, 'bin') : '', + path.join(userProfile, '.bun', 'bin'), + voltaHome ? path.join(voltaHome, 'bin') : '', + path.join(localAppData, 'Volta', 'bin'), + path.join(localAppData, 'Yarn', 'bin'), + path.join(localAppData, 'Yarn', 'Data', 'global', 'node_modules', '.bin'), + scoop ? path.join(scoop, 'shims') : '', + path.join(userProfile, 'scoop', 'shims'), + scoopGlobal ? path.join(scoopGlobal, 'shims') : '', + path.join(programData, 'chocolatey', 'bin'), + path.join(localAppData, 'Microsoft', 'WindowsApps'), + path.join(userProfile, '.opencode', 'bin'), + path.join(userProfile, '.local', 'bin'), + ]; + + const seen = new Set(); + const existing = []; + for (const candidate of candidates) { + const trimmed = typeof candidate === 'string' ? candidate.trim() : ''; + if (!trimmed) continue; + const normalized = trimmed.toLowerCase(); + if (seen.has(normalized)) continue; + seen.add(normalized); + try { + if (fs.existsSync(trimmed)) existing.push(trimmed); + } catch { + } + } + + return existing.join(path.delimiter); + }; + const buildAugmentedPath = () => { - const currentPath = process.env.PATH || ''; + const currentPath = getEnvValue('PATH'); const loginShellPath = getLoginShellPath(); const home = os.homedir(); const currentPathLooksUserConfigured = pathLooksUserConfigured(currentPath, home, path.delimiter); @@ -74,10 +135,11 @@ export const createServerUtilsRuntime = (dependencies) => { }; const buildManagedOpenCodePath = () => { - const currentPath = process.env.PATH || ''; + const currentPath = getEnvValue('PATH'); const loginShellPath = getLoginShellPath(); + const basePath = mergePathValues(loginShellPath || '', currentPath, path.delimiter); - return mergePathValues(loginShellPath || '', currentPath, path.delimiter); + return mergePathValues(basePath, buildWindowsManagedToolchainPath(), path.delimiter); }; const parseSseDataPayload = (block) => { diff --git a/packages/web/server/lib/opencode/server-utils-runtime.test.js b/packages/web/server/lib/opencode/server-utils-runtime.test.js index 452cc056..1329f5fb 100644 --- a/packages/web/server/lib/opencode/server-utils-runtime.test.js +++ b/packages/web/server/lib/opencode/server-utils-runtime.test.js @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; @@ -5,8 +6,19 @@ import { afterEach, describe, expect, it } from 'vitest'; import { createServerUtilsRuntime } from './server-utils-runtime.js'; const originalPath = process.env.PATH; +const tempDirs = []; + +const createTempDir = (prefix) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +}; afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } + if (originalPath === undefined) { delete process.env.PATH; return; @@ -15,11 +27,11 @@ afterEach(() => { process.env.PATH = originalPath; }); -const createRuntime = (loginShellPath) => createServerUtilsRuntime({ - fs: {}, +const createRuntime = (loginShellPath, processLike = { platform: 'linux', env: process.env }) => createServerUtilsRuntime({ + fs, os, path, - process, + process: processLike, openCodeReadyGraceMs: 0, longRequestTimeoutMs: 0, getRuntime: () => ({}), @@ -89,6 +101,47 @@ describe('server utils runtime', () => { ].join(path.delimiter)); }); + it('adds existing Windows package-manager directories to managed OpenCode PATH', () => { + const root = createTempDir('openchamber-win-path-'); + const systemDir = path.join(root, 'System32'); + const appData = path.join(root, 'Roaming'); + const programFiles = path.join(root, 'Program Files'); + const localAppData = path.join(root, 'Local'); + const programData = path.join(root, 'ProgramData'); + const userProfile = path.join(root, 'User'); + + const npmBin = path.join(appData, 'npm'); + const nodeBin = path.join(programFiles, 'nodejs'); + const pnpmHome = path.join(localAppData, 'pnpm'); + const yarnBin = path.join(localAppData, 'Yarn', 'bin'); + const chocoBin = path.join(programData, 'chocolatey', 'bin'); + + for (const dir of [systemDir, npmBin, nodeBin, pnpmHome, yarnBin, chocoBin]) { + fs.mkdirSync(dir, { recursive: true }); + } + + const runtime = createRuntime(null, { + platform: 'win32', + env: { + PATH: systemDir, + APPDATA: appData, + ProgramFiles: programFiles, + LOCALAPPDATA: localAppData, + ProgramData: programData, + USERPROFILE: userProfile, + }, + }); + + expect(runtime.buildManagedOpenCodePath()).toBe([ + systemDir, + npmBin, + nodeBin, + pnpmHome, + yarnBin, + chocoBin, + ].join(path.delimiter)); + }); + it('preserves user-configured process PATH order before appending shell-only entries', () => { const home = os.homedir(); process.env.PATH = [