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 +2 -25
- package/dist/ui/components/ChatInput.js +19 -1
- package/dist/ui/components/CommandPanel.d.ts +2 -1
- package/dist/ui/components/CommandPanel.js +38 -8
- package/dist/ui/components/FileList.js +59 -37
- package/dist/ui/components/Menu.js +1 -1
- package/dist/ui/pages/ChatScreen.js +18 -0
- package/dist/ui/pages/WelcomeScreen.js +4 -5
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Box, Text
|
|
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"
|
|
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
|
-
: "
|
|
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,
|
|
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,
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
React.createElement(Text, { color: index ===
|
|
17
|
-
index ===
|
|
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 ===
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
109
|
+
onFilteredCountChange(allFilteredFiles.length);
|
|
94
110
|
}
|
|
95
|
-
}, [
|
|
111
|
+
}, [allFilteredFiles.length, onFilteredCountChange]);
|
|
96
112
|
// Expose methods to parent
|
|
97
113
|
useImperativeHandle(ref, () => ({
|
|
98
114
|
getSelectedFile: () => {
|
|
99
|
-
if (
|
|
100
|
-
return
|
|
115
|
+
if (allFilteredFiles.length > 0 && selectedIndex < allFilteredFiles.length && allFilteredFiles[selectedIndex]) {
|
|
116
|
+
return allFilteredFiles[selectedIndex].path;
|
|
101
117
|
}
|
|
102
118
|
return null;
|
|
103
119
|
}
|
|
104
|
-
}), [
|
|
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
|
-
|
|
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 ===
|
|
124
|
-
|
|
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
|
-
"
|
|
127
|
-
|
|
128
|
-
" more
|
|
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%',
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
}
|