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.
- package/dist/api/anthropic.d.ts +1 -1
- package/dist/api/anthropic.js +52 -76
- package/dist/api/chat.d.ts +4 -4
- package/dist/api/chat.js +32 -17
- package/dist/api/gemini.d.ts +1 -1
- package/dist/api/gemini.js +20 -13
- package/dist/api/responses.d.ts +5 -5
- package/dist/api/responses.js +29 -27
- package/dist/app.js +4 -1
- package/dist/hooks/useClipboard.d.ts +4 -0
- package/dist/hooks/useClipboard.js +120 -0
- package/dist/hooks/useCommandHandler.d.ts +26 -0
- package/dist/hooks/useCommandHandler.js +158 -0
- package/dist/hooks/useCommandPanel.d.ts +16 -0
- package/dist/hooks/useCommandPanel.js +53 -0
- package/dist/hooks/useConversation.d.ts +9 -1
- package/dist/hooks/useConversation.js +152 -58
- package/dist/hooks/useFilePicker.d.ts +17 -0
- package/dist/hooks/useFilePicker.js +91 -0
- package/dist/hooks/useHistoryNavigation.d.ts +21 -0
- package/dist/hooks/useHistoryNavigation.js +50 -0
- package/dist/hooks/useInputBuffer.d.ts +6 -0
- package/dist/hooks/useInputBuffer.js +29 -0
- package/dist/hooks/useKeyboardInput.d.ts +51 -0
- package/dist/hooks/useKeyboardInput.js +272 -0
- package/dist/hooks/useSnapshotState.d.ts +12 -0
- package/dist/hooks/useSnapshotState.js +28 -0
- package/dist/hooks/useStreamingState.d.ts +24 -0
- package/dist/hooks/useStreamingState.js +96 -0
- package/dist/hooks/useVSCodeState.d.ts +8 -0
- package/dist/hooks/useVSCodeState.js +63 -0
- package/dist/mcp/filesystem.d.ts +24 -5
- package/dist/mcp/filesystem.js +52 -17
- package/dist/mcp/todo.js +4 -8
- package/dist/ui/components/ChatInput.js +68 -557
- package/dist/ui/components/DiffViewer.js +57 -30
- package/dist/ui/components/FileList.js +70 -26
- package/dist/ui/components/MessageList.d.ts +6 -0
- package/dist/ui/components/MessageList.js +47 -15
- package/dist/ui/components/ShimmerText.d.ts +9 -0
- package/dist/ui/components/ShimmerText.js +30 -0
- package/dist/ui/components/TodoTree.d.ts +1 -1
- package/dist/ui/components/TodoTree.js +0 -4
- package/dist/ui/components/ToolConfirmation.js +14 -6
- package/dist/ui/pages/ChatScreen.js +159 -359
- package/dist/ui/pages/CustomHeadersScreen.d.ts +6 -0
- package/dist/ui/pages/CustomHeadersScreen.js +104 -0
- package/dist/ui/pages/WelcomeScreen.js +5 -0
- package/dist/utils/apiConfig.d.ts +10 -0
- package/dist/utils/apiConfig.js +51 -0
- package/dist/utils/incrementalSnapshot.d.ts +8 -0
- package/dist/utils/incrementalSnapshot.js +63 -0
- package/dist/utils/mcpToolsManager.js +6 -1
- package/dist/utils/retryUtils.d.ts +22 -0
- package/dist/utils/retryUtils.js +180 -0
- package/dist/utils/sessionConverter.js +80 -17
- package/dist/utils/sessionManager.js +35 -4
- package/dist/utils/textUtils.d.ts +4 -0
- package/dist/utils/textUtils.js +19 -0
- package/dist/utils/todoPreprocessor.d.ts +1 -1
- package/dist/utils/todoPreprocessor.js +0 -1
- package/dist/utils/vscodeConnection.d.ts +8 -0
- package/dist/utils/vscodeConnection.js +44 -0
- package/package.json +1 -1
- package/readme.md +3 -1
|
@@ -2,10 +2,24 @@ import React from 'react';
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import * as Diff from 'diff';
|
|
4
4
|
export default function DiffViewer({ oldContent = '', newContent, filename }) {
|
|
5
|
+
// Strip line numbers if present (format: "123→content")
|
|
6
|
+
const stripLineNumbers = (content) => {
|
|
7
|
+
return content
|
|
8
|
+
.split('\n')
|
|
9
|
+
.map(line => {
|
|
10
|
+
// Match line number prefix pattern: " 123→"
|
|
11
|
+
const match = line.match(/^\s*\d+→(.*)$/);
|
|
12
|
+
return match ? match[1] : line;
|
|
13
|
+
})
|
|
14
|
+
.join('\n');
|
|
15
|
+
};
|
|
16
|
+
// Clean the content from filesystem line numbers
|
|
17
|
+
const cleanOldContent = stripLineNumbers(oldContent);
|
|
18
|
+
const cleanNewContent = stripLineNumbers(newContent);
|
|
5
19
|
// If no old content, show as new file creation
|
|
6
|
-
const isNewFile = !
|
|
20
|
+
const isNewFile = !cleanOldContent || cleanOldContent.trim() === '';
|
|
7
21
|
if (isNewFile) {
|
|
8
|
-
const allLines =
|
|
22
|
+
const allLines = cleanNewContent.split('\n');
|
|
9
23
|
const totalLines = allLines.length;
|
|
10
24
|
const lineNumberWidth = String(totalLines).length;
|
|
11
25
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
@@ -18,19 +32,19 @@ export default function DiffViewer({ oldContent = '', newContent, filename }) {
|
|
|
18
32
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
19
33
|
String(index + 1).padStart(lineNumberWidth, ' '),
|
|
20
34
|
" \u2502"),
|
|
21
|
-
React.createElement(Text, { color: "white", backgroundColor: "
|
|
35
|
+
React.createElement(Text, { color: "white", backgroundColor: "#006400" },
|
|
22
36
|
"+ ",
|
|
23
37
|
line)))))));
|
|
24
38
|
}
|
|
25
|
-
// Generate diff
|
|
26
|
-
const diffResult = Diff.diffLines(
|
|
27
|
-
// Calculate line numbers
|
|
39
|
+
// Generate diff using a unified diff format
|
|
40
|
+
const diffResult = Diff.diffLines(cleanOldContent, cleanNewContent);
|
|
41
|
+
// Calculate line numbers
|
|
42
|
+
const totalOldLines = cleanOldContent.split('\n').length;
|
|
43
|
+
const totalNewLines = cleanNewContent.split('\n').length;
|
|
44
|
+
const lineNumberWidth = Math.max(String(totalOldLines).length, String(totalNewLines).length, 2);
|
|
45
|
+
const displayLines = [];
|
|
28
46
|
let oldLineNum = 1;
|
|
29
47
|
let newLineNum = 1;
|
|
30
|
-
const totalOldLines = oldContent.split('\n').length;
|
|
31
|
-
const totalNewLines = newContent.split('\n').length;
|
|
32
|
-
const lineNumberWidth = Math.max(String(totalOldLines).length, String(totalNewLines).length);
|
|
33
|
-
const displayLines = [];
|
|
34
48
|
diffResult.forEach((part) => {
|
|
35
49
|
const lines = part.value.replace(/\n$/, '').split('\n');
|
|
36
50
|
lines.forEach((line) => {
|
|
@@ -38,6 +52,7 @@ export default function DiffViewer({ oldContent = '', newContent, filename }) {
|
|
|
38
52
|
displayLines.push({
|
|
39
53
|
type: 'added',
|
|
40
54
|
content: line,
|
|
55
|
+
oldLineNum: null,
|
|
41
56
|
newLineNum: newLineNum++
|
|
42
57
|
});
|
|
43
58
|
}
|
|
@@ -45,7 +60,8 @@ export default function DiffViewer({ oldContent = '', newContent, filename }) {
|
|
|
45
60
|
displayLines.push({
|
|
46
61
|
type: 'removed',
|
|
47
62
|
content: line,
|
|
48
|
-
oldLineNum: oldLineNum
|
|
63
|
+
oldLineNum: oldLineNum++,
|
|
64
|
+
newLineNum: null
|
|
49
65
|
});
|
|
50
66
|
}
|
|
51
67
|
else {
|
|
@@ -65,29 +81,40 @@ export default function DiffViewer({ oldContent = '', newContent, filename }) {
|
|
|
65
81
|
' ',
|
|
66
82
|
filename))),
|
|
67
83
|
React.createElement(Box, { flexDirection: "column" }, displayLines.map((displayLine, index) => {
|
|
84
|
+
const oldNum = displayLine.oldLineNum !== null
|
|
85
|
+
? String(displayLine.oldLineNum).padStart(lineNumberWidth, ' ')
|
|
86
|
+
: ' '.repeat(lineNumberWidth);
|
|
87
|
+
const newNum = displayLine.newLineNum !== null
|
|
88
|
+
? String(displayLine.newLineNum).padStart(lineNumberWidth, ' ')
|
|
89
|
+
: ' '.repeat(lineNumberWidth);
|
|
68
90
|
if (displayLine.type === 'added') {
|
|
69
|
-
return (React.createElement(Box, { key: index },
|
|
70
|
-
React.createElement(
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
91
|
+
return (React.createElement(Box, { key: index, flexDirection: "row" },
|
|
92
|
+
React.createElement(Box, { flexShrink: 0 },
|
|
93
|
+
React.createElement(Text, { color: "gray", dimColor: true }, oldNum),
|
|
94
|
+
React.createElement(Text, { color: "green", dimColor: true }, ' + '),
|
|
95
|
+
React.createElement(Text, { color: "gray", dimColor: true }, newNum),
|
|
96
|
+
React.createElement(Text, { dimColor: true }, " \u2502 ")),
|
|
97
|
+
React.createElement(Box, null,
|
|
98
|
+
React.createElement(Text, { color: "white", backgroundColor: "#006400", wrap: "truncate-end" }, ' ' + displayLine.content))));
|
|
76
99
|
}
|
|
77
100
|
if (displayLine.type === 'removed') {
|
|
78
|
-
return (React.createElement(Box, { key: index },
|
|
79
|
-
React.createElement(
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
101
|
+
return (React.createElement(Box, { key: index, flexDirection: "row" },
|
|
102
|
+
React.createElement(Box, { flexShrink: 0 },
|
|
103
|
+
React.createElement(Text, { color: "gray", dimColor: true }, oldNum),
|
|
104
|
+
React.createElement(Text, { color: "red", dimColor: true }, ' - '),
|
|
105
|
+
React.createElement(Text, { color: "gray", dimColor: true }, newNum),
|
|
106
|
+
React.createElement(Text, { dimColor: true }, " \u2502 ")),
|
|
107
|
+
React.createElement(Box, null,
|
|
108
|
+
React.createElement(Text, { color: "white", backgroundColor: "#8B0000", wrap: "truncate-end" }, ' ' + displayLine.content))));
|
|
85
109
|
}
|
|
86
110
|
// Unchanged lines
|
|
87
|
-
return (React.createElement(Box, { key: index },
|
|
88
|
-
React.createElement(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
return (React.createElement(Box, { key: index, flexDirection: "row" },
|
|
112
|
+
React.createElement(Box, { flexShrink: 0 },
|
|
113
|
+
React.createElement(Text, { color: "gray", dimColor: true }, oldNum),
|
|
114
|
+
React.createElement(Text, { dimColor: true }, ' '),
|
|
115
|
+
React.createElement(Text, { color: "gray", dimColor: true }, newNum),
|
|
116
|
+
React.createElement(Text, { dimColor: true }, " \u2502 ")),
|
|
117
|
+
React.createElement(Box, null,
|
|
118
|
+
React.createElement(Text, { dimColor: true, wrap: "truncate-end" }, displayLine.content))));
|
|
92
119
|
}))));
|
|
93
120
|
}
|
|
@@ -1,49 +1,84 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo, useCallback, forwardRef, useImperativeHandle, memo } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useMemo, useCallback, forwardRef, useImperativeHandle, memo, } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10, rootPath = process.cwd(), onFilteredCountChange }, ref) => {
|
|
5
|
+
const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10, rootPath = process.cwd(), onFilteredCountChange, }, ref) => {
|
|
6
6
|
const [files, setFiles] = useState([]);
|
|
7
7
|
const [isLoading, setIsLoading] = useState(false);
|
|
8
8
|
// Fixed maximum display items to prevent rendering issues
|
|
9
9
|
const MAX_DISPLAY_ITEMS = 5;
|
|
10
10
|
const effectiveMaxItems = useMemo(() => {
|
|
11
|
-
return maxItems
|
|
11
|
+
return maxItems
|
|
12
|
+
? Math.min(maxItems, MAX_DISPLAY_ITEMS)
|
|
13
|
+
: MAX_DISPLAY_ITEMS;
|
|
12
14
|
}, [maxItems]);
|
|
13
|
-
// Get files from directory - optimized
|
|
15
|
+
// Get files from directory - optimized for performance with no depth limit
|
|
14
16
|
const loadFiles = useCallback(async () => {
|
|
15
|
-
const getFilesRecursively = async (dir, depth = 0
|
|
16
|
-
if (depth > maxDepth)
|
|
17
|
-
return [];
|
|
17
|
+
const getFilesRecursively = async (dir, depth = 0) => {
|
|
18
18
|
try {
|
|
19
|
-
const entries = await fs.promises.readdir(dir, {
|
|
19
|
+
const entries = await fs.promises.readdir(dir, {
|
|
20
|
+
withFileTypes: true,
|
|
21
|
+
});
|
|
20
22
|
let result = [];
|
|
23
|
+
// Common ignore patterns for better performance
|
|
24
|
+
const ignorePatterns = [
|
|
25
|
+
'node_modules',
|
|
26
|
+
'dist',
|
|
27
|
+
'build',
|
|
28
|
+
'coverage',
|
|
29
|
+
'.git',
|
|
30
|
+
'.vscode',
|
|
31
|
+
'.idea',
|
|
32
|
+
'out',
|
|
33
|
+
'target',
|
|
34
|
+
'bin',
|
|
35
|
+
'obj',
|
|
36
|
+
'.next',
|
|
37
|
+
'.nuxt',
|
|
38
|
+
'vendor',
|
|
39
|
+
'__pycache__',
|
|
40
|
+
'.pytest_cache',
|
|
41
|
+
'.mypy_cache',
|
|
42
|
+
'venv',
|
|
43
|
+
'.venv',
|
|
44
|
+
'env',
|
|
45
|
+
'.env',
|
|
46
|
+
];
|
|
21
47
|
for (const entry of entries) {
|
|
22
|
-
// Skip hidden files and
|
|
48
|
+
// Skip hidden files and ignore patterns
|
|
23
49
|
if (entry.name.startsWith('.') ||
|
|
24
|
-
entry.name
|
|
25
|
-
entry.name === 'dist' ||
|
|
26
|
-
entry.name === 'build') {
|
|
50
|
+
ignorePatterns.includes(entry.name)) {
|
|
27
51
|
continue;
|
|
28
52
|
}
|
|
29
53
|
const fullPath = path.join(dir, entry.name);
|
|
54
|
+
// Skip if file is too large (> 10MB) for performance
|
|
55
|
+
try {
|
|
56
|
+
const stats = await fs.promises.stat(fullPath);
|
|
57
|
+
if (!entry.isDirectory() && stats.size > 10 * 1024 * 1024) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
30
64
|
let relativePath = path.relative(rootPath, fullPath);
|
|
31
65
|
// Ensure relative paths start with ./ for consistency
|
|
32
|
-
if (!relativePath.startsWith('.') &&
|
|
66
|
+
if (!relativePath.startsWith('.') &&
|
|
67
|
+
!path.isAbsolute(relativePath)) {
|
|
33
68
|
relativePath = './' + relativePath;
|
|
34
69
|
}
|
|
35
70
|
result.push({
|
|
36
71
|
name: entry.name,
|
|
37
72
|
path: relativePath,
|
|
38
|
-
isDirectory: entry.isDirectory()
|
|
73
|
+
isDirectory: entry.isDirectory(),
|
|
39
74
|
});
|
|
40
|
-
// Recursively get files from subdirectories
|
|
41
|
-
if (entry.isDirectory()
|
|
42
|
-
const subFiles = await getFilesRecursively(fullPath, depth + 1
|
|
75
|
+
// Recursively get files from subdirectories (no depth limit)
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
const subFiles = await getFilesRecursively(fullPath, depth + 1);
|
|
43
78
|
result = result.concat(subFiles);
|
|
44
79
|
}
|
|
45
|
-
// Limit total files for performance
|
|
46
|
-
if (result.length >
|
|
80
|
+
// Limit total files for performance (increased from 500 to 2000)
|
|
81
|
+
if (result.length > 2000) {
|
|
47
82
|
break;
|
|
48
83
|
}
|
|
49
84
|
}
|
|
@@ -112,16 +147,18 @@ const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10
|
|
|
112
147
|
// Expose methods to parent
|
|
113
148
|
useImperativeHandle(ref, () => ({
|
|
114
149
|
getSelectedFile: () => {
|
|
115
|
-
if (allFilteredFiles.length > 0 &&
|
|
150
|
+
if (allFilteredFiles.length > 0 &&
|
|
151
|
+
selectedIndex < allFilteredFiles.length &&
|
|
152
|
+
allFilteredFiles[selectedIndex]) {
|
|
116
153
|
return allFilteredFiles[selectedIndex].path;
|
|
117
154
|
}
|
|
118
155
|
return null;
|
|
119
|
-
}
|
|
156
|
+
},
|
|
120
157
|
}), [allFilteredFiles, selectedIndex]);
|
|
121
158
|
// Calculate display index for the scrolling window
|
|
122
159
|
// MUST be before early returns to avoid hook order issues
|
|
123
160
|
const displaySelectedIndex = useMemo(() => {
|
|
124
|
-
return filteredFiles.findIndex(
|
|
161
|
+
return filteredFiles.findIndex(file => {
|
|
125
162
|
const originalIndex = allFilteredFiles.indexOf(file);
|
|
126
163
|
return originalIndex === selectedIndex;
|
|
127
164
|
});
|
|
@@ -140,15 +177,22 @@ const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10
|
|
|
140
177
|
return (React.createElement(Box, { paddingX: 1, marginTop: 1, flexDirection: "column" },
|
|
141
178
|
React.createElement(Box, { marginBottom: 1 },
|
|
142
179
|
React.createElement(Text, { color: "blue", bold: true },
|
|
143
|
-
"\uD83D\uDDD0 Files
|
|
144
|
-
|
|
180
|
+
"\uD83D\uDDD0 Files",
|
|
181
|
+
' ',
|
|
182
|
+
allFilteredFiles.length > effectiveMaxItems &&
|
|
183
|
+
`(${selectedIndex + 1}/${allFilteredFiles.length})`)),
|
|
145
184
|
filteredFiles.map((file, index) => (React.createElement(Box, { key: file.path },
|
|
146
|
-
React.createElement(Text, { backgroundColor: index === displaySelectedIndex ?
|
|
185
|
+
React.createElement(Text, { backgroundColor: index === displaySelectedIndex ? '#1E3A8A' : undefined, color: index === displaySelectedIndex
|
|
186
|
+
? '#FFFFFF'
|
|
187
|
+
: file.isDirectory
|
|
188
|
+
? 'cyan'
|
|
189
|
+
: 'white' }, file.path)))),
|
|
147
190
|
allFilteredFiles.length > effectiveMaxItems && (React.createElement(Box, { marginTop: 1 },
|
|
148
191
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
149
192
|
"\u2191\u2193 to scroll \u00B7 ",
|
|
150
193
|
allFilteredFiles.length - effectiveMaxItems,
|
|
151
|
-
|
|
194
|
+
' ',
|
|
195
|
+
"more hidden")))));
|
|
152
196
|
}));
|
|
153
197
|
FileList.displayName = 'FileList';
|
|
154
198
|
export default FileList;
|
|
@@ -33,6 +33,12 @@ export interface Message {
|
|
|
33
33
|
toolResult?: string;
|
|
34
34
|
toolCallId?: string;
|
|
35
35
|
toolPending?: boolean;
|
|
36
|
+
terminalResult?: {
|
|
37
|
+
stdout?: string;
|
|
38
|
+
stderr?: string;
|
|
39
|
+
exitCode?: number;
|
|
40
|
+
command?: string;
|
|
41
|
+
};
|
|
36
42
|
}
|
|
37
43
|
interface Props {
|
|
38
44
|
messages: Message[];
|
|
@@ -15,12 +15,18 @@ const MessageList = memo(({ messages, animationFrame, maxMessages = 6 }) => {
|
|
|
15
15
|
? STREAM_COLORS[animationFrame]
|
|
16
16
|
: 'cyan';
|
|
17
17
|
return (React.createElement(Box, { key: index },
|
|
18
|
-
React.createElement(Text, { color: iconColor, bold: true }, message.role === 'user'
|
|
18
|
+
React.createElement(Text, { color: iconColor, bold: true }, message.role === 'user'
|
|
19
|
+
? '⛇'
|
|
20
|
+
: message.role === 'command'
|
|
21
|
+
? '⌘'
|
|
22
|
+
: '❆'),
|
|
19
23
|
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(Text, { color: "gray" },
|
|
20
24
|
"\u2514\u2500 ",
|
|
21
25
|
message.commandName)) : (React.createElement(React.Fragment, null,
|
|
22
26
|
React.createElement(MarkdownRenderer, { content: message.content || ' ', color: message.role === 'user' ? 'gray' : undefined }),
|
|
23
|
-
(message.systemInfo ||
|
|
27
|
+
(message.systemInfo ||
|
|
28
|
+
message.files ||
|
|
29
|
+
message.images) && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
24
30
|
message.systemInfo && (React.createElement(React.Fragment, null,
|
|
25
31
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
26
32
|
"\u2514\u2500 Platform: ",
|
|
@@ -29,38 +35,64 @@ const MessageList = memo(({ messages, animationFrame, maxMessages = 6 }) => {
|
|
|
29
35
|
"\u2514\u2500 Shell: ",
|
|
30
36
|
message.systemInfo.shell),
|
|
31
37
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
32
|
-
"\u2514\u2500 Working Directory:
|
|
38
|
+
"\u2514\u2500 Working Directory:",
|
|
39
|
+
' ',
|
|
33
40
|
message.systemInfo.workingDirectory))),
|
|
34
41
|
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
|
|
35
42
|
? `└─ [image #{fileIndex + 1}] ${file.path}`
|
|
36
|
-
: `└─ Read \`${file.path}\`${file.exists
|
|
43
|
+
: `└─ Read \`${file.path}\`${file.exists
|
|
44
|
+
? ` (total line ${file.lineCount})`
|
|
45
|
+
: ' (file not found)'}`))))),
|
|
37
46
|
message.images && message.images.length > 0 && (React.createElement(React.Fragment, null, message.images.map((_image, imageIndex) => (React.createElement(Text, { key: imageIndex, color: "gray", dimColor: true },
|
|
38
47
|
"\u2514\u2500 [image #",
|
|
39
48
|
imageIndex + 1,
|
|
40
49
|
"]"))))))),
|
|
41
|
-
message.toolCall &&
|
|
50
|
+
message.toolCall &&
|
|
51
|
+
message.toolCall.name === 'terminal-execute' &&
|
|
52
|
+
message.toolCall.arguments.command && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
42
53
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
43
|
-
"\u2514\u2500 Command:
|
|
54
|
+
"\u2514\u2500 Command:",
|
|
55
|
+
' ',
|
|
44
56
|
React.createElement(Text, { color: "white" }, message.toolCall.arguments.command)),
|
|
45
57
|
React.createElement(Text, { color: "gray", dimColor: true },
|
|
46
|
-
"\u2514\u2500 Exit Code:
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
"\u2514\u2500 Exit Code:",
|
|
59
|
+
' ',
|
|
60
|
+
React.createElement(Text, { color: message.toolCall.arguments.exitCode === 0
|
|
61
|
+
? 'green'
|
|
62
|
+
: 'red' }, message.toolCall.arguments.exitCode)),
|
|
63
|
+
message.toolCall.arguments.stdout &&
|
|
64
|
+
message.toolCall.arguments.stdout.trim().length >
|
|
65
|
+
0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
49
66
|
React.createElement(Text, { color: "green", dimColor: true }, "\u2514\u2500 stdout:"),
|
|
50
67
|
React.createElement(Box, { paddingLeft: 2 },
|
|
51
|
-
React.createElement(Text, { color: "white" }, message.toolCall.arguments.stdout
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
React.createElement(Text, { color: "white" }, message.toolCall.arguments.stdout
|
|
69
|
+
.trim()
|
|
70
|
+
.split('\n')
|
|
71
|
+
.slice(0, 20)
|
|
72
|
+
.join('\n')),
|
|
73
|
+
message.toolCall.arguments.stdout
|
|
74
|
+
.trim()
|
|
75
|
+
.split('\n').length > 20 && (React.createElement(Text, { color: "gray", dimColor: true }, "... (output truncated)"))))),
|
|
76
|
+
message.toolCall.arguments.stderr &&
|
|
77
|
+
message.toolCall.arguments.stderr.trim().length >
|
|
78
|
+
0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
54
79
|
React.createElement(Text, { color: "red", dimColor: true }, "\u2514\u2500 stderr:"),
|
|
55
80
|
React.createElement(Box, { paddingLeft: 2 },
|
|
56
|
-
React.createElement(Text, { color: "red" }, message.toolCall.arguments.stderr
|
|
57
|
-
|
|
81
|
+
React.createElement(Text, { color: "red" }, message.toolCall.arguments.stderr
|
|
82
|
+
.trim()
|
|
83
|
+
.split('\n')
|
|
84
|
+
.slice(0, 10)
|
|
85
|
+
.join('\n')),
|
|
86
|
+
message.toolCall.arguments.stderr
|
|
87
|
+
.trim()
|
|
88
|
+
.split('\n').length > 10 && (React.createElement(Text, { color: "gray", dimColor: true }, "... (output truncated)"))))))),
|
|
58
89
|
message.discontinued && (React.createElement(Text, { color: "red", bold: true }, "\u2514\u2500 user discontinue")))))));
|
|
59
90
|
})));
|
|
60
91
|
}, (prevProps, nextProps) => {
|
|
61
92
|
const hasStreamingMessage = nextProps.messages.some(m => m.streaming);
|
|
62
93
|
if (hasStreamingMessage) {
|
|
63
|
-
return prevProps.messages === nextProps.messages &&
|
|
94
|
+
return (prevProps.messages === nextProps.messages &&
|
|
95
|
+
prevProps.animationFrame === nextProps.animationFrame);
|
|
64
96
|
}
|
|
65
97
|
return prevProps.messages === nextProps.messages;
|
|
66
98
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ShimmerTextProps {
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* ShimmerText component that displays text with a white shimmer effect flowing through yellow text
|
|
7
|
+
*/
|
|
8
|
+
export default function ShimmerText({ text }: ShimmerTextProps): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
/**
|
|
5
|
+
* ShimmerText component that displays text with a white shimmer effect flowing through yellow text
|
|
6
|
+
*/
|
|
7
|
+
export default function ShimmerText({ text }) {
|
|
8
|
+
const [frame, setFrame] = useState(0);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const interval = setInterval(() => {
|
|
11
|
+
setFrame(prev => (prev + 1) % (text.length + 5));
|
|
12
|
+
}, 100); // Update every 100ms for smooth animation
|
|
13
|
+
return () => clearInterval(interval);
|
|
14
|
+
}, [text.length]);
|
|
15
|
+
// Build the colored text with shimmer effect
|
|
16
|
+
let output = '';
|
|
17
|
+
for (let i = 0; i < text.length; i++) {
|
|
18
|
+
const char = text[i];
|
|
19
|
+
const distance = Math.abs(i - frame);
|
|
20
|
+
// Bright cyan shimmer in the center (distance 0-1)
|
|
21
|
+
if (distance <= 1) {
|
|
22
|
+
output += chalk.hex('#00FFFF')(char); // Bright cyan/aqua
|
|
23
|
+
}
|
|
24
|
+
// Deep blue for the rest (base color)
|
|
25
|
+
else {
|
|
26
|
+
output += chalk.hex('#1ACEB0')(char); // Steel blue
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return React.createElement(Text, null, output);
|
|
30
|
+
}
|
|
@@ -21,8 +21,6 @@ export default function TodoTree({ todos }) {
|
|
|
21
21
|
switch (status) {
|
|
22
22
|
case 'completed':
|
|
23
23
|
return '[x]';
|
|
24
|
-
case 'in_progress':
|
|
25
|
-
return '[~]';
|
|
26
24
|
case 'pending':
|
|
27
25
|
return '[ ]';
|
|
28
26
|
}
|
|
@@ -31,8 +29,6 @@ export default function TodoTree({ todos }) {
|
|
|
31
29
|
switch (status) {
|
|
32
30
|
case 'completed':
|
|
33
31
|
return 'green';
|
|
34
|
-
case 'in_progress':
|
|
35
|
-
return 'yellow';
|
|
36
32
|
case 'pending':
|
|
37
33
|
return 'gray';
|
|
38
34
|
}
|
|
@@ -13,8 +13,16 @@ function formatArgumentValue(value, maxLength = 100) {
|
|
|
13
13
|
return stringValue.substring(0, maxLength) + '...';
|
|
14
14
|
}
|
|
15
15
|
// Helper function to convert parsed arguments to tree display format
|
|
16
|
-
function formatArgumentsAsTree(args) {
|
|
17
|
-
|
|
16
|
+
function formatArgumentsAsTree(args, toolName) {
|
|
17
|
+
// For filesystem-create and filesystem-edit, exclude content fields
|
|
18
|
+
const excludeFields = new Set();
|
|
19
|
+
if (toolName === 'filesystem-create') {
|
|
20
|
+
excludeFields.add('content');
|
|
21
|
+
}
|
|
22
|
+
if (toolName === 'filesystem-edit') {
|
|
23
|
+
excludeFields.add('newContent');
|
|
24
|
+
}
|
|
25
|
+
const keys = Object.keys(args).filter(key => !excludeFields.has(key));
|
|
18
26
|
return keys.map((key, index) => ({
|
|
19
27
|
key,
|
|
20
28
|
value: formatArgumentValue(args[key]),
|
|
@@ -29,12 +37,12 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
|
|
|
29
37
|
return null;
|
|
30
38
|
try {
|
|
31
39
|
const parsed = JSON.parse(toolArguments);
|
|
32
|
-
return formatArgumentsAsTree(parsed);
|
|
40
|
+
return formatArgumentsAsTree(parsed, toolName);
|
|
33
41
|
}
|
|
34
42
|
catch {
|
|
35
43
|
return null;
|
|
36
44
|
}
|
|
37
|
-
}, [toolArguments]);
|
|
45
|
+
}, [toolArguments, toolName]);
|
|
38
46
|
// Parse and format all tools arguments for display (multiple tools)
|
|
39
47
|
const formattedAllTools = useMemo(() => {
|
|
40
48
|
if (!allTools || allTools.length === 0)
|
|
@@ -44,7 +52,7 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
|
|
|
44
52
|
const parsed = JSON.parse(tool.function.arguments);
|
|
45
53
|
return {
|
|
46
54
|
name: tool.function.name,
|
|
47
|
-
args: formatArgumentsAsTree(parsed)
|
|
55
|
+
args: formatArgumentsAsTree(parsed, tool.function.name)
|
|
48
56
|
};
|
|
49
57
|
}
|
|
50
58
|
catch {
|
|
@@ -75,7 +83,7 @@ export default function ToolConfirmation({ toolName, toolArguments, allTools, on
|
|
|
75
83
|
onConfirm(item.value);
|
|
76
84
|
}
|
|
77
85
|
};
|
|
78
|
-
return (React.createElement(Box, { flexDirection: "column", marginX: 1, marginY: 1,
|
|
86
|
+
return (React.createElement(Box, { flexDirection: "column", marginX: 1, marginY: 1, paddingX: 1 },
|
|
79
87
|
React.createElement(Box, { marginBottom: 1 },
|
|
80
88
|
React.createElement(Text, { bold: true, color: "yellow" }, "[Tool Confirmation]")),
|
|
81
89
|
!formattedAllTools && (React.createElement(React.Fragment, null,
|