@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.
Files changed (106) hide show
  1. package/.claude/commands/agents/architect.md +1162 -0
  2. package/.claude/commands/agents/architect.meta.yaml +124 -0
  3. package/.claude/commands/agents/builder.md +1432 -0
  4. package/.claude/commands/agents/builder.meta.yaml +117 -0
  5. package/.claude/commands/agents/chronicler.md +633 -0
  6. package/.claude/commands/agents/chronicler.meta.yaml +217 -0
  7. package/.claude/commands/agents/guardian.md +456 -0
  8. package/.claude/commands/agents/guardian.meta.yaml +127 -0
  9. package/.claude/commands/agents/strategist.md +483 -0
  10. package/.claude/commands/agents/strategist.meta.yaml +158 -0
  11. package/.claude/commands/agents/system-designer.md +1137 -0
  12. package/.claude/commands/agents/system-designer.meta.yaml +156 -0
  13. package/.claude/commands/devflow-help.md +93 -0
  14. package/.claude/commands/devflow-status.md +60 -0
  15. package/.claude/commands/quick/create-adr.md +82 -0
  16. package/.claude/commands/quick/new-feature.md +57 -0
  17. package/.claude/commands/quick/security-check.md +54 -0
  18. package/.claude/commands/quick/system-design.md +58 -0
  19. package/.claude_project +52 -0
  20. package/.devflow/agents/architect.meta.yaml +122 -0
  21. package/.devflow/agents/builder.meta.yaml +116 -0
  22. package/.devflow/agents/chronicler.meta.yaml +222 -0
  23. package/.devflow/agents/guardian.meta.yaml +127 -0
  24. package/.devflow/agents/strategist.meta.yaml +158 -0
  25. package/.devflow/agents/system-designer.meta.yaml +265 -0
  26. package/.devflow/project.yaml +242 -0
  27. package/.gitignore-template +84 -0
  28. package/LICENSE +21 -0
  29. package/README.md +249 -0
  30. package/bin/devflow.js +54 -0
  31. package/lib/autopilot.js +235 -0
  32. package/lib/autopilotConstants.js +213 -0
  33. package/lib/constants.js +95 -0
  34. package/lib/init.js +200 -0
  35. package/lib/update.js +181 -0
  36. package/lib/utils.js +157 -0
  37. package/lib/web.js +119 -0
  38. package/package.json +57 -0
  39. package/web/CHANGELOG.md +192 -0
  40. package/web/README.md +156 -0
  41. package/web/app/api/autopilot/execute/route.ts +102 -0
  42. package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
  43. package/web/app/api/files/route.ts +280 -0
  44. package/web/app/api/files/tree/route.ts +160 -0
  45. package/web/app/api/git/route.ts +201 -0
  46. package/web/app/api/health/route.ts +94 -0
  47. package/web/app/api/project/open/route.ts +134 -0
  48. package/web/app/api/search/route.ts +247 -0
  49. package/web/app/api/specs/route.ts +405 -0
  50. package/web/app/api/terminal/route.ts +222 -0
  51. package/web/app/globals.css +160 -0
  52. package/web/app/ide/layout.tsx +43 -0
  53. package/web/app/ide/page.tsx +216 -0
  54. package/web/app/layout.tsx +34 -0
  55. package/web/app/page.tsx +303 -0
  56. package/web/components/agents/AgentIcons.tsx +281 -0
  57. package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
  58. package/web/components/autopilot/AutopilotPanel.tsx +299 -0
  59. package/web/components/dashboard/DashboardPanel.tsx +393 -0
  60. package/web/components/editor/Breadcrumbs.tsx +134 -0
  61. package/web/components/editor/EditorPanel.tsx +120 -0
  62. package/web/components/editor/EditorTabs.tsx +229 -0
  63. package/web/components/editor/MarkdownPreview.tsx +154 -0
  64. package/web/components/editor/MermaidDiagram.tsx +113 -0
  65. package/web/components/editor/MonacoEditor.tsx +177 -0
  66. package/web/components/editor/TabContextMenu.tsx +207 -0
  67. package/web/components/git/GitPanel.tsx +534 -0
  68. package/web/components/layout/Shell.tsx +15 -0
  69. package/web/components/layout/StatusBar.tsx +100 -0
  70. package/web/components/modals/CommandPalette.tsx +393 -0
  71. package/web/components/modals/GlobalSearch.tsx +348 -0
  72. package/web/components/modals/QuickOpen.tsx +241 -0
  73. package/web/components/modals/RecentFiles.tsx +208 -0
  74. package/web/components/projects/ProjectSelector.tsx +147 -0
  75. package/web/components/settings/SettingItem.tsx +150 -0
  76. package/web/components/settings/SettingsPanel.tsx +323 -0
  77. package/web/components/specs/SpecsPanel.tsx +1091 -0
  78. package/web/components/terminal/TerminalPanel.tsx +683 -0
  79. package/web/components/ui/ContextMenu.tsx +182 -0
  80. package/web/components/ui/LoadingSpinner.tsx +66 -0
  81. package/web/components/ui/ResizeHandle.tsx +110 -0
  82. package/web/components/ui/Skeleton.tsx +108 -0
  83. package/web/components/ui/SkipLinks.tsx +37 -0
  84. package/web/components/ui/Toaster.tsx +57 -0
  85. package/web/hooks/useFocusTrap.ts +141 -0
  86. package/web/hooks/useKeyboardShortcuts.ts +169 -0
  87. package/web/hooks/useListNavigation.ts +237 -0
  88. package/web/lib/autopilotConstants.ts +213 -0
  89. package/web/lib/constants/agents.ts +67 -0
  90. package/web/lib/git.ts +339 -0
  91. package/web/lib/ptyManager.ts +191 -0
  92. package/web/lib/specsParser.ts +299 -0
  93. package/web/lib/stores/autopilotStore.ts +288 -0
  94. package/web/lib/stores/fileStore.ts +550 -0
  95. package/web/lib/stores/gitStore.ts +386 -0
  96. package/web/lib/stores/projectStore.ts +196 -0
  97. package/web/lib/stores/settingsStore.ts +126 -0
  98. package/web/lib/stores/specsStore.ts +297 -0
  99. package/web/lib/stores/uiStore.ts +175 -0
  100. package/web/lib/types/index.ts +177 -0
  101. package/web/lib/utils.ts +98 -0
  102. package/web/next.config.js +50 -0
  103. package/web/package.json +54 -0
  104. package/web/postcss.config.js +6 -0
  105. package/web/tailwind.config.ts +68 -0
  106. 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
+ }