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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { Alert } from '@inkjs/ui';
|
|
4
|
+
const TodoPickerPanel = memo(({ todos, selectedIndex, selectedTodos, visible, maxHeight, isLoading = false, searchQuery = '', totalCount = 0, }) => {
|
|
5
|
+
// Fixed maximum display items to prevent rendering issues
|
|
6
|
+
const MAX_DISPLAY_ITEMS = 5;
|
|
7
|
+
const effectiveMaxItems = maxHeight
|
|
8
|
+
? Math.min(maxHeight, MAX_DISPLAY_ITEMS)
|
|
9
|
+
: MAX_DISPLAY_ITEMS;
|
|
10
|
+
// Limit displayed todos
|
|
11
|
+
const displayedTodos = useMemo(() => {
|
|
12
|
+
if (todos.length <= effectiveMaxItems) {
|
|
13
|
+
return todos;
|
|
14
|
+
}
|
|
15
|
+
// Show todos around the selected index
|
|
16
|
+
const halfWindow = Math.floor(effectiveMaxItems / 2);
|
|
17
|
+
let startIndex = Math.max(0, selectedIndex - halfWindow);
|
|
18
|
+
let endIndex = Math.min(todos.length, startIndex + effectiveMaxItems);
|
|
19
|
+
// Adjust if we're near the end
|
|
20
|
+
if (endIndex - startIndex < effectiveMaxItems) {
|
|
21
|
+
startIndex = Math.max(0, endIndex - effectiveMaxItems);
|
|
22
|
+
}
|
|
23
|
+
return todos.slice(startIndex, endIndex);
|
|
24
|
+
}, [todos, selectedIndex, effectiveMaxItems]);
|
|
25
|
+
// Calculate actual selected index in the displayed subset
|
|
26
|
+
const displayedSelectedIndex = useMemo(() => {
|
|
27
|
+
return displayedTodos.findIndex(todo => {
|
|
28
|
+
const originalIndex = todos.indexOf(todo);
|
|
29
|
+
return originalIndex === selectedIndex;
|
|
30
|
+
});
|
|
31
|
+
}, [displayedTodos, todos, selectedIndex]);
|
|
32
|
+
// Don't show panel if not visible
|
|
33
|
+
if (!visible) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// Show loading state
|
|
37
|
+
if (isLoading) {
|
|
38
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
39
|
+
React.createElement(Box, { width: "100%" },
|
|
40
|
+
React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
41
|
+
React.createElement(Box, null,
|
|
42
|
+
React.createElement(Text, { color: "yellow", bold: true }, "TODO Selection")),
|
|
43
|
+
React.createElement(Box, { marginTop: 1 },
|
|
44
|
+
React.createElement(Alert, { variant: "info" }, "Scanning project for TODO comments..."))))));
|
|
45
|
+
}
|
|
46
|
+
// Show message if no todos found
|
|
47
|
+
if (todos.length === 0 && !searchQuery) {
|
|
48
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
49
|
+
React.createElement(Box, { width: "100%" },
|
|
50
|
+
React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
51
|
+
React.createElement(Box, null,
|
|
52
|
+
React.createElement(Text, { color: "yellow", bold: true }, "TODO Selection")),
|
|
53
|
+
React.createElement(Box, { marginTop: 1 },
|
|
54
|
+
React.createElement(Alert, { variant: "info" }, "No TODO comments found in the project"))))));
|
|
55
|
+
}
|
|
56
|
+
// Show message if search has no results
|
|
57
|
+
if (todos.length === 0 && searchQuery) {
|
|
58
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
59
|
+
React.createElement(Box, { width: "100%" },
|
|
60
|
+
React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
61
|
+
React.createElement(Box, null,
|
|
62
|
+
React.createElement(Text, { color: "yellow", bold: true }, "TODO Selection")),
|
|
63
|
+
React.createElement(Box, { marginTop: 1 },
|
|
64
|
+
React.createElement(Alert, { variant: "warning" },
|
|
65
|
+
"No TODOs match \"",
|
|
66
|
+
searchQuery,
|
|
67
|
+
"\" (Total: ",
|
|
68
|
+
totalCount,
|
|
69
|
+
")")),
|
|
70
|
+
React.createElement(Box, { marginTop: 1 },
|
|
71
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Type to filter \u00B7 Backspace to clear search"))))));
|
|
72
|
+
}
|
|
73
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
74
|
+
React.createElement(Box, { width: "100%" },
|
|
75
|
+
React.createElement(Box, { flexDirection: "column", width: "100%" },
|
|
76
|
+
React.createElement(Box, null,
|
|
77
|
+
React.createElement(Text, { color: "yellow", bold: true },
|
|
78
|
+
"Select TODOs",
|
|
79
|
+
' ',
|
|
80
|
+
todos.length > effectiveMaxItems &&
|
|
81
|
+
`(${selectedIndex + 1}/${todos.length})`,
|
|
82
|
+
searchQuery && ` - Filtering: "${searchQuery}"`,
|
|
83
|
+
searchQuery &&
|
|
84
|
+
totalCount > todos.length &&
|
|
85
|
+
` (${todos.length}/${totalCount})`)),
|
|
86
|
+
React.createElement(Box, { marginTop: 1 },
|
|
87
|
+
React.createElement(Text, { color: "gray", dimColor: true }, searchQuery
|
|
88
|
+
? 'Type to filter · Backspace to clear · Space: toggle · Enter: confirm'
|
|
89
|
+
: 'Type to search · Space: toggle · Enter: confirm · Esc: cancel')),
|
|
90
|
+
displayedTodos.map((todo, index) => {
|
|
91
|
+
const isSelected = index === displayedSelectedIndex;
|
|
92
|
+
const isChecked = selectedTodos.has(todo.id);
|
|
93
|
+
return (React.createElement(Box, { key: todo.id, flexDirection: "column", width: "100%" },
|
|
94
|
+
React.createElement(Text, { color: isSelected ? 'green' : 'gray', bold: true },
|
|
95
|
+
isSelected ? '❯ ' : ' ',
|
|
96
|
+
isChecked ? '[✓]' : '[ ]',
|
|
97
|
+
" ",
|
|
98
|
+
todo.file,
|
|
99
|
+
":",
|
|
100
|
+
todo.line),
|
|
101
|
+
React.createElement(Box, { marginLeft: 5 },
|
|
102
|
+
React.createElement(Text, { color: isSelected ? 'green' : 'gray', dimColor: !isSelected },
|
|
103
|
+
"\u2514\u2500 ",
|
|
104
|
+
todo.content))));
|
|
105
|
+
}),
|
|
106
|
+
todos.length > effectiveMaxItems && (React.createElement(Box, { marginTop: 1 },
|
|
107
|
+
React.createElement(Text, { color: "gray", dimColor: true },
|
|
108
|
+
"\u2191\u2193 to scroll \u00B7 ",
|
|
109
|
+
todos.length - effectiveMaxItems,
|
|
110
|
+
" more hidden"))),
|
|
111
|
+
selectedTodos.size > 0 && (React.createElement(Box, { marginTop: 1 },
|
|
112
|
+
React.createElement(Text, { color: "cyan" },
|
|
113
|
+
selectedTodos.size,
|
|
114
|
+
" TODO(s) selected")))))));
|
|
115
|
+
});
|
|
116
|
+
TodoPickerPanel.displayName = 'TodoPickerPanel';
|
|
117
|
+
export default TodoPickerPanel;
|
|
@@ -11,6 +11,8 @@ import '../../utils/commands/review.js';
|
|
|
11
11
|
import '../../utils/commands/role.js';
|
|
12
12
|
import '../../utils/commands/usage.js';
|
|
13
13
|
import '../../utils/commands/export.js';
|
|
14
|
+
import '../../utils/commands/agent.js';
|
|
15
|
+
import '../../utils/commands/todoPicker.js';
|
|
14
16
|
type Props = {
|
|
15
17
|
skipWelcome?: boolean;
|
|
16
18
|
};
|
|
@@ -45,6 +45,8 @@ import '../../utils/commands/review.js';
|
|
|
45
45
|
import '../../utils/commands/role.js';
|
|
46
46
|
import '../../utils/commands/usage.js';
|
|
47
47
|
import '../../utils/commands/export.js';
|
|
48
|
+
import '../../utils/commands/agent.js';
|
|
49
|
+
import '../../utils/commands/todoPicker.js';
|
|
48
50
|
export default function ChatScreen({ skipWelcome }) {
|
|
49
51
|
const [messages, setMessages] = useState([]);
|
|
50
52
|
const [isSaving] = useState(false);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface CommandResult {
|
|
2
2
|
success: boolean;
|
|
3
3
|
message?: string;
|
|
4
|
-
action?: 'clear' | 'resume' | 'info' | 'showMcpInfo' | 'toggleYolo' | 'initProject' | 'compact' | 'showSessionPanel' | 'showMcpPanel' | 'showUsagePanel' | 'home' | 'review' | 'exportChat';
|
|
4
|
+
action?: 'clear' | 'resume' | 'info' | 'showMcpInfo' | 'toggleYolo' | 'initProject' | 'compact' | 'showSessionPanel' | 'showMcpPanel' | 'showUsagePanel' | 'home' | 'review' | 'exportChat' | 'showAgentPicker' | 'showTodoPicker';
|
|
5
5
|
prompt?: string;
|
|
6
6
|
alreadyConnected?: boolean;
|
|
7
7
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { registerCommand } from '../commandExecutor.js';
|
|
2
|
+
// Agent picker command handler - shows agent selection panel
|
|
3
|
+
registerCommand('agent-', {
|
|
4
|
+
execute: () => {
|
|
5
|
+
return {
|
|
6
|
+
success: true,
|
|
7
|
+
action: 'showAgentPicker',
|
|
8
|
+
message: 'Showing sub-agent selection panel'
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
export default {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { registerCommand } from '../commandExecutor.js';
|
|
2
|
+
// Todo picker command handler - shows todo selection panel
|
|
3
|
+
registerCommand('todo-', {
|
|
4
|
+
execute: () => {
|
|
5
|
+
return {
|
|
6
|
+
success: true,
|
|
7
|
+
action: 'showTodoPicker',
|
|
8
|
+
message: 'Showing TODO comment selection panel',
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
export default {};
|
|
@@ -63,10 +63,8 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
|
|
|
63
63
|
// Local session-approved tools for this sub-agent execution
|
|
64
64
|
// This ensures tools approved during execution are immediately recognized
|
|
65
65
|
const sessionApprovedTools = new Set();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
while (iteration < maxIterations) {
|
|
69
|
-
iteration++;
|
|
66
|
+
// eslint-disable-next-line no-constant-condition
|
|
67
|
+
while (true) {
|
|
70
68
|
// Check abort signal
|
|
71
69
|
if (abortSignal?.aborted) {
|
|
72
70
|
return {
|
|
@@ -260,14 +258,7 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
|
|
|
260
258
|
// Add tool results to conversation
|
|
261
259
|
messages.push(...toolResults);
|
|
262
260
|
// Continue to next iteration if there were tool calls
|
|
263
|
-
// The loop will continue until no more tool calls
|
|
264
|
-
}
|
|
265
|
-
if (iteration >= maxIterations) {
|
|
266
|
-
return {
|
|
267
|
-
success: false,
|
|
268
|
-
result: finalResponse,
|
|
269
|
-
error: 'Sub-agent exceeded maximum iterations',
|
|
270
|
-
};
|
|
261
|
+
// The loop will continue until no more tool calls
|
|
271
262
|
}
|
|
272
263
|
return {
|
|
273
264
|
success: true,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const IGNORE_PATTERNS = [
|
|
4
|
+
'node_modules',
|
|
5
|
+
'.git',
|
|
6
|
+
'dist',
|
|
7
|
+
'build',
|
|
8
|
+
'coverage',
|
|
9
|
+
'.next',
|
|
10
|
+
'.nuxt',
|
|
11
|
+
'.output',
|
|
12
|
+
'out',
|
|
13
|
+
'.DS_Store',
|
|
14
|
+
'*.log',
|
|
15
|
+
'*.lock',
|
|
16
|
+
'yarn.lock',
|
|
17
|
+
'package-lock.json',
|
|
18
|
+
'pnpm-lock.yaml',
|
|
19
|
+
];
|
|
20
|
+
// Common task markers - support various formats
|
|
21
|
+
// Only include markers that clearly indicate actionable tasks
|
|
22
|
+
const TODO_PATTERNS = [
|
|
23
|
+
// Single-line comments with markers (// TODO, // FIXME, etc.)
|
|
24
|
+
/\/\/\s*(?:TODO|FIXME|HACK|XXX|BUG):?\s*(.+)/i,
|
|
25
|
+
// Block comments (/* TODO */)
|
|
26
|
+
/\/\*\s*(?:TODO|FIXME|HACK|XXX|BUG):?\s*(.+?)\s*\*\//i,
|
|
27
|
+
// Hash comments (# TODO) for Python, Ruby, Shell, etc.
|
|
28
|
+
/#\s*(?:TODO|FIXME|HACK|XXX|BUG):?\s*(.+)/i,
|
|
29
|
+
// HTML/XML comments (<!-- TODO -->)
|
|
30
|
+
/<!--\s*(?:TODO|FIXME|HACK|XXX|BUG):?\s*(.+?)\s*-->/i,
|
|
31
|
+
// JSDoc/PHPDoc style (@todo)
|
|
32
|
+
/\/\*\*?\s*@(?:todo|fixme):?\s*(.+?)(?:\s*\*\/|\n)/i,
|
|
33
|
+
// TODO with brackets/parentheses (common format for task assignment)
|
|
34
|
+
/\/\/\s*TODO\s*[\(\[\{]\s*(.+?)\s*[\)\]\}]/i,
|
|
35
|
+
/#\s*TODO\s*[\(\[\{]\s*(.+?)\s*[\)\]\}]/i,
|
|
36
|
+
// Multi-line block comment TODO (catches TODO on its own line)
|
|
37
|
+
/\/\*[\s\S]*?\bTODO:?\s*(.+?)[\s\S]*?\*\//i,
|
|
38
|
+
];
|
|
39
|
+
function shouldIgnore(filePath) {
|
|
40
|
+
const relativePath = filePath;
|
|
41
|
+
return IGNORE_PATTERNS.some(pattern => {
|
|
42
|
+
if (pattern.includes('*')) {
|
|
43
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
44
|
+
return regex.test(relativePath);
|
|
45
|
+
}
|
|
46
|
+
return relativePath.includes(pattern);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function scanFileForTodos(filePath, rootDir) {
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
52
|
+
const lines = content.split('\n');
|
|
53
|
+
const todos = [];
|
|
54
|
+
lines.forEach((line, index) => {
|
|
55
|
+
for (const pattern of TODO_PATTERNS) {
|
|
56
|
+
const match = line.match(pattern);
|
|
57
|
+
if (match) {
|
|
58
|
+
const todoContent = match[1]?.trim() || '';
|
|
59
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
60
|
+
todos.push({
|
|
61
|
+
id: `${relativePath}:${index + 1}`,
|
|
62
|
+
file: relativePath,
|
|
63
|
+
line: index + 1,
|
|
64
|
+
content: todoContent,
|
|
65
|
+
fullLine: line.trim(),
|
|
66
|
+
});
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return todos;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// Ignore files that can't be read
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function scanDirectory(dir, rootDir) {
|
|
79
|
+
let todos = [];
|
|
80
|
+
try {
|
|
81
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const fullPath = path.join(dir, entry.name);
|
|
84
|
+
const relativePath = path.relative(rootDir, fullPath);
|
|
85
|
+
if (shouldIgnore(relativePath)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
todos = todos.concat(scanDirectory(fullPath, rootDir));
|
|
90
|
+
}
|
|
91
|
+
else if (entry.isFile()) {
|
|
92
|
+
// Only scan text files
|
|
93
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
94
|
+
const textExtensions = [
|
|
95
|
+
'.ts',
|
|
96
|
+
'.tsx',
|
|
97
|
+
'.js',
|
|
98
|
+
'.jsx',
|
|
99
|
+
'.py',
|
|
100
|
+
'.go',
|
|
101
|
+
'.rs',
|
|
102
|
+
'.java',
|
|
103
|
+
'.c',
|
|
104
|
+
'.cpp',
|
|
105
|
+
'.h',
|
|
106
|
+
'.hpp',
|
|
107
|
+
'.cs',
|
|
108
|
+
'.php',
|
|
109
|
+
'.rb',
|
|
110
|
+
'.swift',
|
|
111
|
+
'.kt',
|
|
112
|
+
'.scala',
|
|
113
|
+
'.sh',
|
|
114
|
+
'.bash',
|
|
115
|
+
'.zsh',
|
|
116
|
+
'.fish',
|
|
117
|
+
'.vim',
|
|
118
|
+
'.lua',
|
|
119
|
+
'.sql',
|
|
120
|
+
'.html',
|
|
121
|
+
'.css',
|
|
122
|
+
'.scss',
|
|
123
|
+
'.sass',
|
|
124
|
+
'.less',
|
|
125
|
+
'.vue',
|
|
126
|
+
'.svelte',
|
|
127
|
+
'.md',
|
|
128
|
+
'.txt',
|
|
129
|
+
'.json',
|
|
130
|
+
'.yaml',
|
|
131
|
+
'.yml',
|
|
132
|
+
'.toml',
|
|
133
|
+
'.xml',
|
|
134
|
+
];
|
|
135
|
+
if (textExtensions.includes(ext) || ext === '') {
|
|
136
|
+
todos = todos.concat(scanFileForTodos(fullPath, rootDir));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// Ignore directories that can't be read
|
|
143
|
+
}
|
|
144
|
+
return todos;
|
|
145
|
+
}
|
|
146
|
+
export function scanProjectTodos(projectRoot) {
|
|
147
|
+
return scanDirectory(projectRoot, projectRoot);
|
|
148
|
+
}
|