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
@@ -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 = !oldContent || oldContent.trim() === '';
20
+ const isNewFile = !cleanOldContent || cleanOldContent.trim() === '';
7
21
  if (isNewFile) {
8
- const allLines = newContent.split('\n');
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: "green" },
35
+ React.createElement(Text, { color: "white", backgroundColor: "#006400" },
22
36
  "+ ",
23
37
  line)))))));
24
38
  }
25
- // Generate diff
26
- const diffResult = Diff.diffLines(oldContent, newContent);
27
- // Calculate line numbers and build display lines
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(Text, { color: "gray", dimColor: true },
71
- String(displayLine.newLineNum).padStart(lineNumberWidth, ' '),
72
- " \u2502"),
73
- React.createElement(Text, { color: "white", backgroundColor: "green" },
74
- "+ ",
75
- displayLine.content)));
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(Text, { color: "gray", dimColor: true },
80
- String(displayLine.oldLineNum).padStart(lineNumberWidth, ' '),
81
- " \u2502"),
82
- React.createElement(Text, { color: "white", backgroundColor: "red" },
83
- "- ",
84
- displayLine.content)));
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(Text, { color: "gray", dimColor: true },
89
- String(displayLine.oldLineNum).padStart(lineNumberWidth, ' '),
90
- " \u2502"),
91
- React.createElement(Text, { dimColor: true }, displayLine.content)));
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 ? Math.min(maxItems, MAX_DISPLAY_ITEMS) : MAX_DISPLAY_ITEMS;
11
+ return maxItems
12
+ ? Math.min(maxItems, MAX_DISPLAY_ITEMS)
13
+ : MAX_DISPLAY_ITEMS;
12
14
  }, [maxItems]);
13
- // Get files from directory - optimized to batch updates
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, maxDepth = 3) => {
16
- if (depth > maxDepth)
17
- return [];
17
+ const getFilesRecursively = async (dir, depth = 0) => {
18
18
  try {
19
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
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 common ignore patterns
48
+ // Skip hidden files and ignore patterns
23
49
  if (entry.name.startsWith('.') ||
24
- entry.name === 'node_modules' ||
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('.') && !path.isAbsolute(relativePath)) {
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() && depth < maxDepth) {
42
- const subFiles = await getFilesRecursively(fullPath, depth + 1, maxDepth);
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 > 500) {
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 && selectedIndex < allFilteredFiles.length && allFilteredFiles[selectedIndex]) {
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((file) => {
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
- allFilteredFiles.length > effectiveMaxItems && `(${selectedIndex + 1}/${allFilteredFiles.length})`)),
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 ? "blue" : undefined, color: index === displaySelectedIndex ? "white" : file.isDirectory ? "cyan" : "white" }, file.path)))),
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
- " more hidden")))));
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' ? '⛇' : message.role === 'command' ? '⌘' : '❆'),
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 || message.files || message.images) && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
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 ? ` (total line ${file.lineCount})` : ' (file not found)'}`))))),
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 && message.toolCall.name === 'terminal-execute' && message.toolCall.arguments.command && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
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
- React.createElement(Text, { color: message.toolCall.arguments.exitCode === 0 ? 'green' : 'red' }, message.toolCall.arguments.exitCode)),
48
- message.toolCall.arguments.stdout && message.toolCall.arguments.stdout.trim().length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
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.trim().split('\n').slice(0, 20).join('\n')),
52
- message.toolCall.arguments.stdout.trim().split('\n').length > 20 && (React.createElement(Text, { color: "gray", dimColor: true }, "... (output truncated)"))))),
53
- message.toolCall.arguments.stderr && message.toolCall.arguments.stderr.trim().length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
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.trim().split('\n').slice(0, 10).join('\n')),
57
- message.toolCall.arguments.stderr.trim().split('\n').length > 10 && (React.createElement(Text, { color: "gray", dimColor: true }, "... (output truncated)"))))))),
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 && prevProps.animationFrame === nextProps.animationFrame;
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
+ }
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  interface TodoItem {
3
3
  id: string;
4
4
  content: string;
5
- status: 'pending' | 'in_progress' | 'completed';
5
+ status: 'pending' | 'completed';
6
6
  parentId?: string;
7
7
  }
8
8
  interface TodoTreeProps {
@@ -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
- const keys = Object.keys(args);
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, borderStyle: "round", borderColor: "yellow", paddingX: 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,