OAuth enabled but no valid token found. Starting authentication...

Found expired OAuth token, attempting refresh...
Token refresh successful
## CHANGES

- Added Aider as a new agent option in the interface 🤖
- Improved agent-specific configuration options in modal dialogs 🔧
- Enhanced playbook dropdown to support longer names dynamically 📏
- Fixed cost tracker widget display for all usage scenarios 💰
- Added back button navigation to the Maestro Wizard interface ⬅️
- Improved markdown generation in wizard document creation flow 📝
- Fixed keyboard shortcut isolation in wizard to prevent conflicts ⌨️
- Enhanced agent output parsing for OpenCode and Codex agents 🔍
- Added YOLO mode argument handling for auto-approval features 
- Renamed "Ungrouped" sections to "Ungrouped Agents" for clarity 📁
This commit is contained in:
Pedram Amini
2025-12-17 21:43:11 -06:00
parent 142c022e2c
commit 9857b87473
15 changed files with 523 additions and 102 deletions

View File

@@ -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.
<div align="center">
<a href="https://youtu.be/fmwwTOg7cyA?si=dJ89K54tGflKa5G4">
@@ -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

View File

@@ -201,6 +201,30 @@ export const AGENT_CAPABILITIES: Record<string, AgentCapabilities> = {
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

View File

@@ -115,11 +115,13 @@ const AGENT_DEFINITIONS: Omit<AgentConfig, 'available' | 'path' | 'capabilities'
args: [], // Base args (none for OpenCode - batch mode uses 'run' subcommand)
// OpenCode CLI argument builders
// Batch mode: opencode run --format json [--model provider/model] [--session <id>] [--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<AgentConfig, 'available' | 'path' | 'capabilities'
},
],
},
{
id: 'aider',
name: 'Aider',
binaryName: 'aider',
command: 'aider',
args: [], // Base args (placeholder - to be configured when implemented)
},
];
export class AgentDetector {

View File

@@ -87,6 +87,8 @@ CONTENT:
Continue for as many phases as needed.
**IMPORTANT**: Write the markdown content directly - do NOT wrap it in code fences (like \`\`\`markdown or \`\`\`). The CONTENT section should contain raw markdown text, not a code block containing markdown.
## Project Discovery Conversation
{{CONVERSATION_SUMMARY}}

View File

@@ -452,7 +452,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
{/* Dropdown Menu */}
{showPlaybookDropdown && (
<div
className="absolute top-full left-0 mt-1 w-64 rounded-lg border shadow-lg z-10 overflow-hidden"
className="absolute top-full left-0 mt-1 min-w-64 max-w-[calc(700px-48px)] w-max rounded-lg border shadow-lg z-10 overflow-hidden"
style={{ backgroundColor: theme.colors.bgSidebar, borderColor: theme.colors.border }}
>
<div className="max-h-48 overflow-y-auto">
@@ -465,7 +465,7 @@ export function BatchRunnerModal(props: BatchRunnerModalProps) {
onClick={() => handleLoadPlaybook(pb)}
>
<span
className="flex-1 text-sm truncate"
className="flex-1 text-sm"
style={{ color: theme.colors.textMain }}
>
{pb.name}

View File

@@ -634,14 +634,14 @@ export const MainPanel = forwardRef<MainPanelHandle, MainPanelProps>(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') && (
<span className="text-xs font-mono font-bold px-2 py-0.5 rounded-full border border-green-500/30 text-green-500 bg-green-500/10">
${(activeTab?.usageStats?.totalCostUsd ?? 0).toFixed(2)}
</span>
)}
{/* 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 && (
<div
className="flex flex-col items-end mr-2 relative cursor-pointer"
onMouseEnter={() => {

View File

@@ -55,6 +55,7 @@ export function NewInstanceModal({ isOpen, onClose, onCreate, theme, defaultAgen
const [homeDir, setHomeDir] = useState<string>('');
const [customAgentPaths, setCustomAgentPaths] = useState<Record<string, string>>({});
const [customAgentArgs, setCustomAgentArgs] = useState<Record<string, string>>({});
const [agentConfigs, setAgentConfigs] = useState<Record<string, Record<string, any>>>({});
const nameInputRef = useRef<HTMLInputElement>(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<string, Record<string, any>> = {};
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
</p>
</div>
{/* Agent-specific configuration options (contextWindow, model, etc.) */}
{agent.configOptions && agent.configOptions.length > 0 && agent.configOptions.map((option: any) => (
<div
key={option.key}
className="p-3 rounded border"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
>
<label className="block text-xs font-medium mb-2" style={{ color: theme.colors.textDim }}>
{option.label}
</label>
{option.type === 'number' && (
<input
type="number"
value={agentConfigs[agent.id]?.[option.key] ?? option.default}
onChange={(e) => {
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' && (
<input
type="text"
value={agentConfigs[agent.id]?.[option.key] ?? option.default}
onChange={(e) => {
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' && (
<label className="flex items-center gap-2 cursor-pointer" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={agentConfigs[agent.id]?.[option.key] ?? option.default}
onChange={(e) => {
const newConfig = {
...agentConfigs[agent.id],
[option.key]: e.target.checked
};
setAgentConfigs(prev => ({
...prev,
[agent.id]: newConfig
}));
window.maestro.agents.setConfig(agent.id, newConfig);
}}
className="w-4 h-4"
style={{ accentColor: theme.colors.accent }}
/>
<span className="text-xs" style={{ color: theme.colors.textMain }}>Enabled</span>
</label>
)}
<p className="text-xs opacity-50 mt-2">
{option.description}
</p>
</div>
))}
</div>
)}
</div>
@@ -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<AgentConfig | null>(null);
const [agentConfig, setAgentConfig] = useState<Record<string, any>>({});
const nameInputRef = useRef<HTMLInputElement>(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).
</p>
</div>
{/* Agent-specific configuration options (contextWindow, model, etc.) */}
{agent?.configOptions && agent.configOptions.length > 0 && (
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2" style={{ color: theme.colors.textMain }}>
{agentName} Settings
</label>
<div className="space-y-3">
{agent.configOptions.map((option: any) => (
<div
key={option.key}
className="p-3 rounded border"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
>
<label className="block text-xs font-medium mb-2" style={{ color: theme.colors.textDim }}>
{option.label}
</label>
{option.type === 'number' && (
<input
type="number"
value={agentConfig[option.key] ?? option.default}
onChange={(e) => {
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' && (
<input
type="text"
value={agentConfig[option.key] ?? option.default}
onChange={(e) => {
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' && (
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={agentConfig[option.key] ?? option.default}
onChange={(e) => {
const newConfig = {
...agentConfig,
[option.key]: e.target.checked
};
setAgentConfig(newConfig);
window.maestro.agents.setConfig(session.toolType, newConfig);
}}
className="w-4 h-4"
style={{ accentColor: theme.colors.accent }}
/>
<span className="text-xs" style={{ color: theme.colors.textMain }}>Enabled</span>
</label>
)}
<p className="text-xs opacity-50 mt-2">
{option.description}
</p>
</div>
))}
</div>
</div>
)}
</div>
</Modal>
</div>

View File

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

View File

@@ -1766,7 +1766,7 @@ export function SessionList(props: SessionListProps) {
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-wider flex-1" style={{ color: theme.colors.textDim }}>
{ungroupedCollapsed ? <ChevronRight className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
<Folder className="w-3.5 h-3.5" />
<span>Ungrouped</span>
<span>Ungrouped Agents</span>
</div>
<button
onClick={(e) => {

View File

@@ -266,6 +266,25 @@ export function MaestroWizard({
}
}, [state.isOpen, showExitConfirm, registerLayer, unregisterLayer, handleCloseRequest]);
// Capture-phase handler to intercept Cmd+E before it reaches the main app
// This prevents the wizard's edit/preview toggle from leaking to the AutoRun component
useEffect(() => {
if (!state.isOpen) return;
const handleCaptureKeyDown = (e: KeyboardEvent) => {
// Intercept Cmd+E to prevent it from reaching the main app's AutoRun
if ((e.metaKey || e.ctrlKey) && e.key === 'e' && !e.shiftKey) {
// Don't preventDefault here - let the wizard's internal handlers process it
// Just stop it from propagating to the main app
e.stopPropagation();
}
};
// Use capture phase to intercept before bubbling
document.addEventListener('keydown', handleCaptureKeyDown, true);
return () => document.removeEventListener('keydown', handleCaptureKeyDown, true);
}, [state.isOpen]);
/**
* Render the appropriate screen component based on displayed step
* Uses displayedStep (not currentStep) to allow for transition animations
@@ -405,28 +424,63 @@ export function MaestroWizard({
})}
</div>
{/* Close button */}
<button
onClick={handleCloseRequest}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
style={{ color: theme.colors.textDim }}
title="Close wizard (Escape)"
aria-label="Close wizard"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
{/* Back and Close buttons */}
<div className="flex items-center gap-2">
{/* Back button - only show when past step 1 */}
{currentStepNumber > 1 && (
<button
onClick={() => {
const prevStepNum = currentStepNumber - 1;
const targetStep = INDEX_TO_STEP[prevStepNum];
if (targetStep) {
goToStep(targetStep);
}
}}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors hover:bg-white/10"
style={{ color: theme.colors.textDim }}
title="Go back"
aria-label="Go back to previous step"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back
</button>
)}
{/* Close button */}
<button
onClick={handleCloseRequest}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
style={{ color: theme.colors.textDim }}
title="Close wizard (Escape)"
aria-label="Close wizard"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
{/* Content area - renders the current screen with transition animations */}

View File

@@ -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 }: {
</svg>
);
case 'aider':
// Aider - chat bubble with code brackets
return (
<svg
className="w-12 h-12"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ opacity }}
>
{/* Chat bubble with code brackets */}
<path
d="M8 12C8 9.79 9.79 8 12 8H36C38.21 8 40 9.79 40 12V28C40 30.21 38.21 32 36 32H20L12 40V32H12C9.79 32 8 30.21 8 28V12Z"
stroke={color}
strokeWidth="2"
fill="none"
/>
<path
d="M18 16L14 20L18 24M30 16L34 20L30 24"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
case 'gemini-cli':
// Gemini - Google's sparkle/star logo
return (

View File

@@ -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 && (
<div
className="text-xs font-medium mb-1 flex items-center gap-2"
className="text-xs font-medium mb-2 flex items-center justify-between"
style={{ color: isSystem ? theme.colors.warning : theme.colors.accent }}
>
<span>{isSystem ? '🎼 System' : formatAgentName(agentName)}</span>
{message.confidence !== undefined && (
<div className="flex items-center gap-2">
<span>{isSystem ? '🎼 System' : formatAgentName(agentName)}</span>
{message.confidence !== undefined && (
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{
backgroundColor: `${getConfidenceColor(message.confidence)}20`,
color: getConfidenceColor(message.confidence),
}}
>
{message.confidence}% confident
</span>
)}
</div>
{providerName && !isSystem && (
<span
className="text-xs px-1.5 py-0.5 rounded"
className="text-xs px-2 py-0.5 rounded-full"
style={{
backgroundColor: `${getConfidenceColor(message.confidence)}20`,
color: getConfidenceColor(message.confidence),
backgroundColor: `${theme.colors.accent}15`,
color: theme.colors.accent,
border: `1px solid ${theme.colors.accent}30`,
}}
>
{message.confidence}% confident
{providerName}
</span>
)}
</div>
@@ -765,7 +781,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem
{/* Conversation Area */}
<div
className="flex-1 overflow-y-auto px-6 py-4"
className="flex-1 min-h-0 overflow-y-auto px-6 py-4"
style={{ backgroundColor: theme.colors.bgMain }}
>
{/* Initial Question (shown before first interaction) */}
@@ -790,7 +806,18 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem
{/* Conversation History */}
{state.conversationHistory.map((message) => (
<MessageBubble key={message.id} message={message} theme={theme} agentName={state.agentName || 'Agent'} />
<MessageBubble
key={message.id}
message={message}
theme={theme}
agentName={state.agentName || 'Agent'}
providerName={
state.selectedAgent === 'claude-code' ? 'Claude' :
state.selectedAgent === 'opencode' ? 'OpenCode' :
state.selectedAgent === 'codex' ? 'Codex' :
state.selectedAgent || undefined
}
/>
))}
{/* Streaming Response or Typing Indicator */}
@@ -932,7 +959,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem
<button
onClick={handleSendMessage}
disabled={!inputValue.trim() || state.isConversationLoading}
className="px-4 rounded-lg font-medium transition-all flex items-center gap-2 shrink-0 self-stretch"
className="px-4 rounded-lg font-medium transition-all flex items-center gap-2 shrink-0 self-end"
style={{
backgroundColor:
inputValue.trim() && !state.isConversationLoading
@@ -946,7 +973,7 @@ export function ConversationScreen({ theme }: ConversationScreenProps): JSX.Elem
inputValue.trim() && !state.isConversationLoading
? 'pointer'
: 'not-allowed',
minHeight: '48px',
height: '48px',
}}
>
{state.isConversationLoading ? (

View File

@@ -48,13 +48,25 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
const [announcementKey, setAnnouncementKey] = useState(0);
/**
* Extract the YOLO/permission-skip flag from agent args
* Extract the YOLO/permission-skip flag from agent config.
* Includes the binary name prefix (e.g., "codex run" or "claude --dangerously...")
*/
const getYoloFlag = useCallback((): string | null => {
if (!agentConfig?.args) return null;
// Look for permission-related flags
if (!agentConfig) return null;
const binaryName = agentConfig.binaryName || agentConfig.command || 'agent';
// First check yoloModeArgs (the dedicated property for YOLO mode)
if (agentConfig.yoloModeArgs && agentConfig.yoloModeArgs.length > 0) {
// Return binary name + YOLO mode args
return `${binaryName} ${agentConfig.yoloModeArgs.join(' ')}`;
}
// Fall back to searching in base args
if (!agentConfig.args) return null;
const yoloPatterns = [
/--dangerously-skip-permissions/,
/--dangerously-bypass-approvals/,
/--yolo/,
/--no-confirm/,
/--yes/,
@@ -63,7 +75,7 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
for (const arg of agentConfig.args) {
for (const pattern of yoloPatterns) {
if (pattern.test(arg)) {
return arg;
return `${binaryName} ${arg}`;
}
}
}
@@ -327,7 +339,7 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
/>
{/* Header */}
<div className="text-center mb-8">
<div className="text-center">
{/* Agent greeting - prominent at top */}
<h2
className="text-3xl font-bold mb-6"
@@ -380,8 +392,11 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
</p>
</div>
{/* Main content area */}
<div className="flex-1 flex flex-col items-center justify-center">
{/* Spacer before Project Directory */}
<div className="flex-1" />
{/* Main content area - centered */}
<div className="flex flex-col items-center">
<div className="w-full max-w-xl">
{/* Directory path input with browse button */}
<div className="mb-8">
@@ -604,66 +619,35 @@ export function DirectorySelectionScreen({ theme }: DirectorySelectionScreenProp
</div>
</div>
{/* Footer with navigation buttons */}
<div className="mt-8 flex justify-between items-center">
<button
onClick={handleBack}
className="px-6 py-3 rounded-lg font-medium transition-all outline-none flex items-center gap-2"
style={{
backgroundColor: theme.colors.bgSidebar,
color: theme.colors.textMain,
border: `1px solid ${theme.colors.border}`,
}}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back
</button>
{/* Spacer after content / before Continue button */}
<div className="flex-1" />
{showContinue && (
{/* Continue button - centered */}
{showContinue && (
<div className="flex justify-center">
<button
ref={continueButtonRef}
onClick={handleContinue}
disabled={!isValid || isValidating}
className="px-8 py-3 rounded-lg font-medium transition-all outline-none flex items-center gap-2"
className="px-12 py-3 rounded-lg font-medium transition-all outline-none"
style={{
backgroundColor: isValid && !isValidating ? theme.colors.accent : theme.colors.border,
color: isValid && !isValidating ? theme.colors.accentForeground : theme.colors.textDim,
cursor: isValid && !isValidating ? 'pointer' : 'not-allowed',
opacity: isValid && !isValidating ? 1 : 0.6,
minWidth: '200px',
}}
>
Continue
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
)}
</div>
</div>
)}
{/* Spacer after Continue button / before keyboard hints */}
<div className="flex-1" />
{/* Keyboard hints */}
<div className="mt-4 flex justify-center gap-6">
<div className="flex justify-center gap-6">
<span
className="text-xs flex items-center gap-1"
style={{ color: theme.colors.textDim }}

View File

@@ -335,12 +335,9 @@ class ConversationManager {
// Store resolve for potential early termination
this.session!.pendingResolve = resolve;
// Spawn the agent with the prompt
// Add --include-partial-messages for real-time streaming text display
const argsWithStreaming = [...(agent.args || [])];
if (!argsWithStreaming.includes('--include-partial-messages')) {
argsWithStreaming.push('--include-partial-messages');
}
// Build args based on agent type
// Each agent has different CLI structure for batch mode
const argsForSpawn = this.buildArgsForAgent(agent);
window.maestro.process
.spawn({
@@ -348,7 +345,7 @@ class ConversationManager {
toolType: this.session!.agentType,
cwd: this.session!.directoryPath,
command: agent.command,
args: argsWithStreaming,
args: argsForSpawn,
prompt: prompt,
})
.then(() => {
@@ -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 {

View File

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