snow-ai 0.2.12 → 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.
@@ -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
- { name: 'yolo', description: 'Toggle unattended mode (auto-approve all tools)' },
15
- { name: 'init', description: 'Analyze project and generate/update SNOW.md documentation' },
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
- { name: 'compact', description: 'Compress conversation history using compact model' }
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 || fileQuery !== afterAt || atSymbolPosition !== lastAtIndex) {
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++) { // +2 for @ and space
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) { // This will be 2 after increment
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 && historySelectedIndex < userMessages.length) {
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: Option+V - Paste from clipboard (including images)
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: Option key is also mapped to meta in most terminal emulators
276
- // So we can use key.meta for both platforms
277
- if (key.meta && input === 'v') {
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 && commandSelectedIndex < filteredCommands.length) {
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.filter(img => currentText.includes(img.placeholder)).map(img => ({
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 ') && displayText.includes(' characters #');
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 ? "magenta" : "cyan", dimColor: true },
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 ? "magenta" : "cyan", dimColor: true }, part)) : (React.createElement(Text, { key: partIndex }, part));
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 ? "gray" : "white", color: disabled ? "darkGray" : "black" }, ' '),
604
- React.createElement(Text, { color: disabled ? "darkGray" : "gray", dimColor: true }, disabled ? 'Waiting for response...' : placeholder)));
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,16 +675,19 @@ 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 YOLO MODE ACTIVE - All tools will be auto-approved without confirmation"))),
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
651
- const isAnthropic = (contextUsage.cacheCreationTokens || 0) > 0 || (contextUsage.cacheReadTokens || 0) > 0;
682
+ const isAnthropic = (contextUsage.cacheCreationTokens || 0) > 0 ||
683
+ (contextUsage.cacheReadTokens || 0) > 0;
652
684
  const isOpenAI = (contextUsage.cachedTokens || 0) > 0;
653
- // For Anthropic: Total = inputTokens + cacheCreationTokens
685
+ // For Anthropic: Total = inputTokens + cacheCreationTokens + cacheReadTokens
654
686
  // For OpenAI: Total = inputTokens (cachedTokens are already included in inputTokens)
655
687
  const totalInputTokens = isAnthropic
656
- ? contextUsage.inputTokens + (contextUsage.cacheCreationTokens || 0)
688
+ ? contextUsage.inputTokens +
689
+ (contextUsage.cacheCreationTokens || 0) +
690
+ (contextUsage.cacheReadTokens || 0)
657
691
  : contextUsage.inputTokens;
658
692
  const percentage = Math.min(100, (totalInputTokens / contextUsage.maxContextTokens) * 100);
659
693
  let color;
@@ -683,27 +717,32 @@ end try'`;
683
717
  isAnthropic && (React.createElement(React.Fragment, null,
684
718
  (contextUsage.cacheReadTokens || 0) > 0 && (React.createElement(React.Fragment, null,
685
719
  React.createElement(Text, { color: "cyan" },
686
- "\u21AF ",
720
+ "\u21AF",
721
+ ' ',
687
722
  formatNumber(contextUsage.cacheReadTokens || 0),
688
- " cached"))),
723
+ ' ',
724
+ "cached"))),
689
725
  (contextUsage.cacheCreationTokens || 0) > 0 && (React.createElement(React.Fragment, null,
690
- (contextUsage.cacheReadTokens || 0) > 0 && React.createElement(Text, null, " \u00B7 "),
726
+ (contextUsage.cacheReadTokens || 0) > 0 && (React.createElement(Text, null, " \u00B7 ")),
691
727
  React.createElement(Text, { color: "magenta" },
692
- "\u25C6 ",
728
+ "\u25C6",
729
+ ' ',
693
730
  formatNumber(contextUsage.cacheCreationTokens || 0),
694
- " new cache"))))),
731
+ ' ',
732
+ "new cache"))))),
695
733
  isOpenAI && (React.createElement(Text, { color: "cyan" },
696
734
  "\u21AF ",
697
735
  formatNumber(contextUsage.cachedTokens || 0),
698
- " cached"))))));
736
+ ' ',
737
+ "cached"))))));
699
738
  })()))),
700
739
  React.createElement(Box, { marginTop: 1 },
701
740
  React.createElement(Text, { color: "gray", dimColor: true }, showCommands && getFilteredCommands().length > 0
702
- ? "Type to filter commands"
741
+ ? 'Type to filter commands'
703
742
  : showFilePicker
704
- ? "Type to filter files • Tab/Enter to select • ESC to cancel"
743
+ ? 'Type to filter files • Tab/Enter to select • ESC to cancel'
705
744
  : (() => {
706
- const pasteKey = process.platform === 'darwin' ? 'Option+V' : 'Alt+V';
745
+ const pasteKey = process.platform === 'darwin' ? 'Ctrl+V' : 'Alt+V';
707
746
  return `Ctrl+L: delete to start • Ctrl+R: delete to end • ${pasteKey}: paste images • '@': files • '/': commands`;
708
747
  })()))))));
709
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, maxLines }: Props): React.JSX.Element;
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, maxLines = 100 }) {
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 lines = newContent.split('\n').slice(0, maxLines);
9
- const totalLines = newContent.split('\n').length;
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
- lines.map((line, index) => (React.createElement(Box, { key: index },
19
- React.createElement(Text, { color: "gray", dimColor: true },
20
- String(index + 1).padStart(lineNumberWidth, ' '),
21
- ' '),
22
- React.createElement(Text, { color: "white", backgroundColor: "green" },
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
- diffResult.slice(0, maxLines).map((part, index) => {
47
- const lines = part.value.replace(/\n$/, '').split('\n');
48
- return lines.map((line, lineIndex) => {
49
- if (part.added) {
50
- const currentLineNum = newLineNum++;
51
- return (React.createElement(Box, { key: `${index}-${lineIndex}` },
52
- React.createElement(Text, { color: "gray", dimColor: true },
53
- ' '.repeat(lineNumberWidth),
54
- ' '),
55
- React.createElement(Text, { color: "gray", dimColor: true },
56
- String(currentLineNum).padStart(lineNumberWidth, ' '),
57
- ' '),
58
- React.createElement(Text, { color: "white", backgroundColor: "green" },
59
- "+ ",
60
- line)));
61
- }
62
- if (part.removed) {
63
- const currentLineNum = oldLineNum++;
64
- return (React.createElement(Box, { key: `${index}-${lineIndex}` },
65
- React.createElement(Text, { color: "gray", dimColor: true },
66
- String(currentLineNum).padStart(lineNumberWidth, ' '),
67
- ' '),
68
- React.createElement(Text, { color: "gray", dimColor: true },
69
- ' '.repeat(lineNumberWidth),
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(Box, { borderColor: "gray", borderStyle: "round", paddingX: 2, paddingY: 1, marginBottom: 1 },
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 [~] In Progress \u00B7 [x] Completed"))));
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
- export default function ToolConfirmation({ toolName, onConfirm }) {
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(Box, { marginBottom: 1 },
30
- React.createElement(Text, null,
31
- "Tool: ",
32
- React.createElement(Text, { bold: true, color: "cyan" }, toolName))),
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 })),