@evolve.labs/devflow 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/agents/architect.md +1162 -0
- package/.claude/commands/agents/architect.meta.yaml +124 -0
- package/.claude/commands/agents/builder.md +1432 -0
- package/.claude/commands/agents/builder.meta.yaml +117 -0
- package/.claude/commands/agents/chronicler.md +633 -0
- package/.claude/commands/agents/chronicler.meta.yaml +217 -0
- package/.claude/commands/agents/guardian.md +456 -0
- package/.claude/commands/agents/guardian.meta.yaml +127 -0
- package/.claude/commands/agents/strategist.md +483 -0
- package/.claude/commands/agents/strategist.meta.yaml +158 -0
- package/.claude/commands/agents/system-designer.md +1137 -0
- package/.claude/commands/agents/system-designer.meta.yaml +156 -0
- package/.claude/commands/devflow-help.md +93 -0
- package/.claude/commands/devflow-status.md +60 -0
- package/.claude/commands/quick/create-adr.md +82 -0
- package/.claude/commands/quick/new-feature.md +57 -0
- package/.claude/commands/quick/security-check.md +54 -0
- package/.claude/commands/quick/system-design.md +58 -0
- package/.claude_project +52 -0
- package/.devflow/agents/architect.meta.yaml +122 -0
- package/.devflow/agents/builder.meta.yaml +116 -0
- package/.devflow/agents/chronicler.meta.yaml +222 -0
- package/.devflow/agents/guardian.meta.yaml +127 -0
- package/.devflow/agents/strategist.meta.yaml +158 -0
- package/.devflow/agents/system-designer.meta.yaml +265 -0
- package/.devflow/project.yaml +242 -0
- package/.gitignore-template +84 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/devflow.js +54 -0
- package/lib/autopilot.js +235 -0
- package/lib/autopilotConstants.js +213 -0
- package/lib/constants.js +95 -0
- package/lib/init.js +200 -0
- package/lib/update.js +181 -0
- package/lib/utils.js +157 -0
- package/lib/web.js +119 -0
- package/package.json +57 -0
- package/web/CHANGELOG.md +192 -0
- package/web/README.md +156 -0
- package/web/app/api/autopilot/execute/route.ts +102 -0
- package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
- package/web/app/api/files/route.ts +280 -0
- package/web/app/api/files/tree/route.ts +160 -0
- package/web/app/api/git/route.ts +201 -0
- package/web/app/api/health/route.ts +94 -0
- package/web/app/api/project/open/route.ts +134 -0
- package/web/app/api/search/route.ts +247 -0
- package/web/app/api/specs/route.ts +405 -0
- package/web/app/api/terminal/route.ts +222 -0
- package/web/app/globals.css +160 -0
- package/web/app/ide/layout.tsx +43 -0
- package/web/app/ide/page.tsx +216 -0
- package/web/app/layout.tsx +34 -0
- package/web/app/page.tsx +303 -0
- package/web/components/agents/AgentIcons.tsx +281 -0
- package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
- package/web/components/autopilot/AutopilotPanel.tsx +299 -0
- package/web/components/dashboard/DashboardPanel.tsx +393 -0
- package/web/components/editor/Breadcrumbs.tsx +134 -0
- package/web/components/editor/EditorPanel.tsx +120 -0
- package/web/components/editor/EditorTabs.tsx +229 -0
- package/web/components/editor/MarkdownPreview.tsx +154 -0
- package/web/components/editor/MermaidDiagram.tsx +113 -0
- package/web/components/editor/MonacoEditor.tsx +177 -0
- package/web/components/editor/TabContextMenu.tsx +207 -0
- package/web/components/git/GitPanel.tsx +534 -0
- package/web/components/layout/Shell.tsx +15 -0
- package/web/components/layout/StatusBar.tsx +100 -0
- package/web/components/modals/CommandPalette.tsx +393 -0
- package/web/components/modals/GlobalSearch.tsx +348 -0
- package/web/components/modals/QuickOpen.tsx +241 -0
- package/web/components/modals/RecentFiles.tsx +208 -0
- package/web/components/projects/ProjectSelector.tsx +147 -0
- package/web/components/settings/SettingItem.tsx +150 -0
- package/web/components/settings/SettingsPanel.tsx +323 -0
- package/web/components/specs/SpecsPanel.tsx +1091 -0
- package/web/components/terminal/TerminalPanel.tsx +683 -0
- package/web/components/ui/ContextMenu.tsx +182 -0
- package/web/components/ui/LoadingSpinner.tsx +66 -0
- package/web/components/ui/ResizeHandle.tsx +110 -0
- package/web/components/ui/Skeleton.tsx +108 -0
- package/web/components/ui/SkipLinks.tsx +37 -0
- package/web/components/ui/Toaster.tsx +57 -0
- package/web/hooks/useFocusTrap.ts +141 -0
- package/web/hooks/useKeyboardShortcuts.ts +169 -0
- package/web/hooks/useListNavigation.ts +237 -0
- package/web/lib/autopilotConstants.ts +213 -0
- package/web/lib/constants/agents.ts +67 -0
- package/web/lib/git.ts +339 -0
- package/web/lib/ptyManager.ts +191 -0
- package/web/lib/specsParser.ts +299 -0
- package/web/lib/stores/autopilotStore.ts +288 -0
- package/web/lib/stores/fileStore.ts +550 -0
- package/web/lib/stores/gitStore.ts +386 -0
- package/web/lib/stores/projectStore.ts +196 -0
- package/web/lib/stores/settingsStore.ts +126 -0
- package/web/lib/stores/specsStore.ts +297 -0
- package/web/lib/stores/uiStore.ts +175 -0
- package/web/lib/types/index.ts +177 -0
- package/web/lib/utils.ts +98 -0
- package/web/next.config.js +50 -0
- package/web/package.json +54 -0
- package/web/postcss.config.js +6 -0
- package/web/tailwind.config.ts +68 -0
- package/web/tsconfig.json +41 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef } from 'react';
|
|
4
|
+
import { X, Rocket } from 'lucide-react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import {
|
|
7
|
+
useAutopilotStore,
|
|
8
|
+
DEFAULT_CONFIG,
|
|
9
|
+
type AgentId,
|
|
10
|
+
type AutopilotConfig,
|
|
11
|
+
} from '@/lib/stores/autopilotStore';
|
|
12
|
+
import { useFocusTrap } from '@/hooks/useFocusTrap';
|
|
13
|
+
import { useUIStore } from '@/lib/stores/uiStore';
|
|
14
|
+
|
|
15
|
+
const AGENTS: { id: AgentId; icon: string; name: string; description: string }[] = [
|
|
16
|
+
{ id: 'strategist', icon: '📊', name: 'Planning', description: 'Refina requisitos e cria acceptance criteria' },
|
|
17
|
+
{ id: 'architect', icon: '🏗️', name: 'Design', description: 'Define arquitetura e decisões técnicas' },
|
|
18
|
+
{ id: 'system-designer', icon: '⚙️', name: 'System Design', description: 'Projeta infraestrutura e escala do sistema' },
|
|
19
|
+
{ id: 'builder', icon: '🔨', name: 'Implementation', description: 'Implementa código e cria arquivos' },
|
|
20
|
+
{ id: 'guardian', icon: '🛡️', name: 'Validation', description: 'Revisa segurança e qualidade' },
|
|
21
|
+
{ id: 'chronicler', icon: '📝', name: 'Documentation', description: 'Atualiza documentação' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
interface AutopilotConfigModalProps {
|
|
25
|
+
projectPath: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function AutopilotConfigModal({ projectPath }: AutopilotConfigModalProps) {
|
|
29
|
+
const {
|
|
30
|
+
isConfigModalOpen,
|
|
31
|
+
closeConfigModal,
|
|
32
|
+
selectedSpecTitle,
|
|
33
|
+
startRun,
|
|
34
|
+
} = useAutopilotStore();
|
|
35
|
+
|
|
36
|
+
const [config, setConfig] = useState<AutopilotConfig>(DEFAULT_CONFIG);
|
|
37
|
+
const [isStarting, setIsStarting] = useState(false);
|
|
38
|
+
const [error, setError] = useState<string | null>(null);
|
|
39
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
|
|
41
|
+
// Focus trap for accessibility
|
|
42
|
+
useFocusTrap(modalRef, isConfigModalOpen, {
|
|
43
|
+
onEscape: closeConfigModal,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!isConfigModalOpen) return null;
|
|
47
|
+
|
|
48
|
+
const togglePhase = (agentId: AgentId) => {
|
|
49
|
+
setConfig((prev) => {
|
|
50
|
+
const phases = prev.phases.includes(agentId)
|
|
51
|
+
? prev.phases.filter((p) => p !== agentId)
|
|
52
|
+
: [...prev.phases, agentId];
|
|
53
|
+
return { ...prev, phases };
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleStart = async () => {
|
|
58
|
+
if (config.phases.length === 0) return;
|
|
59
|
+
|
|
60
|
+
setIsStarting(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
|
|
63
|
+
// Ensure terminal is visible
|
|
64
|
+
const uiState = useUIStore.getState();
|
|
65
|
+
if (!uiState.terminalVisible) {
|
|
66
|
+
uiState.toggleTerminal();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Small delay to let terminal mount and create session
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
72
|
+
await startRun(config, projectPath);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error('Failed to start autopilot:', err);
|
|
75
|
+
setError(err instanceof Error ? err.message : 'Failed to start');
|
|
76
|
+
} finally {
|
|
77
|
+
setIsStarting(false);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Estimate time based on phases selected
|
|
82
|
+
const estimatedMinutes = config.phases.reduce((acc, phase) => {
|
|
83
|
+
const times: Record<AgentId, number> = {
|
|
84
|
+
strategist: 2,
|
|
85
|
+
architect: 5,
|
|
86
|
+
'system-designer': 5,
|
|
87
|
+
builder: 10,
|
|
88
|
+
guardian: 5,
|
|
89
|
+
chronicler: 2,
|
|
90
|
+
};
|
|
91
|
+
return acc + (times[phase] || 3);
|
|
92
|
+
}, 0);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
96
|
+
{/* Backdrop */}
|
|
97
|
+
<div
|
|
98
|
+
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
99
|
+
onClick={closeConfigModal}
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
{/* Modal */}
|
|
104
|
+
<div
|
|
105
|
+
ref={modalRef}
|
|
106
|
+
className="relative w-full max-w-lg bg-[#12121a] border border-white/10 rounded-xl shadow-2xl overflow-hidden"
|
|
107
|
+
role="dialog"
|
|
108
|
+
aria-modal="true"
|
|
109
|
+
aria-labelledby="autopilot-modal-title"
|
|
110
|
+
>
|
|
111
|
+
{/* Header */}
|
|
112
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-gradient-to-r from-purple-500/10 to-blue-500/10">
|
|
113
|
+
<div className="flex items-center gap-2">
|
|
114
|
+
<Rocket className="w-5 h-5 text-purple-400" aria-hidden="true" />
|
|
115
|
+
<h2 id="autopilot-modal-title" className="text-lg font-semibold text-white">
|
|
116
|
+
Iniciar Autopilot
|
|
117
|
+
</h2>
|
|
118
|
+
</div>
|
|
119
|
+
<button
|
|
120
|
+
onClick={closeConfigModal}
|
|
121
|
+
className="p-1.5 text-gray-400 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
|
122
|
+
aria-label="Close modal"
|
|
123
|
+
>
|
|
124
|
+
<X className="w-5 h-5" aria-hidden="true" />
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Spec info */}
|
|
129
|
+
<div className="px-6 py-3 border-b border-white/5 bg-white/5">
|
|
130
|
+
<p className="text-sm text-gray-400">Spec:</p>
|
|
131
|
+
<p className="text-white font-medium truncate">{selectedSpecTitle}</p>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Content */}
|
|
135
|
+
<div className="p-6 space-y-6 max-h-[60vh] overflow-y-auto">
|
|
136
|
+
{/* Error message */}
|
|
137
|
+
{error && (
|
|
138
|
+
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
|
|
139
|
+
<p className="text-sm text-red-400">{error}</p>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{/* Phases */}
|
|
144
|
+
<div>
|
|
145
|
+
<h3 className="text-sm font-medium text-gray-300 mb-3">Fases a executar</h3>
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
{AGENTS.map((agent) => (
|
|
148
|
+
<label
|
|
149
|
+
key={agent.id}
|
|
150
|
+
className={cn(
|
|
151
|
+
'flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all',
|
|
152
|
+
config.phases.includes(agent.id)
|
|
153
|
+
? 'bg-purple-500/10 border-purple-500/30'
|
|
154
|
+
: 'bg-white/5 border-white/10 hover:bg-white/10'
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
<input
|
|
158
|
+
type="checkbox"
|
|
159
|
+
checked={config.phases.includes(agent.id)}
|
|
160
|
+
onChange={() => togglePhase(agent.id)}
|
|
161
|
+
className="sr-only"
|
|
162
|
+
/>
|
|
163
|
+
<div
|
|
164
|
+
className={cn(
|
|
165
|
+
'w-5 h-5 rounded border-2 flex items-center justify-center transition-colors',
|
|
166
|
+
config.phases.includes(agent.id)
|
|
167
|
+
? 'bg-purple-500 border-purple-500'
|
|
168
|
+
: 'border-gray-500'
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{config.phases.includes(agent.id) && (
|
|
172
|
+
<svg
|
|
173
|
+
className="w-3 h-3 text-white"
|
|
174
|
+
fill="none"
|
|
175
|
+
viewBox="0 0 24 24"
|
|
176
|
+
stroke="currentColor"
|
|
177
|
+
>
|
|
178
|
+
<path
|
|
179
|
+
strokeLinecap="round"
|
|
180
|
+
strokeLinejoin="round"
|
|
181
|
+
strokeWidth={3}
|
|
182
|
+
d="M5 13l4 4L19 7"
|
|
183
|
+
/>
|
|
184
|
+
</svg>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
<div className="flex-1">
|
|
188
|
+
<div className="flex items-center gap-2">
|
|
189
|
+
<span className="text-lg">{agent.icon}</span>
|
|
190
|
+
<span className="text-sm font-medium text-white">{agent.name}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<p className="text-xs text-gray-500 mt-0.5">{agent.description}</p>
|
|
193
|
+
</div>
|
|
194
|
+
</label>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Estimate */}
|
|
200
|
+
<div className="p-3 bg-white/5 rounded-lg border border-white/10">
|
|
201
|
+
<div className="flex items-center justify-between text-sm">
|
|
202
|
+
<span className="text-gray-400">Tempo estimado:</span>
|
|
203
|
+
<span className="text-gray-300">~{estimatedMinutes} min</span>
|
|
204
|
+
</div>
|
|
205
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
206
|
+
Cada fase executa sequencialmente. Builder pode demorar mais.
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* Footer */}
|
|
212
|
+
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-white/10 bg-[#0a0a0f]">
|
|
213
|
+
<button
|
|
214
|
+
onClick={closeConfigModal}
|
|
215
|
+
className="px-4 py-2 text-sm text-gray-400 hover:text-white transition-colors"
|
|
216
|
+
>
|
|
217
|
+
Cancelar
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
onClick={handleStart}
|
|
221
|
+
disabled={config.phases.length === 0 || isStarting}
|
|
222
|
+
className={cn(
|
|
223
|
+
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
|
|
224
|
+
config.phases.length === 0 || isStarting
|
|
225
|
+
? 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
|
226
|
+
: 'bg-purple-600 hover:bg-purple-500 text-white'
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{isStarting ? (
|
|
230
|
+
<>
|
|
231
|
+
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
232
|
+
Iniciando...
|
|
233
|
+
</>
|
|
234
|
+
) : (
|
|
235
|
+
<>
|
|
236
|
+
<Rocket className="w-4 h-4" />
|
|
237
|
+
Iniciar
|
|
238
|
+
</>
|
|
239
|
+
)}
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
CheckCircle2,
|
|
6
|
+
Circle,
|
|
7
|
+
Loader2,
|
|
8
|
+
AlertCircle,
|
|
9
|
+
Clock,
|
|
10
|
+
Zap,
|
|
11
|
+
ChevronDown,
|
|
12
|
+
ChevronUp,
|
|
13
|
+
X,
|
|
14
|
+
Maximize2,
|
|
15
|
+
Minimize2,
|
|
16
|
+
SkipForward,
|
|
17
|
+
StopCircle,
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import { cn } from '@/lib/utils';
|
|
20
|
+
import {
|
|
21
|
+
useAutopilotStore,
|
|
22
|
+
type PhaseResult,
|
|
23
|
+
type AgentId,
|
|
24
|
+
} from '@/lib/stores/autopilotStore';
|
|
25
|
+
|
|
26
|
+
const AGENT_INFO: Record<AgentId, { icon: string; color: string; name: string }> = {
|
|
27
|
+
strategist: { icon: '📊', color: 'text-blue-400', name: 'Strategist' },
|
|
28
|
+
architect: { icon: '🏗️', color: 'text-purple-400', name: 'Architect' },
|
|
29
|
+
'system-designer': { icon: '⚙️', color: 'text-cyan-400', name: 'System Designer' },
|
|
30
|
+
builder: { icon: '🔨', color: 'text-amber-400', name: 'Builder' },
|
|
31
|
+
guardian: { icon: '🛡️', color: 'text-green-400', name: 'Guardian' },
|
|
32
|
+
chronicler: { icon: '📝', color: 'text-pink-400', name: 'Chronicler' },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function AutopilotPanel() {
|
|
36
|
+
const { status, phases, specTitle, error, reset, currentPhaseIndex, abortRun } = useAutopilotStore();
|
|
37
|
+
const [startTime] = useState(() => Date.now());
|
|
38
|
+
const [elapsed, setElapsed] = useState(0);
|
|
39
|
+
const [expandedPhase, setExpandedPhase] = useState<number | null>(null);
|
|
40
|
+
const [isMaximized, setIsMaximized] = useState(false);
|
|
41
|
+
|
|
42
|
+
// Update elapsed time
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (status !== 'running') return;
|
|
45
|
+
|
|
46
|
+
const interval = setInterval(() => {
|
|
47
|
+
setElapsed(Math.floor((Date.now() - startTime) / 1000));
|
|
48
|
+
}, 1000);
|
|
49
|
+
|
|
50
|
+
return () => clearInterval(interval);
|
|
51
|
+
}, [status, startTime]);
|
|
52
|
+
|
|
53
|
+
// Don't show if idle
|
|
54
|
+
if (status === 'idle' || phases.length === 0) return null;
|
|
55
|
+
|
|
56
|
+
const completedPhases = phases.filter((p) => p.status === 'completed').length;
|
|
57
|
+
const progress = Math.round((completedPhases / phases.length) * 100);
|
|
58
|
+
|
|
59
|
+
const formatTime = (seconds: number) => {
|
|
60
|
+
const mins = Math.floor(seconds / 60);
|
|
61
|
+
const secs = seconds % 60;
|
|
62
|
+
return `${mins}m ${secs}s`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const formatDuration = (ms?: number) => {
|
|
66
|
+
if (!ms) return '';
|
|
67
|
+
const secs = Math.floor(ms / 1000);
|
|
68
|
+
if (secs < 60) return `${secs}s`;
|
|
69
|
+
const mins = Math.floor(secs / 60);
|
|
70
|
+
const remainSecs = secs % 60;
|
|
71
|
+
return `${mins}m ${remainSecs}s`;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const isRunning = status === 'running';
|
|
75
|
+
const isCompleted = status === 'completed';
|
|
76
|
+
const isFailed = status === 'failed';
|
|
77
|
+
const isDone = isCompleted || isFailed;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
className={cn(
|
|
82
|
+
'fixed bg-[#12121a] border border-white/10 rounded-xl shadow-2xl overflow-hidden z-50 transition-all duration-300 flex flex-col',
|
|
83
|
+
isMaximized ? 'inset-4 w-auto h-auto' : 'bottom-4 right-4 w-[480px]'
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
{/* Header */}
|
|
87
|
+
<div className="flex items-center justify-between px-4 py-3 bg-gradient-to-r from-purple-500/20 to-blue-500/20 border-b border-white/10">
|
|
88
|
+
<div className="flex items-center gap-2">
|
|
89
|
+
<Zap className="w-4 h-4 text-purple-400" />
|
|
90
|
+
<span className="font-semibold text-white text-sm">Autopilot</span>
|
|
91
|
+
{isRunning && (
|
|
92
|
+
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded-full animate-pulse">
|
|
93
|
+
Running
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
{isCompleted && (
|
|
97
|
+
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 text-xs rounded-full">
|
|
98
|
+
Complete
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
{isFailed && (
|
|
102
|
+
<span className="px-2 py-0.5 bg-red-500/20 text-red-400 text-xs rounded-full">
|
|
103
|
+
Failed
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex items-center gap-1">
|
|
108
|
+
{isRunning && (
|
|
109
|
+
<button
|
|
110
|
+
onClick={abortRun}
|
|
111
|
+
className="p-1.5 hover:bg-red-500/20 rounded-lg transition-colors text-red-400 hover:text-red-300"
|
|
112
|
+
title="Abort autopilot"
|
|
113
|
+
>
|
|
114
|
+
<StopCircle className="w-4 h-4" />
|
|
115
|
+
</button>
|
|
116
|
+
)}
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => setIsMaximized(!isMaximized)}
|
|
119
|
+
className="p-1.5 hover:bg-white/10 rounded-lg transition-colors text-gray-400 hover:text-white"
|
|
120
|
+
title={isMaximized ? 'Minimize' : 'Maximize'}
|
|
121
|
+
>
|
|
122
|
+
{isMaximized ? (
|
|
123
|
+
<Minimize2 className="w-4 h-4" />
|
|
124
|
+
) : (
|
|
125
|
+
<Maximize2 className="w-4 h-4" />
|
|
126
|
+
)}
|
|
127
|
+
</button>
|
|
128
|
+
{isDone && (
|
|
129
|
+
<button
|
|
130
|
+
onClick={reset}
|
|
131
|
+
className="p-1.5 hover:bg-white/10 rounded-lg transition-colors text-gray-400 hover:text-white"
|
|
132
|
+
title="Close"
|
|
133
|
+
>
|
|
134
|
+
<X className="w-4 h-4" />
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* Spec Title */}
|
|
141
|
+
<div className="px-4 py-2 border-b border-white/5">
|
|
142
|
+
<p className="text-xs text-gray-400 truncate">{specTitle}</p>
|
|
143
|
+
{isRunning && (
|
|
144
|
+
<p className="text-[10px] text-gray-500 mt-0.5">Output streaming in Terminal tab</p>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Error message */}
|
|
149
|
+
{error && (
|
|
150
|
+
<div className="px-4 py-2 bg-red-500/10 border-b border-red-500/20">
|
|
151
|
+
<p className="text-xs text-red-400">{error}</p>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{/* Phases */}
|
|
156
|
+
<div className={cn('overflow-y-auto', isMaximized ? 'flex-1' : 'max-h-80')}>
|
|
157
|
+
{phases.map((phase, index) => (
|
|
158
|
+
<PhaseItem
|
|
159
|
+
key={`${phase.agent}-${index}`}
|
|
160
|
+
phase={phase}
|
|
161
|
+
index={index}
|
|
162
|
+
isCurrent={index === currentPhaseIndex}
|
|
163
|
+
isExpanded={expandedPhase === index || isMaximized}
|
|
164
|
+
onToggle={() => setExpandedPhase(expandedPhase === index ? null : index)}
|
|
165
|
+
isMaximized={isMaximized}
|
|
166
|
+
formatDuration={formatDuration}
|
|
167
|
+
/>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Footer Stats */}
|
|
172
|
+
<div className="px-4 py-3 border-t border-white/10 bg-white/5">
|
|
173
|
+
<div className="flex items-center justify-between text-xs text-gray-400">
|
|
174
|
+
<div className="flex items-center gap-3">
|
|
175
|
+
<span className="flex items-center gap-1">
|
|
176
|
+
<Clock className="w-3 h-3" />
|
|
177
|
+
{formatTime(elapsed)}
|
|
178
|
+
</span>
|
|
179
|
+
<span>
|
|
180
|
+
{completedPhases}/{phases.length} phases
|
|
181
|
+
</span>
|
|
182
|
+
</div>
|
|
183
|
+
<span className="text-purple-400 font-medium">{progress}%</span>
|
|
184
|
+
</div>
|
|
185
|
+
{/* Progress bar */}
|
|
186
|
+
<div className="mt-2 h-1.5 bg-white/10 rounded-full overflow-hidden">
|
|
187
|
+
<div
|
|
188
|
+
className={cn(
|
|
189
|
+
'h-full rounded-full transition-all duration-300',
|
|
190
|
+
isCompleted ? 'bg-green-500' : isFailed ? 'bg-red-500' : 'bg-purple-500'
|
|
191
|
+
)}
|
|
192
|
+
style={{ width: `${progress}%` }}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function PhaseItem({
|
|
201
|
+
phase,
|
|
202
|
+
index,
|
|
203
|
+
isCurrent,
|
|
204
|
+
isExpanded,
|
|
205
|
+
onToggle,
|
|
206
|
+
isMaximized,
|
|
207
|
+
formatDuration,
|
|
208
|
+
}: {
|
|
209
|
+
phase: PhaseResult;
|
|
210
|
+
index: number;
|
|
211
|
+
isCurrent: boolean;
|
|
212
|
+
isExpanded: boolean;
|
|
213
|
+
onToggle: () => void;
|
|
214
|
+
isMaximized?: boolean;
|
|
215
|
+
formatDuration: (ms?: number) => string;
|
|
216
|
+
}) {
|
|
217
|
+
const agent = AGENT_INFO[phase.agent];
|
|
218
|
+
|
|
219
|
+
const getStatusIcon = () => {
|
|
220
|
+
switch (phase.status) {
|
|
221
|
+
case 'completed':
|
|
222
|
+
return <CheckCircle2 className="w-4 h-4 text-green-400" />;
|
|
223
|
+
case 'running':
|
|
224
|
+
return <Loader2 className="w-4 h-4 text-blue-400 animate-spin" />;
|
|
225
|
+
case 'failed':
|
|
226
|
+
return <AlertCircle className="w-4 h-4 text-red-400" />;
|
|
227
|
+
case 'skipped':
|
|
228
|
+
return <SkipForward className="w-4 h-4 text-gray-500" />;
|
|
229
|
+
default:
|
|
230
|
+
return <Circle className="w-4 h-4 text-gray-500" />;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const hasOutput = phase.output && phase.output.length > 0;
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div
|
|
238
|
+
className={cn(
|
|
239
|
+
'border-b border-white/5 last:border-0',
|
|
240
|
+
isCurrent && phase.status === 'running' && 'bg-purple-500/5'
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
<button
|
|
244
|
+
onClick={onToggle}
|
|
245
|
+
className="w-full px-4 py-2.5 flex items-center gap-3 hover:bg-white/5 transition-colors"
|
|
246
|
+
disabled={!hasOutput && !isMaximized}
|
|
247
|
+
>
|
|
248
|
+
{getStatusIcon()}
|
|
249
|
+
<div className="flex-1 text-left">
|
|
250
|
+
<div className="flex items-center gap-2">
|
|
251
|
+
<span className={cn('text-sm font-medium', agent.color)}>
|
|
252
|
+
{agent.icon} {phase.name}
|
|
253
|
+
</span>
|
|
254
|
+
<span className="text-xs text-gray-500">@{phase.agent}</span>
|
|
255
|
+
{phase.duration && (
|
|
256
|
+
<span className="text-xs text-gray-600">{formatDuration(phase.duration)}</span>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
{phase.status === 'running' && (
|
|
260
|
+
<p className="text-xs text-gray-500 mt-0.5">Processing...</p>
|
|
261
|
+
)}
|
|
262
|
+
{phase.error && <p className="text-xs text-red-400 mt-0.5 truncate">{phase.error}</p>}
|
|
263
|
+
</div>
|
|
264
|
+
{hasOutput && !isMaximized && (
|
|
265
|
+
<div className="text-gray-500">
|
|
266
|
+
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
</button>
|
|
270
|
+
|
|
271
|
+
{/* Tasks completed */}
|
|
272
|
+
{phase.tasksCompleted && phase.tasksCompleted.length > 0 && (
|
|
273
|
+
<div className="px-4 py-2 bg-green-500/5 border-t border-white/5">
|
|
274
|
+
<p className="text-[10px] text-green-400/70 uppercase tracking-wider mb-1">Tasks completed</p>
|
|
275
|
+
{phase.tasksCompleted.map((task, i) => (
|
|
276
|
+
<div key={i} className="flex items-center gap-1.5 text-xs text-green-400 py-0.5">
|
|
277
|
+
<CheckCircle2 className="w-3 h-3 flex-shrink-0" />
|
|
278
|
+
<span className="truncate">{task}</span>
|
|
279
|
+
</div>
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{/* Expanded output */}
|
|
285
|
+
{isExpanded && hasOutput && (
|
|
286
|
+
<div className="px-4 py-3 bg-black/20 border-t border-white/5">
|
|
287
|
+
<pre
|
|
288
|
+
className={cn(
|
|
289
|
+
'text-xs text-gray-400 whitespace-pre-wrap overflow-y-auto font-mono',
|
|
290
|
+
isMaximized ? 'max-h-[300px]' : 'max-h-40'
|
|
291
|
+
)}
|
|
292
|
+
>
|
|
293
|
+
{phase.output}
|
|
294
|
+
</pre>
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|