snow-ai 0.3.11 → 0.3.13

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.
@@ -1,9 +1,25 @@
1
1
  import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { historyManager } from '../utils/historyManager.js';
2
3
  export function useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHistorySelect) {
3
4
  const [showHistoryMenu, setShowHistoryMenu] = useState(false);
4
5
  const [historySelectedIndex, setHistorySelectedIndex] = useState(0);
5
6
  const [escapeKeyCount, setEscapeKeyCount] = useState(0);
6
7
  const escapeKeyTimer = useRef(null);
8
+ // Terminal-style history navigation state
9
+ const [currentHistoryIndex, setCurrentHistoryIndex] = useState(-1); // -1 means not in history mode
10
+ const savedInput = useRef(''); // Save current input when entering history mode
11
+ const [persistentHistory, setPersistentHistory] = useState([]);
12
+ const persistentHistoryRef = useRef([]);
13
+ // Keep ref in sync with state
14
+ useEffect(() => {
15
+ persistentHistoryRef.current = persistentHistory;
16
+ }, [persistentHistory]);
17
+ // Load persistent history on mount
18
+ useEffect(() => {
19
+ historyManager.loadHistory().then(entries => {
20
+ setPersistentHistory(entries);
21
+ });
22
+ }, []);
7
23
  // Cleanup timer on unmount
8
24
  useEffect(() => {
9
25
  return () => {
@@ -35,7 +51,62 @@ export function useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHisto
35
51
  triggerUpdate();
36
52
  onHistorySelect(selectedIndex, selectedMessage.content);
37
53
  }
38
- }, [chatHistory, onHistorySelect, buffer, triggerUpdate]);
54
+ }, [chatHistory, onHistorySelect, buffer]);
55
+ // Terminal-style history navigation: navigate up (older)
56
+ const navigateHistoryUp = useCallback(() => {
57
+ const history = persistentHistoryRef.current;
58
+ if (history.length === 0)
59
+ return false;
60
+ // Save current input when first entering history mode
61
+ if (currentHistoryIndex === -1) {
62
+ savedInput.current = buffer.getFullText();
63
+ }
64
+ // Navigate to older message (persistentHistory is already newest first)
65
+ const newIndex = currentHistoryIndex === -1
66
+ ? 0
67
+ : Math.min(history.length - 1, currentHistoryIndex + 1);
68
+ setCurrentHistoryIndex(newIndex);
69
+ const entry = history[newIndex];
70
+ if (entry) {
71
+ buffer.setText(entry.content);
72
+ triggerUpdate();
73
+ }
74
+ return true;
75
+ }, [currentHistoryIndex, buffer]);
76
+ // Terminal-style history navigation: navigate down (newer)
77
+ const navigateHistoryDown = useCallback(() => {
78
+ if (currentHistoryIndex === -1)
79
+ return false;
80
+ const newIndex = currentHistoryIndex - 1;
81
+ const history = persistentHistoryRef.current;
82
+ if (newIndex < 0) {
83
+ // Restore original input
84
+ buffer.setText(savedInput.current);
85
+ setCurrentHistoryIndex(-1);
86
+ savedInput.current = '';
87
+ }
88
+ else {
89
+ setCurrentHistoryIndex(newIndex);
90
+ const entry = history[newIndex];
91
+ if (entry) {
92
+ buffer.setText(entry.content);
93
+ }
94
+ }
95
+ triggerUpdate();
96
+ return true;
97
+ }, [currentHistoryIndex, buffer]);
98
+ // Reset history navigation state
99
+ const resetHistoryNavigation = useCallback(() => {
100
+ setCurrentHistoryIndex(-1);
101
+ savedInput.current = '';
102
+ }, []);
103
+ // Save message to persistent history
104
+ const saveToHistory = useCallback(async (content) => {
105
+ await historyManager.addEntry(content);
106
+ // Reload history to update the list
107
+ const entries = await historyManager.getEntries();
108
+ setPersistentHistory(entries);
109
+ }, []);
39
110
  return {
40
111
  showHistoryMenu,
41
112
  setShowHistoryMenu,
@@ -46,5 +117,11 @@ export function useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHisto
46
117
  escapeKeyTimer,
47
118
  getUserMessages,
48
119
  handleHistorySelect,
120
+ // Terminal-style history navigation
121
+ currentHistoryIndex,
122
+ navigateHistoryUp,
123
+ navigateHistoryDown,
124
+ resetHistoryNavigation,
125
+ saveToHistory,
49
126
  };
50
127
  }
@@ -14,7 +14,7 @@ export function useInputBuffer(viewport) {
14
14
  useEffect(() => {
15
15
  buffer.updateViewport(viewport);
16
16
  triggerUpdate();
17
- }, [viewport.width, viewport.height, buffer, triggerUpdate]);
17
+ }, [viewport.width, viewport.height, buffer]);
18
18
  // Cleanup buffer on unmount
19
19
  useEffect(() => {
20
20
  return () => {
@@ -41,6 +41,11 @@ type KeyboardInputOptions = {
41
41
  infoText: string;
42
42
  }>;
43
43
  handleHistorySelect: (value: string) => void;
44
+ currentHistoryIndex: number;
45
+ navigateHistoryUp: () => boolean;
46
+ navigateHistoryDown: () => boolean;
47
+ resetHistoryNavigation: () => void;
48
+ saveToHistory: (content: string) => Promise<void>;
44
49
  pasteFromClipboard: () => Promise<void>;
45
50
  onSubmit: (message: string, images?: Array<{
46
51
  data: string;
@@ -2,7 +2,7 @@ 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, 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, } = options;
6
6
  // Track paste detection
7
7
  const inputBuffer = useRef('');
8
8
  const inputTimer = useRef(null);
@@ -129,18 +129,18 @@ export function useKeyboardInput(options) {
129
129
  }
130
130
  // Ctrl+L - Delete from cursor to beginning
131
131
  if (key.ctrl && input === 'l') {
132
- const fullText = buffer.getFullText();
132
+ const displayText = buffer.text;
133
133
  const cursorPos = buffer.getCursorPosition();
134
- const afterCursor = fullText.slice(cursorPos);
134
+ const afterCursor = displayText.slice(cursorPos);
135
135
  buffer.setText(afterCursor);
136
136
  forceStateUpdate();
137
137
  return;
138
138
  }
139
139
  // Ctrl+R - Delete from cursor to end
140
140
  if (key.ctrl && input === 'r') {
141
- const fullText = buffer.getFullText();
141
+ const displayText = buffer.text;
142
142
  const cursorPos = buffer.getCursorPosition();
143
- const beforeCursor = fullText.slice(0, cursorPos);
143
+ const beforeCursor = displayText.slice(0, cursorPos);
144
144
  buffer.setText(beforeCursor);
145
145
  forceStateUpdate();
146
146
  return;
@@ -221,6 +221,10 @@ export function useKeyboardInput(options) {
221
221
  }
222
222
  // Enter - submit message
223
223
  if (key.return) {
224
+ // Reset history navigation on submit
225
+ if (currentHistoryIndex !== -1) {
226
+ resetHistoryNavigation();
227
+ }
224
228
  const message = buffer.getFullText().trim();
225
229
  if (message) {
226
230
  // Check if message is a command with arguments (e.g., /review [note])
@@ -253,6 +257,8 @@ export function useKeyboardInput(options) {
253
257
  }));
254
258
  buffer.setText('');
255
259
  forceUpdate({});
260
+ // Save to persistent history
261
+ saveToHistory(message);
256
262
  onSubmit(message, validImages.length > 0 ? validImages : undefined);
257
263
  }
258
264
  return;
@@ -275,23 +281,59 @@ export function useKeyboardInput(options) {
275
281
  return;
276
282
  }
277
283
  if (key.upArrow && !showCommands && !showFilePicker) {
278
- buffer.moveUp();
279
284
  const text = buffer.getFullText();
280
285
  const cursorPos = buffer.getCursorPosition();
281
- updateFilePickerState(text, cursorPos);
286
+ const isEmpty = text.trim() === '';
287
+ const isAtStart = cursorPos === 0;
288
+ const hasNewline = text.includes('\n');
289
+ // Terminal-style history navigation:
290
+ // 1. Empty input box -> navigate history
291
+ // 2. Cursor at start of single line -> navigate history
292
+ // 3. Otherwise -> normal cursor movement
293
+ if (isEmpty || (!hasNewline && isAtStart)) {
294
+ const navigated = navigateHistoryUp();
295
+ if (navigated) {
296
+ updateFilePickerState(buffer.getFullText(), buffer.getCursorPosition());
297
+ triggerUpdate();
298
+ return;
299
+ }
300
+ }
301
+ // Normal cursor movement
302
+ buffer.moveUp();
303
+ updateFilePickerState(buffer.getFullText(), buffer.getCursorPosition());
282
304
  triggerUpdate();
283
305
  return;
284
306
  }
285
307
  if (key.downArrow && !showCommands && !showFilePicker) {
286
- buffer.moveDown();
287
308
  const text = buffer.getFullText();
288
309
  const cursorPos = buffer.getCursorPosition();
289
- updateFilePickerState(text, cursorPos);
310
+ const isEmpty = text.trim() === '';
311
+ const isAtEnd = cursorPos === text.length;
312
+ const hasNewline = text.includes('\n');
313
+ // Terminal-style history navigation:
314
+ // 1. Empty input box -> navigate history (if in history mode)
315
+ // 2. Cursor at end of single line -> navigate history (if in history mode)
316
+ // 3. Otherwise -> normal cursor movement
317
+ if ((isEmpty || (!hasNewline && isAtEnd)) && currentHistoryIndex !== -1) {
318
+ const navigated = navigateHistoryDown();
319
+ if (navigated) {
320
+ updateFilePickerState(buffer.getFullText(), buffer.getCursorPosition());
321
+ triggerUpdate();
322
+ return;
323
+ }
324
+ }
325
+ // Normal cursor movement
326
+ buffer.moveDown();
327
+ updateFilePickerState(buffer.getFullText(), buffer.getCursorPosition());
290
328
  triggerUpdate();
291
329
  return;
292
330
  }
293
331
  // Regular character input
294
332
  if (input && !key.ctrl && !key.meta && !key.escape) {
333
+ // Reset history navigation when user starts typing
334
+ if (currentHistoryIndex !== -1) {
335
+ resetHistoryNavigation();
336
+ }
295
337
  // Ensure focus is active when user is typing (handles delayed focus events)
296
338
  // This is especially important for drag-and-drop operations where focus
297
339
  // events may arrive out of order or be filtered by sanitizeInput
@@ -76,7 +76,7 @@ export function useStreamingState() {
76
76
  });
77
77
  }, 1000);
78
78
  return () => clearInterval(interval);
79
- }, [retryStatus]);
79
+ }, [retryStatus?.isRetrying, retryStatus?.remainingSeconds]);
80
80
  return {
81
81
  isStreaming,
82
82
  setIsStreaming,
@@ -44,6 +44,10 @@ export declare class ACECodeSearchService {
44
44
  * Find symbol definition (go to definition)
45
45
  */
46
46
  findDefinition(symbolName: string, contextFile?: string): Promise<CodeSymbol | null>;
47
+ /**
48
+ * Expand glob patterns with braces like "*.{ts,tsx}" into multiple patterns
49
+ */
50
+ private expandGlobBraces;
47
51
  /**
48
52
  * Strategy 1: Use git grep for fast searching in Git repositories
49
53
  */
@@ -470,6 +470,20 @@ export class ACECodeSearchService {
470
470
  }
471
471
  return null;
472
472
  }
473
+ /**
474
+ * Expand glob patterns with braces like "*.{ts,tsx}" into multiple patterns
475
+ */
476
+ expandGlobBraces(glob) {
477
+ // Match {a,b,c} pattern
478
+ const braceMatch = glob.match(/^(.+)\{([^}]+)\}(.*)$/);
479
+ if (!braceMatch || !braceMatch[1] || !braceMatch[2] || braceMatch[3] === undefined) {
480
+ return [glob];
481
+ }
482
+ const prefix = braceMatch[1];
483
+ const alternatives = braceMatch[2].split(',');
484
+ const suffix = braceMatch[3];
485
+ return alternatives.map(alt => `${prefix}${alt}${suffix}`);
486
+ }
473
487
  /**
474
488
  * Strategy 1: Use git grep for fast searching in Git repositories
475
489
  */
@@ -484,7 +498,9 @@ export class ACECodeSearchService {
484
498
  pattern,
485
499
  ];
486
500
  if (fileGlob) {
487
- args.push('--', fileGlob);
501
+ // Expand glob patterns with braces (e.g., "source/**/*.{ts,tsx}" -> ["source/**/*.ts", "source/**/*.tsx"])
502
+ const expandedGlobs = this.expandGlobBraces(fileGlob);
503
+ args.push('--', ...expandedGlobs);
488
504
  }
489
505
  const child = spawn('git', args, {
490
506
  cwd: this.basePath,
@@ -411,7 +411,7 @@ export class FilesystemMCPService {
411
411
  for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
412
412
  // Yield control periodically to prevent UI freeze
413
413
  if (i % YIELD_INTERVAL === 0) {
414
- await new Promise(resolve => setImmediate(resolve));
414
+ await new Promise(resolve => setTimeout(resolve, 0));
415
415
  }
416
416
  // Quick pre-filter: check first line similarity (only for multi-line searches)
417
417
  if (usePreFilter) {
@@ -456,7 +456,7 @@ export class FilesystemMCPService {
456
456
  for (let i = 0; i <= contentLines.length - correctedSearchLines.length; i++) {
457
457
  // Yield control periodically to prevent UI freeze
458
458
  if (i % YIELD_INTERVAL === 0) {
459
- await new Promise(resolve => setImmediate(resolve));
459
+ await new Promise(resolve => setTimeout(resolve, 0));
460
460
  }
461
461
  const candidateLines = contentLines.slice(i, i + correctedSearchLines.length);
462
462
  const candidateContent = candidateLines.join('\n');
@@ -22,7 +22,7 @@ export async function findClosestMatches(searchContent, fileLines, topN = 3) {
22
22
  for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
23
23
  // Yield control periodically to prevent UI freeze
24
24
  if (i % YIELD_INTERVAL === 0) {
25
- await new Promise(resolve => setImmediate(resolve));
25
+ await new Promise(resolve => setTimeout(resolve, 0));
26
26
  }
27
27
  // Quick pre-filter: check first line similarity (only for multi-line)
28
28
  if (usePreFilter) {
@@ -48,7 +48,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
48
48
  // Use file picker hook
49
49
  const { showFilePicker, setShowFilePicker, fileSelectedIndex, setFileSelectedIndex, fileQuery, setFileQuery, atSymbolPosition, setAtSymbolPosition, filteredFileCount, updateFilePickerState, handleFileSelect, handleFilteredCountChange, fileListRef, } = useFilePicker(buffer, triggerUpdate);
50
50
  // Use history navigation hook
51
- const { showHistoryMenu, setShowHistoryMenu, historySelectedIndex, setHistorySelectedIndex, escapeKeyCount, setEscapeKeyCount, escapeKeyTimer, getUserMessages, handleHistorySelect, } = useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHistorySelect);
51
+ const { showHistoryMenu, setShowHistoryMenu, historySelectedIndex, setHistorySelectedIndex, escapeKeyCount, setEscapeKeyCount, escapeKeyTimer, getUserMessages, handleHistorySelect, currentHistoryIndex, navigateHistoryUp, navigateHistoryDown, resetHistoryNavigation, saveToHistory, } = useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHistorySelect);
52
52
  // Use clipboard hook
53
53
  const { pasteFromClipboard } = useClipboard(buffer, updateCommandPanelState, updateFilePickerState, triggerUpdate);
54
54
  // Use keyboard input hook
@@ -85,6 +85,11 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
85
85
  escapeKeyTimer,
86
86
  getUserMessages,
87
87
  handleHistorySelect,
88
+ currentHistoryIndex,
89
+ navigateHistoryUp,
90
+ navigateHistoryDown,
91
+ resetHistoryNavigation,
92
+ saveToHistory,
88
93
  pasteFromClipboard,
89
94
  onSubmit,
90
95
  ensureFocus,
@@ -95,7 +100,9 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
95
100
  buffer.setText(initialContent);
96
101
  triggerUpdate();
97
102
  }
98
- }, [initialContent, buffer, triggerUpdate]);
103
+ // Only run when initialContent changes
104
+ // eslint-disable-next-line react-hooks/exhaustive-deps
105
+ }, [initialContent]);
99
106
  // Force full re-render when file picker visibility changes to prevent artifacts
100
107
  useEffect(() => {
101
108
  // Use a small delay to ensure the component tree has updated
@@ -103,7 +110,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
103
110
  forceUpdate({});
104
111
  }, 10);
105
112
  return () => clearTimeout(timer);
106
- }, [showFilePicker, forceUpdate]);
113
+ }, [showFilePicker]);
107
114
  // Handle terminal width changes with debounce (like gemini-cli)
108
115
  useEffect(() => {
109
116
  // Skip on initial mount
@@ -117,7 +124,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
117
124
  forceUpdate({});
118
125
  }, 100);
119
126
  return () => clearTimeout(timer);
120
- }, [terminalWidth, forceUpdate]);
127
+ }, [terminalWidth]);
121
128
  // Notify parent of context percentage changes
122
129
  useEffect(() => {
123
130
  if (contextUsage && onContextPercentageChange) {
@@ -302,7 +302,9 @@ export default function ChatScreen({ skipWelcome }) {
302
302
  // Check if all tool results exist after this assistant message
303
303
  for (let j = i + 1; j < messages.length; j++) {
304
304
  const followMsg = messages[j];
305
- if (followMsg && followMsg.role === 'tool' && followMsg.tool_call_id) {
305
+ if (followMsg &&
306
+ followMsg.role === 'tool' &&
307
+ followMsg.tool_call_id) {
306
308
  toolCallIds.delete(followMsg.tool_call_id);
307
309
  }
308
310
  }
@@ -908,7 +910,9 @@ export default function ChatScreen({ skipWelcome }) {
908
910
  React.createElement(Text, { color: "gray", dimColor: true },
909
911
  React.createElement(ShimmerText, { text: streamingState.isReasoning
910
912
  ? 'Deep thinking...'
911
- : 'Thinking...' }),
913
+ : streamingState.streamTokenCount > 0
914
+ ? 'Writing...'
915
+ : 'Thinking...' }),
912
916
  ' ',
913
917
  "(",
914
918
  formatElapsedTime(streamingState.elapsedSeconds),