mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
fix(windows): stabilize local and SSH execution for wizard and sessions
- Use full agent path (claude.exe) on Windows to avoid shell:true which breaks stdin piping for stream-json input mode - Fix SSH condition to check enabled flag, not just config existence - Pass sessionSshRemoteConfig through inline wizard to enable remote execution - Add synopsis SSH inheritance from parent session as backend workaround Fixes local wizard crashing with exit code 1 and SSH sessions incorrectly using local paths when SSH was disabled.
This commit is contained in:
65
BUILDING_WINDOWS.md
Normal file
65
BUILDING_WINDOWS.md
Normal file
@@ -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.
|
||||
@@ -423,6 +423,7 @@ function setupIpcHandlers() {
|
||||
agentConfigsStore,
|
||||
settingsStore: store,
|
||||
getMainWindow: () => mainWindow,
|
||||
sessionsStore,
|
||||
});
|
||||
|
||||
// Persistence operations - extracted to src/main/ipc/handlers/persistence.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,
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface ProcessHandlerDependencies {
|
||||
agentConfigsStore: Store<AgentConfigsData>;
|
||||
settingsStore: Store<MaestroSettings>;
|
||||
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,
|
||||
|
||||
@@ -6325,7 +6325,7 @@ 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,
|
||||
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
|
||||
|
||||
@@ -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({
|
||||
|
||||
1
src/renderer/global.d.ts
vendored
1
src/renderer/global.d.ts
vendored
@@ -80,6 +80,7 @@ interface AgentConfig {
|
||||
binaryName?: string;
|
||||
available: boolean;
|
||||
path?: string;
|
||||
customPath?: string;
|
||||
command: string;
|
||||
args?: string[];
|
||||
hidden?: boolean;
|
||||
|
||||
@@ -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<void>;
|
||||
/** End the wizard and restore previous UI state */
|
||||
endWizard: () => Promise<PreviousUIState | null>;
|
||||
@@ -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<void> => {
|
||||
// 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);
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
Reference in New Issue
Block a user