diff --git a/src/__tests__/main/agents/path-prober.test.ts b/src/__tests__/main/agents/path-prober.test.ts index 816cc7a6..e0d028da 100644 --- a/src/__tests__/main/agents/path-prober.test.ts +++ b/src/__tests__/main/agents/path-prober.test.ts @@ -63,6 +63,21 @@ describe('path-prober', () => { } }); + it('should include nvm4w and npm paths on Windows', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32', configurable: true }); + + try { + const env = getExpandedEnv(); + // Check for nvm4w paths (OpenCode commonly installed here) + expect(env.PATH).toContain('C:\\nvm4w\\nodejs'); + // Check for npm global paths + expect(env.PATH).toMatch(/AppData[\\\/](npm|Roaming[\\\/]npm)/); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }); + } + }); + it('should preserve existing PATH entries', () => { const originalPath = process.env.PATH; const testPath = '/test/custom/path'; diff --git a/src/main/agents/path-prober.ts b/src/main/agents/path-prober.ts index 07c3278d..c30b5497 100644 --- a/src/main/agents/path-prober.ts +++ b/src/main/agents/path-prober.ts @@ -84,6 +84,11 @@ export function getExpandedEnv(): NodeJS.ProcessEnv { // Node.js path.join(programFiles, 'nodejs'), path.join(localAppData, 'Programs', 'node'), + // Node Version Manager for Windows (nvm4w) - OpenCode commonly installed here + 'C:\\nvm4w\\nodejs', + path.join(home, 'nvm4w', 'nodejs'), + // Volta - Node version manager for Windows/macOS/Linux (installs shims to .volta/bin) + path.join(home, '.volta', 'bin'), // Scoop package manager (OpenCode, other tools) path.join(home, 'scoop', 'shims'), path.join(home, 'scoop', 'apps', 'opencode', 'current'), @@ -252,6 +257,9 @@ function getWindowsKnownPaths(binaryName: string): string[] { // Scoop installation (recommended for OpenCode) path.join(home, 'scoop', 'shims', 'opencode.exe'), path.join(home, 'scoop', 'apps', 'opencode', 'current', 'opencode.exe'), + // Volta - Node version manager (OpenCode commonly installed via Volta) + path.join(home, '.volta', 'bin', 'opencode'), + path.join(home, '.volta', 'bin', 'opencode.cmd'), // Chocolatey installation path.join( process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey', @@ -471,13 +479,16 @@ export async function checkBinaryExists(binaryName: string): Promise p); if (process.platform === 'win32' && matches.length > 0) { - // On Windows, prefer .exe over .cmd over extensionless - // This helps with proper execution handling + // On Windows, prefer .exe > extensionless (shell scripts) > .cmd + // This helps avoid cmd.exe limitations and supports PowerShell/bash scripts const exeMatch = matches.find((p) => p.toLowerCase().endsWith('.exe')); const cmdMatch = matches.find((p) => p.toLowerCase().endsWith('.cmd')); + const extensionlessMatch = matches.find( + (p) => !p.toLowerCase().endsWith('.exe') && !p.toLowerCase().endsWith('.cmd') + ); - // Return the best match: .exe > .cmd > first result - let bestMatch = exeMatch || cmdMatch || matches[0]; + // Return the best match: .exe > extensionless shell scripts > .cmd > first result + let bestMatch = exeMatch || extensionlessMatch || cmdMatch || matches[0]; // If the first match doesn't have an extension, check if .cmd or .exe version exists // This handles cases where 'where' returns a path without extension diff --git a/src/main/group-chat/group-chat-router.ts b/src/main/group-chat/group-chat-router.ts index 032a5062..406a22ab 100644 --- a/src/main/group-chat/group-chat-router.ts +++ b/src/main/group-chat/group-chat-router.ts @@ -452,6 +452,7 @@ ${message}`; }); const configResolution = applyAgentConfigOverrides(agent, baseArgs, { agentConfigValues, + sessionCustomModel: chat.moderatorConfig?.customModel, sessionCustomArgs: chat.moderatorConfig?.customArgs, sessionCustomEnvVars: chat.moderatorConfig?.customEnvVars, }); @@ -849,7 +850,9 @@ export async function routeModeratorResponse( // Apply SSH wrapping if configured for this session if (sshStore && matchingSession?.sshRemoteConfig) { - console.log(`[GroupChat:Debug] Applying SSH wrapping for participant ${participantName}...`); + console.log( + `[GroupChat:Debug] Applying SSH wrapping for participant ${participantName}...` + ); const sshWrapped = await wrapSpawnWithSsh( { command: spawnCommand, @@ -1149,6 +1152,7 @@ Review the agent responses above. Either: }); const configResolution = applyAgentConfigOverrides(agent, baseArgs, { agentConfigValues, + sessionCustomModel: chat.moderatorConfig?.customModel, sessionCustomArgs: chat.moderatorConfig?.customArgs, sessionCustomEnvVars: chat.moderatorConfig?.customEnvVars, }); diff --git a/src/main/ipc/handlers/process.ts b/src/main/ipc/handlers/process.ts index 35bec273..8adfbbe2 100644 --- a/src/main/ipc/handlers/process.ts +++ b/src/main/ipc/handlers/process.ts @@ -292,7 +292,8 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void ) as Record; // Determine an explicit shell to use when forcing shell execution on Windows. - // Prefer a user-configured custom shell path, otherwise fall back to COMSPEC/cmd.exe. + // Prefer a user-configured custom shell path, then PowerShell, then ComSpec/cmd.exe. + // PowerShell is preferred over cmd.exe for better script handling and to avoid cmd.exe limits. const customShellPath = settingsStore.get('customShellPath', '') as string; if (customShellPath && customShellPath.trim()) { shellToUse = customShellPath.trim(); @@ -300,8 +301,20 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void customShellPath: shellToUse, }); } else if (!shellToUse) { - // Use COMSPEC if available, otherwise default to cmd.exe - shellToUse = process.env.ComSpec || 'cmd.exe'; + // Try PowerShell if available (common on modern Windows) + // If not, fall back to ComSpec/cmd.exe + // PowerShell handles shell scripts better and avoids cmd.exe command line length limits + const powerShellPath = process.env.PSHOME + ? `${process.env.PSHOME}\\powershell.exe` + : 'powershell'; + shellToUse = powerShellPath; + logger.debug( + 'Using PowerShell for agent execution on Windows (shell script support)', + LOG_CONTEXT, + { + shellPath: shellToUse, + } + ); } logger.info(`Forcing shell execution for agent on Windows for PATH access`, LOG_CONTEXT, { diff --git a/src/main/process-manager/spawners/ChildProcessSpawner.ts b/src/main/process-manager/spawners/ChildProcessSpawner.ts index a6ec3a81..7dfb4b27 100644 --- a/src/main/process-manager/spawners/ChildProcessSpawner.ts +++ b/src/main/process-manager/spawners/ChildProcessSpawner.ts @@ -3,6 +3,7 @@ import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import * as path from 'path'; +import * as fs from 'fs'; import { logger } from '../../utils/logger'; import { getOutputParser } from '../../parsers'; import { getAgentCapabilities } from '../../agents'; @@ -188,6 +189,24 @@ export class ChildProcessSpawner { ); } + // Auto-enable shell for Windows when command is a shell script (extensionless with shebang) + // This handles tools like OpenCode installed via npm with shell scripts + if (isWindows && !useShell && !commandExt && commandHasPath) { + try { + const fileContent = fs.readFileSync(spawnCommand, 'utf8'); + if (fileContent.startsWith('#!')) { + useShell = true; + logger.info( + '[ProcessManager] Auto-enabling shell for Windows to execute shell script', + 'ProcessManager', + { command: spawnCommand, shebang: fileContent.split('\n')[0] } + ); + } + } catch { + // If we can't read the file, just continue without special handling + } + } + if (isWindows && useShell) { logger.debug( '[ProcessManager] Forcing shell=true for agent spawn on Windows (runInShell or auto)', diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index a54fa61f..bdecffbe 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -8433,6 +8433,7 @@ You are taking over this conversation. Based on the context above, provide a bri customPath?: string; customArgs?: string; customEnvVars?: Record; + customModel?: string; } ) => { const chat = await window.maestro.groupChat.create(name, moderatorAgentId, moderatorConfig); diff --git a/src/renderer/components/EditGroupChatModal.tsx b/src/renderer/components/EditGroupChatModal.tsx index d375cf93..13120e15 100644 --- a/src/renderer/components/EditGroupChatModal.tsx +++ b/src/renderer/components/EditGroupChatModal.tsx @@ -347,23 +347,22 @@ export function EditGroupChatModal({ ) : (
{/* Dropdown */} -
+
diff --git a/src/renderer/components/NewGroupChatModal.tsx b/src/renderer/components/NewGroupChatModal.tsx index c04d7c07..8d25541d 100644 --- a/src/renderer/components/NewGroupChatModal.tsx +++ b/src/renderer/components/NewGroupChatModal.tsx @@ -163,15 +163,18 @@ export function NewGroupChatModal({ // Build moderator config from state const buildModeratorConfig = useCallback((): ModeratorConfig | undefined => { - const hasConfig = customPath || customArgs || Object.keys(customEnvVars).length > 0; + const customModelValue = agentConfig.model; + const hasConfig = + customPath || customArgs || Object.keys(customEnvVars).length > 0 || customModelValue; if (!hasConfig) return undefined; return { customPath: customPath || undefined, customArgs: customArgs || undefined, customEnvVars: Object.keys(customEnvVars).length > 0 ? customEnvVars : undefined, + customModel: customModelValue || undefined, }; - }, [customPath, customArgs, customEnvVars]); + }, [customPath, customArgs, customEnvVars, agentConfig]); const handleCreate = useCallback(() => { if (name.trim() && selectedAgent) { @@ -337,23 +340,22 @@ export function NewGroupChatModal({ ) : (
{/* Dropdown */} -
+
diff --git a/src/shared/group-chat-types.ts b/src/shared/group-chat-types.ts index 5fea9675..53e6a48f 100644 --- a/src/shared/group-chat-types.ts +++ b/src/shared/group-chat-types.ts @@ -73,6 +73,8 @@ export interface ModeratorConfig { customArgs?: string; /** Custom environment variables */ customEnvVars?: Record; + /** Custom model selection (e.g., 'ollama/qwen3:8b') */ + customModel?: string; /** SSH remote config for remote execution */ sshRemoteConfig?: { enabled: boolean;