@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,169 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useUIStore } from '@/lib/stores/uiStore';
|
|
5
|
+
import { useFileStore } from '@/lib/stores/fileStore';
|
|
6
|
+
import { useSettingsStore } from '@/lib/stores/settingsStore';
|
|
7
|
+
|
|
8
|
+
export function useKeyboardShortcuts() {
|
|
9
|
+
const { openModal, activeModal, closeModal, toggleSidebar, toggleTerminal, togglePreview } = useUIStore();
|
|
10
|
+
const {
|
|
11
|
+
activeFile,
|
|
12
|
+
saveFile,
|
|
13
|
+
closeFile,
|
|
14
|
+
navigateBack,
|
|
15
|
+
navigateForward,
|
|
16
|
+
canGoBack,
|
|
17
|
+
canGoForward,
|
|
18
|
+
reopenClosedTab,
|
|
19
|
+
} = useFileStore();
|
|
20
|
+
const { openSettings, isSettingsOpen, closeSettings } = useSettingsStore();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
24
|
+
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
|
25
|
+
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
26
|
+
|
|
27
|
+
// Don't trigger shortcuts when typing in inputs
|
|
28
|
+
const target = e.target as HTMLElement;
|
|
29
|
+
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
|
30
|
+
|
|
31
|
+
// Allow Escape to close modals/settings even in inputs
|
|
32
|
+
if (e.key === 'Escape') {
|
|
33
|
+
if (isSettingsOpen) {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
closeSettings();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (activeModal) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
closeModal();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If in input and not using mod key, skip
|
|
46
|
+
if (isInput && !modKey) return;
|
|
47
|
+
|
|
48
|
+
// Cmd/Ctrl + P - Quick Open
|
|
49
|
+
if (modKey && e.key === 'p' && !e.shiftKey) {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
openModal('quickOpen');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Cmd/Ctrl + Shift + F - Global Search
|
|
56
|
+
if (modKey && e.shiftKey && e.key === 'f') {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
openModal('globalSearch');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Cmd/Ctrl + Shift + P - Command Palette
|
|
63
|
+
if (modKey && e.shiftKey && e.key === 'p') {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
openModal('commandPalette');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cmd/Ctrl + B - Toggle Sidebar
|
|
70
|
+
if (modKey && e.key === 'b' && !e.shiftKey) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
toggleSidebar();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Cmd/Ctrl + ` - Toggle Terminal
|
|
77
|
+
if (modKey && e.key === '`') {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
toggleTerminal();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Cmd/Ctrl + Shift + V - Toggle Preview
|
|
84
|
+
if (modKey && e.shiftKey && e.key === 'v') {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
togglePreview();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Cmd/Ctrl + S - Save File
|
|
91
|
+
if (modKey && e.key === 's' && !e.shiftKey) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
if (activeFile) {
|
|
94
|
+
saveFile(activeFile);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Cmd/Ctrl + , - Open Settings
|
|
100
|
+
if (modKey && e.key === ',') {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
openSettings();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Cmd/Ctrl + Tab - Recent Files
|
|
107
|
+
if (modKey && e.key === 'Tab' && !e.shiftKey) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
openModal('recentFiles');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Cmd/Ctrl + W - Close Tab
|
|
114
|
+
if (modKey && e.key === 'w' && !e.shiftKey) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
if (activeFile) {
|
|
117
|
+
closeFile(activeFile);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Cmd/Ctrl + Shift + T - Reopen Closed Tab
|
|
123
|
+
if (modKey && e.shiftKey && e.key === 't') {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
reopenClosedTab();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Alt + Left - Navigate Back
|
|
130
|
+
if (e.altKey && e.key === 'ArrowLeft' && !modKey) {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
if (canGoBack()) {
|
|
133
|
+
navigateBack();
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Alt + Right - Navigate Forward
|
|
139
|
+
if (e.altKey && e.key === 'ArrowRight' && !modKey) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
if (canGoForward()) {
|
|
142
|
+
navigateForward();
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
149
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
150
|
+
}, [
|
|
151
|
+
openModal,
|
|
152
|
+
activeModal,
|
|
153
|
+
closeModal,
|
|
154
|
+
toggleSidebar,
|
|
155
|
+
toggleTerminal,
|
|
156
|
+
togglePreview,
|
|
157
|
+
activeFile,
|
|
158
|
+
saveFile,
|
|
159
|
+
closeFile,
|
|
160
|
+
navigateBack,
|
|
161
|
+
navigateForward,
|
|
162
|
+
canGoBack,
|
|
163
|
+
canGoForward,
|
|
164
|
+
reopenClosedTab,
|
|
165
|
+
openSettings,
|
|
166
|
+
isSettingsOpen,
|
|
167
|
+
closeSettings,
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, KeyboardEvent } from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Custom hook for keyboard navigation in lists.
|
|
7
|
+
* Implements accessible list navigation per WAI-ARIA guidelines.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Arrow up/down navigation
|
|
11
|
+
* - Home/End jump to first/last
|
|
12
|
+
* - Enter to select
|
|
13
|
+
* - Escape to cancel
|
|
14
|
+
* - Type-ahead search (optional)
|
|
15
|
+
*
|
|
16
|
+
* @param options - Configuration options
|
|
17
|
+
* @returns Navigation state and handlers
|
|
18
|
+
*/
|
|
19
|
+
interface UseListNavigationOptions<T> {
|
|
20
|
+
/** Array of items to navigate */
|
|
21
|
+
items: T[];
|
|
22
|
+
/** Called when an item is selected (Enter key) */
|
|
23
|
+
onSelect?: (item: T, index: number) => void;
|
|
24
|
+
/** Called when Escape is pressed */
|
|
25
|
+
onEscape?: () => void;
|
|
26
|
+
/** Navigation orientation: 'vertical' (default) or 'horizontal' */
|
|
27
|
+
orientation?: 'vertical' | 'horizontal';
|
|
28
|
+
/** Whether navigation wraps around (default: true) */
|
|
29
|
+
loop?: boolean;
|
|
30
|
+
/** Initial selected index (default: 0) */
|
|
31
|
+
initialIndex?: number;
|
|
32
|
+
/** Enable type-ahead search (default: false) */
|
|
33
|
+
typeAhead?: boolean;
|
|
34
|
+
/** Function to get searchable text from item for type-ahead */
|
|
35
|
+
getItemText?: (item: T) => string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface UseListNavigationReturn {
|
|
39
|
+
/** Current selected index */
|
|
40
|
+
selectedIndex: number;
|
|
41
|
+
/** Set selected index programmatically */
|
|
42
|
+
setSelectedIndex: (index: number) => void;
|
|
43
|
+
/** Handle keyboard events - attach to container */
|
|
44
|
+
handleKeyDown: (event: KeyboardEvent) => void;
|
|
45
|
+
/** Check if index is selected */
|
|
46
|
+
isSelected: (index: number) => boolean;
|
|
47
|
+
/** Move to next item */
|
|
48
|
+
moveNext: () => void;
|
|
49
|
+
/** Move to previous item */
|
|
50
|
+
movePrevious: () => void;
|
|
51
|
+
/** Move to first item */
|
|
52
|
+
moveFirst: () => void;
|
|
53
|
+
/** Move to last item */
|
|
54
|
+
moveLast: () => void;
|
|
55
|
+
/** Select current item */
|
|
56
|
+
selectCurrent: () => void;
|
|
57
|
+
/** Reset to initial state */
|
|
58
|
+
reset: () => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function useListNavigation<T>({
|
|
62
|
+
items,
|
|
63
|
+
onSelect,
|
|
64
|
+
onEscape,
|
|
65
|
+
orientation = 'vertical',
|
|
66
|
+
loop = true,
|
|
67
|
+
initialIndex = 0,
|
|
68
|
+
typeAhead = false,
|
|
69
|
+
getItemText,
|
|
70
|
+
}: UseListNavigationOptions<T>): UseListNavigationReturn {
|
|
71
|
+
const [selectedIndex, setSelectedIndex] = useState(
|
|
72
|
+
Math.min(initialIndex, Math.max(0, items.length - 1))
|
|
73
|
+
);
|
|
74
|
+
const [searchString, setSearchString] = useState('');
|
|
75
|
+
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
|
|
76
|
+
|
|
77
|
+
// Reset index when items change
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (items.length === 0) {
|
|
80
|
+
setSelectedIndex(-1);
|
|
81
|
+
} else if (selectedIndex >= items.length) {
|
|
82
|
+
setSelectedIndex(items.length - 1);
|
|
83
|
+
}
|
|
84
|
+
}, [items.length, selectedIndex]);
|
|
85
|
+
|
|
86
|
+
const moveNext = useCallback(() => {
|
|
87
|
+
setSelectedIndex((current) => {
|
|
88
|
+
if (items.length === 0) return -1;
|
|
89
|
+
if (current >= items.length - 1) {
|
|
90
|
+
return loop ? 0 : current;
|
|
91
|
+
}
|
|
92
|
+
return current + 1;
|
|
93
|
+
});
|
|
94
|
+
}, [items.length, loop]);
|
|
95
|
+
|
|
96
|
+
const movePrevious = useCallback(() => {
|
|
97
|
+
setSelectedIndex((current) => {
|
|
98
|
+
if (items.length === 0) return -1;
|
|
99
|
+
if (current <= 0) {
|
|
100
|
+
return loop ? items.length - 1 : 0;
|
|
101
|
+
}
|
|
102
|
+
return current - 1;
|
|
103
|
+
});
|
|
104
|
+
}, [items.length, loop]);
|
|
105
|
+
|
|
106
|
+
const moveFirst = useCallback(() => {
|
|
107
|
+
if (items.length > 0) {
|
|
108
|
+
setSelectedIndex(0);
|
|
109
|
+
}
|
|
110
|
+
}, [items.length]);
|
|
111
|
+
|
|
112
|
+
const moveLast = useCallback(() => {
|
|
113
|
+
if (items.length > 0) {
|
|
114
|
+
setSelectedIndex(items.length - 1);
|
|
115
|
+
}
|
|
116
|
+
}, [items.length]);
|
|
117
|
+
|
|
118
|
+
const selectCurrent = useCallback(() => {
|
|
119
|
+
if (selectedIndex >= 0 && selectedIndex < items.length && onSelect) {
|
|
120
|
+
onSelect(items[selectedIndex], selectedIndex);
|
|
121
|
+
}
|
|
122
|
+
}, [selectedIndex, items, onSelect]);
|
|
123
|
+
|
|
124
|
+
const reset = useCallback(() => {
|
|
125
|
+
setSelectedIndex(Math.min(initialIndex, Math.max(0, items.length - 1)));
|
|
126
|
+
setSearchString('');
|
|
127
|
+
if (searchTimeout) {
|
|
128
|
+
clearTimeout(searchTimeout);
|
|
129
|
+
setSearchTimeout(null);
|
|
130
|
+
}
|
|
131
|
+
}, [initialIndex, items.length, searchTimeout]);
|
|
132
|
+
|
|
133
|
+
// Type-ahead search
|
|
134
|
+
const handleTypeAhead = useCallback(
|
|
135
|
+
(char: string) => {
|
|
136
|
+
if (!typeAhead || !getItemText) return false;
|
|
137
|
+
|
|
138
|
+
const newSearchString = searchString + char.toLowerCase();
|
|
139
|
+
setSearchString(newSearchString);
|
|
140
|
+
|
|
141
|
+
// Clear previous timeout
|
|
142
|
+
if (searchTimeout) {
|
|
143
|
+
clearTimeout(searchTimeout);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Set new timeout to reset search
|
|
147
|
+
setSearchTimeout(
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
setSearchString('');
|
|
150
|
+
}, 500)
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Find matching item
|
|
154
|
+
const matchIndex = items.findIndex((item) =>
|
|
155
|
+
getItemText(item).toLowerCase().startsWith(newSearchString)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (matchIndex !== -1) {
|
|
159
|
+
setSelectedIndex(matchIndex);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return false;
|
|
164
|
+
},
|
|
165
|
+
[typeAhead, getItemText, searchString, searchTimeout, items]
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const handleKeyDown = useCallback(
|
|
169
|
+
(event: KeyboardEvent) => {
|
|
170
|
+
const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
|
171
|
+
const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
|
172
|
+
|
|
173
|
+
switch (event.key) {
|
|
174
|
+
case prevKey:
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
movePrevious();
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case nextKey:
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
moveNext();
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case 'Home':
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
moveFirst();
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case 'End':
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
moveLast();
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'Enter':
|
|
195
|
+
case ' ':
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
selectCurrent();
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case 'Escape':
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
if (onEscape) {
|
|
203
|
+
onEscape();
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
default:
|
|
208
|
+
// Type-ahead for printable characters
|
|
209
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
210
|
+
if (handleTypeAhead(event.key)) {
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
[orientation, movePrevious, moveNext, moveFirst, moveLast, selectCurrent, onEscape, handleTypeAhead]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const isSelected = useCallback(
|
|
221
|
+
(index: number) => index === selectedIndex,
|
|
222
|
+
[selectedIndex]
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
selectedIndex,
|
|
227
|
+
setSelectedIndex,
|
|
228
|
+
handleKeyDown,
|
|
229
|
+
isSelected,
|
|
230
|
+
moveNext,
|
|
231
|
+
movePrevious,
|
|
232
|
+
moveFirst,
|
|
233
|
+
moveLast,
|
|
234
|
+
selectCurrent,
|
|
235
|
+
reset,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared autopilot constants and utilities.
|
|
6
|
+
* Used by both /api/autopilot/execute and /api/autopilot/terminal-execute routes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const VALID_AGENTS = [
|
|
10
|
+
'strategist', 'architect', 'system-designer', 'builder', 'guardian', 'chronicler',
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export type AgentName = typeof VALID_AGENTS[number];
|
|
14
|
+
|
|
15
|
+
export function isValidAgent(agent: string): agent is AgentName {
|
|
16
|
+
return VALID_AGENTS.includes(agent as AgentName);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const AGENT_SKILLS: Record<AgentName, string> = {
|
|
20
|
+
strategist: '/agents:strategist',
|
|
21
|
+
architect: '/agents:architect',
|
|
22
|
+
'system-designer': '/agents:system-designer',
|
|
23
|
+
builder: '/agents:builder',
|
|
24
|
+
guardian: '/agents:guardian',
|
|
25
|
+
chronicler: '/agents:chronicler',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const AGENT_PROMPTS: Record<AgentName, string> = {
|
|
29
|
+
strategist: `Analise a spec e refine os requisitos:
|
|
30
|
+
{spec_content}
|
|
31
|
+
|
|
32
|
+
1. Identificar requisitos implícitos
|
|
33
|
+
2. Listar acceptance criteria
|
|
34
|
+
3. Dependências e riscos
|
|
35
|
+
4. Estimar complexidade`,
|
|
36
|
+
|
|
37
|
+
architect: `Defina a arquitetura com base na spec:
|
|
38
|
+
{spec_content}
|
|
39
|
+
|
|
40
|
+
Contexto anterior: {previous_output}
|
|
41
|
+
|
|
42
|
+
1. Arquitetura da solução
|
|
43
|
+
2. Padrões e tecnologias
|
|
44
|
+
3. Componentes necessários
|
|
45
|
+
4. Decisões importantes`,
|
|
46
|
+
|
|
47
|
+
'system-designer': `Projete o system design com base na spec:
|
|
48
|
+
{spec_content}
|
|
49
|
+
|
|
50
|
+
Contexto anterior: {previous_output}
|
|
51
|
+
|
|
52
|
+
1. Back-of-the-envelope estimation
|
|
53
|
+
2. High-level design
|
|
54
|
+
3. Data model e storage
|
|
55
|
+
4. Scalability e reliability`,
|
|
56
|
+
|
|
57
|
+
builder: `Implemente a solução conforme spec e design:
|
|
58
|
+
{spec_content}
|
|
59
|
+
|
|
60
|
+
Contexto anterior: {previous_output}
|
|
61
|
+
|
|
62
|
+
1. Criar/modificar arquivos necessários
|
|
63
|
+
2. Implementar lógica principal
|
|
64
|
+
3. Tratamento de erros`,
|
|
65
|
+
|
|
66
|
+
guardian: `Revise o código implementado:
|
|
67
|
+
{spec_content}
|
|
68
|
+
|
|
69
|
+
Implementação: {previous_output}
|
|
70
|
+
|
|
71
|
+
1. Segurança
|
|
72
|
+
2. Performance
|
|
73
|
+
3. Edge cases
|
|
74
|
+
4. Melhorias necessárias`,
|
|
75
|
+
|
|
76
|
+
chronicler: `Documente as mudanças realizadas:
|
|
77
|
+
{spec_content}
|
|
78
|
+
|
|
79
|
+
Implementação: {previous_output}
|
|
80
|
+
|
|
81
|
+
1. Resumir o que foi implementado
|
|
82
|
+
2. Arquivos criados/modificados
|
|
83
|
+
3. Atualizar tasks na spec`,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const AGENT_TIMEOUTS: Record<AgentName, number> = {
|
|
87
|
+
strategist: 300,
|
|
88
|
+
architect: 600,
|
|
89
|
+
'system-designer': 600,
|
|
90
|
+
builder: 1200,
|
|
91
|
+
guardian: 600,
|
|
92
|
+
chronicler: 300,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Agents that produce actionable output for task tracking
|
|
96
|
+
export const TASK_TRACKING_AGENTS: AgentName[] = ['builder', 'guardian', 'chronicler'];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Load the full agent definition from .claude/commands/agents/{agent}.md
|
|
100
|
+
* Returns the file content or null if not found.
|
|
101
|
+
*/
|
|
102
|
+
export async function loadAgentDefinition(projectPath: string, agent: AgentName): Promise<string | null> {
|
|
103
|
+
const filePath = path.join(projectPath, '.claude', 'commands', 'agents', `${agent}.md`);
|
|
104
|
+
try {
|
|
105
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build the full prompt for a given agent phase.
|
|
113
|
+
* If agentDefinition is provided (from loadAgentDefinition), uses it as context.
|
|
114
|
+
* Otherwise falls back to the slash command text (won't work with --print mode).
|
|
115
|
+
*/
|
|
116
|
+
export function buildPrompt(
|
|
117
|
+
agent: AgentName,
|
|
118
|
+
specContent: string,
|
|
119
|
+
previousOutputs: string[],
|
|
120
|
+
agentDefinition?: string | null,
|
|
121
|
+
): string {
|
|
122
|
+
const prompt = AGENT_PROMPTS[agent]
|
|
123
|
+
.replace('{spec_content}', specContent)
|
|
124
|
+
.replace('{previous_output}', previousOutputs.join('\n---\n') || 'N/A');
|
|
125
|
+
|
|
126
|
+
// Use full agent definition if available, otherwise fallback to skill command
|
|
127
|
+
const agentContext = agentDefinition || AGENT_SKILLS[agent];
|
|
128
|
+
|
|
129
|
+
return `${agentContext}\n\n---\n\n${prompt}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract unchecked task titles from a markdown spec file.
|
|
134
|
+
* Matches lines like: - [ ] Task title
|
|
135
|
+
*/
|
|
136
|
+
export function extractUncheckedTasks(content: string): string[] {
|
|
137
|
+
const tasks: string[] = [];
|
|
138
|
+
const regex = /^\s*[-*]\s*\[ \]\s*(?:\[[^\]]+\]\s*)?(.+)$/gm;
|
|
139
|
+
let match;
|
|
140
|
+
while ((match = regex.exec(content)) !== null) {
|
|
141
|
+
tasks.push(match[1].trim());
|
|
142
|
+
}
|
|
143
|
+
return tasks;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if the agent output mentions a task as completed.
|
|
148
|
+
* Uses case-insensitive substring matching with completion context.
|
|
149
|
+
*/
|
|
150
|
+
export function isTaskMentionedAsCompleted(taskTitle: string, output: string): boolean {
|
|
151
|
+
const lower = output.toLowerCase();
|
|
152
|
+
const taskLower = taskTitle.toLowerCase();
|
|
153
|
+
|
|
154
|
+
if (!lower.includes(taskLower)) return false;
|
|
155
|
+
|
|
156
|
+
// For short task titles (< 10 chars), require a completion keyword nearby
|
|
157
|
+
if (taskLower.length < 10) {
|
|
158
|
+
const completionKeywords = [
|
|
159
|
+
'completed', 'done', 'implemented', 'finished', 'created',
|
|
160
|
+
'added', 'fixed', 'resolved', 'built', 'configured',
|
|
161
|
+
'✅', '✓', '[x]', 'complete',
|
|
162
|
+
];
|
|
163
|
+
const idx = lower.indexOf(taskLower);
|
|
164
|
+
const context = lower.slice(Math.max(0, idx - 200), idx + taskLower.length + 200);
|
|
165
|
+
return completionKeywords.some(kw => context.includes(kw));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* After a phase completes, auto-detect completed tasks and update the spec file.
|
|
173
|
+
*/
|
|
174
|
+
export async function autoUpdateSpecTasks(
|
|
175
|
+
specFilePath: string,
|
|
176
|
+
agentOutput: string
|
|
177
|
+
): Promise<string[]> {
|
|
178
|
+
const completedTasks: string[] = [];
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const content = await fs.readFile(specFilePath, 'utf-8');
|
|
182
|
+
const uncheckedTasks = extractUncheckedTasks(content);
|
|
183
|
+
|
|
184
|
+
if (uncheckedTasks.length === 0) return [];
|
|
185
|
+
|
|
186
|
+
let updatedContent = content;
|
|
187
|
+
for (const taskTitle of uncheckedTasks) {
|
|
188
|
+
if (isTaskMentionedAsCompleted(taskTitle, agentOutput)) {
|
|
189
|
+
const escapedTitle = taskTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
190
|
+
const taskRegex = new RegExp(
|
|
191
|
+
`^(\\s*[-*]\\s*)\\[ \\](\\s*(?:\\[[^\\]]+\\]\\s*)?)${escapedTitle}`,
|
|
192
|
+
'gm'
|
|
193
|
+
);
|
|
194
|
+
updatedContent = updatedContent.replace(taskRegex, (match, prefix, middle) => {
|
|
195
|
+
completedTasks.push(taskTitle);
|
|
196
|
+
return `${prefix}[x]${middle}${taskTitle}`;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (completedTasks.length > 0) {
|
|
202
|
+
await fs.writeFile(specFilePath, updatedContent, 'utf-8');
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('Error auto-updating spec tasks:', error);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return completedTasks;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Completion marker used to detect when an autopilot phase finishes in the terminal. */
|
|
212
|
+
export const PHASE_DONE_MARKER = '___DEVFLOW_PHASE_DONE_';
|
|
213
|
+
export const PHASE_DONE_REGEX = /___DEVFLOW_PHASE_DONE_(\d+)___/;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Agent } from '@/lib/types';
|
|
2
|
+
|
|
3
|
+
// Agent definitions
|
|
4
|
+
export const AGENTS: Agent[] = [
|
|
5
|
+
{
|
|
6
|
+
id: 'strategist',
|
|
7
|
+
name: '@strategist',
|
|
8
|
+
displayName: 'Strategist',
|
|
9
|
+
icon: '📊',
|
|
10
|
+
color: '#3B82F6',
|
|
11
|
+
description: 'Product Manager & Analista - Transforma problemas em planos',
|
|
12
|
+
shortDescription: 'Planejamento & Produto',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'architect',
|
|
16
|
+
name: '@architect',
|
|
17
|
+
displayName: 'Architect',
|
|
18
|
+
icon: '🏗️',
|
|
19
|
+
color: '#8B5CF6',
|
|
20
|
+
description: 'Solutions Architect - Design técnico e decisões',
|
|
21
|
+
shortDescription: 'Design & Arquitetura',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'system-designer',
|
|
25
|
+
name: '@system-designer',
|
|
26
|
+
displayName: 'System Designer',
|
|
27
|
+
icon: '⚙️',
|
|
28
|
+
color: '#06B6D4',
|
|
29
|
+
description: 'System Design Specialist - Infraestrutura em escala',
|
|
30
|
+
shortDescription: 'System Design & Escala',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'builder',
|
|
34
|
+
name: '@builder',
|
|
35
|
+
displayName: 'Builder',
|
|
36
|
+
icon: '🔨',
|
|
37
|
+
color: '#F59E0B',
|
|
38
|
+
description: 'Senior Developer - Implementação de código',
|
|
39
|
+
shortDescription: 'Implementação',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'guardian',
|
|
43
|
+
name: '@guardian',
|
|
44
|
+
displayName: 'Guardian',
|
|
45
|
+
icon: '🛡️',
|
|
46
|
+
color: '#10B981',
|
|
47
|
+
description: 'QA Engineer - Qualidade e segurança',
|
|
48
|
+
shortDescription: 'Qualidade & Testes',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'chronicler',
|
|
52
|
+
name: '@chronicler',
|
|
53
|
+
displayName: 'Chronicler',
|
|
54
|
+
icon: '📝',
|
|
55
|
+
color: '#EC4899',
|
|
56
|
+
description: 'Technical Writer - Documentação',
|
|
57
|
+
shortDescription: 'Documentação',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const getAgentById = (id: string): Agent | undefined => {
|
|
62
|
+
return AGENTS.find(agent => agent.id === id);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const getAgentByName = (name: string): Agent | undefined => {
|
|
66
|
+
return AGENTS.find(agent => agent.name === name);
|
|
67
|
+
};
|