snow-ai 0.2.4 → 0.2.5

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/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { Box, Text, useStdout } from 'ink';
2
+ import { Box, Text } from 'ink';
3
3
  import { Alert } from '@inkjs/ui';
4
4
  import WelcomeScreen from './ui/pages/WelcomeScreen.js';
5
5
  import ApiConfigScreen from './ui/pages/ApiConfigScreen.js';
@@ -14,9 +14,6 @@ export default function App({ version }) {
14
14
  show: false,
15
15
  message: ''
16
16
  });
17
- // Terminal resize handling - force re-render on resize
18
- const { stdout } = useStdout();
19
- const [terminalSize, setTerminalSize] = useState({ columns: stdout?.columns || 80, rows: stdout?.rows || 24 });
20
17
  // Global exit handler
21
18
  useGlobalExit(setExitNotification);
22
19
  // Global navigation handler
@@ -26,26 +23,6 @@ export default function App({ version }) {
26
23
  });
27
24
  return unsubscribe;
28
25
  }, []);
29
- // Terminal resize listener with debounce
30
- useEffect(() => {
31
- if (!stdout)
32
- return;
33
- let resizeTimeout;
34
- const handleResize = () => {
35
- // Debounce resize events - wait for resize to stabilize
36
- clearTimeout(resizeTimeout);
37
- resizeTimeout = setTimeout(() => {
38
- // Clear screen before re-render
39
- stdout.write('\x1Bc'); // Full reset
40
- setTerminalSize({ columns: stdout.columns, rows: stdout.rows });
41
- }, 100); // 100ms debounce
42
- };
43
- stdout.on('resize', handleResize);
44
- return () => {
45
- stdout.off('resize', handleResize);
46
- clearTimeout(resizeTimeout);
47
- };
48
- }, [stdout]);
49
26
  const handleMenuSelect = (value) => {
50
27
  if (value === 'chat' || value === 'settings' || value === 'config' || value === 'models' || value === 'mcp') {
51
28
  setCurrentView(value);
@@ -74,7 +51,7 @@ export default function App({ version }) {
74
51
  return (React.createElement(WelcomeScreen, { version: version, onMenuSelect: handleMenuSelect }));
75
52
  }
76
53
  };
77
- return (React.createElement(Box, { flexDirection: "column", key: `term-${terminalSize.columns}x${terminalSize.rows}` },
54
+ return (React.createElement(Box, { flexDirection: "column" },
78
55
  renderView(),
79
56
  exitNotification.show && (React.createElement(Box, { paddingX: 1 },
80
57
  React.createElement(Alert, { variant: "warning" }, exitNotification.message)))));
@@ -253,6 +253,24 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
253
253
  // For any other key in history menu, just return to prevent interference
254
254
  return;
255
255
  }
256
+ // Ctrl+L - Delete from cursor to beginning
257
+ if (key.ctrl && input === 'l') {
258
+ const fullText = buffer.getFullText();
259
+ const cursorPos = buffer.getCursorPosition();
260
+ const afterCursor = fullText.slice(cursorPos);
261
+ buffer.setText(afterCursor);
262
+ forceStateUpdate();
263
+ return;
264
+ }
265
+ // Ctrl+R - Delete from cursor to end
266
+ if (key.ctrl && input === 'r') {
267
+ const fullText = buffer.getFullText();
268
+ const cursorPos = buffer.getCursorPosition();
269
+ const beforeCursor = fullText.slice(0, cursorPos);
270
+ buffer.setText(beforeCursor);
271
+ forceStateUpdate();
272
+ return;
273
+ }
256
274
  // Alt+V / Option+V - Paste from clipboard (including images)
257
275
  if (key.meta && input === 'v') {
258
276
  try {
@@ -590,5 +608,5 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
590
608
  ? "Type to filter commands"
591
609
  : showFilePicker
592
610
  ? "Type to filter files • Tab/Enter to select • ESC to cancel"
593
- : "Press Ctrl+C twice to exit • Alt+V to paste images • Type '@' for files • Type '/' for commands"))))));
611
+ : "Ctrl+L: delete to start • Ctrl+R: delete to end • Alt+V: paste images • '@': files • '/': commands"))))));
594
612
  }
@@ -8,6 +8,7 @@ interface Props {
8
8
  selectedIndex: number;
9
9
  query: string;
10
10
  visible: boolean;
11
+ maxHeight?: number;
11
12
  }
12
- declare const CommandPanel: React.MemoExoticComponent<({ commands, selectedIndex, query, visible }: Props) => React.JSX.Element | null>;
13
+ declare const CommandPanel: React.MemoExoticComponent<({ commands, selectedIndex, visible, maxHeight }: Props) => React.JSX.Element | null>;
13
14
  export default CommandPanel;
@@ -1,6 +1,31 @@
1
- import React, { memo } from 'react';
1
+ import React, { memo, useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- const CommandPanel = memo(({ commands, selectedIndex, query, visible }) => {
3
+ const CommandPanel = memo(({ commands, selectedIndex, visible, maxHeight }) => {
4
+ // Fixed maximum display items to prevent rendering issues
5
+ const MAX_DISPLAY_ITEMS = 5;
6
+ const effectiveMaxItems = maxHeight ? Math.min(maxHeight, MAX_DISPLAY_ITEMS) : MAX_DISPLAY_ITEMS;
7
+ // Limit displayed commands
8
+ const displayedCommands = useMemo(() => {
9
+ if (commands.length <= effectiveMaxItems) {
10
+ return commands;
11
+ }
12
+ // Show commands around the selected index
13
+ const halfWindow = Math.floor(effectiveMaxItems / 2);
14
+ let startIndex = Math.max(0, selectedIndex - halfWindow);
15
+ let endIndex = Math.min(commands.length, startIndex + effectiveMaxItems);
16
+ // Adjust if we're near the end
17
+ if (endIndex - startIndex < effectiveMaxItems) {
18
+ startIndex = Math.max(0, endIndex - effectiveMaxItems);
19
+ }
20
+ return commands.slice(startIndex, endIndex);
21
+ }, [commands, selectedIndex, effectiveMaxItems]);
22
+ // Calculate actual selected index in the displayed subset
23
+ const displayedSelectedIndex = useMemo(() => {
24
+ return displayedCommands.findIndex((cmd) => {
25
+ const originalIndex = commands.indexOf(cmd);
26
+ return originalIndex === selectedIndex;
27
+ });
28
+ }, [displayedCommands, commands, selectedIndex]);
4
29
  // Don't show panel if not visible or no commands found
5
30
  if (!visible || commands.length === 0) {
6
31
  return null;
@@ -11,16 +36,21 @@ const CommandPanel = memo(({ commands, selectedIndex, query, visible }) => {
11
36
  React.createElement(Box, null,
12
37
  React.createElement(Text, { color: "yellow", bold: true },
13
38
  "Available Commands ",
14
- query && `(${commands.length} matches)`)),
15
- commands.map((command, index) => (React.createElement(Box, { key: command.name, flexDirection: "column", width: "100%" },
16
- React.createElement(Text, { color: index === selectedIndex ? "green" : "gray", bold: true },
17
- index === selectedIndex ? "➣ " : " ",
39
+ commands.length > effectiveMaxItems && `(${selectedIndex + 1}/${commands.length})`)),
40
+ displayedCommands.map((command, index) => (React.createElement(Box, { key: command.name, flexDirection: "column", width: "100%" },
41
+ React.createElement(Text, { color: index === displayedSelectedIndex ? "green" : "gray", bold: true },
42
+ index === displayedSelectedIndex ? "➣ " : " ",
18
43
  "/",
19
44
  command.name),
20
45
  React.createElement(Box, { marginLeft: 3 },
21
- React.createElement(Text, { color: index === selectedIndex ? "green" : "gray", dimColor: true },
46
+ React.createElement(Text, { color: index === displayedSelectedIndex ? "green" : "gray", dimColor: true },
22
47
  "\u2514\u2500 ",
23
- command.description)))))))));
48
+ command.description))))),
49
+ commands.length > effectiveMaxItems && (React.createElement(Box, { marginTop: 1 },
50
+ React.createElement(Text, { color: "gray", dimColor: true },
51
+ "\u2191\u2193 to scroll \u00B7 ",
52
+ commands.length - effectiveMaxItems,
53
+ " more hidden")))))));
24
54
  });
25
55
  CommandPanel.displayName = 'CommandPanel';
26
56
  export default CommandPanel;
@@ -5,6 +5,11 @@ import path from 'path';
5
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
+ // Fixed maximum display items to prevent rendering issues
9
+ const MAX_DISPLAY_ITEMS = 5;
10
+ const effectiveMaxItems = useMemo(() => {
11
+ return maxItems ? Math.min(maxItems, MAX_DISPLAY_ITEMS) : MAX_DISPLAY_ITEMS;
12
+ }, [maxItems]);
8
13
  // Get files from directory - optimized to batch updates
9
14
  const loadFiles = useCallback(async () => {
10
15
  const getFilesRecursively = async (dir, depth = 0, maxDepth = 3) => {
@@ -60,48 +65,59 @@ const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10
60
65
  loadFiles();
61
66
  }
62
67
  }, [visible, loadFiles]);
63
- // Filter files based on query
64
- const filteredFiles = useMemo(() => {
65
- let filtered;
68
+ // Filter files based on query (no limit here, we'll slice for display)
69
+ const allFilteredFiles = useMemo(() => {
66
70
  if (!query.trim()) {
67
- filtered = files.slice(0, maxItems);
68
- }
69
- else {
70
- const queryLower = query.toLowerCase();
71
- filtered = files.filter(file => {
72
- const fileName = file.name.toLowerCase();
73
- const filePath = file.path.toLowerCase();
74
- return fileName.includes(queryLower) || filePath.includes(queryLower);
75
- });
76
- // Sort by relevance (exact name matches first, then path matches)
77
- filtered.sort((a, b) => {
78
- const aNameMatch = a.name.toLowerCase().startsWith(queryLower);
79
- const bNameMatch = b.name.toLowerCase().startsWith(queryLower);
80
- if (aNameMatch && !bNameMatch)
81
- return -1;
82
- if (!aNameMatch && bNameMatch)
83
- return 1;
84
- return a.name.localeCompare(b.name);
85
- });
86
- filtered = filtered.slice(0, maxItems);
71
+ return files;
87
72
  }
73
+ const queryLower = query.toLowerCase();
74
+ const filtered = files.filter(file => {
75
+ const fileName = file.name.toLowerCase();
76
+ const filePath = file.path.toLowerCase();
77
+ return fileName.includes(queryLower) || filePath.includes(queryLower);
78
+ });
79
+ // Sort by relevance (exact name matches first, then path matches)
80
+ filtered.sort((a, b) => {
81
+ const aNameMatch = a.name.toLowerCase().startsWith(queryLower);
82
+ const bNameMatch = b.name.toLowerCase().startsWith(queryLower);
83
+ if (aNameMatch && !bNameMatch)
84
+ return -1;
85
+ if (!aNameMatch && bNameMatch)
86
+ return 1;
87
+ return a.name.localeCompare(b.name);
88
+ });
88
89
  return filtered;
89
- }, [files, query, maxItems]);
90
+ }, [files, query]);
91
+ // Display with scrolling window
92
+ const filteredFiles = useMemo(() => {
93
+ if (allFilteredFiles.length <= effectiveMaxItems) {
94
+ return allFilteredFiles;
95
+ }
96
+ // Show files around the selected index
97
+ const halfWindow = Math.floor(effectiveMaxItems / 2);
98
+ let startIndex = Math.max(0, selectedIndex - halfWindow);
99
+ let endIndex = Math.min(allFilteredFiles.length, startIndex + effectiveMaxItems);
100
+ // Adjust if we're near the end
101
+ if (endIndex - startIndex < effectiveMaxItems) {
102
+ startIndex = Math.max(0, endIndex - effectiveMaxItems);
103
+ }
104
+ return allFilteredFiles.slice(startIndex, endIndex);
105
+ }, [allFilteredFiles, selectedIndex, effectiveMaxItems]);
90
106
  // Notify parent of filtered count changes
91
107
  useEffect(() => {
92
108
  if (onFilteredCountChange) {
93
- onFilteredCountChange(filteredFiles.length);
109
+ onFilteredCountChange(allFilteredFiles.length);
94
110
  }
95
- }, [filteredFiles.length, onFilteredCountChange]);
111
+ }, [allFilteredFiles.length, onFilteredCountChange]);
96
112
  // Expose methods to parent
97
113
  useImperativeHandle(ref, () => ({
98
114
  getSelectedFile: () => {
99
- if (filteredFiles.length > 0 && selectedIndex < filteredFiles.length && filteredFiles[selectedIndex]) {
100
- return filteredFiles[selectedIndex].path;
115
+ if (allFilteredFiles.length > 0 && selectedIndex < allFilteredFiles.length && allFilteredFiles[selectedIndex]) {
116
+ return allFilteredFiles[selectedIndex].path;
101
117
  }
102
118
  return null;
103
119
  }
104
- }), [filteredFiles, selectedIndex]);
120
+ }), [allFilteredFiles, selectedIndex]);
105
121
  if (!visible) {
106
122
  return null;
107
123
  }
@@ -113,19 +129,25 @@ const FileList = memo(forwardRef(({ query, selectedIndex, visible, maxItems = 10
113
129
  return (React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1 },
114
130
  React.createElement(Text, { color: "gray" }, "No files found")));
115
131
  }
132
+ // Calculate display index for the scrolling window
133
+ const displaySelectedIndex = useMemo(() => {
134
+ return filteredFiles.findIndex((file) => {
135
+ const originalIndex = allFilteredFiles.indexOf(file);
136
+ return originalIndex === selectedIndex;
137
+ });
138
+ }, [filteredFiles, allFilteredFiles, selectedIndex]);
116
139
  return (React.createElement(Box, { paddingX: 1, marginTop: 1, flexDirection: "column" },
117
140
  React.createElement(Box, { marginBottom: 1 },
118
141
  React.createElement(Text, { color: "blue", bold: true },
119
- "\uD83D\uDDD0 Files (",
120
- filteredFiles.length,
121
- ")")),
142
+ "\uD83D\uDDD0 Files ",
143
+ allFilteredFiles.length > effectiveMaxItems && `(${selectedIndex + 1}/${allFilteredFiles.length})`)),
122
144
  filteredFiles.map((file, index) => (React.createElement(Box, { key: file.path },
123
- React.createElement(Text, { backgroundColor: index === selectedIndex ? "blue" : undefined, color: index === selectedIndex ? "white" : file.isDirectory ? "cyan" : "white" }, file.path)))),
124
- filteredFiles.length === maxItems && files.length > maxItems && (React.createElement(Box, { marginTop: 1 },
145
+ React.createElement(Text, { backgroundColor: index === displaySelectedIndex ? "blue" : undefined, color: index === displaySelectedIndex ? "white" : file.isDirectory ? "cyan" : "white" }, file.path)))),
146
+ allFilteredFiles.length > effectiveMaxItems && (React.createElement(Box, { marginTop: 1 },
125
147
  React.createElement(Text, { color: "gray", dimColor: true },
126
- "... and ",
127
- files.length - maxItems,
128
- " more files")))));
148
+ "\u2191\u2193 to scroll \u00B7 ",
149
+ allFilteredFiles.length - effectiveMaxItems,
150
+ " more hidden")))));
129
151
  }));
130
152
  FileList.displayName = 'FileList';
131
153
  export default FileList;
@@ -53,7 +53,7 @@ function Menu({ options, onSelect, onSelectionChange, maxHeight }) {
53
53
  const hasMoreBelow = scrollOffset + visibleItemCount < options.length;
54
54
  const moreAboveCount = scrollOffset;
55
55
  const moreBelowCount = options.length - (scrollOffset + visibleItemCount);
56
- return (React.createElement(Box, { flexDirection: "column", width: '100%', borderStyle: 'round', borderColor: "#A9C13E", padding: 1 },
56
+ return (React.createElement(Box, { flexDirection: "column", width: '100%', padding: 1 },
57
57
  React.createElement(Box, { marginBottom: 1 },
58
58
  React.createElement(Text, { color: "cyan" }, "Use \u2191\u2193 keys to navigate, press Enter to select:")),
59
59
  hasMoreAbove && (React.createElement(Box, null,
@@ -69,7 +69,10 @@ export default function ChatScreen({}) {
69
69
  const [isCompressing, setIsCompressing] = useState(false);
70
70
  const [compressionError, setCompressionError] = useState(null);
71
71
  const { stdout } = useStdout();
72
+ const terminalHeight = stdout?.rows || 24;
72
73
  const workingDirectory = process.cwd();
74
+ // Minimum terminal height required for proper rendering
75
+ const MIN_TERMINAL_HEIGHT = 10;
73
76
  // Use session save hook
74
77
  const { saveMessage, clearSavedMessages, initializeFromSession } = useSessionSave();
75
78
  // Sync pendingMessages to ref for real-time access in callbacks
@@ -481,6 +484,21 @@ export default function ChatScreen({}) {
481
484
  if (showMcpInfo) {
482
485
  return (React.createElement(MCPInfoScreen, { onClose: () => setShowMcpInfo(false), panelKey: mcpPanelKey }));
483
486
  }
487
+ // Show warning if terminal is too small
488
+ if (terminalHeight < MIN_TERMINAL_HEIGHT) {
489
+ return (React.createElement(Box, { flexDirection: "column", padding: 2 },
490
+ React.createElement(Box, { borderStyle: "round", borderColor: "red", padding: 1 },
491
+ React.createElement(Text, { color: "red", bold: true }, "\u26A0 Terminal Too Small")),
492
+ React.createElement(Box, { marginTop: 1 },
493
+ React.createElement(Text, { color: "yellow" },
494
+ "Your terminal height is ",
495
+ terminalHeight,
496
+ " lines, but at least ",
497
+ MIN_TERMINAL_HEIGHT,
498
+ " lines are required.")),
499
+ React.createElement(Box, { marginTop: 1 },
500
+ React.createElement(Text, { color: "gray", dimColor: true }, "Please resize your terminal window to continue."))));
501
+ }
484
502
  return (React.createElement(Box, { flexDirection: "column" },
485
503
  React.createElement(Static, { key: remountKey, items: [
486
504
  React.createElement(Box, { key: "header", marginX: 1, borderColor: 'cyan', borderStyle: "round", paddingX: 2, paddingY: 1 },
@@ -46,9 +46,8 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
46
46
  React.createElement(Text, { color: "gray", dimColor: true }, "Intelligent Command Line Assistant"),
47
47
  React.createElement(Text, { color: "magenta", dimColor: true },
48
48
  "Version ",
49
- version))),
50
- onMenuSelect && (React.createElement(Box, null,
51
- React.createElement(Menu, { options: menuOptions, onSelect: onMenuSelect, onSelectionChange: handleSelectionChange }))),
52
- React.createElement(Box, { justifyContent: "space-between" },
53
- React.createElement(Alert, { variant: 'info' }, infoText))));
49
+ version),
50
+ onMenuSelect && (React.createElement(Box, null,
51
+ React.createElement(Menu, { options: menuOptions, onSelect: onMenuSelect, onSelectionChange: handleSelectionChange }))),
52
+ React.createElement(Alert, { variant: 'info' }, infoText)))));
54
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {