snow-ai 0.3.16 β 0.3.18
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/dist/api/systemPrompt.js +5 -3
- package/dist/hooks/useAgentPicker.d.ts +10 -0
- package/dist/hooks/useAgentPicker.js +32 -0
- package/dist/hooks/useCommandPanel.js +8 -0
- package/dist/hooks/useFilePicker.d.ts +1 -0
- package/dist/hooks/useFilePicker.js +102 -23
- package/dist/hooks/useKeyboardInput.d.ts +22 -0
- package/dist/hooks/useKeyboardInput.js +109 -1
- package/dist/hooks/useStreamingState.js +16 -10
- package/dist/hooks/useTodoPicker.d.ts +16 -0
- package/dist/hooks/useTodoPicker.js +94 -0
- package/dist/ui/components/AgentPickerPanel.d.ts +8 -0
- package/dist/ui/components/AgentPickerPanel.js +74 -0
- package/dist/ui/components/ChatInput.js +41 -96
- package/dist/ui/components/FileList.d.ts +1 -0
- package/dist/ui/components/FileList.js +181 -32
- package/dist/ui/components/TodoPickerPanel.d.ts +14 -0
- package/dist/ui/components/TodoPickerPanel.js +117 -0
- package/dist/ui/pages/ChatScreen.d.ts +2 -0
- package/dist/ui/pages/ChatScreen.js +2 -0
- package/dist/utils/commandExecutor.d.ts +1 -1
- package/dist/utils/commands/agent.d.ts +2 -0
- package/dist/utils/commands/agent.js +12 -0
- package/dist/utils/commands/todoPicker.d.ts +2 -0
- package/dist/utils/commands/todoPicker.js +12 -0
- package/dist/utils/subAgentExecutor.js +3 -12
- package/dist/utils/todoScanner.d.ts +8 -0
- package/dist/utils/todoScanner.js +148 -0
- package/package.json +1 -1
package/dist/api/systemPrompt.js
CHANGED
|
@@ -159,12 +159,14 @@ and other shell features. Your capabilities include text processing, data filter
|
|
|
159
159
|
manipulation, workflow automation, and complex command chaining to solve sophisticated
|
|
160
160
|
system administration and data processing challenges.
|
|
161
161
|
|
|
162
|
-
**Sub-Agent:**
|
|
163
|
-
|
|
162
|
+
**Sub-Agent:**
|
|
163
|
+
*If you don't have a sub-agent tool, ignore this feature*
|
|
164
|
+
- A sub-agent is a separate session isolated from the main session, and a sub-agent may have some of the tools described above to focus on solving a specific problem.
|
|
164
165
|
If you have a sub-agent tool, then you can leave some of the work to the sub-agent to solve.
|
|
165
166
|
For example, if you have a sub-agent of a work plan, you can hand over the work plan to the sub-agent to solve when you receive user requirements.
|
|
166
167
|
This way, the master agent can focus on task fulfillment.
|
|
167
|
-
|
|
168
|
+
|
|
169
|
+
- The user may set a sub-agent, and there will be the word \`#agent_*\` in the user's message. \`*\` Is a wildcard,is the tool name of the sub-agent, and you must use this sub-agent.
|
|
168
170
|
|
|
169
171
|
## π Quality Assurance
|
|
170
172
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TextBuffer } from '../utils/textBuffer.js';
|
|
2
|
+
import { type SubAgent } from '../utils/subAgentConfig.js';
|
|
3
|
+
export declare function useAgentPicker(buffer: TextBuffer, triggerUpdate: () => void): {
|
|
4
|
+
showAgentPicker: boolean;
|
|
5
|
+
setShowAgentPicker: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
6
|
+
agentSelectedIndex: number;
|
|
7
|
+
setAgentSelectedIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
8
|
+
agents: SubAgent[];
|
|
9
|
+
handleAgentSelect: (agent: SubAgent) => void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { getSubAgents } from '../utils/subAgentConfig.js';
|
|
3
|
+
export function useAgentPicker(buffer, triggerUpdate) {
|
|
4
|
+
const [showAgentPicker, setShowAgentPicker] = useState(false);
|
|
5
|
+
const [agentSelectedIndex, setAgentSelectedIndex] = useState(0);
|
|
6
|
+
const [agents, setAgents] = useState([]);
|
|
7
|
+
// Load agents when picker is shown
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (showAgentPicker) {
|
|
10
|
+
const loadedAgents = getSubAgents();
|
|
11
|
+
setAgents(loadedAgents);
|
|
12
|
+
setAgentSelectedIndex(0);
|
|
13
|
+
}
|
|
14
|
+
}, [showAgentPicker]);
|
|
15
|
+
// Handle agent selection
|
|
16
|
+
const handleAgentSelect = useCallback((agent) => {
|
|
17
|
+
// Clear buffer and insert agent reference
|
|
18
|
+
buffer.setText('');
|
|
19
|
+
buffer.insert(`#${agent.id} `);
|
|
20
|
+
setShowAgentPicker(false);
|
|
21
|
+
setAgentSelectedIndex(0);
|
|
22
|
+
triggerUpdate();
|
|
23
|
+
}, [buffer, triggerUpdate]);
|
|
24
|
+
return {
|
|
25
|
+
showAgentPicker,
|
|
26
|
+
setShowAgentPicker,
|
|
27
|
+
agentSelectedIndex,
|
|
28
|
+
setAgentSelectedIndex,
|
|
29
|
+
agents,
|
|
30
|
+
handleAgentSelect,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -34,6 +34,14 @@ const commands = [
|
|
|
34
34
|
name: 'export',
|
|
35
35
|
description: 'Export chat conversation to text file with save dialog',
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
name: 'agent-',
|
|
39
|
+
description: 'Select and use a sub-agent to handle specific tasks',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'todo-',
|
|
43
|
+
description: 'Search and select TODO comments from project files',
|
|
44
|
+
},
|
|
37
45
|
];
|
|
38
46
|
export function useCommandPanel(buffer, isProcessing = false) {
|
|
39
47
|
const [showCommands, setShowCommands] = useState(false);
|
|
@@ -10,6 +10,7 @@ export declare function useFilePicker(buffer: TextBuffer, triggerUpdate: () => v
|
|
|
10
10
|
atSymbolPosition: number;
|
|
11
11
|
setAtSymbolPosition: (_pos: number) => void;
|
|
12
12
|
filteredFileCount: number;
|
|
13
|
+
searchMode: "content" | "file";
|
|
13
14
|
updateFilePickerState: (text: string, cursorPos: number) => void;
|
|
14
15
|
handleFileSelect: (filePath: string) => Promise<void>;
|
|
15
16
|
handleFilteredCountChange: (count: number) => void;
|
|
@@ -8,6 +8,7 @@ function filePickerReducer(state, action) {
|
|
|
8
8
|
fileSelectedIndex: 0,
|
|
9
9
|
fileQuery: action.query,
|
|
10
10
|
atSymbolPosition: action.position,
|
|
11
|
+
searchMode: action.searchMode,
|
|
11
12
|
};
|
|
12
13
|
case 'HIDE':
|
|
13
14
|
return {
|
|
@@ -46,6 +47,7 @@ export function useFilePicker(buffer, triggerUpdate) {
|
|
|
46
47
|
fileQuery: '',
|
|
47
48
|
atSymbolPosition: -1,
|
|
48
49
|
filteredFileCount: 0,
|
|
50
|
+
searchMode: 'file',
|
|
49
51
|
});
|
|
50
52
|
const fileListRef = useRef(null);
|
|
51
53
|
// Update file picker state
|
|
@@ -56,41 +58,104 @@ export function useFilePicker(buffer, triggerUpdate) {
|
|
|
56
58
|
}
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
59
|
-
// Find the last '@' symbol before the cursor
|
|
61
|
+
// Find the last '@' or '@@' symbol before the cursor
|
|
60
62
|
const beforeCursor = text.slice(0, cursorPos);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
// Look for @@ first (content search), then @ (file search)
|
|
64
|
+
let searchMode = 'file';
|
|
65
|
+
let position = -1;
|
|
66
|
+
let query = '';
|
|
67
|
+
// Search backwards from cursor to find @@ or @
|
|
68
|
+
for (let i = beforeCursor.length - 1; i >= 0; i--) {
|
|
69
|
+
if (beforeCursor[i] === '@') {
|
|
70
|
+
// Check if this is part of @@
|
|
71
|
+
if (i > 0 && beforeCursor[i - 1] === '@') {
|
|
72
|
+
// Found @@, use content search
|
|
73
|
+
searchMode = 'content';
|
|
74
|
+
position = i - 1; // Position of first @
|
|
75
|
+
const afterDoubleAt = beforeCursor.slice(i + 1);
|
|
76
|
+
// Only activate if no space/newline after @@
|
|
77
|
+
if (!afterDoubleAt.includes(' ') && !afterDoubleAt.includes('\n')) {
|
|
78
|
+
query = afterDoubleAt;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Has space after @@, not valid
|
|
83
|
+
position = -1;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Found single @, check if next char is also @
|
|
89
|
+
if (i < beforeCursor.length - 1 && beforeCursor[i + 1] === '@') {
|
|
90
|
+
// This @ is part of @@, continue searching
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Single @, use file search
|
|
94
|
+
searchMode = 'file';
|
|
95
|
+
position = i;
|
|
96
|
+
const afterAt = beforeCursor.slice(i + 1);
|
|
97
|
+
// Only activate if no space/newline after @
|
|
98
|
+
if (!afterAt.includes(' ') && !afterAt.includes('\n')) {
|
|
99
|
+
query = afterAt;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Has space after @, not valid
|
|
104
|
+
position = -1;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
70
107
|
}
|
|
71
|
-
return;
|
|
72
108
|
}
|
|
73
109
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
if (position !== -1) {
|
|
111
|
+
// For both @ and @@, position points to where we should start replacement
|
|
112
|
+
// For @@, position is the first @
|
|
113
|
+
// For @, position is the single @
|
|
114
|
+
if (!state.showFilePicker ||
|
|
115
|
+
state.fileQuery !== query ||
|
|
116
|
+
state.atSymbolPosition !== position ||
|
|
117
|
+
state.searchMode !== searchMode) {
|
|
118
|
+
dispatch({
|
|
119
|
+
type: 'SHOW',
|
|
120
|
+
query,
|
|
121
|
+
position,
|
|
122
|
+
searchMode,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Hide file picker if no valid @ context found
|
|
128
|
+
if (state.showFilePicker) {
|
|
129
|
+
dispatch({ type: 'HIDE' });
|
|
130
|
+
}
|
|
77
131
|
}
|
|
78
|
-
}, [
|
|
132
|
+
}, [
|
|
133
|
+
state.showFilePicker,
|
|
134
|
+
state.fileQuery,
|
|
135
|
+
state.atSymbolPosition,
|
|
136
|
+
state.searchMode,
|
|
137
|
+
]);
|
|
79
138
|
// Handle file selection
|
|
80
139
|
const handleFileSelect = useCallback(async (filePath) => {
|
|
81
140
|
if (state.atSymbolPosition !== -1) {
|
|
82
141
|
const text = buffer.getFullText();
|
|
83
142
|
const cursorPos = buffer.getCursorPosition();
|
|
84
|
-
// Replace
|
|
143
|
+
// Replace query with selected file path
|
|
144
|
+
// For content search (@@), the filePath already includes line number
|
|
145
|
+
// For file search (@), just the file path
|
|
85
146
|
const beforeAt = text.slice(0, state.atSymbolPosition);
|
|
86
147
|
const afterCursor = text.slice(cursorPos);
|
|
87
|
-
|
|
148
|
+
// Construct the replacement based on search mode
|
|
149
|
+
const prefix = state.searchMode === 'content' ? '@@' : '@';
|
|
150
|
+
const newText = beforeAt + prefix + filePath + ' ' + afterCursor;
|
|
88
151
|
// Set the new text and position cursor after the inserted file path + space
|
|
89
152
|
buffer.setText(newText);
|
|
90
|
-
// Calculate cursor position after the inserted
|
|
153
|
+
// Calculate cursor position after the inserted text
|
|
154
|
+
// prefix length + filePath length + space
|
|
155
|
+
const insertedLength = prefix.length + filePath.length + 1;
|
|
156
|
+
const targetPos = state.atSymbolPosition + insertedLength;
|
|
91
157
|
// Reset cursor to beginning, then move to correct position
|
|
92
|
-
for (let i = 0; i <
|
|
93
|
-
// +2 for @ and space
|
|
158
|
+
for (let i = 0; i < targetPos; i++) {
|
|
94
159
|
if (i < buffer.getFullText().length) {
|
|
95
160
|
buffer.moveRight();
|
|
96
161
|
}
|
|
@@ -98,20 +163,33 @@ export function useFilePicker(buffer, triggerUpdate) {
|
|
|
98
163
|
dispatch({ type: 'SELECT_FILE' });
|
|
99
164
|
triggerUpdate();
|
|
100
165
|
}
|
|
101
|
-
}, [state.atSymbolPosition, buffer]);
|
|
166
|
+
}, [state.atSymbolPosition, state.searchMode, buffer, triggerUpdate]);
|
|
102
167
|
// Handle filtered file count change
|
|
103
168
|
const handleFilteredCountChange = useCallback((count) => {
|
|
104
169
|
dispatch({ type: 'SET_FILTERED_COUNT', count });
|
|
105
170
|
}, []);
|
|
106
171
|
// Wrapper setters for backwards compatibility
|
|
107
172
|
const setShowFilePicker = useCallback((show) => {
|
|
108
|
-
|
|
173
|
+
if (show) {
|
|
174
|
+
dispatch({
|
|
175
|
+
type: 'SHOW',
|
|
176
|
+
query: '',
|
|
177
|
+
position: -1,
|
|
178
|
+
searchMode: 'file',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
dispatch({ type: 'HIDE' });
|
|
183
|
+
}
|
|
109
184
|
}, []);
|
|
110
185
|
const setFileSelectedIndex = useCallback((index) => {
|
|
111
186
|
if (typeof index === 'function') {
|
|
112
187
|
// For functional updates, we need to get current state first
|
|
113
188
|
// This is a simplified version - in production you might want to use a ref
|
|
114
|
-
dispatch({
|
|
189
|
+
dispatch({
|
|
190
|
+
type: 'SET_SELECTED_INDEX',
|
|
191
|
+
index: index(state.fileSelectedIndex),
|
|
192
|
+
});
|
|
115
193
|
}
|
|
116
194
|
else {
|
|
117
195
|
dispatch({ type: 'SET_SELECTED_INDEX', index });
|
|
@@ -131,6 +209,7 @@ export function useFilePicker(buffer, triggerUpdate) {
|
|
|
131
209
|
// Not used, but kept for compatibility
|
|
132
210
|
},
|
|
133
211
|
filteredFileCount: state.filteredFileCount,
|
|
212
|
+
searchMode: state.searchMode,
|
|
134
213
|
updateFilePickerState,
|
|
135
214
|
handleFileSelect,
|
|
136
215
|
handleFilteredCountChange,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TextBuffer } from '../utils/textBuffer.js';
|
|
2
|
+
import type { SubAgent } from '../utils/subAgentConfig.js';
|
|
2
3
|
type KeyboardInputOptions = {
|
|
3
4
|
buffer: TextBuffer;
|
|
4
5
|
disabled: boolean;
|
|
@@ -52,6 +53,27 @@ type KeyboardInputOptions = {
|
|
|
52
53
|
mimeType: string;
|
|
53
54
|
}>) => void;
|
|
54
55
|
ensureFocus: () => void;
|
|
56
|
+
showAgentPicker: boolean;
|
|
57
|
+
setShowAgentPicker: (show: boolean) => void;
|
|
58
|
+
agentSelectedIndex: number;
|
|
59
|
+
setAgentSelectedIndex: (index: number | ((prev: number) => number)) => void;
|
|
60
|
+
agents: SubAgent[];
|
|
61
|
+
handleAgentSelect: (agent: SubAgent) => void;
|
|
62
|
+
showTodoPicker: boolean;
|
|
63
|
+
setShowTodoPicker: (show: boolean) => void;
|
|
64
|
+
todoSelectedIndex: number;
|
|
65
|
+
setTodoSelectedIndex: (index: number | ((prev: number) => number)) => void;
|
|
66
|
+
todos: Array<{
|
|
67
|
+
id: string;
|
|
68
|
+
file: string;
|
|
69
|
+
line: number;
|
|
70
|
+
content: string;
|
|
71
|
+
}>;
|
|
72
|
+
selectedTodos: Set<string>;
|
|
73
|
+
toggleTodoSelection: () => void;
|
|
74
|
+
confirmTodoSelection: () => void;
|
|
75
|
+
todoSearchQuery: string;
|
|
76
|
+
setTodoSearchQuery: (query: string) => void;
|
|
55
77
|
};
|
|
56
78
|
export declare function useKeyboardInput(options: KeyboardInputOptions): void;
|
|
57
79
|
export {};
|
|
@@ -2,7 +2,10 @@ import { useRef, useEffect } from 'react';
|
|
|
2
2
|
import { useInput } from 'ink';
|
|
3
3
|
import { executeCommand } from '../utils/commandExecutor.js';
|
|
4
4
|
export function useKeyboardInput(options) {
|
|
5
|
-
const { buffer, disabled, triggerUpdate, forceUpdate, showCommands, setShowCommands, commandSelectedIndex, setCommandSelectedIndex, getFilteredCommands, updateCommandPanelState, onCommand, showFilePicker, setShowFilePicker, fileSelectedIndex, setFileSelectedIndex, setFileQuery, setAtSymbolPosition, filteredFileCount, updateFilePickerState, handleFileSelect, fileListRef, showHistoryMenu, setShowHistoryMenu, historySelectedIndex, setHistorySelectedIndex, escapeKeyCount, setEscapeKeyCount, escapeKeyTimer, getUserMessages, handleHistorySelect, currentHistoryIndex, navigateHistoryUp, navigateHistoryDown, resetHistoryNavigation, saveToHistory, pasteFromClipboard, onSubmit, ensureFocus, } = options;
|
|
5
|
+
const { buffer, disabled, triggerUpdate, forceUpdate, showCommands, setShowCommands, commandSelectedIndex, setCommandSelectedIndex, getFilteredCommands, updateCommandPanelState, onCommand, showFilePicker, setShowFilePicker, fileSelectedIndex, setFileSelectedIndex, setFileQuery, setAtSymbolPosition, filteredFileCount, updateFilePickerState, handleFileSelect, fileListRef, showHistoryMenu, setShowHistoryMenu, historySelectedIndex, setHistorySelectedIndex, escapeKeyCount, setEscapeKeyCount, escapeKeyTimer, getUserMessages, handleHistorySelect, currentHistoryIndex, navigateHistoryUp, navigateHistoryDown, resetHistoryNavigation, saveToHistory, pasteFromClipboard, onSubmit, ensureFocus, showAgentPicker, setShowAgentPicker, agentSelectedIndex, setAgentSelectedIndex, agents, handleAgentSelect, showTodoPicker, setShowTodoPicker, todoSelectedIndex, setTodoSelectedIndex, todos, selectedTodos, toggleTodoSelection, confirmTodoSelection, todoSearchQuery, setTodoSearchQuery, } = options;
|
|
6
|
+
// Mark variables as used (they are used in useInput closure below)
|
|
7
|
+
void todoSelectedIndex;
|
|
8
|
+
void selectedTodos;
|
|
6
9
|
// Track paste detection
|
|
7
10
|
const inputBuffer = useRef('');
|
|
8
11
|
const inputTimer = useRef(null);
|
|
@@ -54,6 +57,18 @@ export function useKeyboardInput(options) {
|
|
|
54
57
|
}
|
|
55
58
|
// Handle escape key for double-ESC history navigation
|
|
56
59
|
if (key.escape) {
|
|
60
|
+
// Close todo picker if open
|
|
61
|
+
if (showTodoPicker) {
|
|
62
|
+
setShowTodoPicker(false);
|
|
63
|
+
setTodoSelectedIndex(0);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Close agent picker if open
|
|
67
|
+
if (showAgentPicker) {
|
|
68
|
+
setShowAgentPicker(false);
|
|
69
|
+
setAgentSelectedIndex(0);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
57
72
|
// Close file picker if open
|
|
58
73
|
if (showFilePicker) {
|
|
59
74
|
setShowFilePicker(false);
|
|
@@ -99,6 +114,81 @@ export function useKeyboardInput(options) {
|
|
|
99
114
|
}
|
|
100
115
|
return;
|
|
101
116
|
}
|
|
117
|
+
// Handle todo picker navigation
|
|
118
|
+
if (showTodoPicker) {
|
|
119
|
+
// Up arrow in todo picker
|
|
120
|
+
if (key.upArrow) {
|
|
121
|
+
setTodoSelectedIndex(prev => Math.max(0, prev - 1));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Down arrow in todo picker
|
|
125
|
+
if (key.downArrow) {
|
|
126
|
+
const maxIndex = Math.max(0, todos.length - 1);
|
|
127
|
+
setTodoSelectedIndex(prev => Math.min(maxIndex, prev + 1));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Space - toggle selection
|
|
131
|
+
if (input === ' ') {
|
|
132
|
+
toggleTodoSelection();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Enter - confirm selection
|
|
136
|
+
if (key.return) {
|
|
137
|
+
confirmTodoSelection();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Backspace - remove last character from search
|
|
141
|
+
if (key.backspace || key.delete) {
|
|
142
|
+
if (todoSearchQuery.length > 0) {
|
|
143
|
+
setTodoSearchQuery(todoSearchQuery.slice(0, -1));
|
|
144
|
+
setTodoSelectedIndex(0); // Reset to first item
|
|
145
|
+
triggerUpdate();
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Type to search - alphanumeric and common characters
|
|
150
|
+
if (input &&
|
|
151
|
+
input.length === 1 &&
|
|
152
|
+
!key.ctrl &&
|
|
153
|
+
!key.meta &&
|
|
154
|
+
input !== '\x1b' // Ignore escape sequences
|
|
155
|
+
) {
|
|
156
|
+
setTodoSearchQuery(todoSearchQuery + input);
|
|
157
|
+
setTodoSelectedIndex(0); // Reset to first item
|
|
158
|
+
triggerUpdate();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// For any other key in todo picker, just return to prevent interference
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Handle agent picker navigation
|
|
165
|
+
if (showAgentPicker) {
|
|
166
|
+
// Up arrow in agent picker
|
|
167
|
+
if (key.upArrow) {
|
|
168
|
+
setAgentSelectedIndex(prev => Math.max(0, prev - 1));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Down arrow in agent picker
|
|
172
|
+
if (key.downArrow) {
|
|
173
|
+
const maxIndex = Math.max(0, agents.length - 1);
|
|
174
|
+
setAgentSelectedIndex(prev => Math.min(maxIndex, prev + 1));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Enter - select agent
|
|
178
|
+
if (key.return) {
|
|
179
|
+
if (agents.length > 0 && agentSelectedIndex < agents.length) {
|
|
180
|
+
const selectedAgent = agents[agentSelectedIndex];
|
|
181
|
+
if (selectedAgent) {
|
|
182
|
+
handleAgentSelect(selectedAgent);
|
|
183
|
+
setShowAgentPicker(false);
|
|
184
|
+
setAgentSelectedIndex(0);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// For any other key in agent picker, just return to prevent interference
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
102
192
|
// Handle history menu navigation
|
|
103
193
|
if (showHistoryMenu) {
|
|
104
194
|
const userMessages = getUserMessages();
|
|
@@ -203,6 +293,24 @@ export function useKeyboardInput(options) {
|
|
|
203
293
|
commandSelectedIndex < filteredCommands.length) {
|
|
204
294
|
const selectedCommand = filteredCommands[commandSelectedIndex];
|
|
205
295
|
if (selectedCommand) {
|
|
296
|
+
// Special handling for todo- command
|
|
297
|
+
if (selectedCommand.name === 'todo-') {
|
|
298
|
+
buffer.setText('');
|
|
299
|
+
setShowCommands(false);
|
|
300
|
+
setCommandSelectedIndex(0);
|
|
301
|
+
setShowTodoPicker(true);
|
|
302
|
+
triggerUpdate();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Special handling for agent- command
|
|
306
|
+
if (selectedCommand.name === 'agent-') {
|
|
307
|
+
buffer.setText('');
|
|
308
|
+
setShowCommands(false);
|
|
309
|
+
setCommandSelectedIndex(0);
|
|
310
|
+
setShowAgentPicker(true);
|
|
311
|
+
triggerUpdate();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
206
314
|
// Execute command instead of inserting text
|
|
207
315
|
executeCommand(selectedCommand.name).then(result => {
|
|
208
316
|
if (onCommand) {
|
|
@@ -43,20 +43,26 @@ export function useStreamingState() {
|
|
|
43
43
|
}, 1000);
|
|
44
44
|
return () => clearInterval(interval);
|
|
45
45
|
}, [timerStartTime]);
|
|
46
|
+
// Initialize remaining seconds when retry starts
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!retryStatus || !retryStatus.isRetrying)
|
|
49
|
+
return;
|
|
50
|
+
if (retryStatus.remainingSeconds !== undefined)
|
|
51
|
+
return;
|
|
52
|
+
// Initialize remaining seconds from nextDelay (only once)
|
|
53
|
+
setRetryStatus(prev => prev
|
|
54
|
+
? {
|
|
55
|
+
...prev,
|
|
56
|
+
remainingSeconds: Math.ceil(prev.nextDelay / 1000),
|
|
57
|
+
}
|
|
58
|
+
: null);
|
|
59
|
+
}, [retryStatus?.isRetrying, retryStatus?.nextDelay]);
|
|
46
60
|
// Countdown timer for retry delays
|
|
47
61
|
useEffect(() => {
|
|
48
62
|
if (!retryStatus || !retryStatus.isRetrying)
|
|
49
63
|
return;
|
|
50
|
-
|
|
51
|
-
if (retryStatus.remainingSeconds === undefined) {
|
|
52
|
-
setRetryStatus(prev => prev
|
|
53
|
-
? {
|
|
54
|
-
...prev,
|
|
55
|
-
remainingSeconds: Math.ceil(prev.nextDelay / 1000),
|
|
56
|
-
}
|
|
57
|
-
: null);
|
|
64
|
+
if (retryStatus.remainingSeconds === undefined)
|
|
58
65
|
return;
|
|
59
|
-
}
|
|
60
66
|
// Countdown every second
|
|
61
67
|
const interval = setInterval(() => {
|
|
62
68
|
setRetryStatus(prev => {
|
|
@@ -76,7 +82,7 @@ export function useStreamingState() {
|
|
|
76
82
|
});
|
|
77
83
|
}, 1000);
|
|
78
84
|
return () => clearInterval(interval);
|
|
79
|
-
}, [retryStatus?.isRetrying
|
|
85
|
+
}, [retryStatus?.isRetrying]); // β
η§»ι€ remainingSeconds ιΏε
εΎͺη―
|
|
80
86
|
return {
|
|
81
87
|
isStreaming,
|
|
82
88
|
setIsStreaming,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TextBuffer } from '../utils/textBuffer.js';
|
|
2
|
+
import { type TodoItem } from '../utils/todoScanner.js';
|
|
3
|
+
export declare function useTodoPicker(buffer: TextBuffer, triggerUpdate: () => void, projectRoot: string): {
|
|
4
|
+
showTodoPicker: boolean;
|
|
5
|
+
setShowTodoPicker: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
6
|
+
todoSelectedIndex: number;
|
|
7
|
+
setTodoSelectedIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
8
|
+
todos: TodoItem[];
|
|
9
|
+
selectedTodos: Set<string>;
|
|
10
|
+
toggleTodoSelection: () => void;
|
|
11
|
+
confirmTodoSelection: () => void;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
searchQuery: string;
|
|
14
|
+
setSearchQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
15
|
+
totalTodoCount: number;
|
|
16
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import { scanProjectTodos } from '../utils/todoScanner.js';
|
|
3
|
+
export function useTodoPicker(buffer, triggerUpdate, projectRoot) {
|
|
4
|
+
const [showTodoPicker, setShowTodoPicker] = useState(false);
|
|
5
|
+
const [todoSelectedIndex, setTodoSelectedIndex] = useState(0);
|
|
6
|
+
const [allTodos, setAllTodos] = useState([]);
|
|
7
|
+
const [selectedTodos, setSelectedTodos] = useState(new Set());
|
|
8
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
9
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
10
|
+
// Filter todos based on search query
|
|
11
|
+
const filteredTodos = useMemo(() => {
|
|
12
|
+
if (!searchQuery.trim()) {
|
|
13
|
+
return allTodos;
|
|
14
|
+
}
|
|
15
|
+
const query = searchQuery.toLowerCase();
|
|
16
|
+
return allTodos.filter(todo => todo.content.toLowerCase().includes(query) ||
|
|
17
|
+
todo.file.toLowerCase().includes(query));
|
|
18
|
+
}, [allTodos, searchQuery]);
|
|
19
|
+
// Load todos when picker is shown
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (showTodoPicker) {
|
|
22
|
+
setIsLoading(true);
|
|
23
|
+
setSearchQuery('');
|
|
24
|
+
setTodoSelectedIndex(0);
|
|
25
|
+
setSelectedTodos(new Set());
|
|
26
|
+
// Use setTimeout to allow UI to update with loading state
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
const foundTodos = scanProjectTodos(projectRoot);
|
|
29
|
+
setAllTodos(foundTodos);
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
}, 0);
|
|
32
|
+
}
|
|
33
|
+
}, [showTodoPicker, projectRoot]);
|
|
34
|
+
// Toggle selection of current todo
|
|
35
|
+
const toggleTodoSelection = useCallback(() => {
|
|
36
|
+
if (filteredTodos.length > 0 && todoSelectedIndex < filteredTodos.length) {
|
|
37
|
+
const todo = filteredTodos[todoSelectedIndex];
|
|
38
|
+
if (todo) {
|
|
39
|
+
setSelectedTodos(prev => {
|
|
40
|
+
const newSet = new Set(prev);
|
|
41
|
+
if (newSet.has(todo.id)) {
|
|
42
|
+
newSet.delete(todo.id);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
newSet.add(todo.id);
|
|
46
|
+
}
|
|
47
|
+
return newSet;
|
|
48
|
+
});
|
|
49
|
+
triggerUpdate();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, [filteredTodos, todoSelectedIndex, triggerUpdate]);
|
|
53
|
+
// Confirm selection and insert into buffer
|
|
54
|
+
const confirmTodoSelection = useCallback(() => {
|
|
55
|
+
if (selectedTodos.size === 0) {
|
|
56
|
+
// If no todos selected, just close the picker
|
|
57
|
+
setShowTodoPicker(false);
|
|
58
|
+
setTodoSelectedIndex(0);
|
|
59
|
+
triggerUpdate();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Build the text to insert
|
|
63
|
+
const selectedTodoItems = allTodos.filter(todo => selectedTodos.has(todo.id));
|
|
64
|
+
const todoTexts = selectedTodoItems.map(todo => `<${todo.file}:${todo.line}> ${todo.content}`);
|
|
65
|
+
// Clear buffer and insert selected todos
|
|
66
|
+
const currentText = buffer.getFullText().trim();
|
|
67
|
+
buffer.setText('');
|
|
68
|
+
if (currentText) {
|
|
69
|
+
buffer.insert(currentText + '\n' + todoTexts.join('\n'));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
buffer.insert(todoTexts.join('\n'));
|
|
73
|
+
}
|
|
74
|
+
// Reset state
|
|
75
|
+
setShowTodoPicker(false);
|
|
76
|
+
setTodoSelectedIndex(0);
|
|
77
|
+
setSelectedTodos(new Set());
|
|
78
|
+
triggerUpdate();
|
|
79
|
+
}, [buffer, allTodos, selectedTodos, triggerUpdate]);
|
|
80
|
+
return {
|
|
81
|
+
showTodoPicker,
|
|
82
|
+
setShowTodoPicker,
|
|
83
|
+
todoSelectedIndex,
|
|
84
|
+
setTodoSelectedIndex,
|
|
85
|
+
todos: filteredTodos,
|
|
86
|
+
selectedTodos,
|
|
87
|
+
toggleTodoSelection,
|
|
88
|
+
confirmTodoSelection,
|
|
89
|
+
isLoading,
|
|
90
|
+
searchQuery,
|
|
91
|
+
setSearchQuery,
|
|
92
|
+
totalTodoCount: allTodos.length,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
selectedIndex: number;
|
|
4
|
+
visible: boolean;
|
|
5
|
+
maxHeight?: number;
|
|
6
|
+
}
|
|
7
|
+
declare const AgentPickerPanel: React.MemoExoticComponent<({ selectedIndex, visible, maxHeight }: Props) => React.JSX.Element | null>;
|
|
8
|
+
export default AgentPickerPanel;
|