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.
@@ -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,2 @@
1
+ declare const _default: {};
2
+ export default _default;
@@ -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,2 @@
1
+ declare const _default: {};
2
+ export default _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
- const maxIterations = 10; // Prevent infinite loops
67
- let iteration = 0;
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 or max iterations
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,8 @@
1
+ export interface TodoItem {
2
+ id: string;
3
+ file: string;
4
+ line: number;
5
+ content: string;
6
+ fullLine: string;
7
+ }
8
+ export declare function scanProjectTodos(projectRoot: string): TodoItem[];
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {