snow-ai 0.2.11 → 0.2.13
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 +2 -0
- package/dist/api/anthropic.js +64 -18
- package/dist/api/chat.d.ts +3 -0
- package/dist/api/chat.js +5 -4
- package/dist/api/gemini.d.ts +3 -0
- package/dist/api/gemini.js +168 -101
- package/dist/api/responses.d.ts +3 -0
- package/dist/api/responses.js +5 -4
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +149 -40
- package/dist/hooks/useConversation.d.ts +1 -1
- package/dist/hooks/useConversation.js +5 -3
- package/dist/hooks/useGlobalNavigation.js +2 -0
- package/dist/hooks/useToolConfirmation.d.ts +2 -1
- package/dist/hooks/useToolConfirmation.js +2 -1
- package/dist/mcp/filesystem.d.ts +16 -1
- package/dist/mcp/filesystem.js +193 -89
- package/dist/mcp/multiLanguageASTParser.d.ts +67 -0
- package/dist/mcp/multiLanguageASTParser.js +360 -0
- package/dist/mcp/todo.d.ts +1 -1
- package/dist/mcp/todo.js +21 -26
- package/dist/ui/components/ChatInput.d.ts +4 -1
- package/dist/ui/components/ChatInput.js +105 -39
- package/dist/ui/components/DiffViewer.d.ts +1 -2
- package/dist/ui/components/DiffViewer.js +65 -65
- package/dist/ui/components/MCPInfoPanel.js +1 -2
- package/dist/ui/components/TodoTree.js +1 -1
- package/dist/ui/components/ToolConfirmation.d.ts +11 -1
- package/dist/ui/components/ToolConfirmation.js +86 -6
- package/dist/ui/pages/ChatScreen.js +223 -108
- package/dist/ui/pages/SystemPromptConfigScreen.js +25 -12
- package/dist/utils/apiConfig.d.ts +6 -1
- package/dist/utils/apiConfig.js +24 -0
- package/dist/utils/commands/ide.js +18 -1
- package/dist/utils/mcpToolsManager.d.ts +1 -1
- package/dist/utils/mcpToolsManager.js +45 -36
- package/dist/utils/textBuffer.d.ts +5 -0
- package/dist/utils/textBuffer.js +23 -2
- package/dist/utils/vscodeConnection.js +10 -1
- package/package.json +14 -2
- package/readme.md +36 -6
|
@@ -11,18 +11,27 @@ const commands = [
|
|
|
11
11
|
{ name: 'clear', description: 'Clear chat context and conversation history' },
|
|
12
12
|
{ name: 'resume', description: 'Resume a conversation' },
|
|
13
13
|
{ name: 'mcp', description: 'Show Model Context Protocol services and tools' },
|
|
14
|
-
{
|
|
15
|
-
|
|
14
|
+
{
|
|
15
|
+
name: 'yolo',
|
|
16
|
+
description: 'Toggle unattended mode (auto-approve all tools)',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'init',
|
|
20
|
+
description: 'Analyze project and generate/update SNOW.md documentation',
|
|
21
|
+
},
|
|
16
22
|
{ name: 'ide', description: 'Connect to VSCode editor and sync context' },
|
|
17
|
-
{
|
|
23
|
+
{
|
|
24
|
+
name: 'compact',
|
|
25
|
+
description: 'Compress conversation history using compact model',
|
|
26
|
+
},
|
|
18
27
|
];
|
|
19
|
-
export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type your message...', disabled = false, chatHistory = [], onHistorySelect, yoloMode = false, contextUsage, snapshotFileCount }) {
|
|
28
|
+
export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type your message...', disabled = false, chatHistory = [], onHistorySelect, yoloMode = false, contextUsage, snapshotFileCount, }) {
|
|
20
29
|
const { stdout } = useStdout();
|
|
21
30
|
const terminalWidth = stdout?.columns || 80;
|
|
22
31
|
const uiOverhead = 8;
|
|
23
32
|
const viewport = {
|
|
24
33
|
width: Math.max(40, terminalWidth - uiOverhead),
|
|
25
|
-
height: 1
|
|
34
|
+
height: 1,
|
|
26
35
|
};
|
|
27
36
|
const [, forceUpdate] = useState({});
|
|
28
37
|
const lastUpdateTime = useRef(0);
|
|
@@ -33,6 +42,18 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
33
42
|
forceUpdate({});
|
|
34
43
|
}, []);
|
|
35
44
|
const [buffer] = useState(() => new TextBuffer(viewport, triggerUpdate));
|
|
45
|
+
// Cleanup buffer and timers on unmount
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
return () => {
|
|
48
|
+
buffer.destroy();
|
|
49
|
+
if (inputTimer.current) {
|
|
50
|
+
clearTimeout(inputTimer.current);
|
|
51
|
+
}
|
|
52
|
+
if (escapeKeyTimer.current) {
|
|
53
|
+
clearTimeout(escapeKeyTimer.current);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}, [buffer]);
|
|
36
57
|
// Command panel state
|
|
37
58
|
const [showCommands, setShowCommands] = useState(false);
|
|
38
59
|
const [commandSelectedIndex, setCommandSelectedIndex] = useState(0);
|
|
@@ -55,11 +76,10 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
55
76
|
.map((msg, index) => ({ ...msg, originalIndex: index }))
|
|
56
77
|
.filter(msg => msg.role === 'user' && msg.content.trim());
|
|
57
78
|
// Keep original order (oldest first, newest last) and map with display numbers
|
|
58
|
-
return userMessages
|
|
59
|
-
.map((msg, index) => ({
|
|
79
|
+
return userMessages.map((msg, index) => ({
|
|
60
80
|
label: `${index + 1}. ${msg.content.slice(0, 50)}${msg.content.length > 50 ? '...' : ''}`,
|
|
61
81
|
value: msg.originalIndex.toString(),
|
|
62
|
-
infoText: msg.content
|
|
82
|
+
infoText: msg.content,
|
|
63
83
|
}));
|
|
64
84
|
}, [chatHistory]);
|
|
65
85
|
// Get filtered commands based on current input
|
|
@@ -100,7 +120,9 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
100
120
|
// Check if there's no space between '@' and cursor
|
|
101
121
|
const afterAt = beforeCursor.slice(lastAtIndex + 1);
|
|
102
122
|
if (!afterAt.includes(' ') && !afterAt.includes('\n')) {
|
|
103
|
-
if (!showFilePicker ||
|
|
123
|
+
if (!showFilePicker ||
|
|
124
|
+
fileQuery !== afterAt ||
|
|
125
|
+
atSymbolPosition !== lastAtIndex) {
|
|
104
126
|
setShowFilePicker(true);
|
|
105
127
|
setFileSelectedIndex(0);
|
|
106
128
|
setFileQuery(afterAt);
|
|
@@ -138,7 +160,8 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
138
160
|
buffer.setText(newText);
|
|
139
161
|
// Calculate cursor position after the inserted file path + space
|
|
140
162
|
// Reset cursor to beginning, then move to correct position
|
|
141
|
-
for (let i = 0; i < atSymbolPosition + filePath.length + 2; i++) {
|
|
163
|
+
for (let i = 0; i < atSymbolPosition + filePath.length + 2; i++) {
|
|
164
|
+
// +2 for @ and space
|
|
142
165
|
if (i < buffer.getFullText().length) {
|
|
143
166
|
buffer.moveRight();
|
|
144
167
|
}
|
|
@@ -166,7 +189,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
166
189
|
useEffect(() => {
|
|
167
190
|
const newViewport = {
|
|
168
191
|
width: Math.max(40, terminalWidth - uiOverhead),
|
|
169
|
-
height: 1
|
|
192
|
+
height: 1,
|
|
170
193
|
};
|
|
171
194
|
buffer.updateViewport(newViewport);
|
|
172
195
|
triggerUpdate();
|
|
@@ -210,7 +233,8 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
210
233
|
setEscapeKeyCount(0);
|
|
211
234
|
}, 500);
|
|
212
235
|
// Check for double escape
|
|
213
|
-
if (escapeKeyCount >= 1) {
|
|
236
|
+
if (escapeKeyCount >= 1) {
|
|
237
|
+
// This will be 2 after increment
|
|
214
238
|
const userMessages = getUserMessages();
|
|
215
239
|
if (userMessages.length > 0) {
|
|
216
240
|
setShowHistoryMenu(true);
|
|
@@ -240,7 +264,8 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
240
264
|
}
|
|
241
265
|
// Enter - select history item
|
|
242
266
|
if (key.return) {
|
|
243
|
-
if (userMessages.length > 0 &&
|
|
267
|
+
if (userMessages.length > 0 &&
|
|
268
|
+
historySelectedIndex < userMessages.length) {
|
|
244
269
|
const selectedMessage = userMessages[historySelectedIndex];
|
|
245
270
|
if (selectedMessage) {
|
|
246
271
|
handleHistorySelect(selectedMessage.value);
|
|
@@ -269,12 +294,14 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
269
294
|
forceStateUpdate();
|
|
270
295
|
return;
|
|
271
296
|
}
|
|
272
|
-
// Windows: Alt+V, macOS:
|
|
297
|
+
// Windows: Alt+V, macOS: Ctrl+V - Paste from clipboard (including images)
|
|
273
298
|
// In Ink, key.meta represents:
|
|
274
299
|
// - On Windows/Linux: Alt key (Meta key)
|
|
275
|
-
// - On macOS:
|
|
276
|
-
|
|
277
|
-
|
|
300
|
+
// - On macOS: We use Ctrl+V to avoid conflict with VSCode shortcuts
|
|
301
|
+
const isPasteShortcut = process.platform === 'darwin'
|
|
302
|
+
? key.ctrl && input === 'v'
|
|
303
|
+
: key.meta && input === 'v';
|
|
304
|
+
if (isPasteShortcut) {
|
|
278
305
|
try {
|
|
279
306
|
// Try to read image from clipboard
|
|
280
307
|
if (process.platform === 'win32') {
|
|
@@ -283,7 +310,7 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
283
310
|
const psScript = `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $clipboard = [System.Windows.Forms.Clipboard]::GetImage(); if ($clipboard -ne $null) { $ms = New-Object System.IO.MemoryStream; $clipboard.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); $bytes = $ms.ToArray(); $ms.Close(); [Convert]::ToBase64String($bytes) }`;
|
|
284
311
|
const base64 = execSync(`powershell -Command "${psScript}"`, {
|
|
285
312
|
encoding: 'utf-8',
|
|
286
|
-
timeout: 5000
|
|
313
|
+
timeout: 5000,
|
|
287
314
|
}).trim();
|
|
288
315
|
if (base64 && base64.length > 100) {
|
|
289
316
|
const dataUrl = `data:image/png;base64,${base64}`;
|
|
@@ -312,7 +339,7 @@ on error
|
|
|
312
339
|
end try'`;
|
|
313
340
|
const hasImage = execSync(checkScript, {
|
|
314
341
|
encoding: 'utf-8',
|
|
315
|
-
timeout: 2000
|
|
342
|
+
timeout: 2000,
|
|
316
343
|
}).trim();
|
|
317
344
|
if (hasImage === 'hasImage') {
|
|
318
345
|
// Save clipboard image to temporary file and read it
|
|
@@ -320,12 +347,12 @@ end try'`;
|
|
|
320
347
|
const saveScript = `osascript -e 'set imgData to the clipboard as «class PNGf»' -e 'set fileRef to open for access POSIX file "${tmpFile}" with write permission' -e 'write imgData to fileRef' -e 'close access fileRef'`;
|
|
321
348
|
execSync(saveScript, {
|
|
322
349
|
encoding: 'utf-8',
|
|
323
|
-
timeout: 3000
|
|
350
|
+
timeout: 3000,
|
|
324
351
|
});
|
|
325
352
|
// Read the file as base64
|
|
326
353
|
const base64 = execSync(`base64 -i "${tmpFile}"`, {
|
|
327
354
|
encoding: 'utf-8',
|
|
328
|
-
timeout: 2000
|
|
355
|
+
timeout: 2000,
|
|
329
356
|
}).trim();
|
|
330
357
|
// Clean up temp file
|
|
331
358
|
try {
|
|
@@ -357,19 +384,19 @@ end try'`;
|
|
|
357
384
|
if (process.platform === 'win32') {
|
|
358
385
|
clipboardText = execSync('powershell -Command "Get-Clipboard"', {
|
|
359
386
|
encoding: 'utf-8',
|
|
360
|
-
timeout: 2000
|
|
387
|
+
timeout: 2000,
|
|
361
388
|
}).trim();
|
|
362
389
|
}
|
|
363
390
|
else if (process.platform === 'darwin') {
|
|
364
391
|
clipboardText = execSync('pbpaste', {
|
|
365
392
|
encoding: 'utf-8',
|
|
366
|
-
timeout: 2000
|
|
393
|
+
timeout: 2000,
|
|
367
394
|
}).trim();
|
|
368
395
|
}
|
|
369
396
|
else {
|
|
370
397
|
clipboardText = execSync('xclip -selection clipboard -o', {
|
|
371
398
|
encoding: 'utf-8',
|
|
372
|
-
timeout: 2000
|
|
399
|
+
timeout: 2000,
|
|
373
400
|
}).trim();
|
|
374
401
|
}
|
|
375
402
|
if (clipboardText) {
|
|
@@ -436,7 +463,8 @@ end try'`;
|
|
|
436
463
|
}
|
|
437
464
|
// Enter - select command
|
|
438
465
|
if (key.return) {
|
|
439
|
-
if (filteredCommands.length > 0 &&
|
|
466
|
+
if (filteredCommands.length > 0 &&
|
|
467
|
+
commandSelectedIndex < filteredCommands.length) {
|
|
440
468
|
const selectedCommand = filteredCommands[commandSelectedIndex];
|
|
441
469
|
if (selectedCommand) {
|
|
442
470
|
// Execute command instead of inserting text
|
|
@@ -462,9 +490,11 @@ end try'`;
|
|
|
462
490
|
// 获取图片数据,但只包含占位符仍然存在的图片
|
|
463
491
|
const currentText = buffer.text; // 使用内部文本(包含占位符)
|
|
464
492
|
const allImages = buffer.getImages();
|
|
465
|
-
const validImages = allImages
|
|
493
|
+
const validImages = allImages
|
|
494
|
+
.filter(img => currentText.includes(img.placeholder))
|
|
495
|
+
.map(img => ({
|
|
466
496
|
data: img.data,
|
|
467
|
-
mimeType: img.mimeType
|
|
497
|
+
mimeType: img.mimeType,
|
|
468
498
|
}));
|
|
469
499
|
buffer.setText('');
|
|
470
500
|
forceUpdate({});
|
|
@@ -548,7 +578,8 @@ end try'`;
|
|
|
548
578
|
const displayText = buffer.text;
|
|
549
579
|
const cursorPos = buffer.getCursorPosition();
|
|
550
580
|
// 检查是否包含粘贴占位符或图片占位符并高亮显示
|
|
551
|
-
const hasPastePlaceholder = displayText.includes('[Paste ') &&
|
|
581
|
+
const hasPastePlaceholder = displayText.includes('[Paste ') &&
|
|
582
|
+
displayText.includes(' characters #');
|
|
552
583
|
const hasImagePlaceholder = displayText.includes('[image #');
|
|
553
584
|
if (hasPastePlaceholder || hasImagePlaceholder) {
|
|
554
585
|
const atCursor = (() => {
|
|
@@ -571,7 +602,7 @@ end try'`;
|
|
|
571
602
|
cursorRendered = true;
|
|
572
603
|
const beforeCursorInPart = cpSlice(part, 0, cursorPos - partStart);
|
|
573
604
|
const afterCursorInPart = cpSlice(part, cursorPos - partStart + 1);
|
|
574
|
-
return (React.createElement(React.Fragment, { key: partIndex }, isPlaceholder ? (React.createElement(Text, { color: isImagePlaceholder ?
|
|
605
|
+
return (React.createElement(React.Fragment, { key: partIndex }, isPlaceholder ? (React.createElement(Text, { color: isImagePlaceholder ? 'magenta' : 'cyan', dimColor: true },
|
|
575
606
|
beforeCursorInPart,
|
|
576
607
|
React.createElement(Text, { backgroundColor: "white", color: "black" }, atCursor),
|
|
577
608
|
afterCursorInPart)) : (React.createElement(React.Fragment, null,
|
|
@@ -580,7 +611,7 @@ end try'`;
|
|
|
580
611
|
afterCursorInPart))));
|
|
581
612
|
}
|
|
582
613
|
else {
|
|
583
|
-
return isPlaceholder ? (React.createElement(Text, { key: partIndex, color: isImagePlaceholder ?
|
|
614
|
+
return isPlaceholder ? (React.createElement(Text, { key: partIndex, color: isImagePlaceholder ? 'magenta' : 'cyan', dimColor: true }, part)) : (React.createElement(Text, { key: partIndex }, part));
|
|
584
615
|
}
|
|
585
616
|
});
|
|
586
617
|
return (React.createElement(Text, null,
|
|
@@ -600,8 +631,8 @@ end try'`;
|
|
|
600
631
|
}
|
|
601
632
|
else {
|
|
602
633
|
return (React.createElement(React.Fragment, null,
|
|
603
|
-
React.createElement(Text, { backgroundColor: disabled ?
|
|
604
|
-
React.createElement(Text, { color: disabled ?
|
|
634
|
+
React.createElement(Text, { backgroundColor: disabled ? 'gray' : 'white', color: disabled ? 'darkGray' : 'black' }, ' '),
|
|
635
|
+
React.createElement(Text, { color: disabled ? 'darkGray' : 'gray', dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
|
|
605
636
|
}
|
|
606
637
|
}, [buffer, disabled, placeholder]);
|
|
607
638
|
return (React.createElement(Box, { flexDirection: "column", marginX: 1, key: `input-${showFilePicker ? 'picker' : 'normal'}` },
|
|
@@ -644,10 +675,21 @@ end try'`;
|
|
|
644
675
|
React.createElement(Box, null,
|
|
645
676
|
React.createElement(FileList, { ref: fileListRef, query: fileQuery, selectedIndex: fileSelectedIndex, visible: showFilePicker, maxItems: 10, rootPath: process.cwd(), onFilteredCountChange: handleFilteredCountChange })),
|
|
646
677
|
yoloMode && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
|
|
647
|
-
React.createElement(Text, { color: "yellow", dimColor: true }, "\u2741
|
|
678
|
+
React.createElement(Text, { color: "yellow", dimColor: true }, "\u2741 YOLO MODE ACTIVE - All tools will be auto-approved without confirmation"))),
|
|
648
679
|
contextUsage && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
|
|
649
680
|
React.createElement(Text, { color: "gray", dimColor: true }, (() => {
|
|
650
|
-
|
|
681
|
+
// Determine which caching system is being used
|
|
682
|
+
const isAnthropic = (contextUsage.cacheCreationTokens || 0) > 0 ||
|
|
683
|
+
(contextUsage.cacheReadTokens || 0) > 0;
|
|
684
|
+
const isOpenAI = (contextUsage.cachedTokens || 0) > 0;
|
|
685
|
+
// For Anthropic: Total = inputTokens + cacheCreationTokens + cacheReadTokens
|
|
686
|
+
// For OpenAI: Total = inputTokens (cachedTokens are already included in inputTokens)
|
|
687
|
+
const totalInputTokens = isAnthropic
|
|
688
|
+
? contextUsage.inputTokens +
|
|
689
|
+
(contextUsage.cacheCreationTokens || 0) +
|
|
690
|
+
(contextUsage.cacheReadTokens || 0)
|
|
691
|
+
: contextUsage.inputTokens;
|
|
692
|
+
const percentage = Math.min(100, (totalInputTokens / contextUsage.maxContextTokens) * 100);
|
|
651
693
|
let color;
|
|
652
694
|
if (percentage < 50)
|
|
653
695
|
color = 'green';
|
|
@@ -662,21 +704,45 @@ end try'`;
|
|
|
662
704
|
return `${(num / 1000).toFixed(1)}k`;
|
|
663
705
|
return num.toString();
|
|
664
706
|
};
|
|
707
|
+
const hasCacheMetrics = isAnthropic || isOpenAI;
|
|
665
708
|
return (React.createElement(React.Fragment, null,
|
|
666
709
|
React.createElement(Text, { color: color },
|
|
667
710
|
percentage.toFixed(1),
|
|
668
711
|
"%"),
|
|
669
712
|
React.createElement(Text, null, " \u00B7 "),
|
|
670
|
-
React.createElement(Text, { color: color }, formatNumber(
|
|
671
|
-
React.createElement(Text, null, " tokens")
|
|
713
|
+
React.createElement(Text, { color: color }, formatNumber(totalInputTokens)),
|
|
714
|
+
React.createElement(Text, null, " tokens"),
|
|
715
|
+
hasCacheMetrics && (React.createElement(React.Fragment, null,
|
|
716
|
+
React.createElement(Text, null, " \u00B7 "),
|
|
717
|
+
isAnthropic && (React.createElement(React.Fragment, null,
|
|
718
|
+
(contextUsage.cacheReadTokens || 0) > 0 && (React.createElement(React.Fragment, null,
|
|
719
|
+
React.createElement(Text, { color: "cyan" },
|
|
720
|
+
"\u21AF",
|
|
721
|
+
' ',
|
|
722
|
+
formatNumber(contextUsage.cacheReadTokens || 0),
|
|
723
|
+
' ',
|
|
724
|
+
"cached"))),
|
|
725
|
+
(contextUsage.cacheCreationTokens || 0) > 0 && (React.createElement(React.Fragment, null,
|
|
726
|
+
(contextUsage.cacheReadTokens || 0) > 0 && (React.createElement(Text, null, " \u00B7 ")),
|
|
727
|
+
React.createElement(Text, { color: "magenta" },
|
|
728
|
+
"\u25C6",
|
|
729
|
+
' ',
|
|
730
|
+
formatNumber(contextUsage.cacheCreationTokens || 0),
|
|
731
|
+
' ',
|
|
732
|
+
"new cache"))))),
|
|
733
|
+
isOpenAI && (React.createElement(Text, { color: "cyan" },
|
|
734
|
+
"\u21AF ",
|
|
735
|
+
formatNumber(contextUsage.cachedTokens || 0),
|
|
736
|
+
' ',
|
|
737
|
+
"cached"))))));
|
|
672
738
|
})()))),
|
|
673
739
|
React.createElement(Box, { marginTop: 1 },
|
|
674
740
|
React.createElement(Text, { color: "gray", dimColor: true }, showCommands && getFilteredCommands().length > 0
|
|
675
|
-
?
|
|
741
|
+
? 'Type to filter commands'
|
|
676
742
|
: showFilePicker
|
|
677
|
-
?
|
|
743
|
+
? 'Type to filter files • Tab/Enter to select • ESC to cancel'
|
|
678
744
|
: (() => {
|
|
679
|
-
const pasteKey = process.platform === 'darwin' ? '
|
|
745
|
+
const pasteKey = process.platform === 'darwin' ? 'Ctrl+V' : 'Alt+V';
|
|
680
746
|
return `Ctrl+L: delete to start • Ctrl+R: delete to end • ${pasteKey}: paste images • '@': files • '/': commands`;
|
|
681
747
|
})()))))));
|
|
682
748
|
}
|
|
@@ -3,7 +3,6 @@ interface Props {
|
|
|
3
3
|
oldContent?: string;
|
|
4
4
|
newContent: string;
|
|
5
5
|
filename?: string;
|
|
6
|
-
maxLines?: number;
|
|
7
6
|
}
|
|
8
|
-
export default function DiffViewer({ oldContent, newContent, filename
|
|
7
|
+
export default function DiffViewer({ oldContent, newContent, filename }: Props): React.JSX.Element;
|
|
9
8
|
export {};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import * as Diff from 'diff';
|
|
4
|
-
export default function DiffViewer({ oldContent = '', newContent, filename
|
|
4
|
+
export default function DiffViewer({ oldContent = '', newContent, filename }) {
|
|
5
5
|
// If no old content, show as new file creation
|
|
6
6
|
const isNewFile = !oldContent || oldContent.trim() === '';
|
|
7
7
|
if (isNewFile) {
|
|
8
|
-
const
|
|
9
|
-
const totalLines =
|
|
8
|
+
const allLines = newContent.split('\n');
|
|
9
|
+
const totalLines = allLines.length;
|
|
10
10
|
const lineNumberWidth = String(totalLines).length;
|
|
11
11
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
12
12
|
React.createElement(Box, { marginBottom: 1 },
|
|
@@ -14,80 +14,80 @@ export default function DiffViewer({ oldContent = '', newContent, filename, maxL
|
|
|
14
14
|
filename && (React.createElement(Text, { color: "cyan" },
|
|
15
15
|
' ',
|
|
16
16
|
filename))),
|
|
17
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
line)))),
|
|
25
|
-
totalLines > maxLines && (React.createElement(Box, { marginTop: 1 },
|
|
26
|
-
React.createElement(Text, { dimColor: true },
|
|
27
|
-
"... ",
|
|
28
|
-
totalLines - maxLines,
|
|
29
|
-
" more lines"))))));
|
|
17
|
+
React.createElement(Box, { flexDirection: "column" }, allLines.map((line, index) => (React.createElement(Box, { key: index },
|
|
18
|
+
React.createElement(Text, { color: "gray", dimColor: true },
|
|
19
|
+
String(index + 1).padStart(lineNumberWidth, ' '),
|
|
20
|
+
" \u2502"),
|
|
21
|
+
React.createElement(Text, { color: "white", backgroundColor: "green" },
|
|
22
|
+
"+ ",
|
|
23
|
+
line)))))));
|
|
30
24
|
}
|
|
31
25
|
// Generate diff
|
|
32
26
|
const diffResult = Diff.diffLines(oldContent, newContent);
|
|
33
|
-
// Calculate line numbers
|
|
27
|
+
// Calculate line numbers and build display lines
|
|
34
28
|
let oldLineNum = 1;
|
|
35
29
|
let newLineNum = 1;
|
|
36
30
|
const totalOldLines = oldContent.split('\n').length;
|
|
37
31
|
const totalNewLines = newContent.split('\n').length;
|
|
38
32
|
const lineNumberWidth = Math.max(String(totalOldLines).length, String(totalNewLines).length);
|
|
33
|
+
const displayLines = [];
|
|
34
|
+
diffResult.forEach((part) => {
|
|
35
|
+
const lines = part.value.replace(/\n$/, '').split('\n');
|
|
36
|
+
lines.forEach((line) => {
|
|
37
|
+
if (part.added) {
|
|
38
|
+
displayLines.push({
|
|
39
|
+
type: 'added',
|
|
40
|
+
content: line,
|
|
41
|
+
newLineNum: newLineNum++
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else if (part.removed) {
|
|
45
|
+
displayLines.push({
|
|
46
|
+
type: 'removed',
|
|
47
|
+
content: line,
|
|
48
|
+
oldLineNum: oldLineNum++
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
displayLines.push({
|
|
53
|
+
type: 'unchanged',
|
|
54
|
+
content: line,
|
|
55
|
+
oldLineNum: oldLineNum++,
|
|
56
|
+
newLineNum: newLineNum++
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
39
61
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
40
62
|
React.createElement(Box, { marginBottom: 1 },
|
|
41
63
|
React.createElement(Text, { bold: true, color: "yellow" }, "[File Modified]"),
|
|
42
64
|
filename && (React.createElement(Text, { color: "cyan" },
|
|
43
65
|
' ',
|
|
44
66
|
filename))),
|
|
45
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
React.createElement(Text, { color: "white", backgroundColor: "red" },
|
|
72
|
-
"- ",
|
|
73
|
-
line)));
|
|
74
|
-
}
|
|
75
|
-
// Unchanged lines
|
|
76
|
-
const currentOldLineNum = oldLineNum++;
|
|
77
|
-
const currentNewLineNum = newLineNum++;
|
|
78
|
-
return (React.createElement(Box, { key: `${index}-${lineIndex}` },
|
|
79
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
80
|
-
String(currentOldLineNum).padStart(lineNumberWidth, ' '),
|
|
81
|
-
' '),
|
|
82
|
-
React.createElement(Text, { color: "gray", dimColor: true },
|
|
83
|
-
String(currentNewLineNum).padStart(lineNumberWidth, ' '),
|
|
84
|
-
' '),
|
|
85
|
-
React.createElement(Text, { dimColor: true }, line)));
|
|
86
|
-
});
|
|
87
|
-
}).flat(),
|
|
88
|
-
diffResult.length > maxLines && (React.createElement(Box, { marginTop: 1 },
|
|
89
|
-
React.createElement(Text, { dimColor: true },
|
|
90
|
-
"... ",
|
|
91
|
-
diffResult.length - maxLines,
|
|
92
|
-
" more lines"))))));
|
|
67
|
+
React.createElement(Box, { flexDirection: "column" }, displayLines.map((displayLine, index) => {
|
|
68
|
+
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)));
|
|
76
|
+
}
|
|
77
|
+
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)));
|
|
85
|
+
}
|
|
86
|
+
// 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)));
|
|
92
|
+
}))));
|
|
93
93
|
}
|
|
@@ -37,8 +37,7 @@ export default function MCPInfoPanel() {
|
|
|
37
37
|
};
|
|
38
38
|
}, []);
|
|
39
39
|
if (isLoading) {
|
|
40
|
-
return (React.createElement(
|
|
41
|
-
React.createElement(Text, { color: "gray" }, "Loading MCP services...")));
|
|
40
|
+
return (React.createElement(Text, { color: "gray" }, "Loading MCP services..."));
|
|
42
41
|
}
|
|
43
42
|
if (errorMessage) {
|
|
44
43
|
return (React.createElement(Box, { borderColor: "red", borderStyle: "round", paddingX: 2, paddingY: 1, marginBottom: 1 },
|
|
@@ -56,5 +56,5 @@ export default function TodoTree({ todos }) {
|
|
|
56
56
|
React.createElement(Text, { bold: true, color: "cyan" }, "TODO List")),
|
|
57
57
|
rootTodos.map(todo => renderTodo(todo)),
|
|
58
58
|
React.createElement(Box, { marginTop: 0 },
|
|
59
|
-
React.createElement(Text, { dimColor: true, color: "gray" }, "[ ] Pending \u00B7 [
|
|
59
|
+
React.createElement(Text, { dimColor: true, color: "gray" }, "[ ] Pending \u00B7 [x] Completed"))));
|
|
60
60
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
export type ConfirmationResult = 'approve' | 'approve_always' | 'reject';
|
|
3
|
+
export interface ToolCall {
|
|
4
|
+
id: string;
|
|
5
|
+
type: 'function';
|
|
6
|
+
function: {
|
|
7
|
+
name: string;
|
|
8
|
+
arguments: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
3
11
|
interface Props {
|
|
4
12
|
toolName: string;
|
|
13
|
+
toolArguments?: string;
|
|
14
|
+
allTools?: ToolCall[];
|
|
5
15
|
onConfirm: (result: ConfirmationResult) => void;
|
|
6
16
|
}
|
|
7
|
-
export default function ToolConfirmation({ toolName, onConfirm }: Props): React.JSX.Element;
|
|
17
|
+
export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm }: Props): React.JSX.Element;
|
|
8
18
|
export {};
|
|
@@ -1,8 +1,60 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
|
-
|
|
4
|
+
// Helper function to format argument values with truncation
|
|
5
|
+
function formatArgumentValue(value, maxLength = 100) {
|
|
6
|
+
if (value === null || value === undefined) {
|
|
7
|
+
return String(value);
|
|
8
|
+
}
|
|
9
|
+
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
10
|
+
if (stringValue.length <= maxLength) {
|
|
11
|
+
return stringValue;
|
|
12
|
+
}
|
|
13
|
+
return stringValue.substring(0, maxLength) + '...';
|
|
14
|
+
}
|
|
15
|
+
// Helper function to convert parsed arguments to tree display format
|
|
16
|
+
function formatArgumentsAsTree(args) {
|
|
17
|
+
const keys = Object.keys(args);
|
|
18
|
+
return keys.map((key, index) => ({
|
|
19
|
+
key,
|
|
20
|
+
value: formatArgumentValue(args[key]),
|
|
21
|
+
isLast: index === keys.length - 1
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export default function ToolConfirmation({ toolName, toolArguments, allTools, onConfirm }) {
|
|
5
25
|
const [hasSelected, setHasSelected] = useState(false);
|
|
26
|
+
// Parse and format tool arguments for display (single tool)
|
|
27
|
+
const formattedArgs = useMemo(() => {
|
|
28
|
+
if (!toolArguments)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(toolArguments);
|
|
32
|
+
return formatArgumentsAsTree(parsed);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}, [toolArguments]);
|
|
38
|
+
// Parse and format all tools arguments for display (multiple tools)
|
|
39
|
+
const formattedAllTools = useMemo(() => {
|
|
40
|
+
if (!allTools || allTools.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
return allTools.map(tool => {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(tool.function.arguments);
|
|
45
|
+
return {
|
|
46
|
+
name: tool.function.name,
|
|
47
|
+
args: formatArgumentsAsTree(parsed)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return {
|
|
52
|
+
name: tool.function.name,
|
|
53
|
+
args: []
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}, [allTools]);
|
|
6
58
|
const items = [
|
|
7
59
|
{
|
|
8
60
|
label: 'Approve (once)',
|
|
@@ -26,10 +78,38 @@ export default function ToolConfirmation({ toolName, onConfirm }) {
|
|
|
26
78
|
return (React.createElement(Box, { flexDirection: "column", marginX: 1, marginY: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1 },
|
|
27
79
|
React.createElement(Box, { marginBottom: 1 },
|
|
28
80
|
React.createElement(Text, { bold: true, color: "yellow" }, "[Tool Confirmation]")),
|
|
29
|
-
React.createElement(
|
|
30
|
-
React.createElement(
|
|
31
|
-
|
|
32
|
-
|
|
81
|
+
!formattedAllTools && (React.createElement(React.Fragment, null,
|
|
82
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
83
|
+
React.createElement(Text, null,
|
|
84
|
+
"Tool: ",
|
|
85
|
+
React.createElement(Text, { bold: true, color: "cyan" }, toolName))),
|
|
86
|
+
formattedArgs && formattedArgs.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
87
|
+
React.createElement(Text, { dimColor: true }, "Arguments:"),
|
|
88
|
+
formattedArgs.map((arg, index) => (React.createElement(Box, { key: index, flexDirection: "column" },
|
|
89
|
+
React.createElement(Text, { color: "gray", dimColor: true },
|
|
90
|
+
arg.isLast ? '└─' : '├─',
|
|
91
|
+
" ",
|
|
92
|
+
arg.key,
|
|
93
|
+
": ",
|
|
94
|
+
React.createElement(Text, { color: "white" }, arg.value))))))))),
|
|
95
|
+
formattedAllTools && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
96
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
97
|
+
React.createElement(Text, null,
|
|
98
|
+
"Tools: ",
|
|
99
|
+
React.createElement(Text, { bold: true, color: "cyan" },
|
|
100
|
+
formattedAllTools.length,
|
|
101
|
+
" tools in parallel"))),
|
|
102
|
+
formattedAllTools.map((tool, toolIndex) => (React.createElement(Box, { key: toolIndex, flexDirection: "column", marginBottom: toolIndex < formattedAllTools.length - 1 ? 1 : 0 },
|
|
103
|
+
React.createElement(Text, { color: "cyan", bold: true },
|
|
104
|
+
toolIndex + 1,
|
|
105
|
+
". ",
|
|
106
|
+
tool.name),
|
|
107
|
+
tool.args.length > 0 && (React.createElement(Box, { flexDirection: "column", paddingLeft: 2 }, tool.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: "gray", dimColor: true },
|
|
108
|
+
arg.isLast ? '└─' : '├─',
|
|
109
|
+
" ",
|
|
110
|
+
arg.key,
|
|
111
|
+
": ",
|
|
112
|
+
React.createElement(Text, { color: "white" }, arg.value))))))))))),
|
|
33
113
|
React.createElement(Box, { marginBottom: 1 },
|
|
34
114
|
React.createElement(Text, { dimColor: true }, "Select action:")),
|
|
35
115
|
!hasSelected && (React.createElement(SelectInput, { items: items, onSelect: handleSelect })),
|