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:
chr1syy
2026-01-15 22:40:39 +01:00
parent 98b5a05f56
commit aafe0d6a48
9 changed files with 162 additions and 11 deletions

65
BUILDING_WINDOWS.md Normal file
View 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.

View File

@@ -423,6 +423,7 @@ function setupIpcHandlers() {
agentConfigsStore,
settingsStore: store,
getMainWindow: () => mainWindow,
sessionsStore,
});
// Persistence operations - extracted to src/main/ipc/handlers/persistence.ts

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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({

View File

@@ -80,6 +80,7 @@ interface AgentConfig {
binaryName?: string;
available: boolean;
path?: string;
customPath?: string;
command: string;
args?: string[];
hidden?: boolean;

View File

@@ -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);

View File

@@ -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?.();