snow-ai 0.3.23 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -0
- package/dist/hooks/useConversation.d.ts +0 -5
- package/dist/hooks/useConversation.js +91 -44
- package/dist/hooks/useFilePicker.d.ts +1 -1
- package/dist/hooks/useFilePicker.js +13 -7
- package/dist/hooks/useInputBuffer.d.ts +1 -1
- package/dist/hooks/useInputBuffer.js +24 -9
- package/dist/hooks/useStreamingState.js +2 -2
- package/dist/hooks/useVSCodeState.js +23 -6
- package/dist/mcp/filesystem.js +1 -1
- package/dist/ui/components/ChatInput.js +14 -9
- package/dist/ui/components/MarkdownRenderer.js +2 -14
- package/dist/ui/components/MessageList.d.ts +0 -1
- package/dist/ui/components/MessageList.js +1 -2
- package/dist/ui/components/ToolConfirmation.d.ts +1 -1
- package/dist/ui/components/ToolConfirmation.js +63 -22
- package/dist/ui/pages/ChatScreen.js +1 -5
- package/dist/ui/pages/ConfigScreen.js +8 -6
- package/dist/ui/pages/HeadlessModeScreen.js +0 -1
- package/dist/ui/pages/ProxyConfigScreen.d.ts +1 -1
- package/dist/ui/pages/ProxyConfigScreen.js +6 -6
- package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +7 -0
- package/dist/ui/pages/SensitiveCommandConfigScreen.js +262 -0
- package/dist/ui/pages/SubAgentConfigScreen.js +1 -1
- package/dist/ui/pages/WelcomeScreen.js +14 -3
- package/dist/utils/patch-highlight.d.ts +5 -0
- package/dist/utils/patch-highlight.js +23 -0
- package/dist/utils/sensitiveCommandManager.d.ts +53 -0
- package/dist/utils/sensitiveCommandManager.js +308 -0
- package/package.json +4 -2
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import './utils/patch-highlight.js';
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// CRITICAL: Patch cli-highlight BEFORE any other imports
|
|
3
|
+
// This must be the first import to ensure the patch is applied before cli-markdown loads
|
|
4
|
+
import './utils/patch-highlight.js';
|
|
2
5
|
import React from 'react';
|
|
3
6
|
import { render, Text, Box } from 'ink';
|
|
4
7
|
import Spinner from 'ink-spinner';
|
|
@@ -12,11 +12,6 @@ export type ConversationHandlerOptions = {
|
|
|
12
12
|
saveMessage: (message: any) => Promise<void>;
|
|
13
13
|
setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
|
|
14
14
|
setStreamTokenCount: React.Dispatch<React.SetStateAction<number>>;
|
|
15
|
-
setCurrentTodos: React.Dispatch<React.SetStateAction<Array<{
|
|
16
|
-
id: string;
|
|
17
|
-
content: string;
|
|
18
|
-
status: 'pending' | 'completed';
|
|
19
|
-
}>>>;
|
|
20
15
|
requestToolConfirmation: (toolCall: ToolCall, batchToolNames?: string, allTools?: ToolCall[]) => Promise<string>;
|
|
21
16
|
isToolAutoApproved: (toolName: string) => boolean;
|
|
22
17
|
addMultipleToAlwaysApproved: (toolNames: string[]) => void;
|
|
@@ -20,7 +20,7 @@ import { shouldAutoCompress, performAutoCompression, } from '../utils/autoCompre
|
|
|
20
20
|
export async function handleConversationWithTools(options) {
|
|
21
21
|
const { userContent, imageContents, controller,
|
|
22
22
|
// messages, // No longer used - we load from session instead to get complete history with tool calls
|
|
23
|
-
saveMessage, setMessages, setStreamTokenCount,
|
|
23
|
+
saveMessage, setMessages, setStreamTokenCount, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, yoloMode, setContextUsage, setIsReasoning, setRetryStatus, } = options;
|
|
24
24
|
// Create a wrapper function for adding single tool to always-approved list
|
|
25
25
|
const addToAlwaysApproved = (toolName) => {
|
|
26
26
|
addMultipleToAlwaysApproved([toolName]);
|
|
@@ -33,10 +33,6 @@ export async function handleConversationWithTools(options) {
|
|
|
33
33
|
const todoService = getTodoService();
|
|
34
34
|
// Get existing TODO list
|
|
35
35
|
const existingTodoList = await todoService.getTodoList(currentSession.id);
|
|
36
|
-
// Update UI state
|
|
37
|
-
if (existingTodoList) {
|
|
38
|
-
setCurrentTodos(existingTodoList.todos);
|
|
39
|
-
}
|
|
40
36
|
// Collect all MCP tools
|
|
41
37
|
const mcpTools = await collectAllMCPTools();
|
|
42
38
|
// Build conversation history with TODO context as pinned user message
|
|
@@ -364,8 +360,28 @@ export async function handleConversationWithTools(options) {
|
|
|
364
360
|
const autoApprovedTools = [];
|
|
365
361
|
for (const toolCall of receivedToolCalls) {
|
|
366
362
|
// Check both global approved list and session-approved list
|
|
367
|
-
|
|
368
|
-
sessionApprovedTools.has(toolCall.function.name)
|
|
363
|
+
const isApproved = isToolAutoApproved(toolCall.function.name) ||
|
|
364
|
+
sessionApprovedTools.has(toolCall.function.name);
|
|
365
|
+
// Check if this is a sensitive command (terminal-execute with sensitive pattern)
|
|
366
|
+
let isSensitiveCommand = false;
|
|
367
|
+
if (toolCall.function.name === 'terminal-execute') {
|
|
368
|
+
try {
|
|
369
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
370
|
+
const { isSensitiveCommand: checkSensitiveCommand } = await import('../utils/sensitiveCommandManager.js').then(m => ({
|
|
371
|
+
isSensitiveCommand: m.isSensitiveCommand,
|
|
372
|
+
}));
|
|
373
|
+
const sensitiveCheck = checkSensitiveCommand(args.command);
|
|
374
|
+
isSensitiveCommand = sensitiveCheck.isSensitive;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// If parsing fails, treat as normal command
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// If sensitive command, always require confirmation regardless of approval status
|
|
381
|
+
if (isSensitiveCommand) {
|
|
382
|
+
toolsNeedingConfirmation.push(toolCall);
|
|
383
|
+
}
|
|
384
|
+
else if (isApproved) {
|
|
369
385
|
autoApprovedTools.push(toolCall);
|
|
370
386
|
}
|
|
371
387
|
else {
|
|
@@ -374,24 +390,79 @@ export async function handleConversationWithTools(options) {
|
|
|
374
390
|
}
|
|
375
391
|
// Request confirmation only once for all tools needing confirmation
|
|
376
392
|
let approvedTools = [...autoApprovedTools];
|
|
377
|
-
// In YOLO mode, auto-approve all tools
|
|
393
|
+
// In YOLO mode, auto-approve all tools EXCEPT sensitive commands
|
|
378
394
|
if (yoloMode) {
|
|
379
|
-
|
|
395
|
+
// Filter out sensitive commands from auto-approval
|
|
396
|
+
const nonSensitiveTools = [];
|
|
397
|
+
const sensitiveTools = [];
|
|
398
|
+
for (const toolCall of toolsNeedingConfirmation) {
|
|
399
|
+
if (toolCall.function.name === 'terminal-execute') {
|
|
400
|
+
try {
|
|
401
|
+
const args = JSON.parse(toolCall.function.arguments);
|
|
402
|
+
const { isSensitiveCommand: checkSensitiveCommand } = await import('../utils/sensitiveCommandManager.js').then(m => ({
|
|
403
|
+
isSensitiveCommand: m.isSensitiveCommand,
|
|
404
|
+
}));
|
|
405
|
+
const sensitiveCheck = checkSensitiveCommand(args.command);
|
|
406
|
+
if (sensitiveCheck.isSensitive) {
|
|
407
|
+
sensitiveTools.push(toolCall);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
nonSensitiveTools.push(toolCall);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
nonSensitiveTools.push(toolCall);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
nonSensitiveTools.push(toolCall);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
approvedTools.push(...nonSensitiveTools);
|
|
422
|
+
// If there are sensitive tools, still need confirmation even in YOLO mode
|
|
423
|
+
if (sensitiveTools.length > 0) {
|
|
424
|
+
const firstTool = sensitiveTools[0];
|
|
425
|
+
const allTools = sensitiveTools.length > 1 ? sensitiveTools : undefined;
|
|
426
|
+
const confirmation = await requestToolConfirmation(firstTool, undefined, allTools);
|
|
427
|
+
if (confirmation === 'reject') {
|
|
428
|
+
setMessages(prev => prev.filter(msg => !msg.toolPending));
|
|
429
|
+
for (const toolCall of sensitiveTools) {
|
|
430
|
+
const rejectionMessage = {
|
|
431
|
+
role: 'tool',
|
|
432
|
+
tool_call_id: toolCall.id,
|
|
433
|
+
content: 'Error: Tool execution rejected by user',
|
|
434
|
+
};
|
|
435
|
+
conversationMessages.push(rejectionMessage);
|
|
436
|
+
saveMessage(rejectionMessage).catch(error => {
|
|
437
|
+
console.error('Failed to save tool rejection message:', error);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
setMessages(prev => [
|
|
441
|
+
...prev,
|
|
442
|
+
{
|
|
443
|
+
role: 'assistant',
|
|
444
|
+
content: 'Tool call rejected, session ended',
|
|
445
|
+
streaming: false,
|
|
446
|
+
},
|
|
447
|
+
]);
|
|
448
|
+
if (options.setIsStreaming) {
|
|
449
|
+
options.setIsStreaming(false);
|
|
450
|
+
}
|
|
451
|
+
freeEncoder();
|
|
452
|
+
return { usage: accumulatedUsage };
|
|
453
|
+
}
|
|
454
|
+
// Approved, add sensitive tools to approved list
|
|
455
|
+
approvedTools.push(...sensitiveTools);
|
|
456
|
+
}
|
|
380
457
|
}
|
|
381
458
|
else if (toolsNeedingConfirmation.length > 0) {
|
|
382
|
-
const firstTool = toolsNeedingConfirmation[0];
|
|
383
|
-
// Use regular CLI confirmation
|
|
384
|
-
// Pass all tools for proper display in confirmation UI
|
|
459
|
+
const firstTool = toolsNeedingConfirmation[0];
|
|
385
460
|
const allTools = toolsNeedingConfirmation.length > 1
|
|
386
461
|
? toolsNeedingConfirmation
|
|
387
462
|
: undefined;
|
|
388
|
-
// Use first tool for confirmation UI, but apply result to all
|
|
389
463
|
const confirmation = await requestToolConfirmation(firstTool, undefined, allTools);
|
|
390
464
|
if (confirmation === 'reject') {
|
|
391
|
-
// Remove pending tool messages
|
|
392
465
|
setMessages(prev => prev.filter(msg => !msg.toolPending));
|
|
393
|
-
// User rejected - need to save tool rejection messages to maintain conversation structure
|
|
394
|
-
// Add tool rejection responses for ALL tools that were rejected
|
|
395
466
|
for (const toolCall of toolsNeedingConfirmation) {
|
|
396
467
|
const rejectionMessage = {
|
|
397
468
|
role: 'tool',
|
|
@@ -403,7 +474,6 @@ export async function handleConversationWithTools(options) {
|
|
|
403
474
|
console.error('Failed to save tool rejection message:', error);
|
|
404
475
|
});
|
|
405
476
|
}
|
|
406
|
-
// User rejected - end conversation
|
|
407
477
|
setMessages(prev => [
|
|
408
478
|
...prev,
|
|
409
479
|
{
|
|
@@ -412,12 +482,11 @@ export async function handleConversationWithTools(options) {
|
|
|
412
482
|
streaming: false,
|
|
413
483
|
},
|
|
414
484
|
]);
|
|
415
|
-
// End streaming immediately
|
|
416
485
|
if (options.setIsStreaming) {
|
|
417
486
|
options.setIsStreaming(false);
|
|
418
487
|
}
|
|
419
488
|
freeEncoder();
|
|
420
|
-
return { usage: accumulatedUsage };
|
|
489
|
+
return { usage: accumulatedUsage };
|
|
421
490
|
}
|
|
422
491
|
// If approved_always, add ALL these tools to both global and session-approved sets
|
|
423
492
|
if (confirmation === 'approve_always') {
|
|
@@ -648,18 +717,6 @@ export async function handleConversationWithTools(options) {
|
|
|
648
717
|
// 即使压缩失败也继续处理工具结果
|
|
649
718
|
}
|
|
650
719
|
}
|
|
651
|
-
// Check if there are TODO related tool calls, if yes refresh TODO list
|
|
652
|
-
const hasTodoTools = approvedTools.some(t => t.function.name.startsWith('todo-'));
|
|
653
|
-
const hasTodoUpdateTools = approvedTools.some(t => t.function.name === 'todo-update');
|
|
654
|
-
if (hasTodoTools) {
|
|
655
|
-
const session = sessionManager.getCurrentSession();
|
|
656
|
-
if (session) {
|
|
657
|
-
const updatedTodoList = await todoService.getTodoList(session.id);
|
|
658
|
-
if (updatedTodoList) {
|
|
659
|
-
setCurrentTodos(updatedTodoList.todos);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
720
|
// Remove only streaming sub-agent content messages (not tool-related messages)
|
|
664
721
|
// Keep sub-agent tool call and tool result messages for display
|
|
665
722
|
setMessages(prev => prev.filter(m => m.role !== 'subagent' ||
|
|
@@ -763,18 +820,6 @@ export async function handleConversationWithTools(options) {
|
|
|
763
820
|
});
|
|
764
821
|
}
|
|
765
822
|
}
|
|
766
|
-
// After all tool results are processed, show TODO panel if there were todo-update calls
|
|
767
|
-
if (hasTodoUpdateTools) {
|
|
768
|
-
setMessages(prev => [
|
|
769
|
-
...prev,
|
|
770
|
-
{
|
|
771
|
-
role: 'assistant',
|
|
772
|
-
content: '',
|
|
773
|
-
streaming: false,
|
|
774
|
-
showTodoTree: true,
|
|
775
|
-
},
|
|
776
|
-
]);
|
|
777
|
-
}
|
|
778
823
|
// Check if there are pending user messages to insert
|
|
779
824
|
if (options.getPendingMessages && options.clearPendingMessages) {
|
|
780
825
|
const pendingMessages = options.getPendingMessages();
|
|
@@ -817,7 +862,9 @@ export async function handleConversationWithTools(options) {
|
|
|
817
862
|
// Clear pending messages
|
|
818
863
|
options.clearPendingMessages();
|
|
819
864
|
// Combine multiple pending messages into one
|
|
820
|
-
const combinedMessage = pendingMessages
|
|
865
|
+
const combinedMessage = pendingMessages
|
|
866
|
+
.map(m => m.text)
|
|
867
|
+
.join('\n\n');
|
|
821
868
|
// Collect all images from pending messages
|
|
822
869
|
const allPendingImages = pendingMessages
|
|
823
870
|
.flatMap(m => m.images || [])
|
|
@@ -11,7 +11,7 @@ export declare function useFilePicker(buffer: TextBuffer, triggerUpdate: () => v
|
|
|
11
11
|
setAtSymbolPosition: (_pos: number) => void;
|
|
12
12
|
filteredFileCount: number;
|
|
13
13
|
searchMode: "content" | "file";
|
|
14
|
-
updateFilePickerState: (
|
|
14
|
+
updateFilePickerState: (_text: string, cursorPos: number) => void;
|
|
15
15
|
handleFileSelect: (filePath: string) => Promise<void>;
|
|
16
16
|
handleFilteredCountChange: (count: number) => void;
|
|
17
17
|
fileListRef: import("react").RefObject<FileListRef>;
|
|
@@ -51,15 +51,19 @@ export function useFilePicker(buffer, triggerUpdate) {
|
|
|
51
51
|
});
|
|
52
52
|
const fileListRef = useRef(null);
|
|
53
53
|
// Update file picker state
|
|
54
|
-
const updateFilePickerState = useCallback((
|
|
55
|
-
|
|
54
|
+
const updateFilePickerState = useCallback((_text, cursorPos) => {
|
|
55
|
+
// Use display text (with placeholders) instead of full text (expanded)
|
|
56
|
+
// to ensure cursor position matches text content
|
|
57
|
+
// Note: _text parameter is ignored, we use buffer.text instead
|
|
58
|
+
const displayText = buffer.text;
|
|
59
|
+
if (!displayText.includes('@')) {
|
|
56
60
|
if (state.showFilePicker) {
|
|
57
61
|
dispatch({ type: 'HIDE' });
|
|
58
62
|
}
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
65
|
// Find the last '@' or '@@' symbol before the cursor
|
|
62
|
-
const beforeCursor =
|
|
66
|
+
const beforeCursor = displayText.slice(0, cursorPos);
|
|
63
67
|
// Look for @@ first (content search), then @ (file search)
|
|
64
68
|
let searchMode = 'file';
|
|
65
69
|
let position = -1;
|
|
@@ -130,6 +134,7 @@ export function useFilePicker(buffer, triggerUpdate) {
|
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
}, [
|
|
137
|
+
buffer,
|
|
133
138
|
state.showFilePicker,
|
|
134
139
|
state.fileQuery,
|
|
135
140
|
state.atSymbolPosition,
|
|
@@ -138,13 +143,14 @@ export function useFilePicker(buffer, triggerUpdate) {
|
|
|
138
143
|
// Handle file selection
|
|
139
144
|
const handleFileSelect = useCallback(async (filePath) => {
|
|
140
145
|
if (state.atSymbolPosition !== -1) {
|
|
141
|
-
|
|
146
|
+
// Use display text (with placeholders) for position calculations
|
|
147
|
+
const displayText = buffer.text;
|
|
142
148
|
const cursorPos = buffer.getCursorPosition();
|
|
143
149
|
// Replace query with selected file path
|
|
144
150
|
// For content search (@@), the filePath already includes line number
|
|
145
151
|
// For file search (@), just the file path
|
|
146
|
-
const beforeAt =
|
|
147
|
-
const afterCursor =
|
|
152
|
+
const beforeAt = displayText.slice(0, state.atSymbolPosition);
|
|
153
|
+
const afterCursor = displayText.slice(cursorPos);
|
|
148
154
|
// Construct the replacement based on search mode
|
|
149
155
|
const prefix = state.searchMode === 'content' ? '@@' : '@';
|
|
150
156
|
const newText = beforeAt + prefix + filePath + ' ' + afterCursor;
|
|
@@ -156,7 +162,7 @@ export function useFilePicker(buffer, triggerUpdate) {
|
|
|
156
162
|
const targetPos = state.atSymbolPosition + insertedLength;
|
|
157
163
|
// Reset cursor to beginning, then move to correct position
|
|
158
164
|
for (let i = 0; i < targetPos; i++) {
|
|
159
|
-
if (i < buffer.
|
|
165
|
+
if (i < buffer.text.length) {
|
|
160
166
|
buffer.moveRight();
|
|
161
167
|
}
|
|
162
168
|
}
|
|
@@ -2,5 +2,5 @@ import { TextBuffer, Viewport } from '../utils/textBuffer.js';
|
|
|
2
2
|
export declare function useInputBuffer(viewport: Viewport): {
|
|
3
3
|
buffer: TextBuffer;
|
|
4
4
|
triggerUpdate: () => void;
|
|
5
|
-
forceUpdate:
|
|
5
|
+
forceUpdate: () => void;
|
|
6
6
|
};
|
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { TextBuffer } from '../utils/textBuffer.js';
|
|
3
3
|
export function useInputBuffer(viewport) {
|
|
4
|
-
const [,
|
|
4
|
+
const [, setForceUpdateState] = useState({});
|
|
5
5
|
const lastUpdateTime = useRef(0);
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const bufferRef = useRef(null);
|
|
7
|
+
// Stable forceUpdate function using useRef
|
|
8
|
+
const forceUpdateRef = useRef(() => {
|
|
9
|
+
setForceUpdateState({});
|
|
10
|
+
});
|
|
11
|
+
// Stable triggerUpdate function using useRef
|
|
12
|
+
const triggerUpdateRef = useRef(() => {
|
|
8
13
|
const now = Date.now();
|
|
9
14
|
lastUpdateTime.current = now;
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
15
|
+
forceUpdateRef.current();
|
|
16
|
+
});
|
|
17
|
+
// Initialize buffer once
|
|
18
|
+
if (!bufferRef.current) {
|
|
19
|
+
bufferRef.current = new TextBuffer(viewport, triggerUpdateRef.current);
|
|
20
|
+
}
|
|
21
|
+
const buffer = bufferRef.current;
|
|
22
|
+
// Expose stable callback functions
|
|
23
|
+
const forceUpdate = useCallback(() => {
|
|
24
|
+
forceUpdateRef.current();
|
|
25
|
+
}, []);
|
|
26
|
+
const triggerUpdate = useCallback(() => {
|
|
27
|
+
triggerUpdateRef.current();
|
|
28
|
+
}, []);
|
|
13
29
|
// Update buffer viewport when viewport changes
|
|
14
30
|
useEffect(() => {
|
|
15
31
|
buffer.updateViewport(viewport);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}, [viewport.width, viewport.height]); // 移除 buffer 和 triggerUpdate 避免循环依赖
|
|
32
|
+
forceUpdateRef.current();
|
|
33
|
+
}, [viewport.width, viewport.height, buffer]);
|
|
19
34
|
// Cleanup buffer on unmount
|
|
20
35
|
useEffect(() => {
|
|
21
36
|
return () => {
|
|
@@ -45,7 +45,7 @@ export function useStreamingState() {
|
|
|
45
45
|
}, [timerStartTime]);
|
|
46
46
|
// Initialize remaining seconds when retry starts
|
|
47
47
|
useEffect(() => {
|
|
48
|
-
if (!retryStatus
|
|
48
|
+
if (!retryStatus?.isRetrying)
|
|
49
49
|
return;
|
|
50
50
|
if (retryStatus.remainingSeconds !== undefined)
|
|
51
51
|
return;
|
|
@@ -56,7 +56,7 @@ export function useStreamingState() {
|
|
|
56
56
|
remainingSeconds: Math.ceil(prev.nextDelay / 1000),
|
|
57
57
|
}
|
|
58
58
|
: null);
|
|
59
|
-
}, [retryStatus?.isRetrying
|
|
59
|
+
}, [retryStatus?.isRetrying]); // Only depend on isRetrying flag
|
|
60
60
|
// Countdown timer for retry delays
|
|
61
61
|
useEffect(() => {
|
|
62
62
|
if (!retryStatus || !retryStatus.isRetrying)
|
|
@@ -1,26 +1,43 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { vscodeConnection } from '../utils/vscodeConnection.js';
|
|
3
3
|
export function useVSCodeState() {
|
|
4
4
|
const [vscodeConnected, setVscodeConnected] = useState(false);
|
|
5
5
|
const [vscodeConnectionStatus, setVscodeConnectionStatus] = useState('disconnected');
|
|
6
6
|
const [editorContext, setEditorContext] = useState({});
|
|
7
|
+
// Use ref to track last status without causing re-renders
|
|
8
|
+
const lastStatusRef = useRef('disconnected');
|
|
9
|
+
// Use ref to track last editor context to avoid unnecessary updates
|
|
10
|
+
const lastEditorContextRef = useRef({});
|
|
7
11
|
// Monitor VSCode connection status and editor context
|
|
8
12
|
useEffect(() => {
|
|
9
13
|
const checkConnectionInterval = setInterval(() => {
|
|
10
14
|
const isConnected = vscodeConnection.isConnected();
|
|
11
15
|
setVscodeConnected(isConnected);
|
|
12
16
|
// Update connection status based on actual connection state
|
|
13
|
-
|
|
17
|
+
// Use ref to avoid reading from state
|
|
18
|
+
if (isConnected && lastStatusRef.current !== 'connected') {
|
|
19
|
+
lastStatusRef.current = 'connected';
|
|
14
20
|
setVscodeConnectionStatus('connected');
|
|
15
21
|
}
|
|
16
|
-
else if (!isConnected &&
|
|
22
|
+
else if (!isConnected && lastStatusRef.current === 'connected') {
|
|
23
|
+
lastStatusRef.current = 'disconnected';
|
|
17
24
|
setVscodeConnectionStatus('disconnected');
|
|
18
25
|
}
|
|
19
26
|
}, 1000);
|
|
20
27
|
const unsubscribe = vscodeConnection.onContextUpdate(context => {
|
|
21
|
-
|
|
28
|
+
// Only update state if context has actually changed
|
|
29
|
+
const hasChanged = context.activeFile !== lastEditorContextRef.current.activeFile ||
|
|
30
|
+
context.selectedText !== lastEditorContextRef.current.selectedText ||
|
|
31
|
+
context.cursorPosition?.line !== lastEditorContextRef.current.cursorPosition?.line ||
|
|
32
|
+
context.cursorPosition?.character !== lastEditorContextRef.current.cursorPosition?.character ||
|
|
33
|
+
context.workspaceFolder !== lastEditorContextRef.current.workspaceFolder;
|
|
34
|
+
if (hasChanged) {
|
|
35
|
+
lastEditorContextRef.current = context;
|
|
36
|
+
setEditorContext(context);
|
|
37
|
+
}
|
|
22
38
|
// When we receive context, it means connection is successful
|
|
23
|
-
if (
|
|
39
|
+
if (lastStatusRef.current !== 'connected') {
|
|
40
|
+
lastStatusRef.current = 'connected';
|
|
24
41
|
setVscodeConnectionStatus('connected');
|
|
25
42
|
}
|
|
26
43
|
});
|
|
@@ -28,7 +45,7 @@ export function useVSCodeState() {
|
|
|
28
45
|
clearInterval(checkConnectionInterval);
|
|
29
46
|
unsubscribe();
|
|
30
47
|
};
|
|
31
|
-
}, [
|
|
48
|
+
}, []); // Remove vscodeConnectionStatus from dependencies
|
|
32
49
|
// Separate effect for handling connecting timeout
|
|
33
50
|
useEffect(() => {
|
|
34
51
|
if (vscodeConnectionStatus !== 'connecting') {
|
package/dist/mcp/filesystem.js
CHANGED
|
@@ -1033,7 +1033,7 @@ export const mcpTools = [
|
|
|
1033
1033
|
},
|
|
1034
1034
|
{
|
|
1035
1035
|
name: 'filesystem-create',
|
|
1036
|
-
description: '
|
|
1036
|
+
description: 'Preferred tool for creating files: Use specified content to create a new file. Before creating the file, you need to determine if the file already exists; if it does, your creation will fail. You should use editing instead of creation, as this tool is more reliable than terminal commands like echo/cat with redirection. If necessary, automatically create the parent directory. If necessary, terminal commands can be used as a fallback.',
|
|
1037
1037
|
inputSchema: {
|
|
1038
1038
|
type: 'object',
|
|
1039
1039
|
properties: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { cpSlice } from '../../utils/textUtils.js';
|
|
4
4
|
import CommandPanel from './CommandPanel.js';
|
|
@@ -41,10 +41,10 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
41
41
|
// Recalculate viewport dimensions to ensure proper resizing
|
|
42
42
|
const uiOverhead = 8;
|
|
43
43
|
const viewportWidth = Math.max(40, terminalWidth - uiOverhead);
|
|
44
|
-
const viewport = {
|
|
44
|
+
const viewport = useMemo(() => ({
|
|
45
45
|
width: viewportWidth,
|
|
46
46
|
height: 1,
|
|
47
|
-
};
|
|
47
|
+
}), [viewportWidth]); // Memoize viewport to prevent unnecessary re-renders
|
|
48
48
|
// Use input buffer hook
|
|
49
49
|
const { buffer, triggerUpdate, forceUpdate } = useInputBuffer(viewport);
|
|
50
50
|
// Use command panel hook
|
|
@@ -169,10 +169,10 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
169
169
|
useEffect(() => {
|
|
170
170
|
// Use a small delay to ensure the component tree has updated
|
|
171
171
|
const timer = setTimeout(() => {
|
|
172
|
-
forceUpdate(
|
|
172
|
+
forceUpdate();
|
|
173
173
|
}, 10);
|
|
174
174
|
return () => clearTimeout(timer);
|
|
175
|
-
}, [showFilePicker]);
|
|
175
|
+
}, [showFilePicker, forceUpdate]);
|
|
176
176
|
// Handle terminal width changes with debounce (like gemini-cli)
|
|
177
177
|
useEffect(() => {
|
|
178
178
|
// Skip on initial mount
|
|
@@ -183,17 +183,22 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
183
183
|
prevTerminalWidthRef.current = terminalWidth;
|
|
184
184
|
// Debounce the re-render to avoid flickering during resize
|
|
185
185
|
const timer = setTimeout(() => {
|
|
186
|
-
forceUpdate(
|
|
186
|
+
forceUpdate();
|
|
187
187
|
}, 100);
|
|
188
188
|
return () => clearTimeout(timer);
|
|
189
|
-
}, [terminalWidth]);
|
|
189
|
+
}, [terminalWidth, forceUpdate]);
|
|
190
190
|
// Notify parent of context percentage changes
|
|
191
|
+
const lastPercentageRef = useRef(0);
|
|
191
192
|
useEffect(() => {
|
|
192
193
|
if (contextUsage && onContextPercentageChange) {
|
|
193
194
|
const percentage = calculateContextPercentage(contextUsage);
|
|
194
|
-
|
|
195
|
+
// Only call callback if percentage has actually changed
|
|
196
|
+
if (percentage !== lastPercentageRef.current) {
|
|
197
|
+
lastPercentageRef.current = percentage;
|
|
198
|
+
onContextPercentageChange(percentage);
|
|
199
|
+
}
|
|
195
200
|
}
|
|
196
|
-
}, [contextUsage]);
|
|
201
|
+
}, [contextUsage, onContextPercentageChange]);
|
|
197
202
|
// Render cursor based on focus state
|
|
198
203
|
const renderCursor = useCallback((char) => {
|
|
199
204
|
if (hasFocus) {
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Text } from 'ink';
|
|
3
|
-
import { highlight } from 'cli-highlight';
|
|
4
3
|
// @ts-expect-error - cli-markdown doesn't have TypeScript definitions
|
|
5
4
|
import cliMarkdown from 'cli-markdown';
|
|
6
5
|
export default function MarkdownRenderer({ content }) {
|
|
7
6
|
// Use cli-markdown for elegant markdown rendering with syntax highlighting
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
code: (code, language) => {
|
|
11
|
-
if (!language)
|
|
12
|
-
return code;
|
|
13
|
-
try {
|
|
14
|
-
return highlight(code, { language, ignoreIllegals: true });
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return code;
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
});
|
|
7
|
+
// The patched highlight function will gracefully handle unknown languages
|
|
8
|
+
const rendered = cliMarkdown(content);
|
|
21
9
|
// Remove excessive trailing newlines and whitespace from cli-markdown output
|
|
22
10
|
// Keep single blank lines for paragraph spacing (better readability)
|
|
23
11
|
const trimmedRendered = rendered
|
|
@@ -34,8 +34,7 @@ const MessageList = memo(({ messages, animationFrame, maxMessages = 6 }) => {
|
|
|
34
34
|
React.createElement(Box, { marginLeft: 2 },
|
|
35
35
|
React.createElement(Text, { color: "gray" }, message.content || ' ')))) : (React.createElement(React.Fragment, null,
|
|
36
36
|
message.role === 'user' ? (React.createElement(Text, { color: "gray" }, message.content || ' ')) : (React.createElement(MarkdownRenderer, { content: message.content || ' ' })),
|
|
37
|
-
(message.files ||
|
|
38
|
-
message.images) && (React.createElement(Box, { flexDirection: "column" },
|
|
37
|
+
(message.files || message.images) && (React.createElement(Box, { flexDirection: "column" },
|
|
39
38
|
message.files && message.files.length > 0 && (React.createElement(React.Fragment, null, message.files.map((file, fileIndex) => (React.createElement(Text, { key: fileIndex, color: "gray", dimColor: true }, file.isImage
|
|
40
39
|
? `└─ [image #{fileIndex + 1}] ${file.path}`
|
|
41
40
|
: `└─ Read \`${file.path}\`${file.exists
|
|
@@ -14,5 +14,5 @@ interface Props {
|
|
|
14
14
|
allTools?: ToolCall[];
|
|
15
15
|
onConfirm: (result: ConfirmationResult) => void;
|
|
16
16
|
}
|
|
17
|
-
export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm }: Props): React.JSX.Element;
|
|
17
|
+
export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm, }: Props): React.JSX.Element;
|
|
18
18
|
export {};
|