snow-ai 0.1.12 → 0.2.1

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.
Files changed (97) hide show
  1. package/dist/api/chat.d.ts +65 -2
  2. package/dist/api/chat.js +299 -16
  3. package/dist/api/responses.d.ts +52 -0
  4. package/dist/api/responses.js +541 -0
  5. package/dist/api/systemPrompt.d.ts +4 -0
  6. package/dist/api/systemPrompt.js +43 -0
  7. package/dist/app.js +15 -4
  8. package/dist/cli.js +17 -1
  9. package/dist/hooks/useConversation.d.ts +32 -0
  10. package/dist/hooks/useConversation.js +403 -0
  11. package/dist/hooks/useGlobalNavigation.d.ts +6 -0
  12. package/dist/hooks/useGlobalNavigation.js +15 -0
  13. package/dist/hooks/useSessionManagement.d.ts +10 -0
  14. package/dist/hooks/useSessionManagement.js +43 -0
  15. package/dist/hooks/useSessionSave.d.ts +8 -0
  16. package/dist/hooks/useSessionSave.js +52 -0
  17. package/dist/hooks/useToolConfirmation.d.ts +18 -0
  18. package/dist/hooks/useToolConfirmation.js +49 -0
  19. package/dist/mcp/bash.d.ts +57 -0
  20. package/dist/mcp/bash.js +138 -0
  21. package/dist/mcp/filesystem.d.ts +307 -0
  22. package/dist/mcp/filesystem.js +520 -0
  23. package/dist/mcp/todo.d.ts +55 -0
  24. package/dist/mcp/todo.js +329 -0
  25. package/dist/test/logger-test.d.ts +1 -0
  26. package/dist/test/logger-test.js +7 -0
  27. package/dist/types/index.d.ts +1 -1
  28. package/dist/ui/components/ChatInput.d.ts +15 -2
  29. package/dist/ui/components/ChatInput.js +445 -59
  30. package/dist/ui/components/CommandPanel.d.ts +2 -2
  31. package/dist/ui/components/CommandPanel.js +11 -7
  32. package/dist/ui/components/DiffViewer.d.ts +9 -0
  33. package/dist/ui/components/DiffViewer.js +93 -0
  34. package/dist/ui/components/FileList.d.ts +14 -0
  35. package/dist/ui/components/FileList.js +131 -0
  36. package/dist/ui/components/MCPInfoPanel.d.ts +2 -0
  37. package/dist/ui/components/MCPInfoPanel.js +74 -0
  38. package/dist/ui/components/MCPInfoScreen.d.ts +7 -0
  39. package/dist/ui/components/MCPInfoScreen.js +27 -0
  40. package/dist/ui/components/MarkdownRenderer.d.ts +7 -0
  41. package/dist/ui/components/MarkdownRenderer.js +110 -0
  42. package/dist/ui/components/Menu.d.ts +5 -2
  43. package/dist/ui/components/Menu.js +60 -9
  44. package/dist/ui/components/MessageList.d.ts +30 -2
  45. package/dist/ui/components/MessageList.js +64 -12
  46. package/dist/ui/components/PendingMessages.js +1 -1
  47. package/dist/ui/components/ScrollableSelectInput.d.ts +29 -0
  48. package/dist/ui/components/ScrollableSelectInput.js +157 -0
  49. package/dist/ui/components/SessionListScreen.d.ts +7 -0
  50. package/dist/ui/components/SessionListScreen.js +196 -0
  51. package/dist/ui/components/SessionListScreenWrapper.d.ts +7 -0
  52. package/dist/ui/components/SessionListScreenWrapper.js +14 -0
  53. package/dist/ui/components/TodoTree.d.ts +15 -0
  54. package/dist/ui/components/TodoTree.js +60 -0
  55. package/dist/ui/components/ToolConfirmation.d.ts +8 -0
  56. package/dist/ui/components/ToolConfirmation.js +38 -0
  57. package/dist/ui/components/ToolResultPreview.d.ts +12 -0
  58. package/dist/ui/components/ToolResultPreview.js +115 -0
  59. package/dist/ui/pages/ChatScreen.d.ts +4 -0
  60. package/dist/ui/pages/ChatScreen.js +385 -196
  61. package/dist/ui/pages/MCPConfigScreen.d.ts +6 -0
  62. package/dist/ui/pages/MCPConfigScreen.js +55 -0
  63. package/dist/ui/pages/ModelConfigScreen.js +73 -12
  64. package/dist/ui/pages/WelcomeScreen.js +17 -11
  65. package/dist/utils/apiConfig.d.ts +12 -0
  66. package/dist/utils/apiConfig.js +95 -9
  67. package/dist/utils/commandExecutor.d.ts +2 -1
  68. package/dist/utils/commands/init.d.ts +2 -0
  69. package/dist/utils/commands/init.js +93 -0
  70. package/dist/utils/commands/mcp.d.ts +2 -0
  71. package/dist/utils/commands/mcp.js +12 -0
  72. package/dist/utils/commands/resume.d.ts +2 -0
  73. package/dist/utils/commands/resume.js +12 -0
  74. package/dist/utils/commands/yolo.d.ts +2 -0
  75. package/dist/utils/commands/yolo.js +12 -0
  76. package/dist/utils/fileUtils.d.ts +44 -0
  77. package/dist/utils/fileUtils.js +222 -0
  78. package/dist/utils/index.d.ts +4 -0
  79. package/dist/utils/index.js +6 -0
  80. package/dist/utils/logger.d.ts +31 -0
  81. package/dist/utils/logger.js +97 -0
  82. package/dist/utils/mcpToolsManager.d.ts +47 -0
  83. package/dist/utils/mcpToolsManager.js +476 -0
  84. package/dist/utils/messageFormatter.d.ts +12 -0
  85. package/dist/utils/messageFormatter.js +32 -0
  86. package/dist/utils/sessionConverter.d.ts +6 -0
  87. package/dist/utils/sessionConverter.js +61 -0
  88. package/dist/utils/sessionManager.d.ts +39 -0
  89. package/dist/utils/sessionManager.js +141 -0
  90. package/dist/utils/textBuffer.d.ts +36 -7
  91. package/dist/utils/textBuffer.js +265 -179
  92. package/dist/utils/todoPreprocessor.d.ts +5 -0
  93. package/dist/utils/todoPreprocessor.js +19 -0
  94. package/dist/utils/toolExecutor.d.ts +21 -0
  95. package/dist/utils/toolExecutor.js +28 -0
  96. package/package.json +12 -3
  97. package/readme.md +2 -2
@@ -1,15 +1,20 @@
1
1
  import React, { useState, useEffect, useRef, useCallback } from 'react';
2
2
  import { Box, Text, useStdout, useInput } from 'ink';
3
3
  import { TextBuffer } from '../../utils/textBuffer.js';
4
- import { cpSlice } from '../../utils/textUtils.js';
4
+ import { cpSlice, cpLen } from '../../utils/textUtils.js';
5
5
  import CommandPanel from './CommandPanel.js';
6
6
  import { executeCommand } from '../../utils/commandExecutor.js';
7
+ import FileList from './FileList.js';
8
+ import { execSync } from 'child_process';
7
9
  // Command Definition
8
10
  const commands = [
9
11
  { name: 'clear', description: 'Clear chat context and conversation history' },
10
- { name: 'agents', description: 'Manage agent configurations' }
12
+ { name: 'resume', description: 'Resume a conversation' },
13
+ { name: 'mcp', description: 'Show Model Context Protocol services and tools' },
14
+ { name: 'yolo', description: 'Toggle unattended mode (auto-approve all tools)' },
15
+ { name: 'init', description: 'Analyze project and generate/update SNOW.md documentation' }
11
16
  ];
12
- export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type your message...', disabled = false }) {
17
+ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type your message...', disabled = false, chatHistory = [], onHistorySelect, yoloMode = false, contextUsage }) {
13
18
  const { stdout } = useStdout();
14
19
  const terminalWidth = stdout?.columns || 80;
15
20
  const uiOverhead = 8;
@@ -23,6 +28,32 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
23
28
  // Command panel state
24
29
  const [showCommands, setShowCommands] = useState(false);
25
30
  const [commandSelectedIndex, setCommandSelectedIndex] = useState(0);
31
+ // File picker state
32
+ const [showFilePicker, setShowFilePicker] = useState(false);
33
+ const [fileSelectedIndex, setFileSelectedIndex] = useState(0);
34
+ const [fileQuery, setFileQuery] = useState('');
35
+ const [atSymbolPosition, setAtSymbolPosition] = useState(-1);
36
+ const [filteredFileCount, setFilteredFileCount] = useState(0);
37
+ // Refs
38
+ const fileListRef = useRef(null);
39
+ // History navigation state
40
+ const [showHistoryMenu, setShowHistoryMenu] = useState(false);
41
+ const [historySelectedIndex, setHistorySelectedIndex] = useState(0);
42
+ const [escapeKeyCount, setEscapeKeyCount] = useState(0);
43
+ const escapeKeyTimer = useRef(null);
44
+ // Get user messages from chat history for navigation
45
+ const getUserMessages = useCallback(() => {
46
+ const userMessages = chatHistory
47
+ .map((msg, index) => ({ ...msg, originalIndex: index }))
48
+ .filter(msg => msg.role === 'user' && msg.content.trim());
49
+ // Keep original order (oldest first, newest last) and map with display numbers
50
+ return userMessages
51
+ .map((msg, index) => ({
52
+ label: `${index + 1}. ${msg.content.slice(0, 50)}${msg.content.length > 50 ? '...' : ''}`,
53
+ value: msg.originalIndex.toString(),
54
+ infoText: msg.content
55
+ }));
56
+ }, [chatHistory]);
26
57
  // Get filtered commands based on current input
27
58
  const getFilteredCommands = useCallback(() => {
28
59
  const text = buffer.getFullText();
@@ -43,15 +74,92 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
43
74
  setCommandSelectedIndex(0);
44
75
  }
45
76
  }, []);
77
+ // Update file picker state
78
+ const updateFilePickerState = useCallback((text, cursorPos) => {
79
+ if (!text.includes('@')) {
80
+ if (showFilePicker) {
81
+ setShowFilePicker(false);
82
+ setFileSelectedIndex(0);
83
+ setFileQuery('');
84
+ setAtSymbolPosition(-1);
85
+ }
86
+ return;
87
+ }
88
+ // Find the last '@' symbol before the cursor
89
+ const beforeCursor = text.slice(0, cursorPos);
90
+ const lastAtIndex = beforeCursor.lastIndexOf('@');
91
+ if (lastAtIndex !== -1) {
92
+ // Check if there's no space between '@' and cursor
93
+ const afterAt = beforeCursor.slice(lastAtIndex + 1);
94
+ if (!afterAt.includes(' ') && !afterAt.includes('\n')) {
95
+ if (!showFilePicker || fileQuery !== afterAt || atSymbolPosition !== lastAtIndex) {
96
+ setShowFilePicker(true);
97
+ setFileSelectedIndex(0);
98
+ setFileQuery(afterAt);
99
+ setAtSymbolPosition(lastAtIndex);
100
+ }
101
+ return;
102
+ }
103
+ }
104
+ // Hide file picker if no valid @ context found
105
+ if (showFilePicker) {
106
+ setShowFilePicker(false);
107
+ setFileSelectedIndex(0);
108
+ setFileQuery('');
109
+ setAtSymbolPosition(-1);
110
+ }
111
+ }, [showFilePicker, fileQuery, atSymbolPosition]);
112
+ // Force immediate state update for critical operations like backspace
113
+ const forceStateUpdate = useCallback(() => {
114
+ const text = buffer.getFullText();
115
+ const cursorPos = buffer.getCursorPosition();
116
+ updateFilePickerState(text, cursorPos);
117
+ updateCommandPanelState(text);
118
+ forceUpdate({});
119
+ }, [buffer, updateFilePickerState, updateCommandPanelState]);
46
120
  // Force re-render when buffer changes
47
121
  const triggerUpdate = useCallback(() => {
48
122
  const now = Date.now();
49
- // Avoid too frequent updates
50
- if (now - lastUpdateTime.current > 16) { // ~60fps limit
51
- lastUpdateTime.current = now;
52
- forceUpdate({});
123
+ lastUpdateTime.current = now;
124
+ forceUpdate({});
125
+ }, []);
126
+ // Handle file selection
127
+ const handleFileSelect = useCallback(async (filePath) => {
128
+ if (atSymbolPosition !== -1) {
129
+ const text = buffer.getFullText();
130
+ const cursorPos = buffer.getCursorPosition();
131
+ // Replace @query with @filePath + space
132
+ const beforeAt = text.slice(0, atSymbolPosition);
133
+ const afterCursor = text.slice(cursorPos);
134
+ const newText = beforeAt + '@' + filePath + ' ' + afterCursor;
135
+ // Set the new text and position cursor after the inserted file path + space
136
+ buffer.setText(newText);
137
+ // Calculate cursor position after the inserted file path + space
138
+ // Reset cursor to beginning, then move to correct position
139
+ for (let i = 0; i < atSymbolPosition + filePath.length + 2; i++) { // +2 for @ and space
140
+ if (i < buffer.getFullText().length) {
141
+ buffer.moveRight();
142
+ }
143
+ }
144
+ setShowFilePicker(false);
145
+ setFileSelectedIndex(0);
146
+ setFileQuery('');
147
+ setAtSymbolPosition(-1);
148
+ triggerUpdate();
53
149
  }
150
+ }, [atSymbolPosition, buffer, triggerUpdate]);
151
+ // Handle filtered file count change
152
+ const handleFilteredCountChange = useCallback((count) => {
153
+ setFilteredFileCount(count);
54
154
  }, []);
155
+ // Force full re-render when file picker visibility changes to prevent artifacts
156
+ useEffect(() => {
157
+ // Use a small delay to ensure the component tree has updated
158
+ const timer = setTimeout(() => {
159
+ forceUpdate({});
160
+ }, 10);
161
+ return () => clearTimeout(timer);
162
+ }, [showFilePicker]);
55
163
  // Update buffer viewport when terminal width changes
56
164
  useEffect(() => {
57
165
  const newViewport = {
@@ -61,18 +169,183 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
61
169
  buffer.updateViewport(newViewport);
62
170
  triggerUpdate();
63
171
  }, [terminalWidth, buffer, triggerUpdate]);
172
+ // Track paste detection
173
+ const inputBuffer = useRef('');
174
+ const inputTimer = useRef(null);
64
175
  // Handle input using useInput hook instead of raw stdin
65
176
  useInput((input, key) => {
66
177
  if (disabled)
67
178
  return;
179
+ // Debug: Log key presses
180
+ // console.error('Input:', JSON.stringify(input), 'Key:', JSON.stringify(key));
181
+ // Handle escape key for double-ESC history navigation
182
+ if (key.escape) {
183
+ // Close file picker if open
184
+ if (showFilePicker) {
185
+ setShowFilePicker(false);
186
+ setFileSelectedIndex(0);
187
+ setFileQuery('');
188
+ setAtSymbolPosition(-1);
189
+ return;
190
+ }
191
+ // Don't interfere with existing ESC behavior if in command panel
192
+ if (showCommands) {
193
+ setShowCommands(false);
194
+ setCommandSelectedIndex(0);
195
+ return;
196
+ }
197
+ // Handle history navigation
198
+ if (showHistoryMenu) {
199
+ setShowHistoryMenu(false);
200
+ return;
201
+ }
202
+ // Count escape key presses for double-ESC detection
203
+ setEscapeKeyCount(prev => prev + 1);
204
+ // Clear any existing timer
205
+ if (escapeKeyTimer.current) {
206
+ clearTimeout(escapeKeyTimer.current);
207
+ }
208
+ // Set timer to reset count after 500ms
209
+ escapeKeyTimer.current = setTimeout(() => {
210
+ setEscapeKeyCount(0);
211
+ }, 500);
212
+ // Check for double escape
213
+ if (escapeKeyCount >= 1) { // This will be 2 after increment
214
+ const userMessages = getUserMessages();
215
+ if (userMessages.length > 0) {
216
+ setShowHistoryMenu(true);
217
+ setHistorySelectedIndex(0); // Reset selection to first item
218
+ setEscapeKeyCount(0);
219
+ if (escapeKeyTimer.current) {
220
+ clearTimeout(escapeKeyTimer.current);
221
+ escapeKeyTimer.current = null;
222
+ }
223
+ }
224
+ }
225
+ return;
226
+ }
227
+ // Handle history menu navigation
228
+ if (showHistoryMenu) {
229
+ const userMessages = getUserMessages();
230
+ // Up arrow in history menu
231
+ if (key.upArrow) {
232
+ setHistorySelectedIndex(prev => Math.max(0, prev - 1));
233
+ return;
234
+ }
235
+ // Down arrow in history menu
236
+ if (key.downArrow) {
237
+ const maxIndex = Math.max(0, userMessages.length - 1);
238
+ setHistorySelectedIndex(prev => Math.min(maxIndex, prev + 1));
239
+ return;
240
+ }
241
+ // Enter - select history item
242
+ if (key.return) {
243
+ if (userMessages.length > 0 && historySelectedIndex < userMessages.length) {
244
+ const selectedMessage = userMessages[historySelectedIndex];
245
+ if (selectedMessage) {
246
+ handleHistorySelect(selectedMessage.value);
247
+ }
248
+ }
249
+ return;
250
+ }
251
+ // For any other key in history menu, just return to prevent interference
252
+ return;
253
+ }
254
+ // Alt+V / Option+V - Paste from clipboard (including images)
255
+ if (key.meta && input === 'v') {
256
+ try {
257
+ // Try to read image from clipboard using PowerShell (Windows)
258
+ if (process.platform === 'win32') {
259
+ try {
260
+ const psScript = `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $clipboard = [System.Windows.Forms.Clipboard]::GetImage(); if ($clipboard -ne $null) { $ms = New-Object System.IO.MemoryStream; $clipboard.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); $bytes = $ms.ToArray(); $ms.Close(); [Convert]::ToBase64String($bytes) }`;
261
+ const base64 = execSync(`powershell -Command "${psScript}"`, {
262
+ encoding: 'utf-8',
263
+ timeout: 5000
264
+ }).trim();
265
+ if (base64 && base64.length > 100) {
266
+ const dataUrl = `data:image/png;base64,${base64}`;
267
+ buffer.insertImage(dataUrl, 'image/png');
268
+ const text = buffer.getFullText();
269
+ const cursorPos = buffer.getCursorPosition();
270
+ updateCommandPanelState(text);
271
+ updateFilePickerState(text, cursorPos);
272
+ triggerUpdate();
273
+ return;
274
+ }
275
+ }
276
+ catch (imgError) {
277
+ // No image in clipboard or error, fall through to text
278
+ }
279
+ }
280
+ // If no image, try to read text from clipboard
281
+ try {
282
+ let clipboardText = '';
283
+ if (process.platform === 'win32') {
284
+ clipboardText = execSync('powershell -Command "Get-Clipboard"', {
285
+ encoding: 'utf-8',
286
+ timeout: 2000
287
+ }).trim();
288
+ }
289
+ else if (process.platform === 'darwin') {
290
+ clipboardText = execSync('pbpaste', {
291
+ encoding: 'utf-8',
292
+ timeout: 2000
293
+ }).trim();
294
+ }
295
+ else {
296
+ clipboardText = execSync('xclip -selection clipboard -o', {
297
+ encoding: 'utf-8',
298
+ timeout: 2000
299
+ }).trim();
300
+ }
301
+ if (clipboardText) {
302
+ buffer.insert(clipboardText);
303
+ const fullText = buffer.getFullText();
304
+ const cursorPos = buffer.getCursorPosition();
305
+ updateCommandPanelState(fullText);
306
+ updateFilePickerState(fullText, cursorPos);
307
+ triggerUpdate();
308
+ }
309
+ }
310
+ catch (textError) {
311
+ console.error('Failed to read text from clipboard:', textError);
312
+ }
313
+ }
314
+ catch (error) {
315
+ console.error('Failed to read from clipboard:', error);
316
+ }
317
+ return;
318
+ }
68
319
  // Backspace
69
320
  if (key.backspace || key.delete) {
70
321
  buffer.backspace();
71
- const text = buffer.getFullText();
72
- updateCommandPanelState(text);
73
- triggerUpdate();
322
+ forceStateUpdate();
74
323
  return;
75
324
  }
325
+ // Handle file picker navigation
326
+ if (showFilePicker) {
327
+ // Up arrow in file picker
328
+ if (key.upArrow) {
329
+ setFileSelectedIndex(prev => Math.max(0, prev - 1));
330
+ return;
331
+ }
332
+ // Down arrow in file picker
333
+ if (key.downArrow) {
334
+ const maxIndex = Math.max(0, filteredFileCount - 1);
335
+ setFileSelectedIndex(prev => Math.min(maxIndex, prev + 1));
336
+ return;
337
+ }
338
+ // Tab or Enter - select file
339
+ if (key.tab || key.return) {
340
+ if (filteredFileCount > 0 && fileSelectedIndex < filteredFileCount) {
341
+ const selectedFile = fileListRef.current?.getSelectedFile();
342
+ if (selectedFile) {
343
+ handleFileSelect(selectedFile);
344
+ }
345
+ }
346
+ return;
347
+ }
348
+ }
76
349
  // Handle command panel navigation
77
350
  if (showCommands) {
78
351
  const filteredCommands = getFilteredCommands();
@@ -112,95 +385,208 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
112
385
  if (key.return) {
113
386
  const message = buffer.getFullText().trim();
114
387
  if (message) {
388
+ // 获取图片数据
389
+ const images = buffer.getImages().map(img => ({
390
+ data: img.data,
391
+ mimeType: img.mimeType
392
+ }));
115
393
  buffer.setText('');
116
394
  forceUpdate({});
117
- onSubmit(message);
395
+ onSubmit(message, images.length > 0 ? images : undefined);
118
396
  }
119
397
  return;
120
398
  }
121
399
  // Arrow keys for cursor movement
122
400
  if (key.leftArrow) {
123
401
  buffer.moveLeft();
402
+ const text = buffer.getFullText();
403
+ const cursorPos = buffer.getCursorPosition();
404
+ updateFilePickerState(text, cursorPos);
124
405
  triggerUpdate();
125
406
  return;
126
407
  }
127
408
  if (key.rightArrow) {
128
409
  buffer.moveRight();
410
+ const text = buffer.getFullText();
411
+ const cursorPos = buffer.getCursorPosition();
412
+ updateFilePickerState(text, cursorPos);
129
413
  triggerUpdate();
130
414
  return;
131
415
  }
132
- if (key.upArrow && !showCommands) {
416
+ if (key.upArrow && !showCommands && !showFilePicker) {
133
417
  buffer.moveUp();
418
+ const text = buffer.getFullText();
419
+ const cursorPos = buffer.getCursorPosition();
420
+ updateFilePickerState(text, cursorPos);
134
421
  triggerUpdate();
135
422
  return;
136
423
  }
137
- if (key.downArrow && !showCommands) {
424
+ if (key.downArrow && !showCommands && !showFilePicker) {
138
425
  buffer.moveDown();
426
+ const text = buffer.getFullText();
427
+ const cursorPos = buffer.getCursorPosition();
428
+ updateFilePickerState(text, cursorPos);
139
429
  triggerUpdate();
140
430
  return;
141
431
  }
142
432
  // Regular character input
143
433
  if (input && !key.ctrl && !key.meta && !key.escape) {
144
- buffer.insert(input);
145
- const text = buffer.getFullText();
146
- updateCommandPanelState(text);
147
- triggerUpdate();
148
- }
149
- });
150
- // Handle paste events - useInput should handle paste automatically
151
- // but we may need to handle large pastes specially
152
- useEffect(() => {
153
- const handlePaste = (event) => {
154
- if (event.clipboardData) {
155
- const pastedText = event.clipboardData.getData('text');
156
- if (pastedText && pastedText.length > 0) {
157
- // Let TextBuffer handle the paste processing
158
- buffer.insert(pastedText);
434
+ // Accumulate input for paste detection
435
+ inputBuffer.current += input;
436
+ // Clear existing timer
437
+ if (inputTimer.current) {
438
+ clearTimeout(inputTimer.current);
439
+ }
440
+ // Set timer to process accumulated input
441
+ inputTimer.current = setTimeout(() => {
442
+ const accumulated = inputBuffer.current;
443
+ inputBuffer.current = '';
444
+ // If we accumulated input, it's likely a paste
445
+ if (accumulated) {
446
+ buffer.insert(accumulated);
159
447
  const text = buffer.getFullText();
448
+ const cursorPos = buffer.getCursorPosition();
160
449
  updateCommandPanelState(text);
450
+ updateFilePickerState(text, cursorPos);
161
451
  triggerUpdate();
162
452
  }
163
- }
164
- };
165
- // Note: This might not work in all terminal environments
166
- // but the useInput hook should handle most paste scenarios
167
- if (typeof window !== 'undefined') {
168
- window.addEventListener('paste', handlePaste);
169
- return () => window.removeEventListener('paste', handlePaste);
170
- }
171
- return undefined;
172
- }, [buffer, updateCommandPanelState, triggerUpdate]);
173
- const visualLines = buffer.viewportVisualLines;
174
- const [cursorRow, cursorCol] = buffer.visualCursor;
453
+ }, 10); // Short delay to accumulate rapid input
454
+ }
455
+ });
456
+ // Handle history selection
457
+ const handleHistorySelect = useCallback((value) => {
458
+ const selectedIndex = parseInt(value, 10);
459
+ const selectedMessage = chatHistory[selectedIndex];
460
+ if (selectedMessage && onHistorySelect) {
461
+ // Put the message content in the input buffer
462
+ buffer.setText(selectedMessage.content);
463
+ setShowHistoryMenu(false);
464
+ triggerUpdate();
465
+ onHistorySelect(selectedIndex, selectedMessage.content);
466
+ }
467
+ }, [chatHistory, onHistorySelect, buffer, triggerUpdate]);
175
468
  // Render content with cursor and paste placeholders
176
469
  const renderContent = useCallback(() => {
177
470
  if (buffer.text.length > 0) {
178
- return visualLines.map((line, index) => (React.createElement(Box, { key: `line-${index}` },
179
- React.createElement(Text, null, index === cursorRow ? (React.createElement(React.Fragment, null,
180
- cpSlice(line, 0, cursorCol),
471
+ // 使用buffer的内部文本而不是getFullText(),这样可以显示占位符
472
+ const displayText = buffer.text;
473
+ const cursorPos = buffer.getCursorPosition();
474
+ // 检查是否包含粘贴占位符或图片占位符并高亮显示
475
+ const hasPastePlaceholder = displayText.includes('[Paste ') && displayText.includes(' characters #');
476
+ const hasImagePlaceholder = displayText.includes('[image #');
477
+ if (hasPastePlaceholder || hasImagePlaceholder) {
478
+ const atCursor = (() => {
479
+ const charInfo = buffer.getCharAtCursor();
480
+ return charInfo.char === '\n' ? ' ' : charInfo.char;
481
+ })();
482
+ // 分割文本并高亮占位符(粘贴和图片)
483
+ const parts = displayText.split(/(\[Paste \d+ characters #\d+\]|\[image #\d+\])/);
484
+ let processedLength = 0;
485
+ let cursorRendered = false;
486
+ const elements = parts.map((part, partIndex) => {
487
+ const isPastePlaceholder = part.match(/^\[Paste \d+ characters #\d+\]$/);
488
+ const isImagePlaceholder = part.match(/^\[image #\d+\]$/);
489
+ const isPlaceholder = isPastePlaceholder || isImagePlaceholder;
490
+ const partStart = processedLength;
491
+ const partEnd = processedLength + cpLen(part);
492
+ processedLength = partEnd;
493
+ // 检查光标是否在这个部分
494
+ if (cursorPos >= partStart && cursorPos < partEnd) {
495
+ cursorRendered = true;
496
+ const beforeCursorInPart = cpSlice(part, 0, cursorPos - partStart);
497
+ const afterCursorInPart = cpSlice(part, cursorPos - partStart + 1);
498
+ return (React.createElement(React.Fragment, { key: partIndex }, isPlaceholder ? (React.createElement(Text, { color: isImagePlaceholder ? "magenta" : "cyan", dimColor: true },
499
+ beforeCursorInPart,
500
+ React.createElement(Text, { backgroundColor: "white", color: "black" }, atCursor),
501
+ afterCursorInPart)) : (React.createElement(React.Fragment, null,
502
+ beforeCursorInPart,
503
+ React.createElement(Text, { backgroundColor: "white", color: "black" }, atCursor),
504
+ afterCursorInPart))));
505
+ }
506
+ else {
507
+ return isPlaceholder ? (React.createElement(Text, { key: partIndex, color: isImagePlaceholder ? "magenta" : "cyan", dimColor: true }, part)) : (React.createElement(Text, { key: partIndex }, part));
508
+ }
509
+ });
510
+ return (React.createElement(Text, null,
511
+ elements,
512
+ !cursorRendered && (React.createElement(Text, { backgroundColor: "white", color: "black" }, ' '))));
513
+ }
514
+ else {
515
+ // 普通文本渲染
516
+ return (React.createElement(Text, null,
517
+ cpSlice(displayText, 0, cursorPos),
181
518
  React.createElement(Text, { backgroundColor: "white", color: "black" }, (() => {
182
519
  const charInfo = buffer.getCharAtCursor();
183
520
  return charInfo.char === '\n' ? ' ' : charInfo.char;
184
521
  })()),
185
- cpSlice(line, cursorCol + 1))) : (
186
- // Check for paste placeholders and highlight them
187
- line.includes('[Paste ') && line.includes(' line #') ? (React.createElement(Text, null, line.split(/(\[Paste \d+ line #\d+\])/).map((part, partIndex) => part.match(/^\[Paste \d+ line #\d+\]$/) ? (React.createElement(Text, { key: partIndex, color: "cyan", dimColor: true }, part)) : (React.createElement(Text, { key: partIndex }, part))))) : (line || ' '))))));
522
+ cpSlice(displayText, cursorPos + 1)));
523
+ }
188
524
  }
189
525
  else {
190
- return (React.createElement(Box, null,
526
+ return (React.createElement(React.Fragment, null,
191
527
  React.createElement(Text, { backgroundColor: disabled ? "gray" : "white", color: disabled ? "darkGray" : "black" }, ' '),
192
528
  React.createElement(Text, { color: disabled ? "darkGray" : "gray", dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
193
529
  }
194
- }, [visualLines, cursorRow, cursorCol, buffer, placeholder]);
195
- return (React.createElement(Box, { flexDirection: "column", width: "100%" },
196
- React.createElement(Box, { flexDirection: "row", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, width: "100%" },
197
- React.createElement(Text, { color: "cyan", bold: true },
198
- "\u27A3",
199
- ' '),
200
- React.createElement(Box, { flexDirection: "column", flexGrow: 1 }, renderContent())),
201
- React.createElement(CommandPanel, { commands: getFilteredCommands(), selectedIndex: commandSelectedIndex, query: buffer.getFullText().slice(1), visible: showCommands }),
202
- React.createElement(Box, { marginTop: 1 },
203
- React.createElement(Text, { color: "gray", dimColor: true }, showCommands && getFilteredCommands().length > 0
204
- ? "Type to filter commands"
205
- : "Press Ctrl+C twice to exit"))));
530
+ }, [buffer, disabled, placeholder]);
531
+ return (React.createElement(Box, { flexDirection: "column", marginX: 1, key: `input-${showFilePicker ? 'picker' : 'normal'}` },
532
+ showHistoryMenu && (React.createElement(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "#A9C13E", padding: 1 },
533
+ React.createElement(Box, { marginBottom: 1 },
534
+ React.createElement(Text, { color: "cyan" }, "Use \u2191\u2193 keys to navigate, press Enter to select:")),
535
+ React.createElement(Box, { flexDirection: "column" },
536
+ (() => {
537
+ const userMessages = getUserMessages();
538
+ const maxHeight = 8;
539
+ const visibleMessages = userMessages.slice(0, maxHeight);
540
+ return visibleMessages.map((message, index) => (React.createElement(Box, { key: message.value },
541
+ React.createElement(Text, { color: index === historySelectedIndex ? 'green' : 'white', bold: true },
542
+ index === historySelectedIndex ? '➣ ' : ' ',
543
+ message.label))));
544
+ })(),
545
+ getUserMessages().length > 8 && (React.createElement(Box, null,
546
+ React.createElement(Text, { color: "gray", dimColor: true },
547
+ "... and ",
548
+ getUserMessages().length - 8,
549
+ " more items")))))),
550
+ !showHistoryMenu && (React.createElement(React.Fragment, null,
551
+ React.createElement(Box, { flexDirection: "row", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, flexGrow: 1 },
552
+ React.createElement(Text, { color: "cyan", bold: true },
553
+ "\u27A3",
554
+ ' '),
555
+ React.createElement(Box, { flexGrow: 1 }, renderContent())),
556
+ React.createElement(CommandPanel, { commands: getFilteredCommands(), selectedIndex: commandSelectedIndex, query: buffer.getFullText().slice(1), visible: showCommands }),
557
+ React.createElement(Box, null,
558
+ React.createElement(FileList, { ref: fileListRef, query: fileQuery, selectedIndex: fileSelectedIndex, visible: showFilePicker, maxItems: 10, rootPath: process.cwd(), onFilteredCountChange: handleFilteredCountChange })),
559
+ yoloMode && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
560
+ React.createElement(Text, { color: "yellow", dimColor: true }, "\u2741 YOLO MODE ACTIVE - All tools will be auto-approved without confirmation"))),
561
+ contextUsage && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
562
+ React.createElement(Text, { color: "gray", dimColor: true }, (() => {
563
+ const percentage = Math.min(100, (contextUsage.inputTokens / contextUsage.maxContextTokens) * 100);
564
+ let color;
565
+ if (percentage < 50)
566
+ color = 'green';
567
+ else if (percentage < 75)
568
+ color = 'yellow';
569
+ else if (percentage < 90)
570
+ color = 'orange';
571
+ else
572
+ color = 'red';
573
+ const formatNumber = (num) => {
574
+ if (num >= 1000)
575
+ return `${(num / 1000).toFixed(1)}k`;
576
+ return num.toString();
577
+ };
578
+ return (React.createElement(React.Fragment, null,
579
+ React.createElement(Text, { color: color },
580
+ percentage.toFixed(1),
581
+ "%"),
582
+ React.createElement(Text, null, " \u00B7 "),
583
+ React.createElement(Text, { color: color }, formatNumber(contextUsage.inputTokens)),
584
+ React.createElement(Text, null, " tokens")));
585
+ })()))),
586
+ React.createElement(Box, { marginTop: 1 },
587
+ React.createElement(Text, { color: "gray", dimColor: true }, showCommands && getFilteredCommands().length > 0
588
+ ? "Type to filter commands"
589
+ : showFilePicker
590
+ ? "Type to filter files • Tab/Enter to select • ESC to cancel"
591
+ : "Press Ctrl+C twice to exit • Alt+V to paste images • Type '@' for files • Type '/' for commands"))))));
206
592
  }
@@ -9,5 +9,5 @@ interface Props {
9
9
  query: string;
10
10
  visible: boolean;
11
11
  }
12
- export default function CommandPanel({ commands, selectedIndex, query, visible }: Props): React.JSX.Element | null;
13
- export {};
12
+ declare const CommandPanel: React.MemoExoticComponent<({ commands, selectedIndex, query, visible }: Props) => React.JSX.Element | null>;
13
+ export default CommandPanel;
@@ -1,6 +1,6 @@
1
- import React from 'react';
1
+ import React, { memo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- export default function CommandPanel({ commands, selectedIndex, query, visible }) {
3
+ const CommandPanel = memo(({ commands, selectedIndex, query, visible }) => {
4
4
  // Don't show panel if not visible or no commands found
5
5
  if (!visible || commands.length === 0) {
6
6
  return null;
@@ -12,11 +12,15 @@ export default function CommandPanel({ commands, selectedIndex, query, visible }
12
12
  React.createElement(Text, { color: "yellow", bold: true },
13
13
  "Available Commands ",
14
14
  query && `(${commands.length} matches)`)),
15
- commands.map((command, index) => (React.createElement(Box, { key: command.name, flexDirection: "row", width: "100%" },
16
- React.createElement(Text, { color: index === selectedIndex ? "green" : "gray" },
15
+ commands.map((command, index) => (React.createElement(Box, { key: command.name, flexDirection: "column", width: "100%" },
16
+ React.createElement(Text, { color: index === selectedIndex ? "green" : "gray", bold: true },
17
17
  index === selectedIndex ? "➣ " : " ",
18
18
  "/",
19
19
  command.name),
20
- React.createElement(Box, { marginLeft: 2 },
21
- React.createElement(Text, { color: index === selectedIndex ? "green" : "gray", dimColor: true }, command.description)))))))));
22
- }
20
+ React.createElement(Box, { marginLeft: 3 },
21
+ React.createElement(Text, { color: index === selectedIndex ? "green" : "gray", dimColor: true },
22
+ "\u2514\u2500 ",
23
+ command.description)))))))));
24
+ });
25
+ CommandPanel.displayName = 'CommandPanel';
26
+ export default CommandPanel;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface Props {
3
+ oldContent?: string;
4
+ newContent: string;
5
+ filename?: string;
6
+ maxLines?: number;
7
+ }
8
+ export default function DiffViewer({ oldContent, newContent, filename, maxLines }: Props): React.JSX.Element;
9
+ export {};