snow-ai 0.2.10 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/anthropic.d.ts +34 -0
- package/dist/api/anthropic.js +337 -0
- package/dist/api/chat.js +36 -9
- package/dist/api/gemini.d.ts +32 -0
- package/dist/api/gemini.js +243 -0
- package/dist/api/responses.js +47 -40
- package/dist/app.js +4 -1
- package/dist/hooks/useConversation.js +27 -9
- package/dist/ui/components/ChatInput.js +74 -15
- package/dist/ui/pages/ApiConfigScreen.js +41 -6
- package/dist/ui/pages/ModelConfigScreen.js +87 -32
- package/dist/ui/pages/SystemPromptConfigScreen.d.ts +6 -0
- package/dist/ui/pages/SystemPromptConfigScreen.js +70 -0
- package/dist/ui/pages/WelcomeScreen.js +5 -0
- package/dist/utils/apiConfig.d.ts +6 -2
- package/dist/utils/apiConfig.js +36 -14
- package/dist/utils/textBuffer.d.ts +9 -2
- package/dist/utils/textBuffer.js +114 -22
- package/package.json +3 -1
|
@@ -8,6 +8,7 @@ export default function ApiConfigScreen({ onBack, onSave }) {
|
|
|
8
8
|
const [baseUrl, setBaseUrl] = useState('');
|
|
9
9
|
const [apiKey, setApiKey] = useState('');
|
|
10
10
|
const [requestMethod, setRequestMethod] = useState('chat');
|
|
11
|
+
const [anthropicBeta, setAnthropicBeta] = useState(false);
|
|
11
12
|
const [currentField, setCurrentField] = useState('baseUrl');
|
|
12
13
|
const [errors, setErrors] = useState([]);
|
|
13
14
|
const [isEditing, setIsEditing] = useState(false);
|
|
@@ -20,15 +21,29 @@ export default function ApiConfigScreen({ onBack, onSave }) {
|
|
|
20
21
|
label: 'Responses - New responses API (2025, with built-in tools)',
|
|
21
22
|
value: 'responses',
|
|
22
23
|
},
|
|
24
|
+
{
|
|
25
|
+
label: 'Gemini - Google Gemini API',
|
|
26
|
+
value: 'gemini',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'Anthropic - Claude API',
|
|
30
|
+
value: 'anthropic',
|
|
31
|
+
},
|
|
23
32
|
];
|
|
24
33
|
useEffect(() => {
|
|
25
34
|
const config = getOpenAiConfig();
|
|
26
35
|
setBaseUrl(config.baseUrl);
|
|
27
36
|
setApiKey(config.apiKey);
|
|
28
37
|
setRequestMethod(config.requestMethod || 'chat');
|
|
38
|
+
setAnthropicBeta(config.anthropicBeta || false);
|
|
29
39
|
}, []);
|
|
30
40
|
useInput((input, key) => {
|
|
31
|
-
//
|
|
41
|
+
// Allow Escape key to exit Select component without changes
|
|
42
|
+
if (isEditing && currentField === 'requestMethod' && key.escape) {
|
|
43
|
+
setIsEditing(false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Don't handle other input when Select component is active
|
|
32
47
|
if (isEditing && currentField === 'requestMethod') {
|
|
33
48
|
return;
|
|
34
49
|
}
|
|
@@ -36,7 +51,7 @@ export default function ApiConfigScreen({ onBack, onSave }) {
|
|
|
36
51
|
if (input === 's' && (key.ctrl || key.meta)) {
|
|
37
52
|
const validationErrors = validateApiConfig({ baseUrl, apiKey, requestMethod });
|
|
38
53
|
if (validationErrors.length === 0) {
|
|
39
|
-
updateOpenAiConfig({ baseUrl, apiKey, requestMethod });
|
|
54
|
+
updateOpenAiConfig({ baseUrl, apiKey, requestMethod, anthropicBeta });
|
|
40
55
|
setErrors([]);
|
|
41
56
|
onSave();
|
|
42
57
|
}
|
|
@@ -47,7 +62,7 @@ export default function ApiConfigScreen({ onBack, onSave }) {
|
|
|
47
62
|
else if (key.escape) {
|
|
48
63
|
const validationErrors = validateApiConfig({ baseUrl, apiKey, requestMethod });
|
|
49
64
|
if (validationErrors.length === 0) {
|
|
50
|
-
updateOpenAiConfig({ baseUrl, apiKey, requestMethod });
|
|
65
|
+
updateOpenAiConfig({ baseUrl, apiKey, requestMethod, anthropicBeta });
|
|
51
66
|
setErrors([]);
|
|
52
67
|
}
|
|
53
68
|
onBack();
|
|
@@ -58,8 +73,13 @@ export default function ApiConfigScreen({ onBack, onSave }) {
|
|
|
58
73
|
setIsEditing(false);
|
|
59
74
|
}
|
|
60
75
|
else {
|
|
61
|
-
// Enter edit mode for current field
|
|
62
|
-
|
|
76
|
+
// Enter edit mode for current field (toggle for checkbox)
|
|
77
|
+
if (currentField === 'anthropicBeta') {
|
|
78
|
+
setAnthropicBeta(!anthropicBeta);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
setIsEditing(true);
|
|
82
|
+
}
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
else if (!isEditing && key.upArrow) {
|
|
@@ -69,6 +89,9 @@ export default function ApiConfigScreen({ onBack, onSave }) {
|
|
|
69
89
|
else if (currentField === 'requestMethod') {
|
|
70
90
|
setCurrentField('apiKey');
|
|
71
91
|
}
|
|
92
|
+
else if (currentField === 'anthropicBeta') {
|
|
93
|
+
setCurrentField('requestMethod');
|
|
94
|
+
}
|
|
72
95
|
}
|
|
73
96
|
else if (!isEditing && key.downArrow) {
|
|
74
97
|
if (currentField === 'baseUrl') {
|
|
@@ -77,6 +100,9 @@ export default function ApiConfigScreen({ onBack, onSave }) {
|
|
|
77
100
|
else if (currentField === 'apiKey') {
|
|
78
101
|
setCurrentField('requestMethod');
|
|
79
102
|
}
|
|
103
|
+
else if (currentField === 'requestMethod') {
|
|
104
|
+
setCurrentField('anthropicBeta');
|
|
105
|
+
}
|
|
80
106
|
}
|
|
81
107
|
});
|
|
82
108
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
@@ -114,7 +140,16 @@ export default function ApiConfigScreen({ onBack, onSave }) {
|
|
|
114
140
|
setIsEditing(false); // Auto exit edit mode after selection
|
|
115
141
|
} }))),
|
|
116
142
|
(!isEditing || currentField !== 'requestMethod') && (React.createElement(Box, { marginLeft: 3 },
|
|
117
|
-
React.createElement(Text, { color: "gray" }, requestMethodOptions.find(opt => opt.value === requestMethod)?.label || 'Not set')))))
|
|
143
|
+
React.createElement(Text, { color: "gray" }, requestMethodOptions.find(opt => opt.value === requestMethod)?.label || 'Not set'))))),
|
|
144
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
145
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
146
|
+
React.createElement(Text, { color: currentField === 'anthropicBeta' ? 'green' : 'white' },
|
|
147
|
+
currentField === 'anthropicBeta' ? '➣ ' : ' ',
|
|
148
|
+
"Anthropic Beta (for Claude API):"),
|
|
149
|
+
React.createElement(Box, { marginLeft: 3 },
|
|
150
|
+
React.createElement(Text, { color: "gray" },
|
|
151
|
+
anthropicBeta ? '☑ Enabled' : '☐ Disabled',
|
|
152
|
+
" (Press Enter to toggle)"))))),
|
|
118
153
|
errors.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 2 },
|
|
119
154
|
React.createElement(Text, { color: "red", bold: true }, "Errors:"),
|
|
120
155
|
errors.map((error, index) => (React.createElement(Text, { key: index, color: "red" },
|
|
@@ -8,6 +8,7 @@ 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 [maxTokens, setMaxTokens] = useState(4096);
|
|
11
12
|
const [compactBaseUrl, setCompactBaseUrl] = useState('');
|
|
12
13
|
const [compactApiKey, setCompactApiKey] = useState('');
|
|
13
14
|
const [compactModelName, setCompactModelName] = useState('');
|
|
@@ -24,6 +25,7 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
24
25
|
setAdvancedModel(config.advancedModel || '');
|
|
25
26
|
setBasicModel(config.basicModel || '');
|
|
26
27
|
setMaxContextTokens(config.maxContextTokens || 4000);
|
|
28
|
+
setMaxTokens(config.maxTokens || 4096);
|
|
27
29
|
setCompactBaseUrl(config.compactModel?.baseUrl || '');
|
|
28
30
|
setCompactApiKey(config.compactModel?.apiKey || '');
|
|
29
31
|
setCompactModelName(config.compactModel?.modelName || '');
|
|
@@ -65,6 +67,8 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
65
67
|
return basicModel;
|
|
66
68
|
if (currentField === 'maxContextTokens')
|
|
67
69
|
return maxContextTokens.toString();
|
|
70
|
+
if (currentField === 'maxTokens')
|
|
71
|
+
return maxTokens.toString();
|
|
68
72
|
if (currentField === 'compactBaseUrl')
|
|
69
73
|
return compactBaseUrl;
|
|
70
74
|
if (currentField === 'compactApiKey')
|
|
@@ -92,6 +96,12 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
92
96
|
setMaxContextTokens(numValue);
|
|
93
97
|
}
|
|
94
98
|
}
|
|
99
|
+
else if (currentField === 'maxTokens') {
|
|
100
|
+
const numValue = parseInt(value, 10);
|
|
101
|
+
if (!isNaN(numValue) && numValue > 0) {
|
|
102
|
+
setMaxTokens(numValue);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
95
105
|
setIsEditing(false);
|
|
96
106
|
setSearchTerm(''); // Reset search when selection is made
|
|
97
107
|
};
|
|
@@ -160,6 +170,32 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
160
170
|
setIsEditing(false);
|
|
161
171
|
}
|
|
162
172
|
}
|
|
173
|
+
else if (currentField === 'maxTokens') {
|
|
174
|
+
// Handle numeric input for maxTokens
|
|
175
|
+
if (input && input.match(/[0-9]/)) {
|
|
176
|
+
const newValue = parseInt(maxTokens.toString() + input, 10);
|
|
177
|
+
if (!isNaN(newValue)) {
|
|
178
|
+
setMaxTokens(newValue);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (key.backspace || key.delete) {
|
|
182
|
+
const currentStr = maxTokens.toString();
|
|
183
|
+
const newStr = currentStr.slice(0, -1);
|
|
184
|
+
const newValue = parseInt(newStr, 10);
|
|
185
|
+
if (!isNaN(newValue)) {
|
|
186
|
+
setMaxTokens(newValue);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
setMaxTokens(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (key.return) {
|
|
193
|
+
// Save value, but enforce minimum of 100
|
|
194
|
+
const finalValue = maxTokens < 100 ? 100 : maxTokens;
|
|
195
|
+
setMaxTokens(finalValue);
|
|
196
|
+
setIsEditing(false);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
163
199
|
else if (currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName') {
|
|
164
200
|
// Handle text input for compact model fields
|
|
165
201
|
if (key.return) {
|
|
@@ -205,6 +241,7 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
205
241
|
advancedModel,
|
|
206
242
|
basicModel,
|
|
207
243
|
maxContextTokens,
|
|
244
|
+
maxTokens,
|
|
208
245
|
};
|
|
209
246
|
// 只有当所有字段都填写时才保存 compactModel
|
|
210
247
|
if (compactBaseUrl && compactApiKey && compactModelName) {
|
|
@@ -222,6 +259,7 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
222
259
|
advancedModel,
|
|
223
260
|
basicModel,
|
|
224
261
|
maxContextTokens,
|
|
262
|
+
maxTokens,
|
|
225
263
|
};
|
|
226
264
|
// 只有当所有字段都填写时才保存 compactModel
|
|
227
265
|
if (compactBaseUrl && compactApiKey && compactModelName) {
|
|
@@ -235,10 +273,10 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
235
273
|
onBack();
|
|
236
274
|
}
|
|
237
275
|
else if (key.return) {
|
|
238
|
-
// Load models first for model fields, or enter edit mode directly for maxContextTokens and compact fields
|
|
276
|
+
// Load models first for model fields, or enter edit mode directly for maxContextTokens/maxTokens and compact fields
|
|
239
277
|
setSearchTerm(''); // Reset search when entering edit mode
|
|
240
278
|
const isCompactField = currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName';
|
|
241
|
-
if (currentField === 'maxContextTokens' || isCompactField) {
|
|
279
|
+
if (currentField === 'maxContextTokens' || currentField === 'maxTokens' || isCompactField) {
|
|
242
280
|
setIsEditing(true);
|
|
243
281
|
}
|
|
244
282
|
else {
|
|
@@ -254,7 +292,7 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
254
292
|
else if (input === 'm') {
|
|
255
293
|
// 快捷键:按 'm' 直接进入手动输入模式
|
|
256
294
|
const isCompactField = currentField === 'compactBaseUrl' || currentField === 'compactApiKey' || currentField === 'compactModelName';
|
|
257
|
-
if (currentField !== 'maxContextTokens' && !isCompactField) {
|
|
295
|
+
if (currentField !== 'maxContextTokens' && currentField !== 'maxTokens' && !isCompactField) {
|
|
258
296
|
setManualInputMode(true);
|
|
259
297
|
setManualInputValue(getCurrentValue());
|
|
260
298
|
}
|
|
@@ -266,9 +304,12 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
266
304
|
else if (currentField === 'maxContextTokens') {
|
|
267
305
|
setCurrentField('basicModel');
|
|
268
306
|
}
|
|
269
|
-
else if (currentField === '
|
|
307
|
+
else if (currentField === 'maxTokens') {
|
|
270
308
|
setCurrentField('maxContextTokens');
|
|
271
309
|
}
|
|
310
|
+
else if (currentField === 'compactBaseUrl') {
|
|
311
|
+
setCurrentField('maxTokens');
|
|
312
|
+
}
|
|
272
313
|
else if (currentField === 'compactApiKey') {
|
|
273
314
|
setCurrentField('compactBaseUrl');
|
|
274
315
|
}
|
|
@@ -284,6 +325,9 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
284
325
|
setCurrentField('maxContextTokens');
|
|
285
326
|
}
|
|
286
327
|
else if (currentField === 'maxContextTokens') {
|
|
328
|
+
setCurrentField('maxTokens');
|
|
329
|
+
}
|
|
330
|
+
else if (currentField === 'maxTokens') {
|
|
287
331
|
setCurrentField('compactBaseUrl');
|
|
288
332
|
}
|
|
289
333
|
else if (currentField === 'compactBaseUrl') {
|
|
@@ -296,18 +340,18 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
296
340
|
});
|
|
297
341
|
if (baseUrlMissing) {
|
|
298
342
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
299
|
-
React.createElement(Box, { marginBottom:
|
|
343
|
+
React.createElement(Box, { marginBottom: 1, borderStyle: "double", borderColor: "cyan", paddingX: 1, paddingY: 0 },
|
|
300
344
|
React.createElement(Box, { flexDirection: "column" },
|
|
301
345
|
React.createElement(Gradient, { name: 'rainbow' }, "Model Configuration"),
|
|
302
346
|
React.createElement(Text, { color: "gray", dimColor: true }, "Configure AI models for different tasks"))),
|
|
303
|
-
React.createElement(Box, { marginBottom:
|
|
347
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
304
348
|
React.createElement(Alert, { variant: "error" }, "Base URL not configured. Please configure API settings first before setting up models.")),
|
|
305
349
|
React.createElement(Box, { flexDirection: "column" },
|
|
306
350
|
React.createElement(Alert, { variant: "info" }, "Press Esc to return to main menu"))));
|
|
307
351
|
}
|
|
308
352
|
if (loading) {
|
|
309
353
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
310
|
-
React.createElement(Box, { marginBottom:
|
|
354
|
+
React.createElement(Box, { marginBottom: 1, borderStyle: "double", paddingX: 1, paddingY: 0 },
|
|
311
355
|
React.createElement(Box, { flexDirection: "column" },
|
|
312
356
|
React.createElement(Gradient, { name: "rainbow" }, "Model Configuration"),
|
|
313
357
|
React.createElement(Text, { color: "gray", dimColor: true }, "Loading available models...")))));
|
|
@@ -315,15 +359,15 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
315
359
|
// 手动输入模式的界面
|
|
316
360
|
if (manualInputMode) {
|
|
317
361
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
318
|
-
React.createElement(Box, { marginBottom:
|
|
362
|
+
React.createElement(Box, { marginBottom: 1, borderStyle: "double", borderColor: "cyan", paddingX: 1, paddingY: 0 },
|
|
319
363
|
React.createElement(Box, { flexDirection: "column" },
|
|
320
364
|
React.createElement(Gradient, { name: 'rainbow' }, "Manual Input Model"),
|
|
321
365
|
React.createElement(Text, { color: "gray", dimColor: true }, "Enter model name manually"))),
|
|
322
|
-
React.createElement(Box, { flexDirection: "column", marginBottom:
|
|
366
|
+
React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
323
367
|
React.createElement(Text, { color: "cyan" },
|
|
324
368
|
currentField === 'advancedModel' ? 'Advanced Model' : 'Basic Model',
|
|
325
369
|
":"),
|
|
326
|
-
React.createElement(Box, { marginLeft: 2
|
|
370
|
+
React.createElement(Box, { marginLeft: 2 },
|
|
327
371
|
React.createElement(Text, { color: "green" },
|
|
328
372
|
`> ${manualInputValue}`,
|
|
329
373
|
React.createElement(Text, { color: "white" }, "_")))),
|
|
@@ -332,82 +376,93 @@ export default function ModelConfigScreen({ onBack, onSave }) {
|
|
|
332
376
|
React.createElement(Alert, { variant: "info" }, "Press Enter to confirm, Esc to cancel"))));
|
|
333
377
|
}
|
|
334
378
|
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
335
|
-
React.createElement(Box, { marginBottom:
|
|
379
|
+
React.createElement(Box, { marginBottom: 1, borderStyle: "double", borderColor: "cyan", paddingX: 1, paddingY: 0 },
|
|
336
380
|
React.createElement(Box, { flexDirection: "column" },
|
|
337
381
|
React.createElement(Gradient, { name: 'rainbow' }, "Model Configuration"),
|
|
338
382
|
React.createElement(Text, { color: "gray", dimColor: true }, "Configure AI models for different tasks"))),
|
|
339
|
-
React.createElement(Box, { flexDirection: "column"
|
|
340
|
-
React.createElement(Box,
|
|
383
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
384
|
+
React.createElement(Box, null,
|
|
341
385
|
React.createElement(Box, { flexDirection: "column" },
|
|
342
386
|
React.createElement(Text, { color: currentField === 'advancedModel' ? 'green' : 'white' },
|
|
343
387
|
currentField === 'advancedModel' ? '➣ ' : ' ',
|
|
344
388
|
"Advanced Model (Main Work):"),
|
|
345
|
-
currentField === 'advancedModel' && isEditing && (React.createElement(Box, { marginLeft:
|
|
389
|
+
currentField === 'advancedModel' && isEditing && (React.createElement(Box, { marginLeft: 2 }, loading ? (React.createElement(Text, { color: "yellow" }, "Loading models...")) : (React.createElement(Box, { flexDirection: "column" },
|
|
346
390
|
searchTerm && (React.createElement(Text, { color: "cyan" },
|
|
347
391
|
"Filter: ",
|
|
348
392
|
searchTerm)),
|
|
349
393
|
React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange }))))),
|
|
350
|
-
(!isEditing || currentField !== 'advancedModel') && (React.createElement(Box, { marginLeft:
|
|
394
|
+
(!isEditing || currentField !== 'advancedModel') && (React.createElement(Box, { marginLeft: 2 },
|
|
351
395
|
React.createElement(Text, { color: "gray" }, advancedModel || 'Not set'))))),
|
|
352
|
-
React.createElement(Box,
|
|
396
|
+
React.createElement(Box, null,
|
|
353
397
|
React.createElement(Box, { flexDirection: "column" },
|
|
354
398
|
React.createElement(Text, { color: currentField === 'basicModel' ? 'green' : 'white' },
|
|
355
399
|
currentField === 'basicModel' ? '➣ ' : ' ',
|
|
356
400
|
"Basic Model (Summary & Analysis):"),
|
|
357
|
-
currentField === 'basicModel' && isEditing && (React.createElement(Box, { marginLeft:
|
|
401
|
+
currentField === 'basicModel' && isEditing && (React.createElement(Box, { marginLeft: 2 }, loading ? (React.createElement(Text, { color: "yellow" }, "Loading models...")) : (React.createElement(Box, { flexDirection: "column" },
|
|
358
402
|
searchTerm && (React.createElement(Text, { color: "cyan" },
|
|
359
403
|
"Filter: ",
|
|
360
404
|
searchTerm)),
|
|
361
405
|
React.createElement(Select, { options: getCurrentOptions(), defaultValue: getCurrentValue(), onChange: handleModelChange }))))),
|
|
362
|
-
(!isEditing || currentField !== 'basicModel') && (React.createElement(Box, { marginLeft:
|
|
406
|
+
(!isEditing || currentField !== 'basicModel') && (React.createElement(Box, { marginLeft: 2 },
|
|
363
407
|
React.createElement(Text, { color: "gray" }, basicModel || 'Not set'))))),
|
|
364
|
-
React.createElement(Box,
|
|
408
|
+
React.createElement(Box, null,
|
|
365
409
|
React.createElement(Box, { flexDirection: "column" },
|
|
366
410
|
React.createElement(Text, { color: currentField === 'maxContextTokens' ? 'green' : 'white' },
|
|
367
411
|
currentField === 'maxContextTokens' ? '➣ ' : ' ',
|
|
368
412
|
"Max Context Tokens (Auto-compress when reached):"),
|
|
369
|
-
currentField === 'maxContextTokens' && isEditing && (React.createElement(Box, { marginLeft:
|
|
413
|
+
currentField === 'maxContextTokens' && isEditing && (React.createElement(Box, { marginLeft: 2 },
|
|
370
414
|
React.createElement(Text, { color: "cyan" },
|
|
371
415
|
"Enter value: ",
|
|
372
416
|
maxContextTokens))),
|
|
373
|
-
(!isEditing || currentField !== 'maxContextTokens') && (React.createElement(Box, { marginLeft:
|
|
417
|
+
(!isEditing || currentField !== 'maxContextTokens') && (React.createElement(Box, { marginLeft: 2 },
|
|
374
418
|
React.createElement(Text, { color: "gray" }, maxContextTokens))))),
|
|
375
|
-
React.createElement(Box,
|
|
419
|
+
React.createElement(Box, null,
|
|
420
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
421
|
+
React.createElement(Text, { color: currentField === 'maxTokens' ? 'green' : 'white' },
|
|
422
|
+
currentField === 'maxTokens' ? '➣ ' : ' ',
|
|
423
|
+
"Max Tokens (Max tokens for single response):"),
|
|
424
|
+
currentField === 'maxTokens' && isEditing && (React.createElement(Box, { marginLeft: 2 },
|
|
425
|
+
React.createElement(Text, { color: "cyan" },
|
|
426
|
+
"Enter value: ",
|
|
427
|
+
maxTokens))),
|
|
428
|
+
(!isEditing || currentField !== 'maxTokens') && (React.createElement(Box, { marginLeft: 2 },
|
|
429
|
+
React.createElement(Text, { color: "gray" }, maxTokens))))),
|
|
430
|
+
React.createElement(Box, { marginTop: 1 },
|
|
376
431
|
React.createElement(Text, { color: "cyan", bold: true }, "Compact Model (Context Compression):")),
|
|
377
|
-
React.createElement(Box,
|
|
432
|
+
React.createElement(Box, null,
|
|
378
433
|
React.createElement(Box, { flexDirection: "column" },
|
|
379
434
|
React.createElement(Text, { color: currentField === 'compactBaseUrl' ? 'green' : 'white' },
|
|
380
435
|
currentField === 'compactBaseUrl' ? '➣ ' : ' ',
|
|
381
436
|
"Base URL:"),
|
|
382
|
-
currentField === 'compactBaseUrl' && isEditing && (React.createElement(Box, { marginLeft:
|
|
437
|
+
currentField === 'compactBaseUrl' && isEditing && (React.createElement(Box, { marginLeft: 2 },
|
|
383
438
|
React.createElement(Text, { color: "cyan" },
|
|
384
439
|
compactBaseUrl,
|
|
385
440
|
React.createElement(Text, { color: "white" }, "_")))),
|
|
386
|
-
(!isEditing || currentField !== 'compactBaseUrl') && (React.createElement(Box, { marginLeft:
|
|
441
|
+
(!isEditing || currentField !== 'compactBaseUrl') && (React.createElement(Box, { marginLeft: 2 },
|
|
387
442
|
React.createElement(Text, { color: "gray" }, compactBaseUrl || 'Not set'))))),
|
|
388
|
-
React.createElement(Box,
|
|
443
|
+
React.createElement(Box, null,
|
|
389
444
|
React.createElement(Box, { flexDirection: "column" },
|
|
390
445
|
React.createElement(Text, { color: currentField === 'compactApiKey' ? 'green' : 'white' },
|
|
391
446
|
currentField === 'compactApiKey' ? '➣ ' : ' ',
|
|
392
447
|
"API Key:"),
|
|
393
|
-
currentField === 'compactApiKey' && isEditing && (React.createElement(Box, { marginLeft:
|
|
448
|
+
currentField === 'compactApiKey' && isEditing && (React.createElement(Box, { marginLeft: 2 },
|
|
394
449
|
React.createElement(Text, { color: "cyan" },
|
|
395
450
|
compactApiKey.replace(/./g, '*'),
|
|
396
451
|
React.createElement(Text, { color: "white" }, "_")))),
|
|
397
|
-
(!isEditing || currentField !== 'compactApiKey') && (React.createElement(Box, { marginLeft:
|
|
452
|
+
(!isEditing || currentField !== 'compactApiKey') && (React.createElement(Box, { marginLeft: 2 },
|
|
398
453
|
React.createElement(Text, { color: "gray" }, compactApiKey ? compactApiKey.replace(/./g, '*') : 'Not set'))))),
|
|
399
|
-
React.createElement(Box,
|
|
454
|
+
React.createElement(Box, null,
|
|
400
455
|
React.createElement(Box, { flexDirection: "column" },
|
|
401
456
|
React.createElement(Text, { color: currentField === 'compactModelName' ? 'green' : 'white' },
|
|
402
457
|
currentField === 'compactModelName' ? '➣ ' : ' ',
|
|
403
458
|
"Model Name:"),
|
|
404
|
-
currentField === 'compactModelName' && isEditing && (React.createElement(Box, { marginLeft:
|
|
459
|
+
currentField === 'compactModelName' && isEditing && (React.createElement(Box, { marginLeft: 2 },
|
|
405
460
|
React.createElement(Text, { color: "cyan" },
|
|
406
461
|
compactModelName,
|
|
407
462
|
React.createElement(Text, { color: "white" }, "_")))),
|
|
408
|
-
(!isEditing || currentField !== 'compactModelName') && (React.createElement(Box, { marginLeft:
|
|
463
|
+
(!isEditing || currentField !== 'compactModelName') && (React.createElement(Box, { marginLeft: 2 },
|
|
409
464
|
React.createElement(Text, { color: "gray" }, compactModelName || 'Not set')))))),
|
|
410
|
-
React.createElement(Box, { flexDirection: "column" }, isEditing ? (React.createElement(React.Fragment, null,
|
|
465
|
+
React.createElement(Box, { flexDirection: "column", marginTop: 1 }, isEditing ? (React.createElement(React.Fragment, null,
|
|
411
466
|
React.createElement(Alert, { variant: "info" }, "Editing mode: Type to filter models, \u2191\u2193 to select, Enter to confirm"))) : (React.createElement(React.Fragment, null,
|
|
412
467
|
React.createElement(Alert, { variant: "info" }, "Use \u2191\u2193 to navigate, Enter to edit, M for manual input, Ctrl+S or Esc to save"))))));
|
|
413
468
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useApp } from 'ink';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir, platform } from 'os';
|
|
7
|
+
import { getOpenAiConfig, updateOpenAiConfig, } from '../../utils/apiConfig.js';
|
|
8
|
+
import { SYSTEM_PROMPT } from '../../api/systemPrompt.js';
|
|
9
|
+
const CONFIG_DIR = join(homedir(), '.snow');
|
|
10
|
+
const SYSTEM_PROMPT_FILE = join(CONFIG_DIR, 'system-prompt.txt');
|
|
11
|
+
function getSystemEditor() {
|
|
12
|
+
if (platform() === 'win32') {
|
|
13
|
+
return 'notepad';
|
|
14
|
+
}
|
|
15
|
+
return process.env['EDITOR'] || 'vim';
|
|
16
|
+
}
|
|
17
|
+
function ensureConfigDirectory() {
|
|
18
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
19
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export default function SystemPromptConfigScreen({ onBack }) {
|
|
23
|
+
const { exit } = useApp();
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const openEditor = async () => {
|
|
26
|
+
ensureConfigDirectory();
|
|
27
|
+
// 读取当前配置的自定义系统提示词,如果为空则使用默认系统提示词
|
|
28
|
+
const config = getOpenAiConfig();
|
|
29
|
+
const currentPrompt = config.systemPrompt || SYSTEM_PROMPT;
|
|
30
|
+
// 写入临时文件供编辑
|
|
31
|
+
writeFileSync(SYSTEM_PROMPT_FILE, currentPrompt, 'utf8');
|
|
32
|
+
const editor = getSystemEditor();
|
|
33
|
+
exit();
|
|
34
|
+
const child = spawn(editor, [SYSTEM_PROMPT_FILE], {
|
|
35
|
+
stdio: 'inherit'
|
|
36
|
+
});
|
|
37
|
+
child.on('close', () => {
|
|
38
|
+
// 读取编辑后的内容
|
|
39
|
+
if (existsSync(SYSTEM_PROMPT_FILE)) {
|
|
40
|
+
try {
|
|
41
|
+
const editedContent = readFileSync(SYSTEM_PROMPT_FILE, 'utf8');
|
|
42
|
+
// 如果编辑后的内容为空或与默认提示词相同,则保存为空(使用默认)
|
|
43
|
+
// 否则保存自定义提示词
|
|
44
|
+
const trimmedContent = editedContent.trim();
|
|
45
|
+
if (trimmedContent === '' || trimmedContent === SYSTEM_PROMPT.trim()) {
|
|
46
|
+
// 保存为空,表示使用默认提示词
|
|
47
|
+
updateOpenAiConfig({ systemPrompt: undefined });
|
|
48
|
+
console.log('System prompt reset to default. Please use `snow` to restart!');
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// 保存自定义提示词
|
|
52
|
+
updateOpenAiConfig({ systemPrompt: editedContent });
|
|
53
|
+
console.log('Custom system prompt saved successfully! Please use `snow` to restart!');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Failed to read edited content:', error instanceof Error ? error.message : 'Unknown error');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
process.exit(0);
|
|
61
|
+
});
|
|
62
|
+
child.on('error', (error) => {
|
|
63
|
+
console.error('Failed to open editor:', error.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
openEditor();
|
|
68
|
+
}, [exit, onBack]);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
@@ -22,6 +22,11 @@ export default function WelcomeScreen({ version = '1.0.0', onMenuSelect, }) {
|
|
|
22
22
|
value: 'models',
|
|
23
23
|
infoText: 'Configure AI models for different tasks',
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
label: 'System Prompt Settings',
|
|
27
|
+
value: 'systemprompt',
|
|
28
|
+
infoText: 'Configure custom system prompt (overrides default)',
|
|
29
|
+
},
|
|
25
30
|
{
|
|
26
31
|
label: 'MCP Settings',
|
|
27
32
|
value: 'mcp',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type RequestMethod = 'chat' | 'responses';
|
|
1
|
+
export type RequestMethod = 'chat' | 'responses' | 'gemini' | 'anthropic';
|
|
2
2
|
export interface CompactModelConfig {
|
|
3
3
|
baseUrl: string;
|
|
4
4
|
apiKey: string;
|
|
@@ -11,7 +11,10 @@ export interface ApiConfig {
|
|
|
11
11
|
advancedModel?: string;
|
|
12
12
|
basicModel?: string;
|
|
13
13
|
maxContextTokens?: number;
|
|
14
|
+
maxTokens?: number;
|
|
14
15
|
compactModel?: CompactModelConfig;
|
|
16
|
+
anthropicBeta?: boolean;
|
|
17
|
+
systemPrompt?: string;
|
|
15
18
|
}
|
|
16
19
|
export interface MCPServer {
|
|
17
20
|
url?: string;
|
|
@@ -23,7 +26,8 @@ export interface MCPConfig {
|
|
|
23
26
|
mcpServers: Record<string, MCPServer>;
|
|
24
27
|
}
|
|
25
28
|
export interface AppConfig {
|
|
26
|
-
|
|
29
|
+
snowcfg: ApiConfig;
|
|
30
|
+
openai?: ApiConfig;
|
|
27
31
|
}
|
|
28
32
|
export declare function loadConfig(): AppConfig;
|
|
29
33
|
export declare function saveConfig(config: AppConfig): void;
|
package/dist/utils/apiConfig.js
CHANGED
|
@@ -2,13 +2,15 @@ import { homedir } from 'os';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
4
4
|
const DEFAULT_CONFIG = {
|
|
5
|
-
|
|
5
|
+
snowcfg: {
|
|
6
6
|
baseUrl: 'https://api.openai.com/v1',
|
|
7
7
|
apiKey: '',
|
|
8
8
|
requestMethod: 'chat',
|
|
9
9
|
advancedModel: '',
|
|
10
10
|
basicModel: '',
|
|
11
11
|
maxContextTokens: 4000,
|
|
12
|
+
maxTokens: 4096,
|
|
13
|
+
anthropicBeta: false,
|
|
12
14
|
},
|
|
13
15
|
};
|
|
14
16
|
const DEFAULT_MCP_CONFIG = {
|
|
@@ -16,13 +18,13 @@ const DEFAULT_MCP_CONFIG = {
|
|
|
16
18
|
};
|
|
17
19
|
const CONFIG_DIR = join(homedir(), '.snow');
|
|
18
20
|
function normalizeRequestMethod(method) {
|
|
19
|
-
if (method === 'chat' || method === 'responses') {
|
|
21
|
+
if (method === 'chat' || method === 'responses' || method === 'gemini' || method === 'anthropic') {
|
|
20
22
|
return method;
|
|
21
23
|
}
|
|
22
24
|
if (method === 'completions') {
|
|
23
25
|
return 'chat';
|
|
24
26
|
}
|
|
25
|
-
return DEFAULT_CONFIG.
|
|
27
|
+
return DEFAULT_CONFIG.snowcfg.requestMethod;
|
|
26
28
|
}
|
|
27
29
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
28
30
|
const MCP_CONFIG_FILE = join(CONFIG_DIR, 'mcp-config.json');
|
|
@@ -47,18 +49,36 @@ export function loadConfig() {
|
|
|
47
49
|
const parsedConfig = JSON.parse(configData);
|
|
48
50
|
const { mcp: legacyMcp, ...restConfig } = parsedConfig;
|
|
49
51
|
const configWithoutMcp = restConfig;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
// 向下兼容:如果存在 openai 配置但没有 snowcfg,则使用 openai 配置
|
|
53
|
+
let apiConfig;
|
|
54
|
+
if (configWithoutMcp.snowcfg) {
|
|
55
|
+
apiConfig = {
|
|
56
|
+
...DEFAULT_CONFIG.snowcfg,
|
|
57
|
+
...configWithoutMcp.snowcfg,
|
|
58
|
+
requestMethod: normalizeRequestMethod(configWithoutMcp.snowcfg.requestMethod),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
else if (configWithoutMcp.openai) {
|
|
62
|
+
// 向下兼容旧版本
|
|
63
|
+
apiConfig = {
|
|
64
|
+
...DEFAULT_CONFIG.snowcfg,
|
|
65
|
+
...configWithoutMcp.openai,
|
|
66
|
+
requestMethod: normalizeRequestMethod(configWithoutMcp.openai.requestMethod),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
apiConfig = {
|
|
71
|
+
...DEFAULT_CONFIG.snowcfg,
|
|
72
|
+
requestMethod: DEFAULT_CONFIG.snowcfg.requestMethod,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
56
75
|
const mergedConfig = {
|
|
57
76
|
...DEFAULT_CONFIG,
|
|
58
77
|
...configWithoutMcp,
|
|
59
|
-
|
|
78
|
+
snowcfg: apiConfig,
|
|
60
79
|
};
|
|
61
|
-
|
|
80
|
+
// 如果是从旧版本迁移过来的,保存新配置
|
|
81
|
+
if (legacyMcp !== undefined || (configWithoutMcp.openai && !configWithoutMcp.snowcfg)) {
|
|
62
82
|
saveConfig(mergedConfig);
|
|
63
83
|
}
|
|
64
84
|
return mergedConfig;
|
|
@@ -70,7 +90,9 @@ export function loadConfig() {
|
|
|
70
90
|
export function saveConfig(config) {
|
|
71
91
|
ensureConfigDirectory();
|
|
72
92
|
try {
|
|
73
|
-
|
|
93
|
+
// 只保留 snowcfg,去除 openai 字段
|
|
94
|
+
const { openai, ...configWithoutOpenai } = config;
|
|
95
|
+
const configData = JSON.stringify(configWithoutOpenai, null, 2);
|
|
74
96
|
writeFileSync(CONFIG_FILE, configData, 'utf8');
|
|
75
97
|
}
|
|
76
98
|
catch (error) {
|
|
@@ -81,13 +103,13 @@ export function updateOpenAiConfig(apiConfig) {
|
|
|
81
103
|
const currentConfig = loadConfig();
|
|
82
104
|
const updatedConfig = {
|
|
83
105
|
...currentConfig,
|
|
84
|
-
|
|
106
|
+
snowcfg: { ...currentConfig.snowcfg, ...apiConfig },
|
|
85
107
|
};
|
|
86
108
|
saveConfig(updatedConfig);
|
|
87
109
|
}
|
|
88
110
|
export function getOpenAiConfig() {
|
|
89
111
|
const config = loadConfig();
|
|
90
|
-
return config.
|
|
112
|
+
return config.snowcfg;
|
|
91
113
|
}
|
|
92
114
|
export function validateApiConfig(config) {
|
|
93
115
|
const errors = [];
|
|
@@ -22,14 +22,17 @@ export declare class TextBuffer {
|
|
|
22
22
|
private viewport;
|
|
23
23
|
private pasteStorage;
|
|
24
24
|
private pasteCounter;
|
|
25
|
-
private lastPasteTime;
|
|
26
25
|
private imageStorage;
|
|
27
26
|
private imageCounter;
|
|
27
|
+
private pasteAccumulator;
|
|
28
|
+
private pasteTimer;
|
|
29
|
+
private pastePlaceholderPosition;
|
|
30
|
+
private onUpdateCallback?;
|
|
28
31
|
private visualLines;
|
|
29
32
|
private visualLineStarts;
|
|
30
33
|
private visualCursorPos;
|
|
31
34
|
private preferredVisualCol;
|
|
32
|
-
constructor(viewport: Viewport);
|
|
35
|
+
constructor(viewport: Viewport, onUpdate?: () => void);
|
|
33
36
|
get text(): string;
|
|
34
37
|
/**
|
|
35
38
|
* 获取完整文本,包括替换占位符为原始内容
|
|
@@ -42,6 +45,10 @@ export declare class TextBuffer {
|
|
|
42
45
|
private scheduleUpdate;
|
|
43
46
|
setText(text: string): void;
|
|
44
47
|
insert(input: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* 完成粘贴操作,创建占位符
|
|
50
|
+
*/
|
|
51
|
+
private finalizePaste;
|
|
45
52
|
private insertPlainText;
|
|
46
53
|
backspace(): void;
|
|
47
54
|
delete(): void;
|