mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user