snow-ai 0.2.15 → 0.2.16

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 (65) hide show
  1. package/dist/api/anthropic.d.ts +1 -1
  2. package/dist/api/anthropic.js +52 -76
  3. package/dist/api/chat.d.ts +4 -4
  4. package/dist/api/chat.js +32 -17
  5. package/dist/api/gemini.d.ts +1 -1
  6. package/dist/api/gemini.js +20 -13
  7. package/dist/api/responses.d.ts +5 -5
  8. package/dist/api/responses.js +29 -27
  9. package/dist/app.js +4 -1
  10. package/dist/hooks/useClipboard.d.ts +4 -0
  11. package/dist/hooks/useClipboard.js +120 -0
  12. package/dist/hooks/useCommandHandler.d.ts +26 -0
  13. package/dist/hooks/useCommandHandler.js +158 -0
  14. package/dist/hooks/useCommandPanel.d.ts +16 -0
  15. package/dist/hooks/useCommandPanel.js +53 -0
  16. package/dist/hooks/useConversation.d.ts +9 -1
  17. package/dist/hooks/useConversation.js +152 -58
  18. package/dist/hooks/useFilePicker.d.ts +17 -0
  19. package/dist/hooks/useFilePicker.js +91 -0
  20. package/dist/hooks/useHistoryNavigation.d.ts +21 -0
  21. package/dist/hooks/useHistoryNavigation.js +50 -0
  22. package/dist/hooks/useInputBuffer.d.ts +6 -0
  23. package/dist/hooks/useInputBuffer.js +29 -0
  24. package/dist/hooks/useKeyboardInput.d.ts +51 -0
  25. package/dist/hooks/useKeyboardInput.js +272 -0
  26. package/dist/hooks/useSnapshotState.d.ts +12 -0
  27. package/dist/hooks/useSnapshotState.js +28 -0
  28. package/dist/hooks/useStreamingState.d.ts +24 -0
  29. package/dist/hooks/useStreamingState.js +96 -0
  30. package/dist/hooks/useVSCodeState.d.ts +8 -0
  31. package/dist/hooks/useVSCodeState.js +63 -0
  32. package/dist/mcp/filesystem.d.ts +24 -5
  33. package/dist/mcp/filesystem.js +52 -17
  34. package/dist/mcp/todo.js +4 -8
  35. package/dist/ui/components/ChatInput.js +68 -557
  36. package/dist/ui/components/DiffViewer.js +57 -30
  37. package/dist/ui/components/FileList.js +70 -26
  38. package/dist/ui/components/MessageList.d.ts +6 -0
  39. package/dist/ui/components/MessageList.js +47 -15
  40. package/dist/ui/components/ShimmerText.d.ts +9 -0
  41. package/dist/ui/components/ShimmerText.js +30 -0
  42. package/dist/ui/components/TodoTree.d.ts +1 -1
  43. package/dist/ui/components/TodoTree.js +0 -4
  44. package/dist/ui/components/ToolConfirmation.js +14 -6
  45. package/dist/ui/pages/ChatScreen.js +159 -359
  46. package/dist/ui/pages/CustomHeadersScreen.d.ts +6 -0
  47. package/dist/ui/pages/CustomHeadersScreen.js +104 -0
  48. package/dist/ui/pages/WelcomeScreen.js +5 -0
  49. package/dist/utils/apiConfig.d.ts +10 -0
  50. package/dist/utils/apiConfig.js +51 -0
  51. package/dist/utils/incrementalSnapshot.d.ts +8 -0
  52. package/dist/utils/incrementalSnapshot.js +63 -0
  53. package/dist/utils/mcpToolsManager.js +6 -1
  54. package/dist/utils/retryUtils.d.ts +22 -0
  55. package/dist/utils/retryUtils.js +180 -0
  56. package/dist/utils/sessionConverter.js +80 -17
  57. package/dist/utils/sessionManager.js +35 -4
  58. package/dist/utils/textUtils.d.ts +4 -0
  59. package/dist/utils/textUtils.js +19 -0
  60. package/dist/utils/todoPreprocessor.d.ts +1 -1
  61. package/dist/utils/todoPreprocessor.js +0 -1
  62. package/dist/utils/vscodeConnection.d.ts +8 -0
  63. package/dist/utils/vscodeConnection.js +44 -0
  64. package/package.json +1 -1
  65. package/readme.md +3 -1
@@ -10,11 +10,15 @@ import { getOpenAiConfig } from '../utils/apiConfig.js';
10
10
  import { sessionManager } from '../utils/sessionManager.js';
11
11
  import { formatTodoContext } from '../utils/todoPreprocessor.js';
12
12
  import { formatToolCallMessage } from '../utils/messageFormatter.js';
13
+ import { vscodeConnection } from '../utils/vscodeConnection.js';
14
+ import { filesystemService } from '../mcp/filesystem.js';
13
15
  /**
14
16
  * Handle conversation with streaming and tool calls
15
17
  */
16
18
  export async function handleConversationWithTools(options) {
17
- const { userContent, imageContents, controller, messages, saveMessage, setMessages, setStreamTokenCount, setCurrentTodos, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, yoloMode, setContextUsage } = options;
19
+ const { userContent, imageContents, controller,
20
+ // messages, // No longer used - we load from session instead to get complete history with tool calls
21
+ saveMessage, setMessages, setStreamTokenCount, setCurrentTodos, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, yoloMode, setContextUsage, setIsReasoning, setRetryStatus } = options;
18
22
  // Step 1: Ensure session exists and get existing TODOs
19
23
  let currentSession = sessionManager.getCurrentSession();
20
24
  if (!currentSession) {
@@ -41,12 +45,13 @@ export async function handleConversationWithTools(options) {
41
45
  content: todoContext
42
46
  });
43
47
  }
44
- // Add history messages
45
- conversationMessages.push(...messages.filter(msg => msg.role !== 'command').map(msg => ({
46
- role: msg.role,
47
- content: msg.content,
48
- images: msg.images
49
- })));
48
+ // Add history messages from session (includes tool_calls and tool results)
49
+ // Load from session to get complete conversation history with tool interactions
50
+ const session = sessionManager.getCurrentSession();
51
+ if (session && session.messages.length > 0) {
52
+ // Use session messages directly (they are already in API format)
53
+ conversationMessages.push(...session.messages);
54
+ }
50
55
  // Add current user message
51
56
  conversationMessages.push({
52
57
  role: 'user',
@@ -87,10 +92,22 @@ export async function handleConversationWithTools(options) {
87
92
  // Stream AI response - choose API based on config
88
93
  let toolCallAccumulator = ''; // Accumulate tool call deltas for token counting
89
94
  let reasoningAccumulator = ''; // Accumulate reasoning summary deltas for token counting (Responses API only)
95
+ let chunkCount = 0; // Track number of chunks received (to delay clearing retry status)
90
96
  // Get or create session for cache key
91
97
  const currentSession = sessionManager.getCurrentSession();
92
98
  // Use session ID as cache key to ensure same session requests share cache
93
99
  const cacheKey = currentSession?.id;
100
+ // 重试回调函数
101
+ const onRetry = (error, attempt, nextDelay) => {
102
+ if (setRetryStatus) {
103
+ setRetryStatus({
104
+ isRetrying: true,
105
+ attempt,
106
+ nextDelay,
107
+ errorMessage: error.message
108
+ });
109
+ }
110
+ };
94
111
  const streamGenerator = config.requestMethod === 'anthropic'
95
112
  ? createStreamingAnthropicCompletion({
96
113
  model,
@@ -99,14 +116,14 @@ export async function handleConversationWithTools(options) {
99
116
  max_tokens: config.maxTokens || 4096,
100
117
  tools: mcpTools.length > 0 ? mcpTools : undefined,
101
118
  sessionId: currentSession?.id
102
- }, controller.signal)
119
+ }, controller.signal, onRetry)
103
120
  : config.requestMethod === 'gemini'
104
121
  ? createStreamingGeminiCompletion({
105
122
  model,
106
123
  messages: conversationMessages,
107
124
  temperature: 0,
108
125
  tools: mcpTools.length > 0 ? mcpTools : undefined
109
- }, controller.signal)
126
+ }, controller.signal, onRetry)
110
127
  : config.requestMethod === 'responses'
111
128
  ? createStreamingResponse({
112
129
  model,
@@ -114,18 +131,32 @@ export async function handleConversationWithTools(options) {
114
131
  temperature: 0,
115
132
  tools: mcpTools.length > 0 ? mcpTools : undefined,
116
133
  prompt_cache_key: cacheKey // Use session ID as cache key
117
- }, controller.signal)
134
+ }, controller.signal, onRetry)
118
135
  : createStreamingChatCompletion({
119
136
  model,
120
137
  messages: conversationMessages,
121
138
  temperature: 0,
122
139
  tools: mcpTools.length > 0 ? mcpTools : undefined
123
- }, controller.signal);
140
+ }, controller.signal, onRetry);
124
141
  for await (const chunk of streamGenerator) {
125
142
  if (controller.signal.aborted)
126
143
  break;
127
- if (chunk.type === 'content' && chunk.content) {
144
+ // Clear retry status after a delay when first chunk arrives
145
+ // This gives users time to see the retry message (500ms delay)
146
+ chunkCount++;
147
+ if (setRetryStatus && chunkCount === 1) {
148
+ setTimeout(() => {
149
+ setRetryStatus(null);
150
+ }, 500);
151
+ }
152
+ if (chunk.type === 'reasoning_started') {
153
+ // Reasoning started (Responses API only) - set reasoning state
154
+ setIsReasoning?.(true);
155
+ }
156
+ else if (chunk.type === 'content' && chunk.content) {
128
157
  // Accumulate content and update token count
158
+ // When content starts, reasoning is done
159
+ setIsReasoning?.(false);
129
160
  streamedContent += chunk.content;
130
161
  try {
131
162
  const tokens = encoder.encode(streamedContent + toolCallAccumulator + reasoningAccumulator);
@@ -236,66 +267,117 @@ export async function handleConversationWithTools(options) {
236
267
  }
237
268
  else if (toolsNeedingConfirmation.length > 0) {
238
269
  const firstTool = toolsNeedingConfirmation[0]; // Safe: length > 0 guarantees this exists
239
- // Pass all tools for proper display in confirmation UI
240
- const allTools = toolsNeedingConfirmation.length > 1
241
- ? toolsNeedingConfirmation
242
- : undefined;
243
- // Use first tool for confirmation UI, but apply result to all
244
- const confirmation = await requestToolConfirmation(firstTool, undefined, allTools);
245
- if (confirmation === 'reject') {
246
- // Remove pending tool messages
247
- setMessages(prev => prev.filter(msg => !msg.toolPending));
248
- // User rejected - end conversation
249
- setMessages(prev => [...prev, {
250
- role: 'assistant',
251
- content: 'Tool call rejected, session ended',
252
- streaming: false
253
- }]);
254
- // End streaming immediately
255
- if (options.setIsStreaming) {
256
- options.setIsStreaming(false);
270
+ // Check if we should use DIFF+APPLY for filesystem tools
271
+ let usedDiffApply = false;
272
+ if (vscodeConnection.isConnected()) {
273
+ // Try to use DIFF+APPLY for filesystem_create or filesystem_edit
274
+ if (firstTool.function.name === 'filesystem-create' || firstTool.function.name === 'filesystem-edit') {
275
+ try {
276
+ const args = JSON.parse(firstTool.function.arguments);
277
+ if (firstTool.function.name === 'filesystem-create') {
278
+ // For create, show diff with empty old content
279
+ const confirmation = await vscodeConnection.requestDiffApply(args.filePath, '', // Empty old content for new file
280
+ args.content);
281
+ if (confirmation === 'reject') {
282
+ // Remove pending tool messages
283
+ setMessages(prev => prev.filter(msg => !msg.toolPending));
284
+ setMessages(prev => [...prev, {
285
+ role: 'assistant',
286
+ content: 'Tool call rejected, session ended',
287
+ streaming: false
288
+ }]);
289
+ if (options.setIsStreaming) {
290
+ options.setIsStreaming(false);
291
+ }
292
+ encoder.free();
293
+ return;
294
+ }
295
+ if (confirmation === 'approve_always') {
296
+ addMultipleToAlwaysApproved([firstTool.function.name]);
297
+ sessionApprovedTools.add(firstTool.function.name);
298
+ }
299
+ approvedTools.push(...toolsNeedingConfirmation);
300
+ usedDiffApply = true;
301
+ }
302
+ else if (firstTool.function.name === 'filesystem-edit') {
303
+ // For edit, read the file first to get old content
304
+ const fileContent = await filesystemService.getFileContent(args.filePath, args.startLine, args.endLine);
305
+ const confirmation = await vscodeConnection.requestDiffApply(args.filePath, fileContent.content, args.newContent);
306
+ if (confirmation === 'reject') {
307
+ // Remove pending tool messages
308
+ setMessages(prev => prev.filter(msg => !msg.toolPending));
309
+ setMessages(prev => [...prev, {
310
+ role: 'assistant',
311
+ content: 'Tool call rejected, session ended',
312
+ streaming: false
313
+ }]);
314
+ if (options.setIsStreaming) {
315
+ options.setIsStreaming(false);
316
+ }
317
+ encoder.free();
318
+ return;
319
+ }
320
+ if (confirmation === 'approve_always') {
321
+ addMultipleToAlwaysApproved([firstTool.function.name]);
322
+ sessionApprovedTools.add(firstTool.function.name);
323
+ }
324
+ approvedTools.push(...toolsNeedingConfirmation);
325
+ usedDiffApply = true;
326
+ }
327
+ }
328
+ catch (error) {
329
+ // If DIFF+APPLY fails, fall back to regular confirmation
330
+ console.error('Failed to use DIFF+APPLY:', error);
331
+ }
257
332
  }
258
- encoder.free();
259
- return; // Exit the conversation loop
260
333
  }
261
- // If approved_always, add ALL these tools to both global and session-approved sets
262
- if (confirmation === 'approve_always') {
263
- const toolNamesToAdd = toolsNeedingConfirmation.map(t => t.function.name);
264
- // Add to global state (async, for future sessions)
265
- addMultipleToAlwaysApproved(toolNamesToAdd);
266
- // Add to local session set (sync, for this conversation)
267
- toolNamesToAdd.forEach(name => sessionApprovedTools.add(name));
334
+ // If we didn't use DIFF+APPLY, fall back to regular CLI confirmation
335
+ if (!usedDiffApply) {
336
+ // Pass all tools for proper display in confirmation UI
337
+ const allTools = toolsNeedingConfirmation.length > 1
338
+ ? toolsNeedingConfirmation
339
+ : undefined;
340
+ // Use first tool for confirmation UI, but apply result to all
341
+ const confirmation = await requestToolConfirmation(firstTool, undefined, allTools);
342
+ if (confirmation === 'reject') {
343
+ // Remove pending tool messages
344
+ setMessages(prev => prev.filter(msg => !msg.toolPending));
345
+ // User rejected - end conversation
346
+ setMessages(prev => [...prev, {
347
+ role: 'assistant',
348
+ content: 'Tool call rejected, session ended',
349
+ streaming: false
350
+ }]);
351
+ // End streaming immediately
352
+ if (options.setIsStreaming) {
353
+ options.setIsStreaming(false);
354
+ }
355
+ encoder.free();
356
+ return; // Exit the conversation loop
357
+ }
358
+ // If approved_always, add ALL these tools to both global and session-approved sets
359
+ if (confirmation === 'approve_always') {
360
+ const toolNamesToAdd = toolsNeedingConfirmation.map(t => t.function.name);
361
+ // Add to global state (async, for future sessions)
362
+ addMultipleToAlwaysApproved(toolNamesToAdd);
363
+ // Add to local session set (sync, for this conversation)
364
+ toolNamesToAdd.forEach(name => sessionApprovedTools.add(name));
365
+ }
366
+ // Add all tools to approved list
367
+ approvedTools.push(...toolsNeedingConfirmation);
268
368
  }
269
- // Add all tools to approved list
270
- approvedTools.push(...toolsNeedingConfirmation);
271
369
  }
272
370
  // Execute approved tools
273
371
  const toolResults = await executeToolCalls(approvedTools);
274
372
  // Check if there are TODO related tool calls, if yes refresh TODO list
275
- // Only show TODO panel for todo-update, not for other todo operations
276
- const shouldShowTodoPanel = approvedTools.some(t => t.function.name === 'todo-update');
277
373
  const hasTodoTools = approvedTools.some(t => t.function.name.startsWith('todo-'));
374
+ const hasTodoUpdateTools = approvedTools.some(t => t.function.name === 'todo-update');
278
375
  if (hasTodoTools) {
279
376
  const session = sessionManager.getCurrentSession();
280
377
  if (session) {
281
378
  const updatedTodoList = await todoService.getTodoList(session.id);
282
379
  if (updatedTodoList) {
283
380
  setCurrentTodos(updatedTodoList.todos);
284
- // Only show TODO panel for update operations
285
- if (shouldShowTodoPanel) {
286
- // Remove any existing TODO tree messages and add a new one
287
- setMessages(prev => {
288
- // Filter out previous TODO tree messages
289
- const withoutTodoTree = prev.filter(m => !m.showTodoTree);
290
- // Add new TODO tree message
291
- return [...withoutTodoTree, {
292
- role: 'assistant',
293
- content: '[TODO List Updated]',
294
- streaming: false,
295
- showTodoTree: true
296
- }];
297
- });
298
- }
299
381
  }
300
382
  }
301
383
  }
@@ -368,6 +450,18 @@ export async function handleConversationWithTools(options) {
368
450
  console.error('Failed to save tool result:', error);
369
451
  });
370
452
  }
453
+ // After all tool results are processed, show TODO panel if there were todo-update calls
454
+ if (hasTodoUpdateTools) {
455
+ setMessages(prev => [
456
+ ...prev,
457
+ {
458
+ role: 'assistant',
459
+ content: '',
460
+ streaming: false,
461
+ showTodoTree: true
462
+ }
463
+ ]);
464
+ }
371
465
  // Check if there are pending user messages to insert
372
466
  if (options.getPendingMessages && options.clearPendingMessages) {
373
467
  const pendingMessages = options.getPendingMessages();
@@ -0,0 +1,17 @@
1
+ import { TextBuffer } from '../utils/textBuffer.js';
2
+ import { FileListRef } from '../ui/components/FileList.js';
3
+ export declare function useFilePicker(buffer: TextBuffer, triggerUpdate: () => void): {
4
+ showFilePicker: boolean;
5
+ setShowFilePicker: import("react").Dispatch<import("react").SetStateAction<boolean>>;
6
+ fileSelectedIndex: number;
7
+ setFileSelectedIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
8
+ fileQuery: string;
9
+ setFileQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
10
+ atSymbolPosition: number;
11
+ setAtSymbolPosition: import("react").Dispatch<import("react").SetStateAction<number>>;
12
+ filteredFileCount: number;
13
+ updateFilePickerState: (text: string, cursorPos: number) => void;
14
+ handleFileSelect: (filePath: string) => Promise<void>;
15
+ handleFilteredCountChange: (count: number) => void;
16
+ fileListRef: import("react").RefObject<FileListRef>;
17
+ };
@@ -0,0 +1,91 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+ export function useFilePicker(buffer, triggerUpdate) {
3
+ const [showFilePicker, setShowFilePicker] = useState(false);
4
+ const [fileSelectedIndex, setFileSelectedIndex] = useState(0);
5
+ const [fileQuery, setFileQuery] = useState('');
6
+ const [atSymbolPosition, setAtSymbolPosition] = useState(-1);
7
+ const [filteredFileCount, setFilteredFileCount] = useState(0);
8
+ const fileListRef = useRef(null);
9
+ // Update file picker state
10
+ const updateFilePickerState = useCallback((text, cursorPos) => {
11
+ if (!text.includes('@')) {
12
+ if (showFilePicker) {
13
+ setShowFilePicker(false);
14
+ setFileSelectedIndex(0);
15
+ setFileQuery('');
16
+ setAtSymbolPosition(-1);
17
+ }
18
+ return;
19
+ }
20
+ // Find the last '@' symbol before the cursor
21
+ const beforeCursor = text.slice(0, cursorPos);
22
+ const lastAtIndex = beforeCursor.lastIndexOf('@');
23
+ if (lastAtIndex !== -1) {
24
+ // Check if there's no space between '@' and cursor
25
+ const afterAt = beforeCursor.slice(lastAtIndex + 1);
26
+ if (!afterAt.includes(' ') && !afterAt.includes('\n')) {
27
+ if (!showFilePicker ||
28
+ fileQuery !== afterAt ||
29
+ atSymbolPosition !== lastAtIndex) {
30
+ setShowFilePicker(true);
31
+ setFileSelectedIndex(0);
32
+ setFileQuery(afterAt);
33
+ setAtSymbolPosition(lastAtIndex);
34
+ }
35
+ return;
36
+ }
37
+ }
38
+ // Hide file picker if no valid @ context found
39
+ if (showFilePicker) {
40
+ setShowFilePicker(false);
41
+ setFileSelectedIndex(0);
42
+ setFileQuery('');
43
+ setAtSymbolPosition(-1);
44
+ }
45
+ }, [showFilePicker, fileQuery, atSymbolPosition]);
46
+ // Handle file selection
47
+ const handleFileSelect = useCallback(async (filePath) => {
48
+ if (atSymbolPosition !== -1) {
49
+ const text = buffer.getFullText();
50
+ const cursorPos = buffer.getCursorPosition();
51
+ // Replace @query with @filePath + space
52
+ const beforeAt = text.slice(0, atSymbolPosition);
53
+ const afterCursor = text.slice(cursorPos);
54
+ const newText = beforeAt + '@' + filePath + ' ' + afterCursor;
55
+ // Set the new text and position cursor after the inserted file path + space
56
+ buffer.setText(newText);
57
+ // Calculate cursor position after the inserted file path + space
58
+ // Reset cursor to beginning, then move to correct position
59
+ for (let i = 0; i < atSymbolPosition + filePath.length + 2; i++) {
60
+ // +2 for @ and space
61
+ if (i < buffer.getFullText().length) {
62
+ buffer.moveRight();
63
+ }
64
+ }
65
+ setShowFilePicker(false);
66
+ setFileSelectedIndex(0);
67
+ setFileQuery('');
68
+ setAtSymbolPosition(-1);
69
+ triggerUpdate();
70
+ }
71
+ }, [atSymbolPosition, buffer, triggerUpdate]);
72
+ // Handle filtered file count change
73
+ const handleFilteredCountChange = useCallback((count) => {
74
+ setFilteredFileCount(count);
75
+ }, []);
76
+ return {
77
+ showFilePicker,
78
+ setShowFilePicker,
79
+ fileSelectedIndex,
80
+ setFileSelectedIndex,
81
+ fileQuery,
82
+ setFileQuery,
83
+ atSymbolPosition,
84
+ setAtSymbolPosition,
85
+ filteredFileCount,
86
+ updateFilePickerState,
87
+ handleFileSelect,
88
+ handleFilteredCountChange,
89
+ fileListRef,
90
+ };
91
+ }
@@ -0,0 +1,21 @@
1
+ import { TextBuffer } from '../utils/textBuffer.js';
2
+ type ChatMessage = {
3
+ role: string;
4
+ content: string;
5
+ };
6
+ export declare function useHistoryNavigation(buffer: TextBuffer, triggerUpdate: () => void, chatHistory: ChatMessage[], onHistorySelect?: (selectedIndex: number, message: string) => void): {
7
+ showHistoryMenu: boolean;
8
+ setShowHistoryMenu: import("react").Dispatch<import("react").SetStateAction<boolean>>;
9
+ historySelectedIndex: number;
10
+ setHistorySelectedIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
11
+ escapeKeyCount: number;
12
+ setEscapeKeyCount: import("react").Dispatch<import("react").SetStateAction<number>>;
13
+ escapeKeyTimer: import("react").MutableRefObject<NodeJS.Timeout | null>;
14
+ getUserMessages: () => {
15
+ label: string;
16
+ value: string;
17
+ infoText: string;
18
+ }[];
19
+ handleHistorySelect: (value: string) => void;
20
+ };
21
+ export {};
@@ -0,0 +1,50 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+ export function useHistoryNavigation(buffer, triggerUpdate, chatHistory, onHistorySelect) {
3
+ const [showHistoryMenu, setShowHistoryMenu] = useState(false);
4
+ const [historySelectedIndex, setHistorySelectedIndex] = useState(0);
5
+ const [escapeKeyCount, setEscapeKeyCount] = useState(0);
6
+ const escapeKeyTimer = useRef(null);
7
+ // Cleanup timer on unmount
8
+ useEffect(() => {
9
+ return () => {
10
+ if (escapeKeyTimer.current) {
11
+ clearTimeout(escapeKeyTimer.current);
12
+ }
13
+ };
14
+ }, []);
15
+ // Get user messages from chat history for navigation
16
+ const getUserMessages = useCallback(() => {
17
+ const userMessages = chatHistory
18
+ .map((msg, index) => ({ ...msg, originalIndex: index }))
19
+ .filter(msg => msg.role === 'user' && msg.content.trim());
20
+ // Keep original order (oldest first, newest last) and map with display numbers
21
+ return userMessages.map((msg, index) => ({
22
+ label: `${index + 1}. ${msg.content.slice(0, 50)}${msg.content.length > 50 ? '...' : ''}`,
23
+ value: msg.originalIndex.toString(),
24
+ infoText: msg.content,
25
+ }));
26
+ }, [chatHistory]);
27
+ // Handle history selection
28
+ const handleHistorySelect = useCallback((value) => {
29
+ const selectedIndex = parseInt(value, 10);
30
+ const selectedMessage = chatHistory[selectedIndex];
31
+ if (selectedMessage && onHistorySelect) {
32
+ // Put the message content in the input buffer
33
+ buffer.setText(selectedMessage.content);
34
+ setShowHistoryMenu(false);
35
+ triggerUpdate();
36
+ onHistorySelect(selectedIndex, selectedMessage.content);
37
+ }
38
+ }, [chatHistory, onHistorySelect, buffer, triggerUpdate]);
39
+ return {
40
+ showHistoryMenu,
41
+ setShowHistoryMenu,
42
+ historySelectedIndex,
43
+ setHistorySelectedIndex,
44
+ escapeKeyCount,
45
+ setEscapeKeyCount,
46
+ escapeKeyTimer,
47
+ getUserMessages,
48
+ handleHistorySelect,
49
+ };
50
+ }
@@ -0,0 +1,6 @@
1
+ import { TextBuffer, Viewport } from '../utils/textBuffer.js';
2
+ export declare function useInputBuffer(viewport: Viewport): {
3
+ buffer: TextBuffer;
4
+ triggerUpdate: () => void;
5
+ forceUpdate: import("react").Dispatch<import("react").SetStateAction<{}>>;
6
+ };
@@ -0,0 +1,29 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react';
2
+ import { TextBuffer } from '../utils/textBuffer.js';
3
+ export function useInputBuffer(viewport) {
4
+ const [, forceUpdate] = useState({});
5
+ const lastUpdateTime = useRef(0);
6
+ // Force re-render when buffer changes
7
+ const triggerUpdate = useCallback(() => {
8
+ const now = Date.now();
9
+ lastUpdateTime.current = now;
10
+ forceUpdate({});
11
+ }, []);
12
+ const [buffer] = useState(() => new TextBuffer(viewport, triggerUpdate));
13
+ // Update buffer viewport when viewport changes
14
+ useEffect(() => {
15
+ buffer.updateViewport(viewport);
16
+ triggerUpdate();
17
+ }, [viewport.width, viewport.height, buffer, triggerUpdate]);
18
+ // Cleanup buffer on unmount
19
+ useEffect(() => {
20
+ return () => {
21
+ buffer.destroy();
22
+ };
23
+ }, [buffer]);
24
+ return {
25
+ buffer,
26
+ triggerUpdate,
27
+ forceUpdate,
28
+ };
29
+ }
@@ -0,0 +1,51 @@
1
+ import { TextBuffer } from '../utils/textBuffer.js';
2
+ type KeyboardInputOptions = {
3
+ buffer: TextBuffer;
4
+ disabled: boolean;
5
+ triggerUpdate: () => void;
6
+ forceUpdate: React.Dispatch<React.SetStateAction<{}>>;
7
+ showCommands: boolean;
8
+ setShowCommands: (show: boolean) => void;
9
+ commandSelectedIndex: number;
10
+ setCommandSelectedIndex: (index: number | ((prev: number) => number)) => void;
11
+ getFilteredCommands: () => Array<{
12
+ name: string;
13
+ description: string;
14
+ }>;
15
+ updateCommandPanelState: (text: string) => void;
16
+ onCommand?: (commandName: string, result: any) => void;
17
+ showFilePicker: boolean;
18
+ setShowFilePicker: (show: boolean) => void;
19
+ fileSelectedIndex: number;
20
+ setFileSelectedIndex: (index: number | ((prev: number) => number)) => void;
21
+ fileQuery: string;
22
+ setFileQuery: (query: string) => void;
23
+ atSymbolPosition: number;
24
+ setAtSymbolPosition: (pos: number) => void;
25
+ filteredFileCount: number;
26
+ updateFilePickerState: (text: string, cursorPos: number) => void;
27
+ handleFileSelect: (filePath: string) => Promise<void>;
28
+ fileListRef: React.RefObject<{
29
+ getSelectedFile: () => string | null;
30
+ }>;
31
+ showHistoryMenu: boolean;
32
+ setShowHistoryMenu: (show: boolean) => void;
33
+ historySelectedIndex: number;
34
+ setHistorySelectedIndex: (index: number | ((prev: number) => number)) => void;
35
+ escapeKeyCount: number;
36
+ setEscapeKeyCount: (count: number | ((prev: number) => number)) => void;
37
+ escapeKeyTimer: React.MutableRefObject<NodeJS.Timeout | null>;
38
+ getUserMessages: () => Array<{
39
+ label: string;
40
+ value: string;
41
+ infoText: string;
42
+ }>;
43
+ handleHistorySelect: (value: string) => void;
44
+ pasteFromClipboard: () => Promise<void>;
45
+ onSubmit: (message: string, images?: Array<{
46
+ data: string;
47
+ mimeType: string;
48
+ }>) => void;
49
+ };
50
+ export declare function useKeyboardInput(options: KeyboardInputOptions): void;
51
+ export {};