snow-ai 0.3.17 β†’ 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.
@@ -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
- 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.
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
- *If you don't have a sub-agent tool, ignore this feature*
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
- const lastAtIndex = beforeCursor.lastIndexOf('@');
62
- if (lastAtIndex !== -1) {
63
- // Check if there's no space between '@' and cursor
64
- const afterAt = beforeCursor.slice(lastAtIndex + 1);
65
- if (!afterAt.includes(' ') && !afterAt.includes('\n')) {
66
- if (!state.showFilePicker ||
67
- state.fileQuery !== afterAt ||
68
- state.atSymbolPosition !== lastAtIndex) {
69
- dispatch({ type: 'SHOW', query: afterAt, position: lastAtIndex });
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
- // Hide file picker if no valid @ context found
75
- if (state.showFilePicker) {
76
- dispatch({ type: 'HIDE' });
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
- }, [state.showFilePicker, state.fileQuery, state.atSymbolPosition]);
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 @query with @filePath + space
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
- const newText = beforeAt + '@' + filePath + ' ' + afterCursor;
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 file path + space
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 < state.atSymbolPosition + filePath.length + 2; 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
- dispatch({ type: show ? 'SHOW' : 'HIDE', query: '', position: -1 });
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({ type: 'SET_SELECTED_INDEX', index: index(state.fileSelectedIndex) });
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
- // Initialize remaining seconds from nextDelay
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, retryStatus?.remainingSeconds]);
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;