mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
Merge commit 'refs/pull/188/head' of https://github.com/pedramamini/Maestro into 0.15.0-rc
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.
|
||||
@@ -23,6 +23,7 @@
|
||||
"dev:main:prod-data": "npm run build:prompts && tsc -p tsconfig.main.json && npm run build:preload && NODE_ENV=development USE_PROD_DATA=1 electron .",
|
||||
"dev:renderer": "vite",
|
||||
"dev:web": "vite --config vite.config.web.mts",
|
||||
"dev:win": "powershell -NoProfile -ExecutionPolicy Bypass -File ./scripts/start-dev.ps1",
|
||||
"build": "npm run build:prompts && npm run build:main && npm run build:preload && npm run build:renderer && npm run build:web && npm run build:cli",
|
||||
"build:prompts": "node scripts/generate-prompts.mjs",
|
||||
"build:main": "tsc -p tsconfig.main.json",
|
||||
|
||||
16
scripts/start-dev.ps1
Normal file
16
scripts/start-dev.ps1
Normal file
@@ -0,0 +1,16 @@
|
||||
# Opens two PowerShell windows: one for renderer dev, one for building and running Electron
|
||||
# Usage: powershell -NoProfile -ExecutionPolicy Bypass -File ./scripts/start-dev.ps1
|
||||
|
||||
$repoRoot = Resolve-Path -Path (Join-Path $PSScriptRoot '..')
|
||||
$repoRoot = $repoRoot.Path
|
||||
|
||||
# escape single quotes for embedding in command strings
|
||||
$repoRootEscaped = $repoRoot -replace "'","''"
|
||||
|
||||
$cmdRenderer = "Set-Location -LiteralPath '$repoRootEscaped'; npm run dev:renderer"
|
||||
Start-Process powershell -ArgumentList '-NoExit', '-Command', $cmdRenderer
|
||||
|
||||
$cmdBuild = "Set-Location -LiteralPath '$repoRootEscaped'; npm run build:prompts; npx tsc -p tsconfig.main.json; `$env:NODE_ENV='development'; npx electron ."
|
||||
Start-Process powershell -ArgumentList '-NoExit', '-Command', $cmdBuild
|
||||
|
||||
Write-Host "Launched renderer and main developer windows." -ForegroundColor Green
|
||||
@@ -157,7 +157,8 @@ describe('remote-fs', () => {
|
||||
|
||||
await readDirRemote("/path/with spaces/and'quotes", baseConfig, deps);
|
||||
|
||||
expect(deps.execSsh).toHaveBeenCalledWith('ssh', expect.any(Array));
|
||||
// Accept full SSH binary path (e.g., /usr/bin/ssh) for cross-platform compatibility
|
||||
expect(deps.execSsh).toHaveBeenCalledWith(expect.stringMatching(/ssh$/), expect.any(Array));
|
||||
const call = (deps.execSsh as any).mock.calls[0][1];
|
||||
const remoteCommand = call[call.length - 1];
|
||||
// Path should be properly escaped in the command
|
||||
|
||||
@@ -319,8 +319,10 @@ describe('ssh-command-builder', () => {
|
||||
});
|
||||
|
||||
const wrappedCommand = result.args[result.args.length - 1];
|
||||
// The command is wrapped in $SHELL -ilc "..." (-i ensures .bashrc runs fully)
|
||||
expect(wrappedCommand).toBe("$SHELL -ilc \"claude '--print' 'hello'\"");
|
||||
// The command uses $SHELL -lc with profile sourcing for PATH availability
|
||||
expect(wrappedCommand).toBe(
|
||||
"$SHELL -lc \"source ~/.bashrc 2>/dev/null || source ~/.zshrc 2>/dev/null || source ~/.profile 2>/dev/null || source ~/.bash_profile 2>/dev/null || true; claude '--print' 'hello'\""
|
||||
);
|
||||
expect(wrappedCommand).not.toContain('cd');
|
||||
});
|
||||
|
||||
|
||||
@@ -424,6 +424,7 @@ function setupIpcHandlers() {
|
||||
agentConfigsStore,
|
||||
settingsStore: store,
|
||||
getMainWindow: () => mainWindow,
|
||||
sessionsStore,
|
||||
});
|
||||
|
||||
// Persistence operations - extracted to src/main/ipc/handlers/persistence.ts
|
||||
|
||||
@@ -305,17 +305,128 @@ export function registerAgentsHandlers(deps: AgentsHandlerDependencies): void {
|
||||
})
|
||||
);
|
||||
|
||||
// Get a specific agent by ID
|
||||
ipcMain.handle(
|
||||
'agents:get',
|
||||
withIpcErrorLogging(handlerOpts('get'), async (agentId: string) => {
|
||||
const agentDetector = requireDependency(getAgentDetector, 'Agent detector');
|
||||
logger.debug(`Getting agent: ${agentId}`, LOG_CONTEXT);
|
||||
const agent = await agentDetector.getAgent(agentId);
|
||||
// Strip argBuilder functions before sending over IPC
|
||||
return stripAgentFunctions(agent);
|
||||
})
|
||||
);
|
||||
// Get a specific agent by ID (supports SSH remote detection via optional sshRemoteId)
|
||||
ipcMain.handle(
|
||||
'agents:get',
|
||||
withIpcErrorLogging(handlerOpts('get'), async (agentId: string, sshRemoteId?: string) => {
|
||||
logger.debug(`Getting agent: ${agentId}`, LOG_CONTEXT, { sshRemoteId });
|
||||
|
||||
// If SSH remote ID provided, detect agent on remote host
|
||||
if (sshRemoteId) {
|
||||
const sshConfig = getSshRemoteById(settingsStore, sshRemoteId);
|
||||
if (!sshConfig) {
|
||||
logger.warn(`SSH remote not found or disabled: ${sshRemoteId}`, LOG_CONTEXT);
|
||||
// Return the agent definition with unavailable status
|
||||
const agentDef = AGENT_DEFINITIONS.find((a) => a.id === agentId);
|
||||
if (!agentDef) {
|
||||
throw new Error(`Unknown agent: ${agentId}`);
|
||||
}
|
||||
return stripAgentFunctions({
|
||||
...agentDef,
|
||||
available: false,
|
||||
path: undefined,
|
||||
capabilities: getAgentCapabilities(agentDef.id),
|
||||
error: `SSH remote configuration not found: ${sshRemoteId}`,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Getting agent ${agentId} on remote host: ${sshConfig.host}`, LOG_CONTEXT);
|
||||
|
||||
// Find the agent definition
|
||||
const agentDef = AGENT_DEFINITIONS.find((a) => a.id === agentId);
|
||||
if (!agentDef) {
|
||||
throw new Error(`Unknown agent: ${agentId}`);
|
||||
}
|
||||
|
||||
// Build SSH command to check for the binary using 'which'
|
||||
const remoteOptions: RemoteCommandOptions = {
|
||||
command: 'which',
|
||||
args: [agentDef.binaryName],
|
||||
};
|
||||
|
||||
try {
|
||||
const sshCommand = await buildSshCommand(sshConfig, remoteOptions);
|
||||
logger.info(`Executing SSH detection command for '${agentDef.binaryName}'`, LOG_CONTEXT, {
|
||||
command: sshCommand.command,
|
||||
args: sshCommand.args,
|
||||
});
|
||||
|
||||
// Execute with timeout
|
||||
const SSH_TIMEOUT_MS = 10000;
|
||||
const resultPromise = execFileNoThrow(sshCommand.command, sshCommand.args);
|
||||
const timeoutPromise = new Promise<{ exitCode: number; stdout: string; stderr: string }>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`SSH connection timed out after ${SSH_TIMEOUT_MS / 1000}s`)), SSH_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
const result = await Promise.race([resultPromise, timeoutPromise]);
|
||||
|
||||
logger.info(`SSH command result for '${agentDef.binaryName}'`, LOG_CONTEXT, {
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
});
|
||||
|
||||
// Check for SSH connection errors
|
||||
let connectionError: string | undefined;
|
||||
if (result.stderr && (
|
||||
result.stderr.includes('Connection refused') ||
|
||||
result.stderr.includes('Connection timed out') ||
|
||||
result.stderr.includes('No route to host') ||
|
||||
result.stderr.includes('Could not resolve hostname') ||
|
||||
result.stderr.includes('Permission denied')
|
||||
)) {
|
||||
connectionError = result.stderr.trim().split('\n')[0];
|
||||
logger.warn(`SSH connection error for ${sshConfig.host}: ${connectionError}`, LOG_CONTEXT);
|
||||
}
|
||||
|
||||
// Strip ANSI/OSC escape sequences from output
|
||||
const cleanedOutput = stripAnsi(result.stdout);
|
||||
const available = result.exitCode === 0 && cleanedOutput.trim().length > 0;
|
||||
const path = available ? cleanedOutput.trim().split('\n')[0] : undefined;
|
||||
|
||||
if (available) {
|
||||
logger.info(`Agent "${agentDef.name}" found on remote at: ${path}`, LOG_CONTEXT);
|
||||
} else {
|
||||
logger.debug(`Agent "${agentDef.name}" not found on remote`, LOG_CONTEXT);
|
||||
}
|
||||
|
||||
return stripAgentFunctions({
|
||||
...agentDef,
|
||||
available,
|
||||
path,
|
||||
capabilities: getAgentCapabilities(agentDef.id),
|
||||
error: connectionError,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.warn(`Failed to check agent "${agentDef.name}" on remote: ${errorMessage}`, LOG_CONTEXT);
|
||||
return stripAgentFunctions({
|
||||
...agentDef,
|
||||
available: false,
|
||||
capabilities: getAgentCapabilities(agentDef.id),
|
||||
error: `Failed to connect: ${errorMessage}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Local detection
|
||||
const agentDetector = requireDependency(getAgentDetector, 'Agent detector');
|
||||
const agent = await agentDetector.getAgent(agentId);
|
||||
|
||||
// Debug logging for agent availability
|
||||
logger.debug(`Agent retrieved: ${agentId}`, LOG_CONTEXT, {
|
||||
available: agent?.available,
|
||||
hasPath: !!agent?.path,
|
||||
path: agent?.path,
|
||||
command: agent?.command,
|
||||
hasCustomPath: !!agent?.customPath,
|
||||
customPath: agent?.customPath,
|
||||
});
|
||||
|
||||
// Strip argBuilder functions before sending over IPC
|
||||
return stripAgentFunctions(agent);
|
||||
})
|
||||
);
|
||||
|
||||
// Get capabilities for a specific agent
|
||||
ipcMain.handle(
|
||||
|
||||
@@ -90,13 +90,16 @@ export function registerGitHandlers(deps: GitHandlerDependencies): void {
|
||||
gitSettingsStore = deps.settingsStore;
|
||||
// Basic Git operations
|
||||
// All handlers accept optional sshRemoteId and remoteCwd for remote execution
|
||||
|
||||
// --- FIX: Always pass cwd as remoteCwd for remote git operations ---
|
||||
ipcMain.handle(
|
||||
'git:status',
|
||||
withIpcErrorLogging(
|
||||
handlerOpts('status'),
|
||||
async (cwd: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const result = await execGit(['status', '--porcelain'], cwd, sshRemote, remoteCwd);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
const result = await execGit(['status', '--porcelain'], cwd, sshRemote, effectiveRemoteCwd);
|
||||
return { stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
)
|
||||
@@ -109,7 +112,8 @@ export function registerGitHandlers(deps: GitHandlerDependencies): void {
|
||||
async (cwd: string, file?: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
const args = file ? ['diff', file] : ['diff'];
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const result = await execGit(args, cwd, sshRemote, remoteCwd);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
const result = await execGit(args, cwd, sshRemote, effectiveRemoteCwd);
|
||||
return { stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
)
|
||||
@@ -121,11 +125,12 @@ export function registerGitHandlers(deps: GitHandlerDependencies): void {
|
||||
handlerOpts('isRepo'),
|
||||
async (cwd: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
const result = await execGit(
|
||||
['rev-parse', '--is-inside-work-tree'],
|
||||
cwd,
|
||||
sshRemote,
|
||||
remoteCwd
|
||||
effectiveRemoteCwd
|
||||
);
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
@@ -138,7 +143,8 @@ export function registerGitHandlers(deps: GitHandlerDependencies): void {
|
||||
handlerOpts('numstat'),
|
||||
async (cwd: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const result = await execGit(['diff', '--numstat'], cwd, sshRemote, remoteCwd);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
const result = await execGit(['diff', '--numstat'], cwd, sshRemote, effectiveRemoteCwd);
|
||||
return { stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
)
|
||||
@@ -150,11 +156,12 @@ export function registerGitHandlers(deps: GitHandlerDependencies): void {
|
||||
handlerOpts('branch'),
|
||||
async (cwd: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
const result = await execGit(
|
||||
['rev-parse', '--abbrev-ref', 'HEAD'],
|
||||
cwd,
|
||||
sshRemote,
|
||||
remoteCwd
|
||||
effectiveRemoteCwd
|
||||
);
|
||||
return { stdout: result.stdout.trim(), stderr: result.stderr };
|
||||
}
|
||||
@@ -167,26 +174,30 @@ export function registerGitHandlers(deps: GitHandlerDependencies): void {
|
||||
handlerOpts('remote'),
|
||||
async (cwd: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const result = await execGit(['remote', 'get-url', 'origin'], cwd, sshRemote, remoteCwd);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
const result = await execGit(
|
||||
['remote', 'get-url', 'origin'],
|
||||
cwd,
|
||||
sshRemote,
|
||||
effectiveRemoteCwd
|
||||
);
|
||||
return { stdout: result.stdout.trim(), stderr: result.stderr };
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Get all local and remote branches
|
||||
ipcMain.handle(
|
||||
'git:branches',
|
||||
withIpcErrorLogging(
|
||||
handlerOpts('branches'),
|
||||
async (cwd: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
// Get all branches (local and remote) in a simple format
|
||||
// -a for all branches, --format to get clean names
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
const result = await execGit(
|
||||
['branch', '-a', '--format=%(refname:short)'],
|
||||
cwd,
|
||||
sshRemote,
|
||||
remoteCwd
|
||||
effectiveRemoteCwd
|
||||
);
|
||||
if (result.exitCode !== 0) {
|
||||
return { branches: [], stderr: result.stderr };
|
||||
@@ -198,14 +209,14 @@ export function registerGitHandlers(deps: GitHandlerDependencies): void {
|
||||
)
|
||||
);
|
||||
|
||||
// Get all tags
|
||||
ipcMain.handle(
|
||||
'git:tags',
|
||||
withIpcErrorLogging(
|
||||
handlerOpts('tags'),
|
||||
async (cwd: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const result = await execGit(['tag', '--list'], cwd, sshRemote, remoteCwd);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
const result = await execGit(['tag', '--list'], cwd, sshRemote, effectiveRemoteCwd);
|
||||
if (result.exitCode !== 0) {
|
||||
return { tags: [], stderr: result.stderr };
|
||||
}
|
||||
@@ -222,16 +233,17 @@ export function registerGitHandlers(deps: GitHandlerDependencies): void {
|
||||
handlerOpts('info'),
|
||||
async (cwd: string, sshRemoteId?: string, remoteCwd?: string) => {
|
||||
const sshRemote = getSshRemoteById(sshRemoteId);
|
||||
const effectiveRemoteCwd = sshRemote ? remoteCwd || cwd : undefined;
|
||||
// Get comprehensive git info in a single call
|
||||
const [branchResult, remoteResult, statusResult, behindAheadResult] = await Promise.all([
|
||||
execGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd, sshRemote, remoteCwd),
|
||||
execGit(['remote', 'get-url', 'origin'], cwd, sshRemote, remoteCwd),
|
||||
execGit(['status', '--porcelain'], cwd, sshRemote, remoteCwd),
|
||||
execGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd, sshRemote, effectiveRemoteCwd),
|
||||
execGit(['remote', 'get-url', 'origin'], cwd, sshRemote, effectiveRemoteCwd),
|
||||
execGit(['status', '--porcelain'], cwd, sshRemote, effectiveRemoteCwd),
|
||||
execGit(
|
||||
['rev-list', '--left-right', '--count', '@{upstream}...HEAD'],
|
||||
cwd,
|
||||
sshRemote,
|
||||
remoteCwd
|
||||
effectiveRemoteCwd
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -174,6 +174,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[] }>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +135,15 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
containsNewline: config.prompt.includes('\n'),
|
||||
}
|
||||
: undefined,
|
||||
// SSH remote config logging
|
||||
hasSessionSshRemoteConfig: !!config.sessionSshRemoteConfig,
|
||||
sessionSshRemoteConfig: config.sessionSshRemoteConfig
|
||||
? {
|
||||
enabled: config.sessionSshRemoteConfig.enabled,
|
||||
remoteId: config.sessionSshRemoteConfig.remoteId,
|
||||
hasWorkingDirOverride: !!config.sessionSshRemoteConfig.workingDirOverride,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
let finalArgs = buildAgentArgs(agent, {
|
||||
baseArgs: config.args,
|
||||
@@ -241,6 +251,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;
|
||||
@@ -250,6 +267,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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -260,9 +291,19 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
|
||||
// Only consider SSH remote for non-terminal AI agent sessions
|
||||
// SSH is session-level ONLY - no agent-level or global defaults
|
||||
if (config.toolType !== 'terminal' && config.sessionSshRemoteConfig) {
|
||||
// Log SSH evaluation on Windows for debugging
|
||||
if (isWindows) {
|
||||
logger.info(`Evaluating SSH remote config`, LOG_CONTEXT, {
|
||||
toolType: config.toolType,
|
||||
isTerminal: config.toolType === 'terminal',
|
||||
hasSessionSshRemoteConfig: !!config.sessionSshRemoteConfig,
|
||||
sshEnabled: config.sessionSshRemoteConfig?.enabled,
|
||||
willUseSsh: config.toolType !== 'terminal' && config.sessionSshRemoteConfig?.enabled,
|
||||
});
|
||||
}
|
||||
if (config.toolType !== 'terminal' && config.sessionSshRemoteConfig?.enabled) {
|
||||
// Session-level SSH config provided - resolve and use it
|
||||
logger.debug(`Using session-level SSH config`, LOG_CONTEXT, {
|
||||
logger.info(`Using session-level SSH config`, LOG_CONTEXT, {
|
||||
sessionId: config.sessionId,
|
||||
enabled: config.sessionSshRemoteConfig.enabled,
|
||||
remoteId: config.sessionSshRemoteConfig.remoteId,
|
||||
@@ -281,8 +322,14 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
// For SSH execution, we need to include the prompt in the args here
|
||||
// because ProcessManager.spawn() won't add it (we pass prompt: undefined for SSH)
|
||||
// Use promptArgs if available (e.g., OpenCode -p), otherwise use positional arg
|
||||
//
|
||||
// IMPORTANT: For large prompts (>4000 chars), don't embed in command line to avoid
|
||||
// Windows command line length limits (~8191 chars). SSH wrapping adds significant overhead.
|
||||
// Instead, add --input-format stream-json and let ProcessManager send via stdin.
|
||||
const isLargePrompt = config.prompt && config.prompt.length > 4000;
|
||||
let sshArgs = finalArgs;
|
||||
if (config.prompt) {
|
||||
if (config.prompt && !isLargePrompt) {
|
||||
// Small prompt - embed in command line as usual
|
||||
if (agent?.promptArgs) {
|
||||
sshArgs = [...finalArgs, ...agent.promptArgs(config.prompt)];
|
||||
} else if (agent?.noPromptSeparator) {
|
||||
@@ -290,6 +337,15 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
} else {
|
||||
sshArgs = [...finalArgs, '--', config.prompt];
|
||||
}
|
||||
} else if (config.prompt && isLargePrompt) {
|
||||
// Large prompt - use stdin mode
|
||||
// Add --input-format stream-json flag so agent reads from stdin
|
||||
sshArgs = [...finalArgs, '--input-format', 'stream-json'];
|
||||
logger.info(`Using stdin for large prompt in SSH remote execution`, LOG_CONTEXT, {
|
||||
sessionId: config.sessionId,
|
||||
promptLength: config.prompt.length,
|
||||
reason: 'avoid-command-line-length-limit',
|
||||
});
|
||||
}
|
||||
|
||||
// Build the SSH command that wraps the agent execution
|
||||
@@ -300,6 +356,9 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
// the remote shell's PATH resolve it. This avoids using local paths like
|
||||
// '/opt/homebrew/bin/codex' which don't exist on the remote host.
|
||||
const remoteCommand = config.sessionCustomPath || agent?.binaryName || config.command;
|
||||
// Decide whether we'll send input via stdin to the remote command
|
||||
const useStdin = sshArgs.includes('--input-format') && sshArgs.includes('stream-json');
|
||||
|
||||
const sshCommand = await buildSshCommand(sshResult.config, {
|
||||
command: remoteCommand,
|
||||
args: sshArgs,
|
||||
@@ -307,23 +366,31 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
cwd: config.cwd,
|
||||
// Pass custom environment variables to the remote command
|
||||
env: effectiveCustomEnvVars,
|
||||
// Explicitly indicate whether stdin will be used so ssh-command-builder
|
||||
// can avoid forcing a TTY for stream-json modes.
|
||||
useStdin,
|
||||
});
|
||||
|
||||
commandToSpawn = sshCommand.command;
|
||||
argsToSpawn = sshCommand.args;
|
||||
|
||||
logger.info(`SSH remote execution configured`, LOG_CONTEXT, {
|
||||
// Detailed debug logging to diagnose SSH command execution issues
|
||||
logger.debug(`SSH command details for debugging`, LOG_CONTEXT, {
|
||||
sessionId: config.sessionId,
|
||||
toolType: config.toolType,
|
||||
remoteName: sshResult.config.name,
|
||||
remoteHost: sshResult.config.host,
|
||||
source: sshResult.source,
|
||||
localCommand: config.command,
|
||||
remoteCommand: remoteCommand,
|
||||
customPath: config.sessionCustomPath || null,
|
||||
hasCustomEnvVars:
|
||||
!!effectiveCustomEnvVars && Object.keys(effectiveCustomEnvVars).length > 0,
|
||||
sshCommand: `${sshCommand.command} ${sshCommand.args.join(' ')}`,
|
||||
sshBinary: sshCommand.command,
|
||||
sshArgsCount: sshCommand.args.length,
|
||||
sshArgsArray: sshCommand.args,
|
||||
// Show the last arg which contains the wrapped remote command
|
||||
remoteCommandString: sshCommand.args[sshCommand.args.length - 1],
|
||||
// Show the agent command that will execute remotely
|
||||
agentBinary: remoteCommand,
|
||||
agentArgs: sshArgs,
|
||||
agentCwd: config.cwd,
|
||||
// Full invocation for copy-paste debugging
|
||||
fullSshInvocation: `${sshCommand.command} ${sshCommand.args
|
||||
.map((arg) => (arg.includes(' ') ? `'${arg}'` : arg))
|
||||
.join(' ')}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -339,9 +406,14 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
||||
// When using SSH, disable PTY (SSH provides its own terminal handling)
|
||||
// and env vars are passed via the remote command string
|
||||
requiresPty: sshRemoteUsed ? false : agent?.requiresPty,
|
||||
// When using SSH, the prompt was already added to sshArgs above before
|
||||
// building the SSH command, so don't let ProcessManager add it again
|
||||
prompt: sshRemoteUsed ? undefined : config.prompt,
|
||||
// When using SSH with small prompts, the prompt was already added to sshArgs above
|
||||
// For large prompts, pass it to ProcessManager so it can send via stdin
|
||||
prompt:
|
||||
sshRemoteUsed && config.prompt && config.prompt.length > 4000
|
||||
? config.prompt
|
||||
: sshRemoteUsed
|
||||
? undefined
|
||||
: config.prompt,
|
||||
shell: shellToUse,
|
||||
shellArgs: shellArgsStr, // Shell-specific CLI args (for terminal sessions)
|
||||
shellEnvVars: shellEnvVars, // Shell-specific env vars (for terminal sessions)
|
||||
|
||||
@@ -376,7 +376,7 @@ export class ChildProcessSpawner {
|
||||
this.exitHandler.handleError(sessionId, error);
|
||||
});
|
||||
|
||||
// Handle stdin for batch mode
|
||||
// Handle stdin for batch mode and stream-json
|
||||
if (isStreamJsonMode && prompt && images) {
|
||||
// Stream-json mode with images: send the message via stdin
|
||||
const streamJsonMessage = buildStreamJsonMessage(prompt, images);
|
||||
@@ -387,6 +387,19 @@ export class ChildProcessSpawner {
|
||||
});
|
||||
childProcess.stdin?.write(streamJsonMessage + '\n');
|
||||
childProcess.stdin?.end();
|
||||
} else if (isStreamJsonMode && prompt) {
|
||||
// Stream-json mode with prompt but no images: send JSON via stdin
|
||||
const streamJsonMessage = buildStreamJsonMessage(prompt, []);
|
||||
logger.debug(
|
||||
'[ProcessManager] Sending stream-json prompt via stdin (no images)',
|
||||
'ProcessManager',
|
||||
{
|
||||
sessionId,
|
||||
promptLength: prompt.length,
|
||||
}
|
||||
);
|
||||
childProcess.stdin?.write(streamJsonMessage + '\n');
|
||||
childProcess.stdin?.end();
|
||||
} else if (isBatchMode) {
|
||||
// Regular batch mode: close stdin immediately
|
||||
logger.debug('[ProcessManager] Closing stdin for batch mode', 'ProcessManager', {
|
||||
|
||||
@@ -27,6 +27,7 @@ export function getExpandedEnv(): NodeJS.ProcessEnv {
|
||||
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
||||
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
||||
const systemRoot = process.env.SystemRoot || 'C:\\Windows';
|
||||
|
||||
additionalPaths = [
|
||||
path.join(appData, 'npm'),
|
||||
@@ -34,7 +35,9 @@ export function getExpandedEnv(): NodeJS.ProcessEnv {
|
||||
path.join(programFiles, 'cloudflared'),
|
||||
path.join(home, 'scoop', 'shims'),
|
||||
path.join(process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey', 'bin'),
|
||||
path.join(process.env.SystemRoot || 'C:\\Windows', 'System32'),
|
||||
path.join(systemRoot, 'System32'),
|
||||
// Windows OpenSSH (placed last so it's checked first due to unshift loop)
|
||||
path.join(systemRoot, 'System32', 'OpenSSH'),
|
||||
];
|
||||
} else {
|
||||
additionalPaths = [
|
||||
@@ -207,6 +210,20 @@ export async function detectSshPath(): Promise<string | null> {
|
||||
|
||||
if (result.exitCode === 0 && result.stdout.trim()) {
|
||||
sshPathCache = result.stdout.trim().split('\n')[0];
|
||||
} else if (process.platform === 'win32') {
|
||||
// Fallback for Windows: Check the built-in OpenSSH location directly
|
||||
// This is the standard location for Windows 10/11 OpenSSH
|
||||
const fs = await import('fs');
|
||||
const systemRoot = process.env.SystemRoot || 'C:\\Windows';
|
||||
const opensshPath = path.join(systemRoot, 'System32', 'OpenSSH', 'ssh.exe');
|
||||
|
||||
try {
|
||||
if (fs.existsSync(opensshPath)) {
|
||||
sshPathCache = opensshPath;
|
||||
}
|
||||
} catch {
|
||||
// If check fails, leave sshPathCache as null
|
||||
}
|
||||
}
|
||||
|
||||
sshDetectionDone = true;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { execFileNoThrow, ExecResult } from './execFile';
|
||||
import { shellEscape } from './shell-escape';
|
||||
import { sshRemoteManager } from '../ssh-remote-manager';
|
||||
import { logger } from './logger';
|
||||
import { resolveSshPath } from './cliDetection';
|
||||
|
||||
/**
|
||||
* File or directory entry returned from readDir operations.
|
||||
@@ -147,6 +148,9 @@ async function execRemoteCommand(
|
||||
const { maxRetries, baseDelayMs, maxDelayMs } = DEFAULT_RETRY_CONFIG;
|
||||
let lastResult: ExecResult | null = null;
|
||||
|
||||
// Resolve SSH binary path (critical for Windows where spawn() doesn't search PATH)
|
||||
const sshPath = await resolveSshPath();
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const sshArgs = deps.buildSshArgs(config);
|
||||
sshArgs.push(remoteCommand);
|
||||
@@ -154,11 +158,11 @@ async function execRemoteCommand(
|
||||
// Log SSH command for debugging connection issues
|
||||
if (attempt === 0) {
|
||||
logger.debug(
|
||||
`[remote-fs] SSH to ${config.host}: ssh ${sshArgs.slice(0, -1).join(' ')} "<command>"`
|
||||
`[remote-fs] SSH to ${config.host}: ${sshPath} ${sshArgs.slice(0, -1).join(' ')} "<command>"`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await deps.execSsh('ssh', sshArgs);
|
||||
const result = await deps.execSsh(sshPath, sshArgs);
|
||||
lastResult = result;
|
||||
|
||||
// Success - return immediately
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface RemoteCommandOptions {
|
||||
cwd?: string;
|
||||
/** Environment variables to set on the remote (optional) */
|
||||
env?: Record<string, string>;
|
||||
/** Indicates the caller will send input via stdin to the remote command (optional) */
|
||||
useStdin?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +49,7 @@ const DEFAULT_SSH_OPTIONS: Record<string, string> = {
|
||||
StrictHostKeyChecking: 'accept-new', // Auto-accept new host keys
|
||||
ConnectTimeout: '10', // Connection timeout in seconds
|
||||
ClearAllForwardings: 'yes', // Disable port forwarding from SSH config (avoids "Address already in use" errors)
|
||||
RequestTTY: 'force', // Force TTY allocation - required for Claude Code's --print mode to produce output
|
||||
RequestTTY: 'no', // Default: do NOT request a TTY. We only force a TTY for specific remote modes (e.g., --print)
|
||||
LogLevel: 'ERROR', // Suppress SSH warnings like "Pseudo-terminal will not be allocated..."
|
||||
};
|
||||
|
||||
@@ -99,13 +101,21 @@ export function buildRemoteCommand(options: RemoteCommandOptions): string {
|
||||
// Build the command with arguments
|
||||
const commandWithArgs = buildShellCommand(command, args);
|
||||
|
||||
// If command expects JSON via stdin (stream-json), use exec to replace the
|
||||
// shell process so stdin is delivered directly to the agent binary and no
|
||||
// intermediate shell produces control sequences that could corrupt the stream.
|
||||
const hasStreamJsonInput = options.useStdin
|
||||
? true
|
||||
: Array.isArray(args) && args.includes('--input-format') && args.includes('stream-json');
|
||||
const finalCommandWithArgs = hasStreamJsonInput ? `exec ${commandWithArgs}` : commandWithArgs;
|
||||
|
||||
// Combine env exports with command
|
||||
let fullCommand: string;
|
||||
if (envExports.length > 0) {
|
||||
// Prepend env vars inline: VAR1='val1' VAR2='val2' command args
|
||||
fullCommand = `${envExports.join(' ')} ${commandWithArgs}`;
|
||||
fullCommand = `${envExports.join(' ')} ${finalCommandWithArgs}`;
|
||||
} else {
|
||||
fullCommand = commandWithArgs;
|
||||
fullCommand = finalCommandWithArgs;
|
||||
}
|
||||
|
||||
parts.push(fullCommand);
|
||||
@@ -175,9 +185,31 @@ export async function buildSshCommand(
|
||||
// Resolve the SSH binary path (handles packaged Electron apps where PATH is limited)
|
||||
const sshPath = await resolveSshPath();
|
||||
|
||||
// Force TTY allocation - required for Claude Code's --print mode to produce output
|
||||
// Without a TTY, Claude Code with --print hangs indefinitely
|
||||
args.push('-tt');
|
||||
// Decide whether we need to force a TTY for the remote command.
|
||||
// Historically we forced a TTY for Claude Code when running with `--print`.
|
||||
// However, for stream-json input (sending JSON via stdin) a TTY injects terminal
|
||||
// control sequences that corrupt the stream. Only enable forced TTY for cases
|
||||
// that explicitly require it (e.g., `--print` without `--input-format stream-json`).
|
||||
const remoteArgs = remoteOptions.args || [];
|
||||
const hasPrintFlag = remoteArgs.includes('--print');
|
||||
const hasStreamJsonInput = remoteOptions.useStdin
|
||||
? true
|
||||
: remoteArgs.includes('--input-format') && remoteArgs.includes('stream-json');
|
||||
const forceTty = Boolean(hasPrintFlag && !hasStreamJsonInput);
|
||||
|
||||
// Log the decision so callers can debug why a TTY was or was not forced
|
||||
logger.debug('SSH TTY decision', '[ssh-command-builder]', {
|
||||
host: config.host,
|
||||
useStdinFlag: !!remoteOptions.useStdin,
|
||||
hasPrintFlag,
|
||||
hasStreamJsonInput,
|
||||
forceTty,
|
||||
});
|
||||
|
||||
if (forceTty) {
|
||||
// -tt must come first for reliable forced allocation in some SSH implementations
|
||||
args.push('-tt');
|
||||
}
|
||||
|
||||
// When using SSH config, we let SSH handle authentication settings
|
||||
// Only add explicit overrides if provided
|
||||
@@ -192,9 +224,15 @@ export async function buildSshCommand(
|
||||
}
|
||||
|
||||
// Default SSH options for non-interactive operation
|
||||
// These are always needed to ensure BatchMode behavior
|
||||
// These are always needed to ensure BatchMode behavior. If `forceTty` is true,
|
||||
// override RequestTTY to `force` so SSH will allocate a TTY even in non-interactive contexts.
|
||||
for (const [key, value] of Object.entries(DEFAULT_SSH_OPTIONS)) {
|
||||
args.push('-o', `${key}=${value}`);
|
||||
// If we will force a TTY for this command, override the RequestTTY option
|
||||
if (key === 'RequestTTY' && forceTty) {
|
||||
args.push('-o', `${key}=force`);
|
||||
} else {
|
||||
args.push('-o', `${key}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Port specification - only add if not default and not using SSH config
|
||||
@@ -238,18 +276,15 @@ export async function buildSshCommand(
|
||||
});
|
||||
|
||||
// Wrap the command to execute via the user's login shell.
|
||||
// $SHELL -ilc ensures the user's full PATH (including homebrew, nvm, etc.) is available.
|
||||
// -i forces interactive mode (critical for .bashrc to not bail out)
|
||||
// $SHELL -lc ensures the user's full PATH (including homebrew, nvm, etc.) is available.
|
||||
// -l loads login profile for PATH
|
||||
// -c executes the command
|
||||
// Using $SHELL respects the user's configured shell (bash, zsh, etc.)
|
||||
//
|
||||
// WHY -i IS CRITICAL:
|
||||
// On Ubuntu (and many Linux distros), .bashrc has a guard at the top:
|
||||
// case $- in *i*) ;; *) return;; esac
|
||||
// This checks if the shell is interactive before running. Without -i,
|
||||
// .bashrc exits early and user PATH additions (like ~/.local/bin) never load.
|
||||
// The -i flag sets 'i' in $-, allowing .bashrc to run fully.
|
||||
// WHY PROFILE SOURCING IS NEEDED:
|
||||
// On many systems, login shells don't automatically source interactive config files.
|
||||
// We explicitly source profile and rc files to ensure PATH and environment are set up
|
||||
// properly for finding agent binaries like 'claude', 'codex', etc.
|
||||
//
|
||||
// CRITICAL: When Node.js spawn() passes this to SSH without shell:true, SSH runs
|
||||
// the command through the remote's default shell. The key is escaping:
|
||||
@@ -261,20 +296,31 @@ export async function buildSshCommand(
|
||||
// Example transformation for spawn():
|
||||
// Input: cd '/path' && MYVAR='value' claude --print
|
||||
// After escaping: cd '/path' && MYVAR='value' claude --print (no $ to escape here)
|
||||
// Wrapped: $SHELL -ilc "cd '/path' && MYVAR='value' claude --print"
|
||||
// Wrapped: $SHELL -lc "source ~/.bashrc 2>/dev/null; cd '/path' && MYVAR='value' claude --print"
|
||||
// SSH receives this as one argument, passes to remote shell
|
||||
// Remote shell expands $SHELL, executes: /bin/zsh -ilc "cd '/path' ..."
|
||||
// The login shell runs with full PATH from ~/.zprofile AND ~/.bashrc
|
||||
// The login shell runs with full PATH from /etc/profile, ~/.bash_profile, AND ~/.bashrc
|
||||
const escapedCommand = shellEscapeForDoubleQuotes(remoteCommand);
|
||||
const wrappedCommand = `$SHELL -ilc "${escapedCommand}"`;
|
||||
// Source common profile files that work across different shells (bash, zsh, etc.)
|
||||
// This ensures PATH is available for finding agent binaries
|
||||
const profileSourcing =
|
||||
'source ~/.bashrc 2>/dev/null || source ~/.zshrc 2>/dev/null || source ~/.profile 2>/dev/null || source ~/.bash_profile 2>/dev/null || true';
|
||||
const wrappedCommand = `$SHELL -lc "${profileSourcing}; ${escapedCommand}"`;
|
||||
args.push(wrappedCommand);
|
||||
|
||||
// Debug logging to trace the exact command being built
|
||||
logger.info('Built SSH command', '[ssh-command-builder]', {
|
||||
logger.debug('Built SSH command', '[ssh-command-builder]', {
|
||||
host: config.host,
|
||||
username: config.username,
|
||||
port: config.port,
|
||||
useSshConfig: config.useSshConfig,
|
||||
privateKeyPath: config.privateKeyPath ? '***configured***' : undefined,
|
||||
remoteCommand,
|
||||
wrappedCommand,
|
||||
sshPath,
|
||||
sshArgs: args,
|
||||
fullCommand: `${sshPath} ${args.join(' ')}`,
|
||||
// Show the exact command string that will execute on the remote
|
||||
remoteExecutionString: wrappedCommand,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -6331,6 +6331,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
customEnvVars: activeSession.customEnvVars,
|
||||
customModel: activeSession.customModel,
|
||||
customContextWindow: activeSession.customContextWindow,
|
||||
sessionSshRemoteConfig: activeSession.sessionSshRemoteConfig,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6511,7 +6512,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
|
||||
@@ -6593,7 +6595,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
|
||||
|
||||
@@ -578,23 +578,34 @@ export function NewInstanceModal({
|
||||
}
|
||||
}, [isOpen, sourceSession]);
|
||||
|
||||
// Load SSH remote configurations independently of agent detection
|
||||
// This ensures SSH remotes are available even if agent detection fails
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const loadSshConfigs = async () => {
|
||||
try {
|
||||
const sshConfigsResult = await window.maestro.sshRemote.getConfigs();
|
||||
if (sshConfigsResult.success && sshConfigsResult.configs) {
|
||||
setSshRemotes(sshConfigsResult.configs);
|
||||
}
|
||||
} catch (sshError) {
|
||||
console.error('Failed to load SSH remote configs:', sshError);
|
||||
}
|
||||
};
|
||||
loadSshConfigs();
|
||||
}
|
||||
}, [isOpen]);
|
||||
// Load SSH remote configurations independently of agent detection
|
||||
// This ensures SSH remotes are available even if agent detection fails
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const loadSshConfigs = async () => {
|
||||
try {
|
||||
const sshConfigsResult = await window.maestro.sshRemote.getConfigs();
|
||||
if (sshConfigsResult.success && sshConfigsResult.configs) {
|
||||
setSshRemotes(sshConfigsResult.configs);
|
||||
}
|
||||
} catch (sshError) {
|
||||
console.error('Failed to load SSH remote configs:', sshError);
|
||||
}
|
||||
};
|
||||
loadSshConfigs();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Transfer pending SSH config to selected agent automatically
|
||||
// This ensures SSH config is preserved when agent is auto-selected or manually clicked
|
||||
useEffect(() => {
|
||||
if (selectedAgent && agentSshRemoteConfigs['_pending_'] && !agentSshRemoteConfigs[selectedAgent]) {
|
||||
setAgentSshRemoteConfigs(prev => ({
|
||||
...prev,
|
||||
[selectedAgent]: prev['_pending_'],
|
||||
}));
|
||||
}
|
||||
}, [selectedAgent, agentSshRemoteConfigs]);
|
||||
|
||||
// Track the current SSH remote ID for re-detection
|
||||
// Uses _pending_ key when no agent is selected, which is the shared SSH config
|
||||
|
||||
@@ -409,6 +409,12 @@ export interface SerializableWizardState {
|
||||
generatedDocuments: GeneratedDocument[];
|
||||
editedPhase1Content: string | null;
|
||||
wantsTour: boolean;
|
||||
/** Per-session SSH remote configuration (for remote execution) */
|
||||
sessionSshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
remoteId: string | null;
|
||||
workingDirOverride?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -755,6 +761,7 @@ export function WizardProvider({ children }: WizardProviderProps) {
|
||||
generatedDocuments: state.generatedDocuments,
|
||||
editedPhase1Content: state.editedPhase1Content,
|
||||
wantsTour: state.wantsTour,
|
||||
sessionSshRemoteConfig: state.sessionSshRemoteConfig,
|
||||
};
|
||||
}, [
|
||||
state.currentStep,
|
||||
@@ -768,6 +775,7 @@ export function WizardProvider({ children }: WizardProviderProps) {
|
||||
state.generatedDocuments,
|
||||
state.editedPhase1Content,
|
||||
state.wantsTour,
|
||||
state.sessionSshRemoteConfig,
|
||||
]);
|
||||
|
||||
const saveStateForResume = useCallback(() => {
|
||||
|
||||
@@ -74,11 +74,21 @@ export function WizardResumeModal({
|
||||
let dirValid = true;
|
||||
if (resumeState.directoryPath) {
|
||||
try {
|
||||
// Get SSH remote ID from resume state (for remote execution)
|
||||
const sshRemoteId = resumeState.sessionSshRemoteConfig?.enabled
|
||||
? resumeState.sessionSshRemoteConfig.remoteId ?? undefined
|
||||
: undefined;
|
||||
|
||||
// Use git.isRepo which will fail if directory doesn't exist
|
||||
await window.maestro.git.isRepo(resumeState.directoryPath);
|
||||
} catch {
|
||||
// For SSH remotes, pass the path as remoteCwd so git can operate in the correct directory
|
||||
await window.maestro.git.isRepo(
|
||||
resumeState.directoryPath,
|
||||
sshRemoteId,
|
||||
sshRemoteId ? resumeState.directoryPath : undefined
|
||||
);
|
||||
} catch {
|
||||
// Directory doesn't exist or is inaccessible
|
||||
dirValid = false;
|
||||
dirValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -680,12 +680,13 @@ export function ConversationScreen({
|
||||
// Fetch existing docs if continuing from previous session
|
||||
const existingDocs = await fetchExistingDocs();
|
||||
|
||||
await conversationManager.startConversation({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||
});
|
||||
await conversationManager.startConversation({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||
sshRemoteConfig: state.sessionSshRemoteConfig,
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
setConversationStarted(true);
|
||||
@@ -812,6 +813,7 @@ export function ConversationScreen({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
sshRemoteConfig: state.sessionSshRemoteConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1061,13 +1063,14 @@ export function ConversationScreen({
|
||||
}
|
||||
}
|
||||
|
||||
await conversationManager.startConversation({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||
});
|
||||
}
|
||||
await conversationManager.startConversation({
|
||||
agentType: state.selectedAgent,
|
||||
directoryPath: state.directoryPath,
|
||||
projectName: state.agentName || 'My Project',
|
||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||
sshRemoteConfig: state.sessionSshRemoteConfig,
|
||||
});
|
||||
}
|
||||
|
||||
// Send message and wait for response
|
||||
const result = await conversationManager.sendMessage(
|
||||
|
||||
@@ -93,6 +93,9 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
|
||||
const continueButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Ref for debouncing validation
|
||||
const validationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
/**
|
||||
* Fetch agent config when selected agent changes
|
||||
*/
|
||||
@@ -125,6 +128,17 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
|
||||
setIsDetecting(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Cleanup validation timeout on unmount
|
||||
*/
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (validationTimeoutRef.current) {
|
||||
clearTimeout(validationTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Load SSH remote host name when remote is configured
|
||||
*/
|
||||
@@ -288,18 +302,31 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
|
||||
const newPath = e.target.value;
|
||||
setDirectoryPath(newPath);
|
||||
|
||||
// Debounce validation to avoid excessive API calls while typing
|
||||
// Clear any pending validation
|
||||
if (validationTimeoutRef.current) {
|
||||
clearTimeout(validationTimeoutRef.current);
|
||||
validationTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
// Debounce validation to avoid excessive API calls while typing (especially over SSH)
|
||||
if (newPath.trim()) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
validationTimeoutRef.current = setTimeout(() => {
|
||||
validateDirectory(newPath);
|
||||
}, 500);
|
||||
return () => clearTimeout(timeoutId);
|
||||
validationTimeoutRef.current = null;
|
||||
}, 800); // 800ms debounce for SSH remote checks
|
||||
} else {
|
||||
setDirectoryError(null);
|
||||
setIsGitRepo(false);
|
||||
setHasExistingAutoRunDocs(false, 0);
|
||||
}
|
||||
},
|
||||
[setDirectoryPath, setDirectoryError, setIsGitRepo, validateDirectory]
|
||||
[
|
||||
setDirectoryPath,
|
||||
setDirectoryError,
|
||||
setIsGitRepo,
|
||||
setHasExistingAutoRunDocs,
|
||||
validateDirectory,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -726,6 +726,7 @@ export function PreparingPlanScreen({ theme }: PreparingPlanScreenProps): JSX.El
|
||||
projectName: state.agentName || 'My Project',
|
||||
conversationHistory: state.conversationHistory,
|
||||
subfolder: 'Initiation',
|
||||
sshRemoteConfig: state.sessionSshRemoteConfig,
|
||||
},
|
||||
{
|
||||
onStart: () => {
|
||||
|
||||
@@ -37,6 +37,12 @@ export interface ConversationConfig {
|
||||
projectName: string;
|
||||
/** Existing Auto Run documents (when continuing from previous session) */
|
||||
existingDocs?: ExistingDocument[];
|
||||
/** SSH remote configuration (for remote execution) */
|
||||
sshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
remoteId: string | null;
|
||||
workingDirOverride?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,6 +118,12 @@ interface ConversationSession {
|
||||
toolExecutionListenerCleanup?: () => void;
|
||||
/** Timeout ID for response timeout (for cleanup) */
|
||||
responseTimeoutId?: NodeJS.Timeout;
|
||||
/** SSH remote configuration (for remote execution) */
|
||||
sshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
remoteId: string | null;
|
||||
workingDirOverride?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +174,7 @@ class ConversationManager {
|
||||
isActive: true,
|
||||
systemPrompt,
|
||||
outputBuffer: '',
|
||||
sshRemoteConfig: config.sshRemoteConfig,
|
||||
};
|
||||
|
||||
// Log conversation start
|
||||
@@ -172,6 +185,8 @@ class ConversationManager {
|
||||
projectName: config.projectName,
|
||||
hasExistingDocs: !!config.existingDocs,
|
||||
existingDocsCount: config.existingDocs?.length || 0,
|
||||
hasRemoteSsh: !!config.sshRemoteConfig?.enabled,
|
||||
remoteId: config.sshRemoteConfig?.remoteId || null,
|
||||
});
|
||||
|
||||
return sessionId;
|
||||
@@ -220,11 +235,16 @@ class ConversationManager {
|
||||
try {
|
||||
// Get the agent configuration
|
||||
const agent = await window.maestro.agents.get(this.session.agentType);
|
||||
if (!agent || !agent.available) {
|
||||
const error = `Agent ${this.session.agentType} is not available`;
|
||||
wizardDebugLogger.log('error', 'Agent not available', {
|
||||
|
||||
// For SSH remote sessions, skip the availability check since we're executing remotely
|
||||
// The agent detector checks for binaries locally, but we need to execute on the remote host
|
||||
const isRemoteSession =
|
||||
this.session.sshRemoteConfig?.enabled && this.session.sshRemoteConfig?.remoteId;
|
||||
|
||||
if (!agent) {
|
||||
const error = `Agent ${this.session.agentType} configuration not found`;
|
||||
wizardDebugLogger.log('error', 'Agent config not found', {
|
||||
agentType: this.session.agentType,
|
||||
agent: agent ? { available: agent.available } : null,
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
@@ -232,6 +252,39 @@ class ConversationManager {
|
||||
};
|
||||
}
|
||||
|
||||
// Only check availability for local sessions
|
||||
if (!isRemoteSession && !agent.available) {
|
||||
const error = `Agent ${this.session.agentType} is not available locally`;
|
||||
wizardDebugLogger.log('error', 'Agent not available locally', {
|
||||
agentType: this.session.agentType,
|
||||
agent: {
|
||||
available: agent.available,
|
||||
path: agent.path,
|
||||
command: agent.command,
|
||||
customPath: (agent as any).customPath,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
// For remote sessions, log that we're skipping the availability check
|
||||
if (isRemoteSession) {
|
||||
wizardDebugLogger.log(
|
||||
'info',
|
||||
'Executing agent on SSH remote (skipping local availability check)',
|
||||
{
|
||||
agentType: this.session.agentType,
|
||||
remoteId: this.session.sshRemoteConfig?.remoteId,
|
||||
agentCommand: agent.command,
|
||||
agentPath: agent.path,
|
||||
agentCustomPath: (agent as any).customPath,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Build the full prompt with conversation context
|
||||
const fullPrompt = this.buildPromptWithContext(userMessage, conversationHistory);
|
||||
|
||||
@@ -319,8 +372,6 @@ class ConversationManager {
|
||||
}
|
||||
|
||||
return new Promise<SendMessageResult>((resolve) => {
|
||||
console.log('[Wizard] Setting up listeners for session:', this.session!.sessionId);
|
||||
|
||||
wizardDebugLogger.log('spawn', 'Setting up agent spawn', {
|
||||
sessionId: this.session!.sessionId,
|
||||
agentId: agent.id,
|
||||
@@ -331,12 +382,6 @@ class ConversationManager {
|
||||
|
||||
// Set up timeout (20 minutes for wizard's complex prompts - large codebases need time)
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log(
|
||||
'[Wizard] TIMEOUT fired! Session:',
|
||||
this.session?.sessionId,
|
||||
'Buffer length:',
|
||||
this.session?.outputBuffer?.length
|
||||
);
|
||||
wizardDebugLogger.log('timeout', 'Response timeout after 20 minutes', {
|
||||
sessionId: this.session?.sessionId,
|
||||
outputBufferLength: this.session?.outputBuffer?.length || 0,
|
||||
@@ -395,13 +440,13 @@ class ConversationManager {
|
||||
// Set up exit listener
|
||||
this.session!.exitListenerCleanup = window.maestro.process.onExit(
|
||||
(sessionId: string, code: number) => {
|
||||
console.log('[Wizard] Exit event received:', {
|
||||
wizardDebugLogger.log('exit', 'Exit event received', {
|
||||
receivedId: sessionId,
|
||||
expectedId: this.session?.sessionId,
|
||||
code,
|
||||
});
|
||||
if (sessionId === this.session?.sessionId) {
|
||||
console.log('[Wizard] Session ID matched! Processing exit...');
|
||||
wizardDebugLogger.log('exit', 'Session ID matched, processing exit', { sessionId });
|
||||
// Clear timeout since we got a response
|
||||
clearTimeout(timeoutId);
|
||||
if (this.session) {
|
||||
@@ -413,7 +458,12 @@ class ConversationManager {
|
||||
|
||||
if (code === 0) {
|
||||
const parsedResponse = this.parseAgentOutput();
|
||||
console.log('[Wizard] Parsed response:', parsedResponse);
|
||||
wizardDebugLogger.log('data', 'Parsed agent response', {
|
||||
parseSuccess: parsedResponse.parseSuccess,
|
||||
hasStructured: !!parsedResponse.structured,
|
||||
confidence: parsedResponse.structured?.confidence,
|
||||
ready: parsedResponse.structured?.ready,
|
||||
});
|
||||
wizardDebugLogger.log('exit', `Agent exited successfully (code 0)`, {
|
||||
sessionId,
|
||||
outputBufferLength: this.session?.outputBuffer?.length || 0,
|
||||
@@ -430,7 +480,11 @@ class ConversationManager {
|
||||
const detectedError = detectWizardError(rawOutput);
|
||||
|
||||
if (detectedError) {
|
||||
console.log('[Wizard] Detected provider error:', detectedError);
|
||||
wizardDebugLogger.log('error', 'Detected provider error', {
|
||||
errorType: detectedError.type,
|
||||
errorTitle: detectedError.title,
|
||||
errorMessage: detectedError.message,
|
||||
});
|
||||
wizardDebugLogger.log('exit', `Agent exited with provider error (code ${code})`, {
|
||||
sessionId,
|
||||
exitCode: code,
|
||||
@@ -447,24 +501,51 @@ class ConversationManager {
|
||||
detectedError,
|
||||
});
|
||||
} else {
|
||||
// No specific error detected, create generic message
|
||||
const errorMessage = createGenericErrorMessage(rawOutput, code);
|
||||
wizardDebugLogger.log('exit', `Agent exited with error (code ${code})`, {
|
||||
sessionId,
|
||||
exitCode: code,
|
||||
errorMessage,
|
||||
rawOutputLength: rawOutput.length,
|
||||
rawOutputPreview: rawOutput.slice(-500),
|
||||
});
|
||||
resolve({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
rawOutput,
|
||||
});
|
||||
// Try to parse the output as a structured response
|
||||
const parsedResponse = this.parseAgentOutput();
|
||||
if (
|
||||
parsedResponse.parseSuccess &&
|
||||
(parsedResponse.structured || parsedResponse.rawText)
|
||||
) {
|
||||
wizardDebugLogger.log(
|
||||
'exit',
|
||||
`Agent exited nonzero but output parsed as valid response`,
|
||||
{
|
||||
sessionId,
|
||||
exitCode: code,
|
||||
parseSuccess: parsedResponse.parseSuccess,
|
||||
hasStructured: !!parsedResponse.structured,
|
||||
rawTextLength: parsedResponse.rawText?.length,
|
||||
}
|
||||
);
|
||||
resolve({
|
||||
success: true,
|
||||
response: parsedResponse,
|
||||
rawOutput,
|
||||
});
|
||||
} else {
|
||||
// No specific error detected, create generic message
|
||||
const errorMessage = createGenericErrorMessage(rawOutput, code);
|
||||
wizardDebugLogger.log('exit', `Agent exited with error (code ${code})`, {
|
||||
sessionId,
|
||||
exitCode: code,
|
||||
errorMessage,
|
||||
rawOutputLength: rawOutput.length,
|
||||
rawOutputPreview: rawOutput.slice(-500),
|
||||
});
|
||||
resolve({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
rawOutput,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('[Wizard] Session ID mismatch, ignoring exit event');
|
||||
wizardDebugLogger.log('exit', 'Session ID mismatch, ignoring exit event', {
|
||||
receivedId: sessionId,
|
||||
expectedId: this.session?.sessionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -480,6 +561,19 @@ class ConversationManager {
|
||||
// This is critical for packaged Electron apps where PATH may not include agent locations
|
||||
const commandToUse = agent.path || agent.command;
|
||||
|
||||
// Log spawn details to main process
|
||||
wizardDebugLogger.log('spawn', 'Preparing to spawn agent process', {
|
||||
sessionId: this.session!.sessionId,
|
||||
toolType: this.session!.agentType,
|
||||
command: commandToUse,
|
||||
agentPath: agent.path,
|
||||
agentCommand: agent.command,
|
||||
argsCount: argsForSpawn.length,
|
||||
cwd: this.session!.directoryPath,
|
||||
hasRemoteSsh: !!this.session!.sshRemoteConfig?.enabled,
|
||||
remoteId: this.session!.sshRemoteConfig?.remoteId || null,
|
||||
});
|
||||
|
||||
wizardDebugLogger.log('spawn', 'Calling process.spawn', {
|
||||
sessionId: this.session!.sessionId,
|
||||
command: commandToUse,
|
||||
@@ -487,6 +581,8 @@ class ConversationManager {
|
||||
agentCommand: agent.command,
|
||||
args: argsForSpawn,
|
||||
cwd: this.session!.directoryPath,
|
||||
hasRemoteSsh: !!this.session!.sshRemoteConfig?.enabled,
|
||||
remoteId: this.session!.sshRemoteConfig?.remoteId || null,
|
||||
});
|
||||
|
||||
window.maestro.process
|
||||
@@ -497,6 +593,8 @@ class ConversationManager {
|
||||
command: commandToUse,
|
||||
args: argsForSpawn,
|
||||
prompt: prompt,
|
||||
// Pass SSH configuration for remote execution
|
||||
sessionSshRemoteConfig: this.session!.sshRemoteConfig,
|
||||
})
|
||||
.then(() => {
|
||||
wizardDebugLogger.log('spawn', 'Agent process spawned successfully', {
|
||||
@@ -548,11 +646,50 @@ class ConversationManager {
|
||||
return args;
|
||||
}
|
||||
|
||||
case 'codex':
|
||||
case 'codex': {
|
||||
// Codex requires exec batch mode with JSON output for wizard conversations
|
||||
// Must include these explicitly since wizard pre-builds args before IPC handler
|
||||
const args = [];
|
||||
|
||||
// Add batch mode prefix: 'exec'
|
||||
if (agent.batchModePrefix) {
|
||||
args.push(...agent.batchModePrefix);
|
||||
}
|
||||
|
||||
// Add base args (if any)
|
||||
args.push(...(agent.args || []));
|
||||
|
||||
// Add batch mode args: '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check'
|
||||
if (agent.batchModeArgs) {
|
||||
args.push(...agent.batchModeArgs);
|
||||
}
|
||||
|
||||
// Add JSON output: '--json'
|
||||
if (agent.jsonOutputArgs) {
|
||||
args.push(...agent.jsonOutputArgs);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
case 'opencode': {
|
||||
// For Codex and OpenCode, use base args only
|
||||
// The IPC handler will add batchModePrefix, jsonOutputArgs, batchModeArgs, workingDirArgs
|
||||
return [...(agent.args || [])];
|
||||
// OpenCode requires 'run' batch mode with JSON output for wizard conversations
|
||||
const args = [];
|
||||
|
||||
// Add batch mode prefix: 'run'
|
||||
if (agent.batchModePrefix) {
|
||||
args.push(...agent.batchModePrefix);
|
||||
}
|
||||
|
||||
// Add base args (if any)
|
||||
args.push(...(agent.args || []));
|
||||
|
||||
// Add JSON output: '--format json'
|
||||
if (agent.jsonOutputArgs) {
|
||||
args.push(...agent.jsonOutputArgs);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -576,18 +713,23 @@ class ConversationManager {
|
||||
}
|
||||
|
||||
const output = this.session.outputBuffer;
|
||||
console.log('[Wizard] Raw output buffer length:', output.length);
|
||||
console.log('[Wizard] Raw output preview (last 500):', output.slice(-500));
|
||||
wizardDebugLogger.log('data', 'Raw output buffer details', {
|
||||
bufferLength: output.length,
|
||||
bufferPreview: output.slice(-500),
|
||||
});
|
||||
|
||||
// Try to extract the result from stream-json format
|
||||
const extractedResult = this.extractResultFromStreamJson(output);
|
||||
const textToParse = extractedResult || output;
|
||||
|
||||
console.log('[Wizard] Extracted result:', extractedResult ? 'YES' : 'NO (using raw)');
|
||||
console.log('[Wizard] Text to parse:', textToParse.slice(0, 300));
|
||||
wizardDebugLogger.log('data', 'Stream JSON extraction result', {
|
||||
extracted: !!extractedResult,
|
||||
textToParseLength: textToParse.length,
|
||||
textToParsePreview: textToParse.slice(0, 300),
|
||||
});
|
||||
|
||||
const parsed = parseStructuredOutput(textToParse);
|
||||
console.log('[Wizard] Parse result:', {
|
||||
wizardDebugLogger.log('data', 'Parse result', {
|
||||
parseSuccess: parsed.parseSuccess,
|
||||
hasStructured: !!parsed.structured,
|
||||
confidence: parsed.structured?.confidence,
|
||||
|
||||
@@ -18,16 +18,22 @@ import {
|
||||
* Configuration for document generation
|
||||
*/
|
||||
export interface GenerationConfig {
|
||||
/** Agent type to use for generation */
|
||||
agentType: ToolType;
|
||||
/** Working directory for the agent */
|
||||
directoryPath: string;
|
||||
/** Project name from wizard */
|
||||
projectName: string;
|
||||
/** Full conversation history from project discovery */
|
||||
conversationHistory: WizardMessage[];
|
||||
/** Optional subfolder within Auto Run Docs (e.g., "Initiation") */
|
||||
subfolder?: string;
|
||||
/** Agent type to use for generation */
|
||||
agentType: ToolType;
|
||||
/** Working directory for the agent */
|
||||
directoryPath: string;
|
||||
/** Project name from wizard */
|
||||
projectName: string;
|
||||
/** Full conversation history from project discovery */
|
||||
conversationHistory: WizardMessage[];
|
||||
/** Optional subfolder within Auto Run Docs (e.g., "Initiation") */
|
||||
subfolder?: string;
|
||||
/** SSH remote configuration (for remote execution) */
|
||||
sshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
remoteId: string | null;
|
||||
workingDirOverride?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -570,23 +576,52 @@ class PhaseGenerator {
|
||||
callbacks?.onStart?.();
|
||||
callbacks?.onProgress?.('Preparing to generate your Playbook...');
|
||||
|
||||
try {
|
||||
// Get the agent configuration
|
||||
wizardDebugLogger.log('info', 'Fetching agent configuration', {
|
||||
agentType: config.agentType,
|
||||
});
|
||||
const agent = await window.maestro.agents.get(config.agentType);
|
||||
if (!agent || !agent.available) {
|
||||
wizardDebugLogger.log('error', 'Agent not available', {
|
||||
agentType: config.agentType,
|
||||
agent,
|
||||
});
|
||||
throw new Error(`Agent ${config.agentType} is not available`);
|
||||
}
|
||||
wizardDebugLogger.log('info', 'Agent configuration retrieved', {
|
||||
command: agent.command,
|
||||
argsCount: agent.args?.length || 0,
|
||||
});
|
||||
try {
|
||||
// Get the agent configuration
|
||||
wizardDebugLogger.log('info', 'Fetching agent configuration', { agentType: config.agentType });
|
||||
const agent = await window.maestro.agents.get(config.agentType);
|
||||
|
||||
// For SSH remote sessions, skip the availability check since we're executing remotely
|
||||
// The agent detector checks for binaries locally, but we need to execute on the remote host
|
||||
const isRemoteSession = config.sshRemoteConfig?.enabled && config.sshRemoteConfig?.remoteId;
|
||||
|
||||
if (!agent) {
|
||||
wizardDebugLogger.log('error', 'Agent configuration not found', { agentType: config.agentType });
|
||||
throw new Error(`Agent ${config.agentType} configuration not found`);
|
||||
}
|
||||
|
||||
// Only check availability for local sessions
|
||||
if (!isRemoteSession && !agent.available) {
|
||||
wizardDebugLogger.log('error', 'Agent not available locally', { agentType: config.agentType, agent });
|
||||
|
||||
// Provide helpful error message with guidance
|
||||
let errorMsg = `The ${config.agentType} agent is not available locally.`;
|
||||
|
||||
if (agent?.customPath) {
|
||||
errorMsg += `\n\nThe custom path "${agent.customPath}" is not valid. The file may not exist or may not be executable.`;
|
||||
errorMsg += `\n\nTo fix this:\n1. Click "Go Back" to return to agent selection\n2. Click the settings icon on the agent tile\n3. Update the custom path or clear it to use the system PATH\n4. Click "Refresh" to re-detect the agent`;
|
||||
} else {
|
||||
errorMsg += `\n\nThe agent was not found in your system PATH.`;
|
||||
errorMsg += `\n\nTo fix this:\n1. Install ${config.agentType} on your system\n2. Or click "Go Back" and configure a custom path in the agent settings`;
|
||||
}
|
||||
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
// For remote sessions, log that we're skipping the availability check
|
||||
if (isRemoteSession) {
|
||||
wizardDebugLogger.log('info', 'Executing agent on SSH remote (skipping local availability check)', {
|
||||
agentType: config.agentType,
|
||||
remoteId: config.sshRemoteConfig?.remoteId,
|
||||
agentCommand: agent.command,
|
||||
agentPath: agent.path,
|
||||
agentCustomPath: (agent as any).customPath,
|
||||
});
|
||||
}
|
||||
wizardDebugLogger.log('info', 'Agent configuration retrieved', {
|
||||
command: agent.command,
|
||||
argsCount: agent.args?.length || 0,
|
||||
});
|
||||
|
||||
// Generate the prompt
|
||||
const prompt = generateDocumentGenerationPrompt(config);
|
||||
@@ -1054,47 +1089,51 @@ class PhaseGenerator {
|
||||
// This is critical for packaged Electron apps where PATH may not include agent locations
|
||||
const commandToUse = agent.path || agent.command;
|
||||
|
||||
wizardDebugLogger.log('spawn', 'Calling process.spawn', {
|
||||
sessionId,
|
||||
toolType: config.agentType,
|
||||
cwd: config.directoryPath,
|
||||
command: commandToUse,
|
||||
agentPath: agent.path,
|
||||
agentCommand: agent.command,
|
||||
argsCount: argsForSpawn.length,
|
||||
promptLength: prompt.length,
|
||||
});
|
||||
window.maestro.process
|
||||
.spawn({
|
||||
sessionId,
|
||||
toolType: config.agentType,
|
||||
cwd: config.directoryPath,
|
||||
command: commandToUse,
|
||||
args: argsForSpawn,
|
||||
prompt,
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[PhaseGenerator] Agent spawned successfully');
|
||||
wizardDebugLogger.log('spawn', 'Agent spawned successfully', { sessionId });
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error('[PhaseGenerator] Spawn failed:', error.message);
|
||||
wizardDebugLogger.log('error', 'Spawn failed', {
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
this.cleanup();
|
||||
if (fileWatcherCleanup) {
|
||||
fileWatcherCleanup();
|
||||
}
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to spawn agent: ${error.message}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
wizardDebugLogger.log('spawn', 'Calling process.spawn', {
|
||||
sessionId,
|
||||
toolType: config.agentType,
|
||||
cwd: config.directoryPath,
|
||||
command: commandToUse,
|
||||
agentPath: agent.path,
|
||||
agentCommand: agent.command,
|
||||
argsCount: argsForSpawn.length,
|
||||
promptLength: prompt.length,
|
||||
hasRemoteSsh: !!config.sshRemoteConfig?.enabled,
|
||||
remoteId: config.sshRemoteConfig?.remoteId || null,
|
||||
});
|
||||
window.maestro.process
|
||||
.spawn({
|
||||
sessionId,
|
||||
toolType: config.agentType,
|
||||
cwd: config.directoryPath,
|
||||
command: commandToUse,
|
||||
args: argsForSpawn,
|
||||
prompt,
|
||||
// Pass SSH configuration for remote execution
|
||||
sessionSshRemoteConfig: config.sshRemoteConfig,
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[PhaseGenerator] Agent spawned successfully');
|
||||
wizardDebugLogger.log('spawn', 'Agent spawned successfully', { sessionId });
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error('[PhaseGenerator] Spawn failed:', error.message);
|
||||
wizardDebugLogger.log('error', 'Spawn failed', {
|
||||
errorMessage: error.message,
|
||||
errorStack: error.stack,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
this.cleanup();
|
||||
if (fileWatcherCleanup) {
|
||||
fileWatcherCleanup();
|
||||
}
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to spawn agent: ${error.message}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read documents from the Auto Run Docs folder on disk
|
||||
|
||||
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;
|
||||
|
||||
@@ -530,6 +530,15 @@ export function useAgentExecution(deps: UseAgentExecutionDeps): UseAgentExecutio
|
||||
);
|
||||
|
||||
// Spawn with session resume - the IPC handler will use the agent's resumeArgs builder
|
||||
// If no sessionConfig or no sessionSshRemoteConfig, try to get it from the main session (by sessionId)
|
||||
let effectiveSessionSshRemoteConfig = sessionConfig?.sessionSshRemoteConfig;
|
||||
if (!effectiveSessionSshRemoteConfig) {
|
||||
// Try to find the main session and use its SSH config
|
||||
const mainSession = sessionsRef.current.find((s) => s.id === sessionId);
|
||||
if (mainSession && mainSession.sessionSshRemoteConfig) {
|
||||
effectiveSessionSshRemoteConfig = mainSession.sessionSshRemoteConfig;
|
||||
}
|
||||
}
|
||||
const commandToUse = sessionConfig?.customPath || agent.path || agent.command;
|
||||
window.maestro.process
|
||||
.spawn({
|
||||
@@ -546,8 +555,8 @@ export function useAgentExecution(deps: UseAgentExecutionDeps): UseAgentExecutio
|
||||
sessionCustomEnvVars: sessionConfig?.customEnvVars,
|
||||
sessionCustomModel: sessionConfig?.customModel,
|
||||
sessionCustomContextWindow: sessionConfig?.customContextWindow,
|
||||
// Per-session SSH remote config (takes precedence over agent-level SSH config)
|
||||
sessionSshRemoteConfig: sessionConfig?.sessionSshRemoteConfig,
|
||||
// Always use effective SSH remote config if available
|
||||
sessionSshRemoteConfig: effectiveSessionSshRemoteConfig,
|
||||
})
|
||||
.catch(() => {
|
||||
cleanup();
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../../utils/fileExplorer';
|
||||
import { fuzzyMatch } from '../../utils/search';
|
||||
import { gitService } from '../../services/git';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
/**
|
||||
* Retry delay for file tree errors (20 seconds).
|
||||
@@ -26,14 +27,41 @@ const FILE_TREE_RETRY_DELAY_MS = 20000;
|
||||
* we must fall back to sessionSshRemoteConfig.remoteId. See CLAUDE.md "SSH Remote Sessions".
|
||||
*/
|
||||
function getSshContext(session: Session): SshContext | undefined {
|
||||
const sshRemoteId = session.sshRemoteId || session.sessionSshRemoteConfig?.remoteId || undefined;
|
||||
// First check if there's a spawned sshRemoteId (set by agent spawn)
|
||||
let sshRemoteId: string | undefined = session.sshRemoteId;
|
||||
|
||||
// Fall back to sessionSshRemoteConfig if enabled and has a valid remoteId
|
||||
// Note: remoteId can be `null` per the type definition, so we explicitly check for truthiness
|
||||
if (
|
||||
!sshRemoteId &&
|
||||
session.sessionSshRemoteConfig?.enabled &&
|
||||
session.sessionSshRemoteConfig?.remoteId
|
||||
) {
|
||||
sshRemoteId = session.sessionSshRemoteConfig.remoteId;
|
||||
}
|
||||
|
||||
logger.debug('getSshContext: session.sshRemoteId', 'FileTreeManagement', {
|
||||
sshRemoteId: session.sshRemoteId,
|
||||
});
|
||||
logger.debug('getSshContext: session.sessionSshRemoteConfig', 'FileTreeManagement', {
|
||||
sessionSshRemoteConfig: session.sessionSshRemoteConfig,
|
||||
});
|
||||
logger.debug('getSshContext: resolved sshRemoteId', 'FileTreeManagement', { sshRemoteId });
|
||||
|
||||
if (!sshRemoteId) {
|
||||
logger.debug(
|
||||
'getSshContext: No SSH remote ID found, returning undefined',
|
||||
'FileTreeManagement'
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
|
||||
const context = {
|
||||
sshRemoteId,
|
||||
remoteCwd: session.remoteCwd || session.sessionSshRemoteConfig?.workingDirOverride,
|
||||
};
|
||||
logger.debug('getSshContext: Returning context', 'FileTreeManagement', context);
|
||||
return context;
|
||||
}
|
||||
|
||||
export type { RightPanelHandle } from '../../components/RightPanel';
|
||||
@@ -143,7 +171,9 @@ export function useFileTreeManagement(
|
||||
|
||||
return changes;
|
||||
} catch (error) {
|
||||
console.error('File tree refresh error:', error);
|
||||
logger.error('File tree refresh error', 'FileTreeManagement', {
|
||||
error: (error as Error)?.message || 'Unknown error',
|
||||
});
|
||||
const errorMsg = (error as Error)?.message || 'Unknown error';
|
||||
setSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
@@ -229,7 +259,9 @@ export function useFileTreeManagement(
|
||||
await window.maestro.history.reload();
|
||||
rightPanelRef.current?.refreshHistoryPanel();
|
||||
} catch (error) {
|
||||
console.error('Git/file state refresh error:', error);
|
||||
logger.error('Git/file state refresh error', 'FileTreeManagement', {
|
||||
error: (error as Error)?.message || 'Unknown error',
|
||||
});
|
||||
const errorMsg = (error as Error)?.message || 'Unknown error';
|
||||
setSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
@@ -353,7 +385,9 @@ export function useFileTreeManagement(
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('File tree error:', error);
|
||||
logger.error('File tree error', 'FileTreeManagement', {
|
||||
error: error?.message || 'Unknown error',
|
||||
});
|
||||
const errorMsg = error?.message || 'Unknown error';
|
||||
setSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -319,7 +319,7 @@ export class ContextGroomingService {
|
||||
// Use the new single-call groomContext API (spawns batch process with prompt)
|
||||
const groomedText = await window.maestro.context.groomContext(
|
||||
targetProjectRoot,
|
||||
this.config.defaultAgentType,
|
||||
request.targetAgent,
|
||||
prompt
|
||||
);
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ export class ContextSummarizationService {
|
||||
|
||||
const summarizedText = await window.maestro.context.groomContext(
|
||||
request.projectRoot,
|
||||
this.config.defaultAgentType,
|
||||
request.agentType,
|
||||
prompt
|
||||
);
|
||||
console.log('[ContextSummarizer] Received response, length:', summarizedText?.length || 0);
|
||||
@@ -225,7 +225,7 @@ export class ContextSummarizationService {
|
||||
// Use the new single-call groomContext API (spawns batch process with prompt)
|
||||
const summary = await window.maestro.context.groomContext(
|
||||
request.projectRoot,
|
||||
this.config.defaultAgentType,
|
||||
request.agentType,
|
||||
prompt
|
||||
);
|
||||
chunkSummaries.push(summary);
|
||||
@@ -258,7 +258,7 @@ export class ContextSummarizationService {
|
||||
|
||||
const consolidated = await window.maestro.context.groomContext(
|
||||
request.projectRoot,
|
||||
this.config.defaultAgentType,
|
||||
request.agentType,
|
||||
consolidationPrompt
|
||||
);
|
||||
|
||||
|
||||
@@ -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,10 @@ 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,14 +455,56 @@ 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;
|
||||
}
|
||||
|
||||
case 'codex':
|
||||
case 'codex': {
|
||||
// Codex requires exec batch mode with JSON output for wizard conversations
|
||||
// Must include these explicitly since wizard pre-builds args before IPC handler
|
||||
const args = [];
|
||||
|
||||
// Add batch mode prefix: 'exec'
|
||||
if (agent.batchModePrefix) {
|
||||
args.push(...agent.batchModePrefix);
|
||||
}
|
||||
|
||||
// Add base args (if any)
|
||||
args.push(...(agent.args || []));
|
||||
|
||||
// Add batch mode args: '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check'
|
||||
if (agent.batchModeArgs) {
|
||||
args.push(...agent.batchModeArgs);
|
||||
}
|
||||
|
||||
// Add JSON output: '--json'
|
||||
if (agent.jsonOutputArgs) {
|
||||
args.push(...agent.jsonOutputArgs);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
case 'opencode': {
|
||||
return [...(agent.args || [])];
|
||||
// OpenCode requires 'run' batch mode with JSON output for wizard conversations
|
||||
const args = [];
|
||||
|
||||
// Add batch mode prefix: 'run'
|
||||
if (agent.batchModePrefix) {
|
||||
args.push(...agent.batchModePrefix);
|
||||
}
|
||||
|
||||
// Add base args (if any)
|
||||
args.push(...(agent.args || []));
|
||||
|
||||
// Add JSON output: '--format json'
|
||||
if (agent.jsonOutputArgs) {
|
||||
args.push(...agent.jsonOutputArgs);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -506,11 +564,21 @@ export async function sendWizardMessage(
|
||||
|
||||
// Set up timeout (5 minutes for complex prompts)
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log('[InlineWizard] TIMEOUT fired! Session:', session.sessionId);
|
||||
logger.warn('Inline wizard response timeout', '[InlineWizardConversation]', {
|
||||
sessionId: session.sessionId,
|
||||
timeoutMs: 300000,
|
||||
});
|
||||
cleanupListeners();
|
||||
// Kill the orphaned agent process to prevent resource leaks
|
||||
window.maestro.process.kill(session.sessionId).catch((err) => {
|
||||
console.warn('[InlineWizard] Failed to kill timed-out process:', err);
|
||||
logger.warn(
|
||||
'Failed to kill timed-out inline wizard process',
|
||||
'[InlineWizardConversation]',
|
||||
{
|
||||
sessionId: session.sessionId,
|
||||
error: (err as Error)?.message || 'Unknown error',
|
||||
}
|
||||
);
|
||||
});
|
||||
resolve({
|
||||
success: false,
|
||||
@@ -560,7 +628,10 @@ export async function sendWizardMessage(
|
||||
try {
|
||||
callbacks.onThinkingChunk!(content);
|
||||
} catch (err) {
|
||||
console.error('[InlineWizard] onThinkingChunk callback threw error:', err);
|
||||
logger.error('onThinkingChunk callback threw error', '[InlineWizardConversation]', {
|
||||
sessionId: session.sessionId,
|
||||
error: (err as Error)?.message || 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -580,7 +651,11 @@ export async function sendWizardMessage(
|
||||
try {
|
||||
callbacks.onToolExecution!(toolEvent);
|
||||
} catch (err) {
|
||||
console.error('[InlineWizard] onToolExecution callback threw error:', err);
|
||||
logger.error('onToolExecution callback threw error', '[InlineWizardConversation]', {
|
||||
sessionId: session.sessionId,
|
||||
toolName: toolEvent.toolName,
|
||||
error: (err as Error)?.message || 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -648,6 +723,8 @@ export async function sendWizardMessage(
|
||||
command: agent.command,
|
||||
args: argsForSpawn,
|
||||
prompt: fullPrompt,
|
||||
// Pass SSH config for remote execution
|
||||
sessionSshRemoteConfig: session.sessionSshRemoteConfig,
|
||||
})
|
||||
.then(() => {
|
||||
callbacks?.onReceiving?.();
|
||||
|
||||
@@ -575,9 +575,50 @@ function buildArgsForAgent(agent: { id: string; args?: string[] }): string[] {
|
||||
return args;
|
||||
}
|
||||
|
||||
case 'codex':
|
||||
case 'codex': {
|
||||
// Codex requires exec batch mode with JSON output for document generation
|
||||
// Must include these explicitly since wizard pre-builds args before IPC handler
|
||||
const args = [];
|
||||
|
||||
// Add batch mode prefix: 'exec'
|
||||
if ((agent as any).batchModePrefix) {
|
||||
args.push(...(agent as any).batchModePrefix);
|
||||
}
|
||||
|
||||
// Add base args (if any)
|
||||
args.push(...(agent.args || []));
|
||||
|
||||
// Add batch mode args: '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check'
|
||||
if ((agent as any).batchModeArgs) {
|
||||
args.push(...(agent as any).batchModeArgs);
|
||||
}
|
||||
|
||||
// Add JSON output: '--json'
|
||||
if ((agent as any).jsonOutputArgs) {
|
||||
args.push(...(agent as any).jsonOutputArgs);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
case 'opencode': {
|
||||
return [...(agent.args || [])];
|
||||
// OpenCode requires 'run' batch mode with JSON output for document generation
|
||||
const args = [];
|
||||
|
||||
// Add batch mode prefix: 'run'
|
||||
if ((agent as any).batchModePrefix) {
|
||||
args.push(...(agent as any).batchModePrefix);
|
||||
}
|
||||
|
||||
// Add base args (if any)
|
||||
args.push(...(agent.args || []));
|
||||
|
||||
// Add JSON output: '--format json'
|
||||
if ((agent as any).jsonOutputArgs) {
|
||||
args.push(...(agent as any).jsonOutputArgs);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
@@ -46,7 +46,8 @@ export function expandTilde(filePath: string, homeDir?: string): string {
|
||||
}
|
||||
|
||||
if (filePath.startsWith('~/')) {
|
||||
return path.join(home, filePath.slice(2));
|
||||
// Use POSIX path separator for consistency, especially for SSH remote paths
|
||||
return `${home}/${filePath.slice(2)}`;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
|
||||
Reference in New Issue
Block a user