diff --git a/README.md b/README.md index 55cea04e..f8731d52 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Maestro is a cross-platform desktop app for orchestrating your fleet of AI agent Collaborate with AI to create detailed specification documents, then let Auto Run execute them automatically, each task in a fresh session with clean context. Allowing for long running unattended sessions, my current record is nearly 24 hours of continuos runtime. -Run multiple agents in parallel with a Linear/Superhuman-level responsive interface. Currently supporting **Claude Code**, **OpenAI Codex**, and **OpenCode** with plans for additional agentic coding tools (Gemini CLI, Qwen3 Coder) based on user demand. +Run multiple agents in parallel with a Linear/Superhuman-level responsive interface. Currently supporting **Claude Code**, **OpenAI Codex**, and **OpenCode** with plans for additional agentic coding tools (Aider, Gemini CLI, Qwen3 Coder) based on user demand.
@@ -69,7 +69,7 @@ Download the latest release for your platform from the [Releases](https://github - 💰 **Cost Tracking** - Real-time token usage and cost tracking per session and globally. - 🏆 **[Achievements](#achievements)** - Level up from Apprentice to Titan of the Baton based on cumulative Auto Run time. 11 conductor-themed ranks to unlock. -> **Note**: Maestro supports Claude Code, OpenAI Codex, and OpenCode. Support for additional agents (Gemini CLI, Qwen3 Coder) may be added in future releases based on community demand. +> **Note**: Maestro supports Claude Code, OpenAI Codex, and OpenCode. Support for additional agents (Aider, Gemini CLI, Qwen3 Coder) may be added in future releases based on community demand. ### Spec-Driven Workflow diff --git a/src/main/agent-capabilities.ts b/src/main/agent-capabilities.ts index 44aeddff..9b1c6d64 100644 --- a/src/main/agent-capabilities.ts +++ b/src/main/agent-capabilities.ts @@ -201,6 +201,30 @@ export const AGENT_CAPABILITIES: Record = { supportsModelSelection: false, // Not yet investigated }, + /** + * Aider - AI pair programming in your terminal + * https://github.com/paul-gauthier/aider + * + * PLACEHOLDER: Most capabilities set to false until Aider integration is + * implemented. Update this configuration when integrating the agent. + */ + 'aider': { + supportsResume: false, // Not yet investigated + supportsReadOnlyMode: false, // Not yet investigated + supportsJsonOutput: false, // Not yet investigated + supportsSessionId: false, // Not yet investigated + supportsImageInput: true, // Aider supports vision models + supportsSlashCommands: true, // Aider has /commands + supportsSessionStorage: false, // Not yet investigated + supportsCostTracking: true, // Aider tracks costs + supportsUsageStats: true, // Aider shows token usage + supportsBatchMode: false, // Not yet investigated + requiresPromptToStart: false, // Not yet investigated + supportsStreaming: true, // Likely streams + supportsResultMessages: false, // Not yet investigated + supportsModelSelection: true, // --model flag + }, + /** * OpenCode - Open source coding assistant * https://github.com/opencode-ai/opencode diff --git a/src/main/agent-detector.ts b/src/main/agent-detector.ts index ac31dfbb..86f596fa 100644 --- a/src/main/agent-detector.ts +++ b/src/main/agent-detector.ts @@ -115,11 +115,13 @@ const AGENT_DEFINITIONS: Omit] [--agent plan] "prompt" + // Note: 'run' subcommand auto-approves all permissions (YOLO mode is implicit) batchModePrefix: ['run'], // OpenCode uses 'run' subcommand for batch mode jsonOutputArgs: ['--format', 'json'], // JSON output format resumeArgs: (sessionId: string) => ['--session', sessionId], // Resume with session ID readOnlyArgs: ['--agent', 'plan'], // Read-only/plan mode modelArgs: (modelId: string) => ['--model', modelId], // Model selection (e.g., 'ollama/qwen3:8b') + yoloModeArgs: ['run'], // 'run' subcommand auto-approves all permissions (YOLO mode is implicit) // Agent-specific configuration options shown in UI configOptions: [ { @@ -145,6 +147,13 @@ const AGENT_DEFINITIONS: Omit
@@ -465,7 +465,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) { onClick={() => handleLoadPlaybook(pb)} > {pb.name} diff --git a/src/renderer/components/MainPanel.tsx b/src/renderer/components/MainPanel.tsx index de75ac5e..cf12cce3 100644 --- a/src/renderer/components/MainPanel.tsx +++ b/src/renderer/components/MainPanel.tsx @@ -634,14 +634,14 @@ export const MainPanel = forwardRef(function Ma {/* Cost Tracker - styled as pill (hidden when panel is narrow or agent doesn't support cost tracking) - shows active tab's cost */} - {showCostWidget && activeSession.inputMode === 'ai' && activeTab?.agentSessionId && hasCapability('supportsCostTracking') && ( + {showCostWidget && activeSession.inputMode === 'ai' && (activeTab?.agentSessionId || activeTab?.usageStats) && hasCapability('supportsCostTracking') && ( ${(activeTab?.usageStats?.totalCostUsd ?? 0).toFixed(2)} )} {/* Context Window Widget with Tooltip - only show when context window is configured and agent supports usage stats */} - {activeSession.inputMode === 'ai' && activeTab?.agentSessionId && hasCapability('supportsUsageStats') && (activeTab?.usageStats?.contextWindow ?? 0) > 0 && ( + {activeSession.inputMode === 'ai' && (activeTab?.agentSessionId || activeTab?.usageStats) && hasCapability('supportsUsageStats') && (activeTab?.usageStats?.contextWindow ?? 0) > 0 && (
{ diff --git a/src/renderer/components/NewInstanceModal.tsx b/src/renderer/components/NewInstanceModal.tsx index 79e9fa15..b7919ff7 100644 --- a/src/renderer/components/NewInstanceModal.tsx +++ b/src/renderer/components/NewInstanceModal.tsx @@ -55,6 +55,7 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, defaultAgen const [homeDir, setHomeDir] = useState(''); const [customAgentPaths, setCustomAgentPaths] = useState>({}); const [customAgentArgs, setCustomAgentArgs] = useState>({}); + const [agentConfigs, setAgentConfigs] = useState>>({}); const nameInputRef = useRef(null); @@ -94,6 +95,14 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, defaultAgen const args = await window.maestro.agents.getAllCustomArgs(); setCustomAgentArgs(args); + // Load configurations for all agents + const configs: Record> = {}; + for (const agent of detectedAgents) { + const config = await window.maestro.agents.getConfig(agent.id); + configs[agent.id] = config; + } + setAgentConfigs(configs); + // Set default or first available const defaultAvailable = detectedAgents.find((a: AgentConfig) => a.id === defaultAgent && a.available); const firstAvailable = detectedAgents.find((a: AgentConfig) => a.available); @@ -193,8 +202,8 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, defaultAgen useEffect(() => { if (isOpen) { loadAgents(); - // Expand the selected agent by default - setExpandedAgent(defaultAgent); + // Keep all agents collapsed by default + setExpandedAgent(null); } }, [isOpen, defaultAgent]); @@ -439,6 +448,94 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, defaultAgen Additional CLI arguments appended to all calls to this agent

+ + {/* Agent-specific configuration options (contextWindow, model, etc.) */} + {agent.configOptions && agent.configOptions.length > 0 && agent.configOptions.map((option: any) => ( +
+ + {option.type === 'number' && ( + { + const value = e.target.value === '' ? 0 : parseInt(e.target.value, 10); + const newConfig = { + ...agentConfigs[agent.id], + [option.key]: isNaN(value) ? 0 : value + }; + setAgentConfigs(prev => ({ + ...prev, + [agent.id]: newConfig + })); + }} + onBlur={() => { + const currentConfig = agentConfigs[agent.id] || {}; + window.maestro.agents.setConfig(agent.id, currentConfig); + }} + onClick={(e) => e.stopPropagation()} + placeholder={option.default?.toString() || '0'} + min={0} + className="w-full p-2 rounded border bg-transparent outline-none text-xs font-mono" + style={{ borderColor: theme.colors.border, color: theme.colors.textMain }} + /> + )} + {option.type === 'text' && ( + { + const newConfig = { + ...agentConfigs[agent.id], + [option.key]: e.target.value + }; + setAgentConfigs(prev => ({ + ...prev, + [agent.id]: newConfig + })); + }} + onBlur={() => { + const currentConfig = agentConfigs[agent.id] || {}; + window.maestro.agents.setConfig(agent.id, currentConfig); + }} + onClick={(e) => e.stopPropagation()} + placeholder={option.default || ''} + className="w-full p-2 rounded border bg-transparent outline-none text-xs font-mono" + style={{ borderColor: theme.colors.border, color: theme.colors.textMain }} + /> + )} + {option.type === 'checkbox' && ( + + )} +

+ {option.description} +

+
+ ))}
)}
@@ -547,9 +644,24 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, defaultAgen export function EditAgentModal({ isOpen, onClose, onSave, theme, session, existingSessions }: EditAgentModalProps) { const [instanceName, setInstanceName] = useState(''); const [nudgeMessage, setNudgeMessage] = useState(''); + const [agent, setAgent] = useState(null); + const [agentConfig, setAgentConfig] = useState>({}); const nameInputRef = useRef(null); + // Load agent info and config when modal opens + useEffect(() => { + if (isOpen && session) { + // Load agent definition to get configOptions + window.maestro.agents.detect().then((agents: AgentConfig[]) => { + const foundAgent = agents.find(a => a.id === session.toolType); + setAgent(foundAgent || null); + }); + // Load agent config + window.maestro.agents.getConfig(session.toolType).then(setAgentConfig); + } + }, [isOpen, session]); + // Populate form when session changes or modal opens useEffect(() => { if (isOpen && session) { @@ -605,6 +717,7 @@ export function EditAgentModal({ isOpen, onClose, onSave, theme, session, existi 'claude-code': 'Claude Code', 'codex': 'Codex', 'opencode': 'OpenCode', + 'aider': 'Aider', }; const agentName = agentNameMap[session.toolType] || session.toolType; @@ -703,6 +816,88 @@ export function EditAgentModal({ isOpen, onClose, onSave, theme, session, existi {nudgeMessage.length}/{NUDGE_MESSAGE_MAX_LENGTH} characters. This text is added to every message you send to the agent (not visible in chat).

+ + {/* Agent-specific configuration options (contextWindow, model, etc.) */} + {agent?.configOptions && agent.configOptions.length > 0 && ( +
+ +
+ {agent.configOptions.map((option: any) => ( +
+ + {option.type === 'number' && ( + { + const value = e.target.value === '' ? 0 : parseInt(e.target.value, 10); + setAgentConfig(prev => ({ + ...prev, + [option.key]: isNaN(value) ? 0 : value + })); + }} + onBlur={() => { + window.maestro.agents.setConfig(session.toolType, agentConfig); + }} + placeholder={option.default?.toString() || '0'} + min={0} + className="w-full p-2 rounded border bg-transparent outline-none text-xs font-mono" + style={{ borderColor: theme.colors.border, color: theme.colors.textMain }} + /> + )} + {option.type === 'text' && ( + { + setAgentConfig(prev => ({ + ...prev, + [option.key]: e.target.value + })); + }} + onBlur={() => { + window.maestro.agents.setConfig(session.toolType, agentConfig); + }} + placeholder={option.default || ''} + className="w-full p-2 rounded border bg-transparent outline-none text-xs font-mono" + style={{ borderColor: theme.colors.border, color: theme.colors.textMain }} + /> + )} + {option.type === 'checkbox' && ( + + )} +

+ {option.description} +

+
+ ))} +
+
+ )} diff --git a/src/renderer/components/ProcessMonitor.tsx b/src/renderer/components/ProcessMonitor.tsx index da1ae8ef..798f4de3 100644 --- a/src/renderer/components/ProcessMonitor.tsx +++ b/src/renderer/components/ProcessMonitor.tsx @@ -382,7 +382,7 @@ export function ProcessMonitor(props: ProcessMonitorProps) { const rootNode: ProcessNode = { id: 'group-root', type: 'group', - label: 'UNGROUPED', + label: 'UNGROUPED AGENTS', emoji: '📁', expanded: expandedNodes.has('group-root'), children: sessionNodes diff --git a/src/renderer/components/SessionList.tsx b/src/renderer/components/SessionList.tsx index d2fdcecf..99256311 100644 --- a/src/renderer/components/SessionList.tsx +++ b/src/renderer/components/SessionList.tsx @@ -1766,7 +1766,7 @@ export function SessionList(props: SessionListProps) {
{ungroupedCollapsed ? : } - Ungrouped + Ungrouped Agents
+ + + + + {/* Content area - renders the current screen with transition animations */} diff --git a/src/renderer/components/Wizard/screens/AgentSelectionScreen.tsx b/src/renderer/components/Wizard/screens/AgentSelectionScreen.tsx index a900cca5..352d16fe 100644 --- a/src/renderer/components/Wizard/screens/AgentSelectionScreen.tsx +++ b/src/renderer/components/Wizard/screens/AgentSelectionScreen.tsx @@ -62,6 +62,13 @@ const AGENT_TILES: AgentTile[] = [ brandColor: '#F97316', // Orange }, // Coming soon agents at the bottom + { + id: 'aider', + name: 'Aider', + supported: false, + description: 'Coming soon', + brandColor: '#14B8A6', // Teal + }, { id: 'gemini-cli', name: 'Gemini CLI', @@ -145,6 +152,33 @@ function AgentLogo({ agentId, supported, detected, brandColor, theme }: { ); + case 'aider': + // Aider - chat bubble with code brackets + return ( + + {/* Chat bubble with code brackets */} + + + + ); + case 'gemini-cli': // Gemini - Google's sparkle/star logo return ( diff --git a/src/renderer/components/Wizard/screens/ConversationScreen.tsx b/src/renderer/components/Wizard/screens/ConversationScreen.tsx index 2748d30f..90995cc7 100644 --- a/src/renderer/components/Wizard/screens/ConversationScreen.tsx +++ b/src/renderer/components/Wizard/screens/ConversationScreen.tsx @@ -119,10 +119,12 @@ function MessageBubble({ message, theme, agentName, + providerName, }: { message: WizardMessage; theme: Theme; agentName: string; + providerName?: string; }): JSX.Element { const isUser = message.role === 'user'; const isSystem = message.role === 'system'; @@ -147,19 +149,33 @@ function MessageBubble({ {/* Role indicator for non-user messages */} {!isUser && (
- {isSystem ? '🎼 System' : formatAgentName(agentName)} - {message.confidence !== undefined && ( +
+ {isSystem ? '🎼 System' : formatAgentName(agentName)} + {message.confidence !== undefined && ( + + {message.confidence}% confident + + )} +
+ {providerName && !isSystem && ( - {message.confidence}% confident + {providerName} )}
@@ -765,7 +781,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem {/* Conversation Area */}
{/* Initial Question (shown before first interaction) */} @@ -790,7 +806,18 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem {/* Conversation History */} {state.conversationHistory.map((message) => ( - + ))} {/* Streaming Response or Typing Indicator */} @@ -932,7 +959,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem + {/* Spacer after content / before Continue button */} +
- {showContinue && ( + {/* Continue button - centered */} + {showContinue && ( +
- )} -
+
+ )} + + {/* Spacer after Continue button / before keyboard hints */} +
{/* Keyboard hints */} -
+
{ @@ -366,6 +363,45 @@ class ConversationManager { } + /** + * Build CLI args for the agent based on its type and capabilities. + * + * Note: The main process IPC handler (process.ts) automatically adds: + * - batchModePrefix (e.g., 'exec' for Codex, 'run' for OpenCode) + * - batchModeArgs (e.g., YOLO mode flags) + * - jsonOutputArgs (e.g., --json, --format json) + * - workingDirArgs (e.g., -C dir for Codex) + * + * So we only need to add agent-specific flags that aren't covered by + * the standard argument builders. + */ + private buildArgsForAgent(agent: any): string[] { + const agentId = agent.id || this.session?.agentType; + + switch (agentId) { + case 'claude-code': { + // Claude Code: start with base args, add --include-partial-messages for streaming + const args = [...(agent.args || [])]; + if (!args.includes('--include-partial-messages')) { + args.push('--include-partial-messages'); + } + return args; + } + + case 'codex': + case 'opencode': { + // For Codex and OpenCode, use base args only + // The IPC handler will add batchModePrefix, jsonOutputArgs, batchModeArgs, workingDirArgs + return [...(agent.args || [])]; + } + + default: { + // For unknown agents, use base args + return [...(agent.args || [])]; + } + } + } + /** * Parse the accumulated agent output to extract the structured response */ @@ -403,12 +439,67 @@ class ConversationManager { } /** - * Extract the result field from Claude's stream-json output format + * Extract the result text from agent JSON output. + * Handles different agent output formats: + * - Claude Code: stream-json with { type: 'result', result: '...' } + * - OpenCode: JSONL with { type: 'text', part: { text: '...' } } + * - Codex: JSONL with { type: 'message', content: '...' } or similar */ private extractResultFromStreamJson(output: string): string | null { + const agentType = this.session?.agentType; + try { - // Look for the result message in stream-json format const lines = output.split('\n'); + + // For OpenCode: concatenate all text parts + if (agentType === 'opencode') { + const textParts: string[] = []; + for (const line of lines) { + if (!line.trim()) continue; + try { + const msg = JSON.parse(line); + // OpenCode text messages have type: 'text' and part.text + if (msg.type === 'text' && msg.part?.text) { + textParts.push(msg.part.text); + } + } catch { + // Ignore non-JSON lines + } + } + if (textParts.length > 0) { + return textParts.join(''); + } + } + + // For Codex: look for message content + if (agentType === 'codex') { + const textParts: string[] = []; + for (const line of lines) { + if (!line.trim()) continue; + try { + const msg = JSON.parse(line); + // Codex uses agent_message type with content array + if (msg.type === 'agent_message' && msg.content) { + for (const block of msg.content) { + if (block.type === 'text' && block.text) { + textParts.push(block.text); + } + } + } + // Also check for message type with text field (older format) + if (msg.type === 'message' && msg.text) { + textParts.push(msg.text); + } + } catch { + // Ignore non-JSON lines + } + } + if (textParts.length > 0) { + return textParts.join(''); + } + } + + // For Claude Code: look for result message for (const line of lines) { if (!line.trim()) continue; try { diff --git a/src/renderer/types/index.ts b/src/renderer/types/index.ts index 4a5e821b..30ffe308 100644 --- a/src/renderer/types/index.ts +++ b/src/renderer/types/index.ts @@ -455,6 +455,7 @@ export interface AgentConfig { args?: string[]; hidden?: boolean; // If true, agent is hidden from UI (internal use only) configOptions?: AgentConfigOption[]; // Agent-specific configuration options + yoloModeArgs?: string[]; // Args for YOLO/full-access mode (e.g., ['--dangerously-skip-permissions']) } // Process spawning configuration