snow-ai 0.2.3 → 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/api/responses.js +1 -1
- package/dist/mcp/filesystem.d.ts +2 -0
- package/dist/mcp/filesystem.js +22 -1
- package/dist/ui/components/ChatInput.js +22 -2
- 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.d.ts +2 -0
- package/dist/ui/pages/ChatScreen.js +174 -7
- package/dist/ui/pages/ModelConfigScreen.js +118 -5
- package/dist/ui/pages/WelcomeScreen.js +4 -5
- package/dist/utils/apiConfig.d.ts +6 -0
- package/dist/utils/commandExecutor.d.ts +1 -1
- package/dist/utils/commands/compact.d.ts +2 -0
- package/dist/utils/commands/compact.js +12 -0
- package/dist/utils/commands/ide.d.ts +2 -0
- package/dist/utils/commands/ide.js +29 -0
- package/dist/utils/contextCompressor.d.ts +15 -0
- package/dist/utils/contextCompressor.js +69 -0
- package/dist/utils/fileUtils.d.ts +8 -0
- package/dist/utils/fileUtils.js +20 -1
- package/dist/utils/vscodeConnection.d.ts +41 -0
- package/dist/utils/vscodeConnection.js +155 -0
- package/package.json +4 -2
package/dist/api/responses.js
CHANGED
|
@@ -476,7 +476,7 @@ export async function createResponseWithTools(options, maxToolRounds = 5) {
|
|
|
476
476
|
reasoning: options.reasoning || { summary: 'auto', effort: 'high' },
|
|
477
477
|
store: options.store ?? false, // 默认不存储对话历史,提高缓存命中
|
|
478
478
|
include: options.include || ['reasoning.encrypted_content'], // 包含加密推理内容
|
|
479
|
-
prompt_cache_key: options.prompt_cache_key, //
|
|
479
|
+
prompt_cache_key: options.prompt_cache_key, // 缓存键
|
|
480
480
|
});
|
|
481
481
|
const output = response.output;
|
|
482
482
|
if (!output || output.length === 0) {
|
package/dist/mcp/filesystem.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Diagnostic } from '../utils/vscodeConnection.js';
|
|
1
2
|
interface SearchMatch {
|
|
2
3
|
filePath: string;
|
|
3
4
|
lineNumber: number;
|
|
@@ -90,6 +91,7 @@ export declare class FilesystemMCPService {
|
|
|
90
91
|
contextStartLine: number;
|
|
91
92
|
contextEndLine: number;
|
|
92
93
|
totalLines: number;
|
|
94
|
+
diagnostics?: Diagnostic[];
|
|
93
95
|
}>;
|
|
94
96
|
/**
|
|
95
97
|
* Search for code keywords in files within a directory
|
package/dist/mcp/filesystem.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { vscodeConnection } from '../utils/vscodeConnection.js';
|
|
3
4
|
const { resolve, dirname, isAbsolute } = path;
|
|
4
5
|
/**
|
|
5
6
|
* Filesystem MCP Service
|
|
@@ -251,7 +252,17 @@ export class FilesystemMCPService {
|
|
|
251
252
|
const newContextContent = newContextLines.join('\n');
|
|
252
253
|
// Write the modified content back to file
|
|
253
254
|
await fs.writeFile(fullPath, modifiedLines.join('\n'), 'utf-8');
|
|
254
|
-
|
|
255
|
+
// Try to get diagnostics from VS Code after editing
|
|
256
|
+
let diagnostics = [];
|
|
257
|
+
try {
|
|
258
|
+
// Wait a bit for VS Code to process the file change
|
|
259
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
260
|
+
diagnostics = await vscodeConnection.requestDiagnostics(fullPath);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
// Ignore diagnostics errors, they are optional
|
|
264
|
+
}
|
|
265
|
+
const result = {
|
|
255
266
|
message: `File edited successfully: ${filePath} (lines ${startLine}-${adjustedEndLine} replaced)`,
|
|
256
267
|
oldContent,
|
|
257
268
|
newContent: newContextContent,
|
|
@@ -259,6 +270,16 @@ export class FilesystemMCPService {
|
|
|
259
270
|
contextEndLine: newContextEnd,
|
|
260
271
|
totalLines: newTotalLines
|
|
261
272
|
};
|
|
273
|
+
// Add diagnostics if any were found
|
|
274
|
+
if (diagnostics.length > 0) {
|
|
275
|
+
result.diagnostics = diagnostics;
|
|
276
|
+
const errorCount = diagnostics.filter(d => d.severity === 'error').length;
|
|
277
|
+
const warningCount = diagnostics.filter(d => d.severity === 'warning').length;
|
|
278
|
+
if (errorCount > 0 || warningCount > 0) {
|
|
279
|
+
result.message += `\n\n⚠️ Diagnostics detected: ${errorCount} error(s), ${warningCount} warning(s)`;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
262
283
|
}
|
|
263
284
|
catch (error) {
|
|
264
285
|
throw new Error(`Failed to edit file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -12,7 +12,9 @@ const commands = [
|
|
|
12
12
|
{ name: 'resume', description: 'Resume a conversation' },
|
|
13
13
|
{ name: 'mcp', description: 'Show Model Context Protocol services and tools' },
|
|
14
14
|
{ name: 'yolo', description: 'Toggle unattended mode (auto-approve all tools)' },
|
|
15
|
-
{ name: 'init', description: 'Analyze project and generate/update SNOW.md documentation' }
|
|
15
|
+
{ name: 'init', description: 'Analyze project and generate/update SNOW.md documentation' },
|
|
16
|
+
{ name: 'ide', description: 'Connect to VSCode editor and sync context' },
|
|
17
|
+
{ name: 'compact', description: 'Compress conversation history using compact model' }
|
|
16
18
|
];
|
|
17
19
|
export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type your message...', disabled = false, chatHistory = [], onHistorySelect, yoloMode = false, contextUsage }) {
|
|
18
20
|
const { stdout } = useStdout();
|
|
@@ -251,6 +253,24 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
251
253
|
// For any other key in history menu, just return to prevent interference
|
|
252
254
|
return;
|
|
253
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
|
+
}
|
|
254
274
|
// Alt+V / Option+V - Paste from clipboard (including images)
|
|
255
275
|
if (key.meta && input === 'v') {
|
|
256
276
|
try {
|
|
@@ -588,5 +608,5 @@ export default function ChatInput({ onSubmit, onCommand, placeholder = 'Type you
|
|
|
588
608
|
? "Type to filter commands"
|
|
589
609
|
: showFilePicker
|
|
590
610
|
? "Type to filter files • Tab/Enter to select • ESC to cancel"
|
|
591
|
-
: "
|
|
611
|
+
: "Ctrl+L: delete to start • Ctrl+R: delete to end • Alt+V: paste images • '@': files • '/': commands"))))));
|
|
592
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,
|
|
@@ -4,6 +4,8 @@ import '../../utils/commands/resume.js';
|
|
|
4
4
|
import '../../utils/commands/mcp.js';
|
|
5
5
|
import '../../utils/commands/yolo.js';
|
|
6
6
|
import '../../utils/commands/init.js';
|
|
7
|
+
import '../../utils/commands/ide.js';
|
|
8
|
+
import '../../utils/commands/compact.js';
|
|
7
9
|
type Props = {};
|
|
8
10
|
export default function ChatScreen({}: Props): React.JSX.Element;
|
|
9
11
|
export {};
|
|
@@ -18,13 +18,17 @@ import { useSessionManagement } from '../../hooks/useSessionManagement.js';
|
|
|
18
18
|
import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
|
|
19
19
|
import { handleConversationWithTools } from '../../hooks/useConversation.js';
|
|
20
20
|
import { parseAndValidateFileReferences, createMessageWithFileInstructions, getSystemInfo } from '../../utils/fileUtils.js';
|
|
21
|
+
import { compressContext } from '../../utils/contextCompressor.js';
|
|
21
22
|
// Import commands to register them
|
|
22
23
|
import '../../utils/commands/clear.js';
|
|
23
24
|
import '../../utils/commands/resume.js';
|
|
24
25
|
import '../../utils/commands/mcp.js';
|
|
25
26
|
import '../../utils/commands/yolo.js';
|
|
26
27
|
import '../../utils/commands/init.js';
|
|
28
|
+
import '../../utils/commands/ide.js';
|
|
29
|
+
import '../../utils/commands/compact.js';
|
|
27
30
|
import { navigateTo } from '../../hooks/useGlobalNavigation.js';
|
|
31
|
+
import { vscodeConnection } from '../../utils/vscodeConnection.js';
|
|
28
32
|
// Format elapsed time to human readable format
|
|
29
33
|
function formatElapsedTime(seconds) {
|
|
30
34
|
if (seconds < 60) {
|
|
@@ -59,8 +63,16 @@ export default function ChatScreen({}) {
|
|
|
59
63
|
const [contextUsage, setContextUsage] = useState(null);
|
|
60
64
|
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
61
65
|
const [timerStartTime, setTimerStartTime] = useState(null);
|
|
66
|
+
const [vscodeConnected, setVscodeConnected] = useState(false);
|
|
67
|
+
const [vscodeConnectionStatus, setVscodeConnectionStatus] = useState('disconnected');
|
|
68
|
+
const [editorContext, setEditorContext] = useState({});
|
|
69
|
+
const [isCompressing, setIsCompressing] = useState(false);
|
|
70
|
+
const [compressionError, setCompressionError] = useState(null);
|
|
62
71
|
const { stdout } = useStdout();
|
|
72
|
+
const terminalHeight = stdout?.rows || 24;
|
|
63
73
|
const workingDirectory = process.cwd();
|
|
74
|
+
// Minimum terminal height required for proper rendering
|
|
75
|
+
const MIN_TERMINAL_HEIGHT = 10;
|
|
64
76
|
// Use session save hook
|
|
65
77
|
const { saveMessage, clearSavedMessages, initializeFromSession } = useSessionSave();
|
|
66
78
|
// Sync pendingMessages to ref for real-time access in callbacks
|
|
@@ -105,6 +117,60 @@ export default function ChatScreen({}) {
|
|
|
105
117
|
}, 1000);
|
|
106
118
|
return () => clearInterval(interval);
|
|
107
119
|
}, [timerStartTime]);
|
|
120
|
+
// Monitor VSCode connection status and editor context
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
let connectingTimeout = null;
|
|
123
|
+
const checkConnection = setInterval(() => {
|
|
124
|
+
const isConnected = vscodeConnection.isConnected();
|
|
125
|
+
const isServerRunning = vscodeConnection.isServerRunning();
|
|
126
|
+
setVscodeConnected(isConnected);
|
|
127
|
+
// Update connection status based on actual connection state
|
|
128
|
+
if (isConnected && vscodeConnectionStatus !== 'connected') {
|
|
129
|
+
setVscodeConnectionStatus('connected');
|
|
130
|
+
if (connectingTimeout) {
|
|
131
|
+
clearTimeout(connectingTimeout);
|
|
132
|
+
connectingTimeout = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (!isConnected && vscodeConnectionStatus === 'connected') {
|
|
136
|
+
setVscodeConnectionStatus('disconnected');
|
|
137
|
+
}
|
|
138
|
+
else if (vscodeConnectionStatus === 'connecting' && !isServerRunning) {
|
|
139
|
+
// Server failed to start
|
|
140
|
+
setVscodeConnectionStatus('error');
|
|
141
|
+
if (connectingTimeout) {
|
|
142
|
+
clearTimeout(connectingTimeout);
|
|
143
|
+
connectingTimeout = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}, 1000);
|
|
147
|
+
// Set timeout for connecting state (15 seconds)
|
|
148
|
+
if (vscodeConnectionStatus === 'connecting') {
|
|
149
|
+
connectingTimeout = setTimeout(() => {
|
|
150
|
+
if (vscodeConnectionStatus === 'connecting') {
|
|
151
|
+
setVscodeConnectionStatus('error');
|
|
152
|
+
}
|
|
153
|
+
}, 15000);
|
|
154
|
+
}
|
|
155
|
+
const unsubscribe = vscodeConnection.onContextUpdate((context) => {
|
|
156
|
+
setEditorContext(context);
|
|
157
|
+
// When we receive context, it means connection is successful
|
|
158
|
+
if (vscodeConnectionStatus !== 'connected') {
|
|
159
|
+
setVscodeConnectionStatus('connected');
|
|
160
|
+
if (connectingTimeout) {
|
|
161
|
+
clearTimeout(connectingTimeout);
|
|
162
|
+
connectingTimeout = null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return () => {
|
|
167
|
+
clearInterval(checkConnection);
|
|
168
|
+
if (connectingTimeout) {
|
|
169
|
+
clearTimeout(connectingTimeout);
|
|
170
|
+
}
|
|
171
|
+
unsubscribe();
|
|
172
|
+
};
|
|
173
|
+
}, [vscodeConnectionStatus]);
|
|
108
174
|
// Pending messages are now handled inline during tool execution in useConversation
|
|
109
175
|
// Auto-send pending messages when streaming completely stops (as fallback)
|
|
110
176
|
useEffect(() => {
|
|
@@ -140,7 +206,74 @@ export default function ChatScreen({}) {
|
|
|
140
206
|
setStreamTokenCount(0);
|
|
141
207
|
}
|
|
142
208
|
});
|
|
143
|
-
const handleCommandExecution = (commandName, result) => {
|
|
209
|
+
const handleCommandExecution = async (commandName, result) => {
|
|
210
|
+
// Handle /compact command
|
|
211
|
+
if (commandName === 'compact' && result.success && result.action === 'compact') {
|
|
212
|
+
// Set compressing state (不添加命令面板消息)
|
|
213
|
+
setIsCompressing(true);
|
|
214
|
+
setCompressionError(null);
|
|
215
|
+
try {
|
|
216
|
+
// Convert messages to ChatMessage format for compression
|
|
217
|
+
const chatMessages = messages
|
|
218
|
+
.filter(msg => msg.role !== 'command')
|
|
219
|
+
.map(msg => ({
|
|
220
|
+
role: msg.role,
|
|
221
|
+
content: msg.content,
|
|
222
|
+
tool_call_id: msg.toolCallId
|
|
223
|
+
}));
|
|
224
|
+
// Compress the context
|
|
225
|
+
const result = await compressContext(chatMessages);
|
|
226
|
+
// Replace all messages with a summary message (不包含 "Context Compressed" 标题)
|
|
227
|
+
const summaryMessage = {
|
|
228
|
+
role: 'assistant',
|
|
229
|
+
content: result.summary,
|
|
230
|
+
streaming: false
|
|
231
|
+
};
|
|
232
|
+
// Clear session and set new compressed state
|
|
233
|
+
sessionManager.clearCurrentSession();
|
|
234
|
+
clearSavedMessages();
|
|
235
|
+
setMessages([summaryMessage]);
|
|
236
|
+
setRemountKey(prev => prev + 1);
|
|
237
|
+
// Update token usage with compression result
|
|
238
|
+
setContextUsage({
|
|
239
|
+
prompt_tokens: result.usage.prompt_tokens,
|
|
240
|
+
completion_tokens: result.usage.completion_tokens,
|
|
241
|
+
total_tokens: result.usage.total_tokens
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
// Show error message
|
|
246
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown compression error';
|
|
247
|
+
setCompressionError(errorMsg);
|
|
248
|
+
const errorMessage = {
|
|
249
|
+
role: 'assistant',
|
|
250
|
+
content: `**Compression Failed**\n\n${errorMsg}`,
|
|
251
|
+
streaming: false
|
|
252
|
+
};
|
|
253
|
+
setMessages(prev => [...prev, errorMessage]);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
setIsCompressing(false);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Handle /ide command
|
|
261
|
+
if (commandName === 'ide') {
|
|
262
|
+
if (result.success) {
|
|
263
|
+
setVscodeConnectionStatus('connecting');
|
|
264
|
+
// Add command execution feedback
|
|
265
|
+
const commandMessage = {
|
|
266
|
+
role: 'command',
|
|
267
|
+
content: '',
|
|
268
|
+
commandName: commandName
|
|
269
|
+
};
|
|
270
|
+
setMessages(prev => [...prev, commandMessage]);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
setVscodeConnectionStatus('error');
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
144
277
|
if (result.success && result.action === 'clear') {
|
|
145
278
|
if (stdout && typeof stdout.write === 'function') {
|
|
146
279
|
stdout.write('\x1B[3J\x1B[2J\x1B[H');
|
|
@@ -241,8 +374,8 @@ export default function ChatScreen({}) {
|
|
|
241
374
|
const controller = new AbortController();
|
|
242
375
|
setAbortController(controller);
|
|
243
376
|
try {
|
|
244
|
-
// Create message for AI with file read instructions and
|
|
245
|
-
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo);
|
|
377
|
+
// Create message for AI with file read instructions, system info, and editor context
|
|
378
|
+
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, systemInfo, vscodeConnected ? editorContext : undefined);
|
|
246
379
|
// Start conversation with tool support
|
|
247
380
|
await handleConversationWithTools({
|
|
248
381
|
userContent: messageForAI,
|
|
@@ -351,6 +484,21 @@ export default function ChatScreen({}) {
|
|
|
351
484
|
if (showMcpInfo) {
|
|
352
485
|
return (React.createElement(MCPInfoScreen, { onClose: () => setShowMcpInfo(false), panelKey: mcpPanelKey }));
|
|
353
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
|
+
}
|
|
354
502
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
355
503
|
React.createElement(Static, { key: remountKey, items: [
|
|
356
504
|
React.createElement(Box, { key: "header", marginX: 1, borderColor: 'cyan', borderStyle: "round", paddingX: 2, paddingY: 1 },
|
|
@@ -469,8 +617,27 @@ export default function ChatScreen({}) {
|
|
|
469
617
|
React.createElement(Box, { marginX: 1 },
|
|
470
618
|
React.createElement(PendingMessages, { pendingMessages: pendingMessages })),
|
|
471
619
|
pendingToolConfirmation && (React.createElement(ToolConfirmation, { toolName: pendingToolConfirmation.batchToolNames || pendingToolConfirmation.tool.function.name, onConfirm: pendingToolConfirmation.resolve })),
|
|
472
|
-
!pendingToolConfirmation && (React.createElement(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
620
|
+
!pendingToolConfirmation && !isCompressing && (React.createElement(React.Fragment, null,
|
|
621
|
+
React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: "Ask me anything about coding...", disabled: !!pendingToolConfirmation, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: contextUsage ? {
|
|
622
|
+
inputTokens: contextUsage.prompt_tokens,
|
|
623
|
+
maxContextTokens: getOpenAiConfig().maxContextTokens || 4000
|
|
624
|
+
} : undefined }),
|
|
625
|
+
vscodeConnectionStatus !== 'disconnected' && (React.createElement(Box, { marginTop: 1 },
|
|
626
|
+
React.createElement(Text, { color: vscodeConnectionStatus === 'connecting' ? 'yellow' :
|
|
627
|
+
vscodeConnectionStatus === 'connected' ? 'green' :
|
|
628
|
+
vscodeConnectionStatus === 'error' ? 'red' : 'gray', dimColor: vscodeConnectionStatus !== 'error' },
|
|
629
|
+
"\u25CF ",
|
|
630
|
+
vscodeConnectionStatus === 'connecting' ? 'Connecting to VSCode...' :
|
|
631
|
+
vscodeConnectionStatus === 'connected' ? 'VSCode Connected' :
|
|
632
|
+
vscodeConnectionStatus === 'error' ? 'Connection Failed' : 'VSCode',
|
|
633
|
+
vscodeConnectionStatus === 'connected' && editorContext.activeFile && ` | ${editorContext.activeFile}`,
|
|
634
|
+
vscodeConnectionStatus === 'connected' && editorContext.selectedText && ` | ${editorContext.selectedText.length} chars selected`))))),
|
|
635
|
+
isCompressing && (React.createElement(Box, { marginTop: 1 },
|
|
636
|
+
React.createElement(Text, { color: "cyan" },
|
|
637
|
+
React.createElement(Spinner, { type: "dots" }),
|
|
638
|
+
" Compressing conversation history..."))),
|
|
639
|
+
compressionError && (React.createElement(Box, { marginTop: 1 },
|
|
640
|
+
React.createElement(Text, { color: "red" },
|
|
641
|
+
"\u2717 Compression failed: ",
|
|
642
|
+
compressionError)))));
|
|
476
643
|
}
|
|
@@ -8,6 +8,9 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
8
8
|
const [advancedModel, setAdvancedModel] = useState('');
|
|
9
9
|
const [basicModel, setBasicModel] = useState('');
|
|
10
10
|
const [maxContextTokens, setMaxContextTokens] = useState(4000);
|
|
11
|
+
const [compactBaseUrl, setCompactBaseUrl] = useState('');
|
|
12
|
+
const [compactApiKey, setCompactApiKey] = useState('');
|
|
13
|
+
const [compactModelName, setCompactModelName] = useState('');
|
|
11
14
|
const [currentField, setCurrentField] = useState('advancedModel');
|
|
12
15
|
const [isEditing, setIsEditing] = useState(false);
|
|
13
16
|
const [models, setModels] = useState([]);
|
|
@@ -21,6 +24,9 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
21
24
|
setAdvancedModel(config.advancedModel || '');
|
|
22
25
|
setBasicModel(config.basicModel || '');
|
|
23
26
|
setMaxContextTokens(config.maxContextTokens || 4000);
|
|
27
|
+
setCompactBaseUrl(config.compactModel?.baseUrl || '');
|
|
28
|
+
setCompactApiKey(config.compactModel?.apiKey || '');
|
|
29
|
+
setCompactModelName(config.compactModel?.modelName || '');
|
|
24
30
|
if (!config.baseUrl) {
|
|
25
31
|
setBaseUrlMissing(true);
|
|
26
32
|
return;
|
|
@@ -57,7 +63,15 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
57
63
|
return advancedModel;
|
|
58
64
|
if (currentField === 'basicModel')
|
|
59
65
|
return basicModel;
|
|
60
|
-
|
|
66
|
+
if (currentField === 'maxContextTokens')
|
|
67
|
+
return maxContextTokens.toString();
|
|
68
|
+
if (currentField === 'compactBaseUrl')
|
|
69
|
+
return compactBaseUrl;
|
|
70
|
+
if (currentField === 'compactApiKey')
|
|
71
|
+
return compactApiKey;
|
|
72
|
+
if (currentField === 'compactModelName')
|
|
73
|
+
return compactModelName;
|
|
74
|
+
return '';
|
|
61
75
|
};
|
|
62
76
|
const handleModelChange = (value) => {
|
|
63
77
|
// 如果选择了手动输入选项
|
|
@@ -146,6 +160,34 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
146
160
|
setIsEditing(false);
|
|
147
161
|
}
|
|
148
162
|
}
|
|
163
|
+
else if (currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName') {
|
|
164
|
+
// Handle text input for compact model fields
|
|
165
|
+
if (key.return) {
|
|
166
|
+
setIsEditing(false);
|
|
167
|
+
}
|
|
168
|
+
else if (key.backspace || key.delete) {
|
|
169
|
+
if (currentField === 'compactBaseUrl') {
|
|
170
|
+
setCompactBaseUrl(prev => prev.slice(0, -1));
|
|
171
|
+
}
|
|
172
|
+
else if (currentField === 'compactApiKey') {
|
|
173
|
+
setCompactApiKey(prev => prev.slice(0, -1));
|
|
174
|
+
}
|
|
175
|
+
else if (currentField === 'compactModelName') {
|
|
176
|
+
setCompactModelName(prev => prev.slice(0, -1));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (input && input.match(/[a-zA-Z0-9-_./:]/)) {
|
|
180
|
+
if (currentField === 'compactBaseUrl') {
|
|
181
|
+
setCompactBaseUrl(prev => prev + input);
|
|
182
|
+
}
|
|
183
|
+
else if (currentField === 'compactApiKey') {
|
|
184
|
+
setCompactApiKey(prev => prev + input);
|
|
185
|
+
}
|
|
186
|
+
else if (currentField === 'compactModelName') {
|
|
187
|
+
setCompactModelName(prev => prev + input);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
149
191
|
else {
|
|
150
192
|
// Allow typing to filter in edit mode for model selection
|
|
151
193
|
if (input && input.match(/[a-zA-Z0-9-_.]/)) {
|
|
@@ -164,6 +206,14 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
164
206
|
basicModel,
|
|
165
207
|
maxContextTokens,
|
|
166
208
|
};
|
|
209
|
+
// 只有当所有字段都填写时才保存 compactModel
|
|
210
|
+
if (compactBaseUrl && compactApiKey && compactModelName) {
|
|
211
|
+
config.compactModel = {
|
|
212
|
+
baseUrl: compactBaseUrl,
|
|
213
|
+
apiKey: compactApiKey,
|
|
214
|
+
modelName: compactModelName,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
167
217
|
updateOpenAiConfig(config);
|
|
168
218
|
onSave();
|
|
169
219
|
}
|
|
@@ -173,13 +223,22 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
173
223
|
basicModel,
|
|
174
224
|
maxContextTokens,
|
|
175
225
|
};
|
|
226
|
+
// 只有当所有字段都填写时才保存 compactModel
|
|
227
|
+
if (compactBaseUrl && compactApiKey && compactModelName) {
|
|
228
|
+
config.compactModel = {
|
|
229
|
+
baseUrl: compactBaseUrl,
|
|
230
|
+
apiKey: compactApiKey,
|
|
231
|
+
modelName: compactModelName,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
176
234
|
updateOpenAiConfig(config);
|
|
177
235
|
onBack();
|
|
178
236
|
}
|
|
179
237
|
else if (key.return) {
|
|
180
|
-
// Load models first for model fields, or enter edit mode directly for maxContextTokens
|
|
238
|
+
// Load models first for model fields, or enter edit mode directly for maxContextTokens and compact fields
|
|
181
239
|
setSearchTerm(''); // Reset search when entering edit mode
|
|
182
|
-
|
|
240
|
+
const isCompactField = currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName';
|
|
241
|
+
if (currentField === 'maxContextTokens' || isCompactField) {
|
|
183
242
|
setIsEditing(true);
|
|
184
243
|
}
|
|
185
244
|
else {
|
|
@@ -194,7 +253,8 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
194
253
|
}
|
|
195
254
|
else if (input === 'm') {
|
|
196
255
|
// 快捷键:按 'm' 直接进入手动输入模式
|
|
197
|
-
|
|
256
|
+
const isCompactField = currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName';
|
|
257
|
+
if (currentField !== 'maxContextTokens' && !isCompactField) {
|
|
198
258
|
setManualInputMode(true);
|
|
199
259
|
setManualInputValue(getCurrentValue());
|
|
200
260
|
}
|
|
@@ -206,6 +266,15 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
206
266
|
else if (currentField === 'maxContextTokens') {
|
|
207
267
|
setCurrentField('basicModel');
|
|
208
268
|
}
|
|
269
|
+
else if (currentField === 'compactBaseUrl') {
|
|
270
|
+
setCurrentField('maxContextTokens');
|
|
271
|
+
}
|
|
272
|
+
else if (currentField === 'compactApiKey') {
|
|
273
|
+
setCurrentField('compactBaseUrl');
|
|
274
|
+
}
|
|
275
|
+
else if (currentField === 'compactModelName') {
|
|
276
|
+
setCurrentField('compactApiKey');
|
|
277
|
+
}
|
|
209
278
|
}
|
|
210
279
|
else if (key.downArrow) {
|
|
211
280
|
if (currentField === 'advancedModel') {
|
|
@@ -214,6 +283,15 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
214
283
|
else if (currentField === 'basicModel') {
|
|
215
284
|
setCurrentField('maxContextTokens');
|
|
216
285
|
}
|
|
286
|
+
else if (currentField === 'maxContextTokens') {
|
|
287
|
+
setCurrentField('compactBaseUrl');
|
|
288
|
+
}
|
|
289
|
+
else if (currentField === 'compactBaseUrl') {
|
|
290
|
+
setCurrentField('compactApiKey');
|
|
291
|
+
}
|
|
292
|
+
else if (currentField === 'compactApiKey') {
|
|
293
|
+
setCurrentField('compactModelName');
|
|
294
|
+
}
|
|
217
295
|
}
|
|
218
296
|
});
|
|
219
297
|
if (baseUrlMissing) {
|
|
@@ -293,7 +371,42 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
293
371
|
"Enter value: ",
|
|
294
372
|
maxContextTokens))),
|
|
295
373
|
(!isEditing || currentField !== 'maxContextTokens') && (React.createElement(Box, { marginLeft: 3 },
|
|
296
|
-
React.createElement(Text, { color: "gray" }, maxContextTokens)))))
|
|
374
|
+
React.createElement(Text, { color: "gray" }, maxContextTokens))))),
|
|
375
|
+
React.createElement(Box, { marginBottom: 2, marginTop: 1 },
|
|
376
|
+
React.createElement(Text, { color: "cyan", bold: true }, "Compact Model (Context Compression):")),
|
|
377
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
378
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
379
|
+
React.createElement(Text, { color: currentField === 'compactBaseUrl' ? 'green' : 'white' },
|
|
380
|
+
currentField === 'compactBaseUrl' ? '➣ ' : ' ',
|
|
381
|
+
"Base URL:"),
|
|
382
|
+
currentField === 'compactBaseUrl' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
383
|
+
React.createElement(Text, { color: "cyan" },
|
|
384
|
+
compactBaseUrl,
|
|
385
|
+
React.createElement(Text, { color: "white" }, "_")))),
|
|
386
|
+
(!isEditing || currentField !== 'compactBaseUrl') && (React.createElement(Box, { marginLeft: 3 },
|
|
387
|
+
React.createElement(Text, { color: "gray" }, compactBaseUrl || 'Not set'))))),
|
|
388
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
389
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
390
|
+
React.createElement(Text, { color: currentField === 'compactApiKey' ? 'green' : 'white' },
|
|
391
|
+
currentField === 'compactApiKey' ? '➣ ' : ' ',
|
|
392
|
+
"API Key:"),
|
|
393
|
+
currentField === 'compactApiKey' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
394
|
+
React.createElement(Text, { color: "cyan" },
|
|
395
|
+
compactApiKey.replace(/./g, '*'),
|
|
396
|
+
React.createElement(Text, { color: "white" }, "_")))),
|
|
397
|
+
(!isEditing || currentField !== 'compactApiKey') && (React.createElement(Box, { marginLeft: 3 },
|
|
398
|
+
React.createElement(Text, { color: "gray" }, compactApiKey ? compactApiKey.replace(/./g, '*') : 'Not set'))))),
|
|
399
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
400
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
401
|
+
React.createElement(Text, { color: currentField === 'compactModelName' ? 'green' : 'white' },
|
|
402
|
+
currentField === 'compactModelName' ? '➣ ' : ' ',
|
|
403
|
+
"Model Name:"),
|
|
404
|
+
currentField === 'compactModelName' && isEditing && (React.createElement(Box, { marginLeft: 3 },
|
|
405
|
+
React.createElement(Text, { color: "cyan" },
|
|
406
|
+
compactModelName,
|
|
407
|
+
React.createElement(Text, { color: "white" }, "_")))),
|
|
408
|
+
(!isEditing || currentField !== 'compactModelName') && (React.createElement(Box, { marginLeft: 3 },
|
|
409
|
+
React.createElement(Text, { color: "gray" }, compactModelName || 'Not set')))))),
|
|
297
410
|
React.createElement(Box, { flexDirection: "column" }, isEditing ? (React.createElement(React.Fragment, null,
|
|
298
411
|
React.createElement(Alert, { variant: "info" }, "Editing mode: Type to filter models, \u2191\u2193 to select, Enter to confirm"))) : (React.createElement(React.Fragment, null,
|
|
299
412
|
React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate, Enter to edit, M for manual input, Ctrl+S or Esc to save"))))));
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export type RequestMethod = 'chat' | 'responses';
|
|
2
|
+
export interface CompactModelConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
modelName: string;
|
|
6
|
+
}
|
|
2
7
|
export interface ApiConfig {
|
|
3
8
|
baseUrl: string;
|
|
4
9
|
apiKey: string;
|
|
@@ -6,6 +11,7 @@ export interface ApiConfig {
|
|
|
6
11
|
advancedModel?: string;
|
|
7
12
|
basicModel?: string;
|
|
8
13
|
maxContextTokens?: number;
|
|
14
|
+
compactModel?: CompactModelConfig;
|
|
9
15
|
}
|
|
10
16
|
export interface MCPServer {
|
|
11
17
|
url?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface CommandResult {
|
|
2
2
|
success: boolean;
|
|
3
3
|
message?: string;
|
|
4
|
-
action?: 'clear' | 'resume' | 'info' | 'showMcpInfo' | 'goHome' | 'toggleYolo' | 'initProject';
|
|
4
|
+
action?: 'clear' | 'resume' | 'info' | 'showMcpInfo' | 'goHome' | 'toggleYolo' | 'initProject' | 'compact';
|
|
5
5
|
prompt?: string;
|
|
6
6
|
}
|
|
7
7
|
export interface CommandHandler {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { registerCommand } from '../commandExecutor.js';
|
|
2
|
+
// Compact command handler - compress conversation history
|
|
3
|
+
registerCommand('compact', {
|
|
4
|
+
execute: () => {
|
|
5
|
+
return {
|
|
6
|
+
success: true,
|
|
7
|
+
action: 'compact',
|
|
8
|
+
message: 'Compressing conversation history...'
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
export default {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { registerCommand } from '../commandExecutor.js';
|
|
2
|
+
import { vscodeConnection } from '../vscodeConnection.js';
|
|
3
|
+
// IDE connection command handler
|
|
4
|
+
registerCommand('ide', {
|
|
5
|
+
execute: async () => {
|
|
6
|
+
if (vscodeConnection.isConnected()) {
|
|
7
|
+
return {
|
|
8
|
+
success: true,
|
|
9
|
+
action: 'info',
|
|
10
|
+
message: 'Already connected to VSCode editor'
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await vscodeConnection.start();
|
|
15
|
+
return {
|
|
16
|
+
success: true,
|
|
17
|
+
action: 'info',
|
|
18
|
+
message: `VSCode connection server started on port ${vscodeConnection.getPort()}\nPlease connect from the Snow CLI extension in VSCode`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
message: error instanceof Error ? error.message : 'Failed to start IDE connection'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
export default {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChatMessage } from '../api/chat.js';
|
|
2
|
+
export interface CompressionResult {
|
|
3
|
+
summary: string;
|
|
4
|
+
usage: {
|
|
5
|
+
prompt_tokens: number;
|
|
6
|
+
completion_tokens: number;
|
|
7
|
+
total_tokens: number;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compress conversation history using the compact model
|
|
12
|
+
* @param messages - Array of messages to compress
|
|
13
|
+
* @returns Compressed summary and token usage information
|
|
14
|
+
*/
|
|
15
|
+
export declare function compressContext(messages: ChatMessage[]): Promise<CompressionResult>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { getOpenAiConfig } from './apiConfig.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compress conversation history using the compact model
|
|
5
|
+
* @param messages - Array of messages to compress
|
|
6
|
+
* @returns Compressed summary and token usage information
|
|
7
|
+
*/
|
|
8
|
+
export async function compressContext(messages) {
|
|
9
|
+
const config = getOpenAiConfig();
|
|
10
|
+
// Check if compact model is configured
|
|
11
|
+
if (!config.compactModel || !config.compactModel.baseUrl || !config.compactModel.apiKey || !config.compactModel.modelName) {
|
|
12
|
+
throw new Error('Compact model not configured. Please configure it in Model Settings.');
|
|
13
|
+
}
|
|
14
|
+
// Create OpenAI client with compact model config
|
|
15
|
+
const client = new OpenAI({
|
|
16
|
+
apiKey: config.compactModel.apiKey,
|
|
17
|
+
baseURL: config.compactModel.baseUrl,
|
|
18
|
+
});
|
|
19
|
+
// Filter out system messages and create a conversation text
|
|
20
|
+
const conversationText = messages
|
|
21
|
+
.filter(msg => msg.role !== 'system')
|
|
22
|
+
.map(msg => {
|
|
23
|
+
const role = msg.role === 'user' ? 'User' : msg.role === 'assistant' ? 'Assistant' : 'Tool';
|
|
24
|
+
return `${role}: ${msg.content}`;
|
|
25
|
+
})
|
|
26
|
+
.join('\n\n');
|
|
27
|
+
// Create compression prompt
|
|
28
|
+
const compressionPrompt = `Please summarize the following conversation history in a concise way, preserving all important context, decisions, and key information. The summary should be detailed enough to continue the conversation seamlessly.
|
|
29
|
+
|
|
30
|
+
Conversation:
|
|
31
|
+
${conversationText}
|
|
32
|
+
|
|
33
|
+
Summary:`;
|
|
34
|
+
try {
|
|
35
|
+
const response = await client.chat.completions.create({
|
|
36
|
+
model: config.compactModel.modelName,
|
|
37
|
+
messages: [
|
|
38
|
+
{
|
|
39
|
+
role: 'user',
|
|
40
|
+
content: compressionPrompt,
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
});
|
|
44
|
+
const summary = response.choices[0]?.message?.content;
|
|
45
|
+
if (!summary) {
|
|
46
|
+
throw new Error('Failed to generate summary from compact model');
|
|
47
|
+
}
|
|
48
|
+
// Extract usage information
|
|
49
|
+
const usage = response.usage || {
|
|
50
|
+
prompt_tokens: 0,
|
|
51
|
+
completion_tokens: 0,
|
|
52
|
+
total_tokens: 0
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
summary,
|
|
56
|
+
usage: {
|
|
57
|
+
prompt_tokens: usage.prompt_tokens,
|
|
58
|
+
completion_tokens: usage.completion_tokens,
|
|
59
|
+
total_tokens: usage.total_tokens
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof Error) {
|
|
65
|
+
throw new Error(`Context compression failed: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
throw new Error('Unknown error occurred during context compression');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -33,6 +33,14 @@ export declare function createMessageWithFileInstructions(content: string, files
|
|
|
33
33
|
platform: string;
|
|
34
34
|
shell: string;
|
|
35
35
|
workingDirectory: string;
|
|
36
|
+
}, editorContext?: {
|
|
37
|
+
activeFile?: string;
|
|
38
|
+
selectedText?: string;
|
|
39
|
+
cursorPosition?: {
|
|
40
|
+
line: number;
|
|
41
|
+
character: number;
|
|
42
|
+
};
|
|
43
|
+
workspaceFolder?: string;
|
|
36
44
|
}): string;
|
|
37
45
|
/**
|
|
38
46
|
* Get system information (OS, shell, working directory)
|
package/dist/utils/fileUtils.js
CHANGED
|
@@ -154,7 +154,7 @@ export async function parseAndValidateFileReferences(content) {
|
|
|
154
154
|
/**
|
|
155
155
|
* Create message with file read instructions for AI
|
|
156
156
|
*/
|
|
157
|
-
export function createMessageWithFileInstructions(content, files, systemInfo) {
|
|
157
|
+
export function createMessageWithFileInstructions(content, files, systemInfo, editorContext) {
|
|
158
158
|
const parts = [content];
|
|
159
159
|
// Add system info if provided
|
|
160
160
|
if (systemInfo) {
|
|
@@ -165,6 +165,25 @@ export function createMessageWithFileInstructions(content, files, systemInfo) {
|
|
|
165
165
|
];
|
|
166
166
|
parts.push(systemInfoLines.join('\n'));
|
|
167
167
|
}
|
|
168
|
+
// Add editor context if provided (from VSCode connection)
|
|
169
|
+
if (editorContext) {
|
|
170
|
+
const editorLines = [];
|
|
171
|
+
if (editorContext.workspaceFolder) {
|
|
172
|
+
editorLines.push(`└─ VSCode Workspace: ${editorContext.workspaceFolder}`);
|
|
173
|
+
}
|
|
174
|
+
if (editorContext.activeFile) {
|
|
175
|
+
editorLines.push(`└─ Active File: ${editorContext.activeFile}`);
|
|
176
|
+
}
|
|
177
|
+
if (editorContext.cursorPosition) {
|
|
178
|
+
editorLines.push(`└─ Cursor: Line ${editorContext.cursorPosition.line + 1}, Column ${editorContext.cursorPosition.character + 1}`);
|
|
179
|
+
}
|
|
180
|
+
if (editorContext.selectedText) {
|
|
181
|
+
editorLines.push(`└─ Selected Code:\n\`\`\`\n${editorContext.selectedText}\n\`\`\``);
|
|
182
|
+
}
|
|
183
|
+
if (editorLines.length > 0) {
|
|
184
|
+
parts.push(editorLines.join('\n'));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
168
187
|
// Add file instructions if provided
|
|
169
188
|
if (files.length > 0) {
|
|
170
189
|
const fileInstructions = files
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
interface EditorContext {
|
|
2
|
+
activeFile?: string;
|
|
3
|
+
selectedText?: string;
|
|
4
|
+
cursorPosition?: {
|
|
5
|
+
line: number;
|
|
6
|
+
character: number;
|
|
7
|
+
};
|
|
8
|
+
workspaceFolder?: string;
|
|
9
|
+
}
|
|
10
|
+
interface Diagnostic {
|
|
11
|
+
message: string;
|
|
12
|
+
severity: 'error' | 'warning' | 'info' | 'hint';
|
|
13
|
+
line: number;
|
|
14
|
+
character: number;
|
|
15
|
+
source?: string;
|
|
16
|
+
code?: string | number;
|
|
17
|
+
}
|
|
18
|
+
declare class VSCodeConnectionManager {
|
|
19
|
+
private server;
|
|
20
|
+
private client;
|
|
21
|
+
private port;
|
|
22
|
+
private editorContext;
|
|
23
|
+
private listeners;
|
|
24
|
+
start(): Promise<void>;
|
|
25
|
+
stop(): void;
|
|
26
|
+
isConnected(): boolean;
|
|
27
|
+
isServerRunning(): boolean;
|
|
28
|
+
getContext(): EditorContext;
|
|
29
|
+
onContextUpdate(listener: (context: EditorContext) => void): () => void;
|
|
30
|
+
private handleMessage;
|
|
31
|
+
private notifyListeners;
|
|
32
|
+
getPort(): number;
|
|
33
|
+
/**
|
|
34
|
+
* Request diagnostics for a specific file from VS Code
|
|
35
|
+
* @param filePath - The file path to get diagnostics for
|
|
36
|
+
* @returns Promise that resolves with diagnostics array
|
|
37
|
+
*/
|
|
38
|
+
requestDiagnostics(filePath: string): Promise<Diagnostic[]>;
|
|
39
|
+
}
|
|
40
|
+
export declare const vscodeConnection: VSCodeConnectionManager;
|
|
41
|
+
export type { EditorContext, Diagnostic };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
class VSCodeConnectionManager {
|
|
3
|
+
constructor() {
|
|
4
|
+
Object.defineProperty(this, "server", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: null
|
|
9
|
+
});
|
|
10
|
+
Object.defineProperty(this, "client", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: null
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "port", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: 9527
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(this, "editorContext", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: {}
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "listeners", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: []
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async start() {
|
|
36
|
+
// If already running, just return success
|
|
37
|
+
if (this.server) {
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
try {
|
|
42
|
+
this.server = new WebSocketServer({ port: this.port });
|
|
43
|
+
this.server.on('connection', (ws) => {
|
|
44
|
+
this.client = ws;
|
|
45
|
+
ws.on('message', (message) => {
|
|
46
|
+
try {
|
|
47
|
+
const data = JSON.parse(message.toString());
|
|
48
|
+
this.handleMessage(data);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// Ignore invalid JSON
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
ws.on('close', () => {
|
|
55
|
+
this.client = null;
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
this.server.on('listening', () => {
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
this.server.on('error', (error) => {
|
|
62
|
+
reject(error);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
reject(error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
stop() {
|
|
71
|
+
if (this.client) {
|
|
72
|
+
this.client.close();
|
|
73
|
+
this.client = null;
|
|
74
|
+
}
|
|
75
|
+
if (this.server) {
|
|
76
|
+
this.server.close();
|
|
77
|
+
this.server = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
isConnected() {
|
|
81
|
+
return this.client !== null && this.client.readyState === WebSocket.OPEN;
|
|
82
|
+
}
|
|
83
|
+
isServerRunning() {
|
|
84
|
+
return this.server !== null;
|
|
85
|
+
}
|
|
86
|
+
getContext() {
|
|
87
|
+
return { ...this.editorContext };
|
|
88
|
+
}
|
|
89
|
+
onContextUpdate(listener) {
|
|
90
|
+
this.listeners.push(listener);
|
|
91
|
+
return () => {
|
|
92
|
+
this.listeners = this.listeners.filter((l) => l !== listener);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
handleMessage(data) {
|
|
96
|
+
if (data.type === 'context') {
|
|
97
|
+
this.editorContext = {
|
|
98
|
+
activeFile: data.activeFile,
|
|
99
|
+
selectedText: data.selectedText,
|
|
100
|
+
cursorPosition: data.cursorPosition,
|
|
101
|
+
workspaceFolder: data.workspaceFolder
|
|
102
|
+
};
|
|
103
|
+
this.notifyListeners();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
notifyListeners() {
|
|
107
|
+
for (const listener of this.listeners) {
|
|
108
|
+
listener(this.editorContext);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
getPort() {
|
|
112
|
+
return this.port;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Request diagnostics for a specific file from VS Code
|
|
116
|
+
* @param filePath - The file path to get diagnostics for
|
|
117
|
+
* @returns Promise that resolves with diagnostics array
|
|
118
|
+
*/
|
|
119
|
+
async requestDiagnostics(filePath) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
if (!this.client || this.client.readyState !== WebSocket.OPEN) {
|
|
122
|
+
resolve([]); // Return empty array if not connected
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const requestId = Math.random().toString(36).substring(7);
|
|
126
|
+
const timeout = setTimeout(() => {
|
|
127
|
+
cleanup();
|
|
128
|
+
resolve([]); // Timeout, return empty array
|
|
129
|
+
}, 5000); // 5 second timeout
|
|
130
|
+
const handler = (message) => {
|
|
131
|
+
try {
|
|
132
|
+
const data = JSON.parse(message.toString());
|
|
133
|
+
if (data.type === 'diagnostics' && data.requestId === requestId) {
|
|
134
|
+
cleanup();
|
|
135
|
+
resolve(data.diagnostics || []);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
// Ignore invalid JSON
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const cleanup = () => {
|
|
143
|
+
clearTimeout(timeout);
|
|
144
|
+
this.client?.removeListener('message', handler);
|
|
145
|
+
};
|
|
146
|
+
this.client.on('message', handler);
|
|
147
|
+
this.client.send(JSON.stringify({
|
|
148
|
+
type: 'getDiagnostics',
|
|
149
|
+
requestId,
|
|
150
|
+
filePath
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export const vscodeConnection = new VSCodeConnectionManager();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snow-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Intelligent Command Line Assistant powered by AI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -56,13 +56,15 @@
|
|
|
56
56
|
"openai": "^6.1.0",
|
|
57
57
|
"react": "^18.2.0",
|
|
58
58
|
"string-width": "^7.2.0",
|
|
59
|
-
"tiktoken": "^1.0.22"
|
|
59
|
+
"tiktoken": "^1.0.22",
|
|
60
|
+
"ws": "^8.14.2"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@sindresorhus/tsconfig": "^3.0.1",
|
|
63
64
|
"@types/diff": "^7.0.2",
|
|
64
65
|
"@types/figlet": "^1.7.0",
|
|
65
66
|
"@types/react": "^18.0.32",
|
|
67
|
+
"@types/ws": "^8.5.8",
|
|
66
68
|
"@vdemedes/prettier-config": "^2.0.1",
|
|
67
69
|
"ava": "^5.2.0",
|
|
68
70
|
"chalk": "^5.2.0",
|