mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
fix(ssh): stabilize and complete SSH remote execution support across wizard, IPC, and agent detection
- Added full SSH remote support to the wizard, including propagation of SSH config through all phases - Ensured SSH config flows correctly through IPC bridge (renderer → preload → main) - Improved agent detection with optional sshRemoteId and remote `which` resolution - Fixed SSH config transfer during manual agent creation and auto-selection flows - Added extensive debugging and structured logging for SSH execution, agent availability, and wizard message flow - Improved stdin/JSON streaming for large prompts and remote execution - Added input/output JSON stream handling and enhanced logging for SSH transport - Added sourcing of bash profiles to correctly resolve claude-code binary paths on remote hosts
This commit is contained in:
@@ -305,12 +305,112 @@ export function registerAgentsHandlers(deps: AgentsHandlerDependencies): void {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get a specific agent by ID
|
// Get a specific agent by ID (supports SSH remote detection via optional sshRemoteId)
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'agents:get',
|
'agents:get',
|
||||||
withIpcErrorLogging(handlerOpts('get'), async (agentId: string) => {
|
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 agentDetector = requireDependency(getAgentDetector, 'Agent detector');
|
||||||
logger.debug(`Getting agent: ${agentId}`, LOG_CONTEXT);
|
|
||||||
const agent = await agentDetector.getAgent(agentId);
|
const agent = await agentDetector.getAgent(agentId);
|
||||||
// Strip argBuilder functions before sending over IPC
|
// Strip argBuilder functions before sending over IPC
|
||||||
return stripAgentFunctions(agent);
|
return stripAgentFunctions(agent);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { buildSshCommand } from '../../utils/ssh-command-builder';
|
|||||||
import type { SshRemoteConfig } from '../../../shared/types';
|
import type { SshRemoteConfig } from '../../../shared/types';
|
||||||
import { powerManager } from '../../power-manager';
|
import { powerManager } from '../../power-manager';
|
||||||
import { MaestroSettings } from './persistence';
|
import { MaestroSettings } from './persistence';
|
||||||
|
import { getAgentCapabilities } from '../../agent-capabilities';
|
||||||
|
|
||||||
const LOG_CONTEXT = '[ProcessManager]';
|
const LOG_CONTEXT = '[ProcessManager]';
|
||||||
|
|
||||||
@@ -125,15 +126,19 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
hasPrompt: !!config.prompt,
|
hasPrompt: !!config.prompt,
|
||||||
promptLength: config.prompt?.length,
|
promptLength: config.prompt?.length,
|
||||||
// On Windows, show prompt preview to help debug truncation issues
|
// On Windows, show prompt preview to help debug truncation issues
|
||||||
promptPreview:
|
promptPreview: config.prompt && isWindows ? {
|
||||||
config.prompt && isWindows
|
|
||||||
? {
|
|
||||||
first50: config.prompt.substring(0, 50),
|
first50: config.prompt.substring(0, 50),
|
||||||
last50: config.prompt.substring(Math.max(0, config.prompt.length - 50)),
|
last50: config.prompt.substring(Math.max(0, config.prompt.length - 50)),
|
||||||
containsHash: config.prompt.includes('#'),
|
containsHash: config.prompt.includes('#'),
|
||||||
containsNewline: config.prompt.includes('\n'),
|
containsNewline: config.prompt.includes('\n'),
|
||||||
}
|
} : undefined,
|
||||||
: 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, {
|
let finalArgs = buildAgentArgs(agent, {
|
||||||
baseArgs: config.args,
|
baseArgs: config.args,
|
||||||
@@ -257,12 +262,22 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
// Terminal sessions are always local (they need PTY for shell interaction)
|
// Terminal sessions are always local (they need PTY for shell interaction)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
let sshRemoteUsed: SshRemoteConfig | null = null;
|
let sshRemoteUsed: SshRemoteConfig | null = null;
|
||||||
|
let shouldSendPromptViaStdin = false;
|
||||||
|
|
||||||
// Only consider SSH remote for non-terminal AI agent sessions
|
// Only consider SSH remote for non-terminal AI agent sessions
|
||||||
// SSH is session-level ONLY - no agent-level or global defaults
|
// SSH is session-level ONLY - no agent-level or global defaults
|
||||||
|
// 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,
|
||||||
|
willUseSsh: config.toolType !== 'terminal' && !!config.sessionSshRemoteConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (config.toolType !== 'terminal' && config.sessionSshRemoteConfig) {
|
if (config.toolType !== 'terminal' && config.sessionSshRemoteConfig) {
|
||||||
// Session-level SSH config provided - resolve and use it
|
// 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,
|
sessionId: config.sessionId,
|
||||||
enabled: config.sessionSshRemoteConfig.enabled,
|
enabled: config.sessionSshRemoteConfig.enabled,
|
||||||
remoteId: config.sessionSshRemoteConfig.remoteId,
|
remoteId: config.sessionSshRemoteConfig.remoteId,
|
||||||
@@ -278,11 +293,22 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
// SSH remote is configured - wrap the command for remote execution
|
// SSH remote is configured - wrap the command for remote execution
|
||||||
sshRemoteUsed = sshResult.config;
|
sshRemoteUsed = sshResult.config;
|
||||||
|
|
||||||
// For SSH execution, we need to include the prompt in the args here
|
// For SSH execution with stream-json capable agents (like Claude Code),
|
||||||
// because ProcessManager.spawn() won't add it (we pass prompt: undefined for SSH)
|
// we send the prompt via stdin instead of as a CLI arg to avoid command
|
||||||
// Use promptArgs if available (e.g., OpenCode -p), otherwise use positional arg
|
// line length limits and escaping issues. For other agents, we include
|
||||||
|
// the prompt in the args as before.
|
||||||
|
const capabilities = getAgentCapabilities(config.toolType);
|
||||||
let sshArgs = finalArgs;
|
let sshArgs = finalArgs;
|
||||||
|
|
||||||
if (config.prompt) {
|
if (config.prompt) {
|
||||||
|
// If agent supports stream-json input, send prompt via stdin
|
||||||
|
// ProcessManager will detect this and send the prompt as a JSON message
|
||||||
|
if (capabilities.supportsStreamJsonInput) {
|
||||||
|
shouldSendPromptViaStdin = true;
|
||||||
|
// Add --input-format stream-json flag so Claude knows to read from stdin
|
||||||
|
sshArgs = [...finalArgs, '--input-format', 'stream-json'];
|
||||||
|
} else {
|
||||||
|
// For agents that don't support stream-json, add prompt to args
|
||||||
if (agent?.promptArgs) {
|
if (agent?.promptArgs) {
|
||||||
sshArgs = [...finalArgs, ...agent.promptArgs(config.prompt)];
|
sshArgs = [...finalArgs, ...agent.promptArgs(config.prompt)];
|
||||||
} else if (agent?.noPromptSeparator) {
|
} else if (agent?.noPromptSeparator) {
|
||||||
@@ -290,6 +316,7 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
} else {
|
} else {
|
||||||
sshArgs = [...finalArgs, '--', config.prompt];
|
sshArgs = [...finalArgs, '--', config.prompt];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the SSH command that wraps the agent execution
|
// Build the SSH command that wraps the agent execution
|
||||||
@@ -321,9 +348,9 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
localCommand: config.command,
|
localCommand: config.command,
|
||||||
remoteCommand: remoteCommand,
|
remoteCommand: remoteCommand,
|
||||||
customPath: config.sessionCustomPath || null,
|
customPath: config.sessionCustomPath || null,
|
||||||
hasCustomEnvVars:
|
hasCustomEnvVars: !!effectiveCustomEnvVars && Object.keys(effectiveCustomEnvVars).length > 0,
|
||||||
!!effectiveCustomEnvVars && Object.keys(effectiveCustomEnvVars).length > 0,
|
|
||||||
sshCommand: `${sshCommand.command} ${sshCommand.args.join(' ')}`,
|
sshCommand: `${sshCommand.command} ${sshCommand.args.join(' ')}`,
|
||||||
|
promptViaStdin: shouldSendPromptViaStdin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,9 +366,10 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
// When using SSH, disable PTY (SSH provides its own terminal handling)
|
// When using SSH, disable PTY (SSH provides its own terminal handling)
|
||||||
// and env vars are passed via the remote command string
|
// and env vars are passed via the remote command string
|
||||||
requiresPty: sshRemoteUsed ? false : agent?.requiresPty,
|
requiresPty: sshRemoteUsed ? false : agent?.requiresPty,
|
||||||
// When using SSH, the prompt was already added to sshArgs above before
|
// When using SSH with stream-json capable agents, pass the prompt so
|
||||||
// building the SSH command, so don't let ProcessManager add it again
|
// ProcessManager can send it via stdin. For other SSH cases, prompt was
|
||||||
prompt: sshRemoteUsed ? undefined : config.prompt,
|
// already added to sshArgs, so pass undefined to prevent double-adding.
|
||||||
|
prompt: sshRemoteUsed && shouldSendPromptViaStdin ? config.prompt : sshRemoteUsed ? undefined : config.prompt,
|
||||||
shell: shellToUse,
|
shell: shellToUse,
|
||||||
shellArgs: shellArgsStr, // Shell-specific CLI args (for terminal sessions)
|
shellArgs: shellArgsStr, // Shell-specific CLI args (for terminal sessions)
|
||||||
shellEnvVars: shellEnvVars, // Shell-specific env vars (for terminal sessions)
|
shellEnvVars: shellEnvVars, // Shell-specific env vars (for terminal sessions)
|
||||||
@@ -354,6 +382,7 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
// Stats tracking: use cwd as projectPath if not explicitly provided
|
// Stats tracking: use cwd as projectPath if not explicitly provided
|
||||||
projectPath: config.cwd,
|
projectPath: config.cwd,
|
||||||
// SSH remote context (for SSH-specific error messages)
|
// SSH remote context (for SSH-specific error messages)
|
||||||
|
sshRemoteConfig: sshRemoteUsed,
|
||||||
sshRemoteId: sshRemoteUsed?.id,
|
sshRemoteId: sshRemoteUsed?.id,
|
||||||
sshRemoteHost: sshRemoteUsed?.host,
|
sshRemoteHost: sshRemoteUsed?.host,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -238,18 +238,19 @@ export async function buildSshCommand(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Wrap the command to execute via the user's login shell.
|
// 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.
|
// We use bash -lc to ensure the user's full PATH (including homebrew, nvm, etc.) is available.
|
||||||
// -i forces interactive mode (critical for .bashrc to not bail out)
|
// -l loads login profile for PATH (sources /etc/profile, ~/.bash_profile, etc.)
|
||||||
// -l loads login profile for PATH
|
|
||||||
// -c executes the command
|
// -c executes the command
|
||||||
// Using $SHELL respects the user's configured shell (bash, zsh, etc.)
|
|
||||||
//
|
//
|
||||||
// WHY -i IS CRITICAL:
|
// IMPORTANT: We don't use -i (interactive) flag because:
|
||||||
// On Ubuntu (and many Linux distros), .bashrc has a guard at the top:
|
// 1. It can cause issues with shells that check if stdin is a TTY
|
||||||
|
// 2. When using -tt flag, the shell already thinks it's interactive enough
|
||||||
|
// 3. On Ubuntu, we work around the "case $- in *i*)" guard by sourcing ~/.bashrc explicitly
|
||||||
|
//
|
||||||
|
// For bash specifically, we source ~/.bashrc to ensure user PATH additions are loaded.
|
||||||
|
// This handles the common Ubuntu pattern where .bashrc has:
|
||||||
// case $- in *i*) ;; *) return;; esac
|
// case $- in *i*) ;; *) return;; esac
|
||||||
// This checks if the shell is interactive before running. Without -i,
|
// By explicitly sourcing it, we bypass this guard.
|
||||||
// .bashrc exits early and user PATH additions (like ~/.local/bin) never load.
|
|
||||||
// The -i flag sets 'i' in $-, allowing .bashrc to run fully.
|
|
||||||
//
|
//
|
||||||
// CRITICAL: When Node.js spawn() passes this to SSH without shell:true, SSH runs
|
// 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:
|
// the command through the remote's default shell. The key is escaping:
|
||||||
@@ -261,12 +262,15 @@ export async function buildSshCommand(
|
|||||||
// Example transformation for spawn():
|
// Example transformation for spawn():
|
||||||
// Input: cd '/path' && MYVAR='value' claude --print
|
// Input: cd '/path' && MYVAR='value' claude --print
|
||||||
// After escaping: cd '/path' && MYVAR='value' claude --print (no $ to escape here)
|
// After escaping: cd '/path' && MYVAR='value' claude --print (no $ to escape here)
|
||||||
// Wrapped: $SHELL -ilc "cd '/path' && MYVAR='value' claude --print"
|
// Wrapped: bash -lc "source ~/.bashrc 2>/dev/null; cd '/path' && MYVAR='value' claude --print"
|
||||||
// SSH receives this as one argument, passes to remote shell
|
// 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 /etc/profile, ~/.bash_profile, AND ~/.bashrc
|
||||||
// The login shell runs with full PATH from ~/.zprofile AND ~/.bashrc
|
|
||||||
const escapedCommand = shellEscapeForDoubleQuotes(remoteCommand);
|
const escapedCommand = shellEscapeForDoubleQuotes(remoteCommand);
|
||||||
const wrappedCommand = `$SHELL -ilc "${escapedCommand}"`;
|
// Source login/profile files first so PATH modifications made in
|
||||||
|
// ~/.bash_profile or ~/.profile are available for non-interactive
|
||||||
|
// remote commands, then source ~/.bashrc to cover interactive
|
||||||
|
// additions that might also be present.
|
||||||
|
const wrappedCommand = `bash -lc "source ~/.bash_profile 2>/dev/null || source ~/.profile 2>/dev/null; source ~/.bashrc 2>/dev/null; ${escapedCommand}"`;
|
||||||
args.push(wrappedCommand);
|
args.push(wrappedCommand);
|
||||||
|
|
||||||
// Debug logging to trace the exact command being built
|
// Debug logging to trace the exact command being built
|
||||||
|
|||||||
@@ -596,6 +596,17 @@ export function NewInstanceModal({
|
|||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [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
|
// Track the current SSH remote ID for re-detection
|
||||||
// Uses _pending_ key when no agent is selected, which is the shared SSH config
|
// Uses _pending_ key when no agent is selected, which is the shared SSH config
|
||||||
const currentSshRemoteId = useMemo(() => {
|
const currentSshRemoteId = useMemo(() => {
|
||||||
|
|||||||
@@ -685,6 +685,7 @@ export function ConversationScreen({
|
|||||||
directoryPath: state.directoryPath,
|
directoryPath: state.directoryPath,
|
||||||
projectName: state.agentName || 'My Project',
|
projectName: state.agentName || 'My Project',
|
||||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||||
|
sessionSshRemoteConfig: state.sessionSshRemoteConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -1066,6 +1067,7 @@ export function ConversationScreen({
|
|||||||
directoryPath: state.directoryPath,
|
directoryPath: state.directoryPath,
|
||||||
projectName: state.agentName || 'My Project',
|
projectName: state.agentName || 'My Project',
|
||||||
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
existingDocs: existingDocs.length > 0 ? existingDocs : undefined,
|
||||||
|
sessionSshRemoteConfig: state.sessionSshRemoteConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ export interface ConversationConfig {
|
|||||||
projectName: string;
|
projectName: string;
|
||||||
/** Existing Auto Run documents (when continuing from previous session) */
|
/** Existing Auto Run documents (when continuing from previous session) */
|
||||||
existingDocs?: ExistingDocument[];
|
existingDocs?: ExistingDocument[];
|
||||||
|
/** SSH remote configuration for remote agent execution */
|
||||||
|
sessionSshRemoteConfig?: {
|
||||||
|
enabled: boolean;
|
||||||
|
remoteId: string | null;
|
||||||
|
workingDirOverride?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,6 +118,12 @@ interface ConversationSession {
|
|||||||
toolExecutionListenerCleanup?: () => void;
|
toolExecutionListenerCleanup?: () => void;
|
||||||
/** Timeout ID for response timeout (for cleanup) */
|
/** Timeout ID for response timeout (for cleanup) */
|
||||||
responseTimeoutId?: NodeJS.Timeout;
|
responseTimeoutId?: NodeJS.Timeout;
|
||||||
|
/** SSH remote configuration for remote execution */
|
||||||
|
sessionSshRemoteConfig?: {
|
||||||
|
enabled: boolean;
|
||||||
|
remoteId: string | null;
|
||||||
|
workingDirOverride?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,6 +174,7 @@ class ConversationManager {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
outputBuffer: '',
|
outputBuffer: '',
|
||||||
|
sessionSshRemoteConfig: config.sessionSshRemoteConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log conversation start
|
// Log conversation start
|
||||||
@@ -172,6 +185,8 @@ class ConversationManager {
|
|||||||
projectName: config.projectName,
|
projectName: config.projectName,
|
||||||
hasExistingDocs: !!config.existingDocs,
|
hasExistingDocs: !!config.existingDocs,
|
||||||
existingDocsCount: config.existingDocs?.length || 0,
|
existingDocsCount: config.existingDocs?.length || 0,
|
||||||
|
hasRemoteSsh: !!config.sessionSshRemoteConfig?.enabled,
|
||||||
|
remoteId: config.sessionSshRemoteConfig?.remoteId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return sessionId;
|
return sessionId;
|
||||||
@@ -219,12 +234,51 @@ class ConversationManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the agent configuration
|
// Get the agent configuration
|
||||||
const agent = await window.maestro.agents.get(this.session.agentType);
|
// Pass SSH remote ID if SSH is enabled for this session
|
||||||
|
const sshRemoteId = this.session.sessionSshRemoteConfig?.enabled
|
||||||
|
? this.session.sessionSshRemoteConfig.remoteId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
wizardDebugLogger.log('info', 'Fetching agent configuration', {
|
||||||
|
agentType: this.session.agentType,
|
||||||
|
sessionId: this.session.sessionId,
|
||||||
|
hasRemoteSsh: !!this.session.sessionSshRemoteConfig?.enabled,
|
||||||
|
remoteId: this.session.sessionSshRemoteConfig?.remoteId || null,
|
||||||
|
passingSshRemoteId: sshRemoteId || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agent = await window.maestro.agents.get(this.session.agentType, sshRemoteId || undefined);
|
||||||
|
|
||||||
|
// Log to main process (writes to maestro-debug.log on Windows)
|
||||||
|
console.log('[Wizard] Agent fetch result:', {
|
||||||
|
agentType: this.session.agentType,
|
||||||
|
agentExists: !!agent,
|
||||||
|
agentAvailable: agent?.available,
|
||||||
|
agentPath: agent?.path,
|
||||||
|
agentCommand: agent?.command,
|
||||||
|
hasRemoteSsh: !!this.session.sessionSshRemoteConfig?.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
if (!agent || !agent.available) {
|
if (!agent || !agent.available) {
|
||||||
const error = `Agent ${this.session.agentType} is not available`;
|
const error = `Agent ${this.session.agentType} is not available`;
|
||||||
|
|
||||||
|
// Log detailed info about why agent is unavailable
|
||||||
|
console.error('[Wizard] Agent not available - Details:', {
|
||||||
|
agentType: this.session.agentType,
|
||||||
|
agentExists: !!agent,
|
||||||
|
agentAvailable: agent?.available,
|
||||||
|
agentPath: agent?.path,
|
||||||
|
agentError: (agent as any)?.error,
|
||||||
|
sessionSshConfig: this.session.sessionSshRemoteConfig,
|
||||||
|
});
|
||||||
|
|
||||||
wizardDebugLogger.log('error', 'Agent not available', {
|
wizardDebugLogger.log('error', 'Agent not available', {
|
||||||
agentType: this.session.agentType,
|
agentType: this.session.agentType,
|
||||||
agent: agent ? { available: agent.available } : null,
|
agent: agent ? {
|
||||||
|
available: agent.available,
|
||||||
|
path: agent.path,
|
||||||
|
error: (agent as any).error
|
||||||
|
} : null,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -232,6 +286,8 @@ class ConversationManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[Wizard] Agent is available, building prompt...');
|
||||||
|
|
||||||
// Build the full prompt with conversation context
|
// Build the full prompt with conversation context
|
||||||
const fullPrompt = this.buildPromptWithContext(userMessage, conversationHistory);
|
const fullPrompt = this.buildPromptWithContext(userMessage, conversationHistory);
|
||||||
|
|
||||||
@@ -487,6 +543,8 @@ class ConversationManager {
|
|||||||
agentCommand: agent.command,
|
agentCommand: agent.command,
|
||||||
args: argsForSpawn,
|
args: argsForSpawn,
|
||||||
cwd: this.session!.directoryPath,
|
cwd: this.session!.directoryPath,
|
||||||
|
hasRemoteSsh: !!this.session!.sessionSshRemoteConfig?.enabled,
|
||||||
|
remoteId: this.session!.sessionSshRemoteConfig?.remoteId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.maestro.process
|
window.maestro.process
|
||||||
@@ -497,6 +555,7 @@ class ConversationManager {
|
|||||||
command: commandToUse,
|
command: commandToUse,
|
||||||
args: argsForSpawn,
|
args: argsForSpawn,
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
|
sessionSshRemoteConfig: this.session!.sessionSshRemoteConfig,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
wizardDebugLogger.log('spawn', 'Agent process spawned successfully', {
|
wizardDebugLogger.log('spawn', 'Agent process spawned successfully', {
|
||||||
@@ -519,6 +578,7 @@ class ConversationManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build CLI args for the agent based on its type and capabilities.
|
* Build CLI args for the agent based on its type and capabilities.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ export interface GenerationConfig {
|
|||||||
conversationHistory: WizardMessage[];
|
conversationHistory: WizardMessage[];
|
||||||
/** Optional subfolder within Auto Run Docs (e.g., "Initiation") */
|
/** Optional subfolder within Auto Run Docs (e.g., "Initiation") */
|
||||||
subfolder?: string;
|
subfolder?: string;
|
||||||
|
/** SSH remote configuration for remote agent execution */
|
||||||
|
sessionSshRemoteConfig?: {
|
||||||
|
enabled: boolean;
|
||||||
|
remoteId: string | null;
|
||||||
|
workingDirOverride?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1063,6 +1069,8 @@ class PhaseGenerator {
|
|||||||
agentCommand: agent.command,
|
agentCommand: agent.command,
|
||||||
argsCount: argsForSpawn.length,
|
argsCount: argsForSpawn.length,
|
||||||
promptLength: prompt.length,
|
promptLength: prompt.length,
|
||||||
|
hasRemoteSsh: !!config.sessionSshRemoteConfig?.enabled,
|
||||||
|
remoteId: config.sessionSshRemoteConfig?.remoteId || null,
|
||||||
});
|
});
|
||||||
window.maestro.process
|
window.maestro.process
|
||||||
.spawn({
|
.spawn({
|
||||||
@@ -1072,6 +1080,7 @@ class PhaseGenerator {
|
|||||||
command: commandToUse,
|
command: commandToUse,
|
||||||
args: argsForSpawn,
|
args: argsForSpawn,
|
||||||
prompt,
|
prompt,
|
||||||
|
sessionSshRemoteConfig: config.sessionSshRemoteConfig,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('[PhaseGenerator] Agent spawned successfully');
|
console.log('[PhaseGenerator] Agent spawned successfully');
|
||||||
|
|||||||
Reference in New Issue
Block a user