diff --git a/BUILDING_WINDOWS.md b/BUILDING_WINDOWS.md new file mode 100644 index 00000000..d58850ae --- /dev/null +++ b/BUILDING_WINDOWS.md @@ -0,0 +1,65 @@ +# Building and Running on Windows + +This guide provides instructions for setting up your environment and running the Maestro application on a Windows machine. + +## Prerequisites + +Before you begin, ensure you have the following installed: + +* **Node.js:** Version 22 or later. +* **Python:** Version 3 or later. + +Additionally, you will need the Visual Studio Build Tools to compile native Node.js modules used in this project. + +### Installing Visual Studio Build Tools + +1. **Download the Build Tools:** + * Go to the [Visual Studio Downloads page](https://my.visualstudio.com/Downloads?q=Visual%20Studio%202022). + * You may need to log in with a Microsoft account. + * Download the **Build Tools for Visual Studio 2022**. + +2. **Run the Installer:** + * When the installer launches, you will be prompted to select workloads. + * Check the box for **Desktop development with C++**. + * Proceed with the installation. + +3. **Verify the Setup:** + * After the installation is complete, open a PowerShell terminal in the project root. + * Run `npm ci` to install dependencies. If you have already run `npm install`, you can run `npx electron-rebuild` to rebuild the native modules. + +## Running the Application in Development Mode + +There are two ways to run the application in development mode on Windows: using the provided script or by running the steps manually. + +### Using the Development Script (Recommended) + +The easiest way to start the development environment is to use the `dev:win` npm script. This script automates the entire process. + +Open a PowerShell terminal and run: +```powershell +npm run dev:win +``` + +This will handle all the necessary build steps and launch the application. + +### Manual Steps + +If you encounter issues with the `dev:win` script or prefer to run the steps manually, follow this procedure. + +1. **Build the application:** + ```powershell + npm run build + ``` + +2. **Start the Vite renderer:** + ```powershell + npm run dev:renderer + ``` + +3. **Start the Electron main process:** + Open a **new** PowerShell terminal and run the following command: + ```powershell + npm run build:prompts; npx tsc -p tsconfig.main.json; $env:NODE_ENV='development'; npx electron . + ``` + +This will launch the application in development mode with hot-reloading for the renderer. diff --git a/src/main/index.ts b/src/main/index.ts index f7ce194e..2fcffb13 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -423,6 +423,7 @@ function setupIpcHandlers() { agentConfigsStore, settingsStore: store, getMainWindow: () => mainWindow, + sessionsStore, }); // Persistence operations - extracted to src/main/ipc/handlers/persistence.ts diff --git a/src/main/ipc/handlers/index.ts b/src/main/ipc/handlers/index.ts index f06b442b..222f6535 100644 --- a/src/main/ipc/handlers/index.ts +++ b/src/main/ipc/handlers/index.ts @@ -171,6 +171,7 @@ export function registerAllHandlers(deps: HandlerDependencies): void { agentConfigsStore: deps.agentConfigsStore, settingsStore: deps.settingsStore, getMainWindow: deps.getMainWindow, + sessionsStore: deps.sessionsStore, }); registerPersistenceHandlers({ settingsStore: deps.settingsStore, diff --git a/src/main/ipc/handlers/process.ts b/src/main/ipc/handlers/process.ts index 887c4061..2d0784ea 100644 --- a/src/main/ipc/handlers/process.ts +++ b/src/main/ipc/handlers/process.ts @@ -51,6 +51,7 @@ export interface ProcessHandlerDependencies { agentConfigsStore: Store; settingsStore: Store; getMainWindow: () => BrowserWindow | null; + sessionsStore: Store<{ sessions: any[] }>; } /** @@ -66,7 +67,7 @@ export interface ProcessHandlerDependencies { * - runCommand: Execute a single command and capture output */ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void { - const { getProcessManager, getAgentDetector, agentConfigsStore, settingsStore, getMainWindow } = + const { getProcessManager, getAgentDetector, agentConfigsStore, settingsStore, getMainWindow, sessionsStore } = deps; // Spawn a new process for a session @@ -108,6 +109,26 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void const processManager = requireProcessManager(getProcessManager); const agentDetector = requireDependency(getAgentDetector, 'Agent detector'); + // Synopsis SSH Fix: If this is a synopsis session and it's missing SSH config, + // try to inherit it from the parent session. This is a backend workaround for + // a suspected frontend state issue where the remote config isn't passed. + const synopsisMatch = config.sessionId.match(/^(.+)-synopsis-\d+$/); + if (synopsisMatch && (!config.sessionSshRemoteConfig || !config.sessionSshRemoteConfig.enabled)) { + const originalSessionId = synopsisMatch[1]; + const sessions = sessionsStore.get('sessions', []); + const originalSession = sessions.find(s => s.id === originalSessionId); + + if (originalSession && originalSession.sessionSshRemoteConfig?.enabled) { + const sshConfig = originalSession.sessionSshRemoteConfig; + config.sessionSshRemoteConfig = sshConfig; + logger.info(`Inferred SSH config for synopsis session from parent session`, LOG_CONTEXT, { + sessionId: config.sessionId, + originalSessionId: originalSessionId, + sshRemoteId: sshConfig.remoteId, + }); + } + } + // Get agent definition to access config options and argument builders const agent = await agentDetector.getAgent(config.toolType); // Use INFO level on Windows for better visibility in logs @@ -245,6 +266,13 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void // ======================================================================== // Command Resolution: Apply session-level custom path override if set // This allows users to override the detected agent path per-session + // + // WINDOWS FIX: On Windows, prefer the resolved agent path with .exe extension + // to avoid using shell:true in ProcessManager. When shell:true is used, + // stdin piping through cmd.exe is unreliable - data written to stdin may not + // be forwarded to the child process. This breaks stream-json input mode. + // By using the full path with .exe extension, ProcessManager will spawn + // the process directly without cmd.exe wrapper, ensuring stdin works correctly. // ======================================================================== let commandToSpawn = config.sessionCustomPath || config.command; let argsToSpawn = finalArgs; @@ -254,6 +282,20 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void customPath: config.sessionCustomPath, originalCommand: config.command, }); + } else if (isWindows && agent?.path && !config.sessionSshRemoteConfig?.enabled) { + // On Windows LOCAL execution, use the full resolved agent path if it ends with .exe or .com + // This avoids ProcessManager setting shell:true for extensionless commands, + // which breaks stdin piping (needed for stream-json input mode) + // NOTE: Skip this for SSH sessions - SSH uses the remote agent path, not local + const pathExt = require('path').extname(agent.path).toLowerCase(); + if (pathExt === '.exe' || pathExt === '.com') { + commandToSpawn = agent.path; + logger.debug(`Using full agent path on Windows to avoid shell wrapper`, LOG_CONTEXT, { + originalCommand: config.command, + resolvedPath: agent.path, + reason: 'stdin-reliability', + }); + } } // ======================================================================== @@ -271,10 +313,11 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void toolType: config.toolType, isTerminal: config.toolType === 'terminal', hasSessionSshRemoteConfig: !!config.sessionSshRemoteConfig, - willUseSsh: config.toolType !== 'terminal' && !!config.sessionSshRemoteConfig, + sshEnabled: config.sessionSshRemoteConfig?.enabled, + willUseSsh: config.toolType !== 'terminal' && config.sessionSshRemoteConfig?.enabled, }); } - if (config.toolType !== 'terminal' && config.sessionSshRemoteConfig) { + if (config.toolType !== 'terminal' && config.sessionSshRemoteConfig?.enabled) { // Session-level SSH config provided - resolve and use it logger.info(`Using session-level SSH config`, LOG_CONTEXT, { sessionId: config.sessionId, diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index cb2926c7..8abd7afb 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -6325,8 +6325,8 @@ You are taking over this conversation. Based on the context above, provide a bri customArgs: activeSession.customArgs, customEnvVars: activeSession.customEnvVars, customModel: activeSession.customModel, - customContextWindow: activeSession.customContextWindow, - sessionSshRemoteConfig: activeSession.sessionSshRemoteConfig, + customContextWindow: activeSession.customContextWindow,, + sessionSshRemoteConfig: activeSession.sessionSshRemoteConfig, } ); @@ -6507,7 +6507,8 @@ You are taking over this conversation. Based on the context above, provide a bri activeSession.name, // Session/project name activeTab.id, // Tab ID for per-tab isolation activeSession.id, // Session ID for playbook creation - activeSession.autoRunFolderPath // User-configured Auto Run folder path (if set) + activeSession.autoRunFolderPath, // User-configured Auto Run folder path (if set) + activeSession.sessionSshRemoteConfig // SSH remote config for remote execution ); // Rename the tab to "Wizard" immediately when wizard starts @@ -6589,7 +6590,8 @@ You are taking over this conversation. Based on the context above, provide a bri activeSession.name, newTab.id, activeSession.id, - activeSession.autoRunFolderPath // User-configured Auto Run folder path (if set) + activeSession.autoRunFolderPath, // User-configured Auto Run folder path (if set) + activeSession.sessionSshRemoteConfig // SSH remote config for remote execution ); // Show a system log entry diff --git a/src/renderer/components/Wizard/services/phaseGenerator.ts b/src/renderer/components/Wizard/services/phaseGenerator.ts index 37dae9b1..0d4b50ab 100644 --- a/src/renderer/components/Wizard/services/phaseGenerator.ts +++ b/src/renderer/components/Wizard/services/phaseGenerator.ts @@ -1098,8 +1098,8 @@ class PhaseGenerator { agentCommand: agent.command, argsCount: argsForSpawn.length, promptLength: prompt.length, - hasRemoteSsh: !!config.sessionSshRemoteConfig?.enabled, - remoteId: config.sessionSshRemoteConfig?.remoteId || null, + hasRemoteSsh: !!config.sshRemoteConfig?.enabled, + remoteId: config.sshRemoteConfig?.remoteId || null, }); window.maestro.process .spawn({ diff --git a/src/renderer/global.d.ts b/src/renderer/global.d.ts index c4ead106..face6b69 100644 --- a/src/renderer/global.d.ts +++ b/src/renderer/global.d.ts @@ -80,6 +80,7 @@ interface AgentConfig { binaryName?: string; available: boolean; path?: string; + customPath?: string; command: string; args?: string[]; hidden?: boolean; diff --git a/src/renderer/hooks/useInlineWizard.ts b/src/renderer/hooks/useInlineWizard.ts index cdcd3e6d..f31b3ddb 100644 --- a/src/renderer/hooks/useInlineWizard.ts +++ b/src/renderer/hooks/useInlineWizard.ts @@ -143,6 +143,12 @@ export interface InlineWizardState { subfolderPath: string | null; /** User-configured Auto Run folder path (overrides default projectPath/Auto Run Docs) */ autoRunFolderPath: string | null; + /** SSH remote configuration (for remote execution) */ + sessionSshRemoteConfig?: { + enabled: boolean; + remoteId: string | null; + workingDirOverride?: string; + }; } /** @@ -199,6 +205,7 @@ export interface UseInlineWizardReturn { * @param tabId - The tab ID to associate the wizard with * @param sessionId - The session ID for playbook creation * @param autoRunFolderPath - User-configured Auto Run folder path (if set, overrides default projectPath/Auto Run Docs) + * @param sessionSshRemoteConfig - SSH remote configuration (for remote execution) */ startWizard: ( naturalLanguageInput?: string, @@ -208,7 +215,12 @@ export interface UseInlineWizardReturn { sessionName?: string, tabId?: string, sessionId?: string, - autoRunFolderPath?: string + autoRunFolderPath?: string, + sessionSshRemoteConfig?: { + enabled: boolean; + remoteId: string | null; + workingDirOverride?: string; + } ) => Promise; /** End the wizard and restore previous UI state */ endWizard: () => Promise; @@ -450,7 +462,12 @@ export function useInlineWizard(): UseInlineWizardReturn { sessionName?: string, tabId?: string, sessionId?: string, - configuredAutoRunFolderPath?: string + configuredAutoRunFolderPath?: string, + sessionSshRemoteConfig?: { + enabled: boolean; + remoteId: string | null; + workingDirOverride?: string; + } ): Promise => { // Tab ID is required for per-tab wizard management const effectiveTabId = tabId || 'default'; @@ -506,6 +523,7 @@ export function useInlineWizard(): UseInlineWizardReturn { subfolderName: null, subfolderPath: null, autoRunFolderPath: effectiveAutoRunFolderPath, + sessionSshRemoteConfig, })); try { @@ -574,6 +592,7 @@ export function useInlineWizard(): UseInlineWizardReturn { goal: goal || undefined, existingDocs: docsWithContent.length > 0 ? docsWithContent : undefined, autoRunFolderPath: effectiveAutoRunFolderPath, + sessionSshRemoteConfig, }); // Store conversation session per-tab @@ -708,6 +727,7 @@ export function useInlineWizard(): UseInlineWizardReturn { goal: currentState.goal || undefined, existingDocs: undefined, autoRunFolderPath: effectiveAutoRunFolderPath, + sessionSshRemoteConfig: currentState.sessionSshRemoteConfig, }); conversationSessionsMap.current.set(tabId, session); // Update mode to 'new' since we're proceeding with a new plan @@ -888,6 +908,7 @@ export function useInlineWizard(): UseInlineWizardReturn { goal: currentState.goal || undefined, existingDocs: undefined, // Will be loaded separately if needed autoRunFolderPath: effectiveAutoRunFolderPath, + sessionSshRemoteConfig: currentState.sessionSshRemoteConfig, }); conversationSessionsMap.current.set(tabId, session); diff --git a/src/renderer/services/inlineWizardConversation.ts b/src/renderer/services/inlineWizardConversation.ts index 0970a633..00b32217 100644 --- a/src/renderer/services/inlineWizardConversation.ts +++ b/src/renderer/services/inlineWizardConversation.ts @@ -90,6 +90,12 @@ export interface InlineWizardConversationConfig { existingDocs?: ExistingDocument[]; /** Auto Run folder path */ autoRunFolderPath?: string; + /** SSH remote configuration (for remote execution) */ + sessionSshRemoteConfig?: { + enabled: boolean; + remoteId: string | null; + workingDirOverride?: string; + }; } /** @@ -108,6 +114,12 @@ export interface InlineWizardConversationSession { systemPrompt: string; /** Whether the session is active */ isActive: boolean; + /** SSH remote configuration (for remote execution) */ + sessionSshRemoteConfig?: { + enabled: boolean; + remoteId: string | null; + workingDirOverride?: string; + }; } /** @@ -250,6 +262,8 @@ export function startInlineWizardConversation( projectName: config.projectName, systemPrompt, isActive: true, + // Only pass SSH config if it is explicitly enabled to prevent false positives in process manager + sessionSshRemoteConfig: config.sessionSshRemoteConfig?.enabled ? config.sessionSshRemoteConfig : undefined, }; } @@ -439,6 +453,7 @@ function buildArgsForAgent(agent: any): string[] { // The agent can read files to understand the project, but cannot write/edit // This ensures the wizard conversation phase doesn't make code changes if (!args.includes('--allowedTools')) { + // Split tools into separate arguments for better cross-platform compatibility (especially Windows) args.push('--allowedTools', 'Read', 'Glob', 'Grep', 'LS'); } return args; @@ -648,6 +663,8 @@ export async function sendWizardMessage( command: agent.command, args: argsForSpawn, prompt: fullPrompt, + // Pass SSH config for remote execution + sessionSshRemoteConfig: session.sessionSshRemoteConfig, }) .then(() => { callbacks?.onReceiving?.();