snow-ai 0.3.2 → 0.3.4
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/agents/summaryAgent.d.ts +31 -0
- package/dist/agents/summaryAgent.js +256 -0
- package/dist/api/systemPrompt.d.ts +1 -1
- package/dist/api/systemPrompt.js +6 -6
- package/dist/app.d.ts +2 -1
- package/dist/app.js +6 -1
- package/dist/cli.js +11 -6
- package/dist/hooks/useSessionSave.js +13 -2
- package/dist/mcp/todo.d.ts +8 -1
- package/dist/mcp/todo.js +126 -17
- package/dist/ui/pages/ChatScreen.js +12 -5
- package/dist/ui/pages/HeadlessModeScreen.d.ts +7 -0
- package/dist/ui/pages/HeadlessModeScreen.js +391 -0
- package/dist/utils/sessionManager.d.ts +10 -0
- package/dist/utils/sessionManager.js +231 -20
- package/package.json +1 -1
- package/readme.md +78 -57
|
@@ -2,6 +2,8 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
|
+
import { summaryAgent } from '../agents/summaryAgent.js';
|
|
6
|
+
import { getTodoService } from './mcpToolsManager.js';
|
|
5
7
|
class SessionManager {
|
|
6
8
|
constructor() {
|
|
7
9
|
Object.defineProperty(this, "sessionsDir", {
|
|
@@ -16,21 +18,61 @@ class SessionManager {
|
|
|
16
18
|
writable: true,
|
|
17
19
|
value: null
|
|
18
20
|
});
|
|
21
|
+
Object.defineProperty(this, "summaryAbortController", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: null
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(this, "summaryTimeoutId", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: null
|
|
32
|
+
});
|
|
19
33
|
this.sessionsDir = path.join(os.homedir(), '.snow', 'sessions');
|
|
20
34
|
}
|
|
21
|
-
async ensureSessionsDir() {
|
|
35
|
+
async ensureSessionsDir(date) {
|
|
22
36
|
try {
|
|
23
37
|
await fs.mkdir(this.sessionsDir, { recursive: true });
|
|
38
|
+
if (date) {
|
|
39
|
+
const dateFolder = this.formatDateForFolder(date);
|
|
40
|
+
const sessionDir = path.join(this.sessionsDir, dateFolder);
|
|
41
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
42
|
+
}
|
|
24
43
|
}
|
|
25
44
|
catch (error) {
|
|
26
45
|
// Directory already exists or other error
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
|
-
getSessionPath(sessionId) {
|
|
30
|
-
|
|
48
|
+
getSessionPath(sessionId, date) {
|
|
49
|
+
const sessionDate = date || new Date();
|
|
50
|
+
const dateFolder = this.formatDateForFolder(sessionDate);
|
|
51
|
+
const sessionDir = path.join(this.sessionsDir, dateFolder);
|
|
52
|
+
return path.join(sessionDir, `${sessionId}.json`);
|
|
53
|
+
}
|
|
54
|
+
formatDateForFolder(date) {
|
|
55
|
+
const year = date.getFullYear();
|
|
56
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
57
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
58
|
+
return `${year}-${month}-${day}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Cancel any ongoing summary generation
|
|
62
|
+
* This prevents wasted resources and race conditions
|
|
63
|
+
*/
|
|
64
|
+
cancelOngoingSummaryGeneration() {
|
|
65
|
+
if (this.summaryAbortController) {
|
|
66
|
+
this.summaryAbortController.abort();
|
|
67
|
+
this.summaryAbortController = null;
|
|
68
|
+
}
|
|
69
|
+
if (this.summaryTimeoutId) {
|
|
70
|
+
clearTimeout(this.summaryTimeoutId);
|
|
71
|
+
this.summaryTimeoutId = null;
|
|
72
|
+
}
|
|
31
73
|
}
|
|
32
74
|
async createNewSession() {
|
|
33
|
-
await this.ensureSessionsDir();
|
|
75
|
+
await this.ensureSessionsDir(new Date());
|
|
34
76
|
// 使用 UUID v4 生成唯一会话 ID,避免并发冲突
|
|
35
77
|
const sessionId = randomUUID();
|
|
36
78
|
const session = {
|
|
@@ -40,39 +82,86 @@ class SessionManager {
|
|
|
40
82
|
createdAt: Date.now(),
|
|
41
83
|
updatedAt: Date.now(),
|
|
42
84
|
messages: [],
|
|
43
|
-
messageCount: 0
|
|
85
|
+
messageCount: 0,
|
|
44
86
|
};
|
|
45
87
|
this.currentSession = session;
|
|
46
88
|
await this.saveSession(session);
|
|
47
89
|
return session;
|
|
48
90
|
}
|
|
49
91
|
async saveSession(session) {
|
|
50
|
-
|
|
51
|
-
|
|
92
|
+
const sessionDate = new Date(session.createdAt);
|
|
93
|
+
await this.ensureSessionsDir(sessionDate);
|
|
94
|
+
const sessionPath = this.getSessionPath(session.id, sessionDate);
|
|
52
95
|
await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
|
|
53
96
|
}
|
|
54
97
|
async loadSession(sessionId) {
|
|
98
|
+
// 首先尝试从旧格式加载(向下兼容)
|
|
55
99
|
try {
|
|
56
|
-
const
|
|
57
|
-
const data = await fs.readFile(
|
|
100
|
+
const oldSessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
101
|
+
const data = await fs.readFile(oldSessionPath, 'utf-8');
|
|
58
102
|
const session = JSON.parse(data);
|
|
59
103
|
this.currentSession = session;
|
|
60
104
|
return session;
|
|
61
105
|
}
|
|
62
106
|
catch (error) {
|
|
63
|
-
|
|
107
|
+
// 旧格式不存在,搜索日期文件夹
|
|
108
|
+
}
|
|
109
|
+
// 在日期文件夹中查找会话
|
|
110
|
+
try {
|
|
111
|
+
const session = await this.findSessionInDateFolders(sessionId);
|
|
112
|
+
if (session) {
|
|
113
|
+
this.currentSession = session;
|
|
114
|
+
return session;
|
|
115
|
+
}
|
|
64
116
|
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// 搜索失败
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
async findSessionInDateFolders(sessionId) {
|
|
123
|
+
try {
|
|
124
|
+
const files = await fs.readdir(this.sessionsDir);
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const filePath = path.join(this.sessionsDir, file);
|
|
127
|
+
const stat = await fs.stat(filePath);
|
|
128
|
+
if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
|
|
129
|
+
// 这是日期文件夹,查找会话文件
|
|
130
|
+
const sessionPath = path.join(filePath, `${sessionId}.json`);
|
|
131
|
+
try {
|
|
132
|
+
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
133
|
+
const session = JSON.parse(data);
|
|
134
|
+
return session;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// 文件不存在或读取失败,继续搜索
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// 目录读取失败
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
65
147
|
}
|
|
66
148
|
async listSessions() {
|
|
67
149
|
await this.ensureSessionsDir();
|
|
150
|
+
const sessions = [];
|
|
68
151
|
try {
|
|
152
|
+
// 首先处理新的日期文件夹结构
|
|
69
153
|
const files = await fs.readdir(this.sessionsDir);
|
|
70
|
-
const sessions = [];
|
|
71
154
|
for (const file of files) {
|
|
72
|
-
|
|
155
|
+
const filePath = path.join(this.sessionsDir, file);
|
|
156
|
+
const stat = await fs.stat(filePath);
|
|
157
|
+
if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
|
|
158
|
+
// 这是日期文件夹,读取其中的会话文件
|
|
159
|
+
await this.readSessionsFromDir(filePath, sessions);
|
|
160
|
+
}
|
|
161
|
+
else if (file.endsWith('.json')) {
|
|
162
|
+
// 这是旧格式的会话文件(向下兼容)
|
|
73
163
|
try {
|
|
74
|
-
const
|
|
75
|
-
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
164
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
76
165
|
const session = JSON.parse(data);
|
|
77
166
|
sessions.push({
|
|
78
167
|
id: session.id,
|
|
@@ -80,7 +169,7 @@ class SessionManager {
|
|
|
80
169
|
summary: session.summary,
|
|
81
170
|
createdAt: session.createdAt,
|
|
82
171
|
updatedAt: session.updatedAt,
|
|
83
|
-
messageCount: session.messageCount
|
|
172
|
+
messageCount: session.messageCount,
|
|
84
173
|
});
|
|
85
174
|
}
|
|
86
175
|
catch (error) {
|
|
@@ -96,6 +185,35 @@ class SessionManager {
|
|
|
96
185
|
return [];
|
|
97
186
|
}
|
|
98
187
|
}
|
|
188
|
+
async readSessionsFromDir(dirPath, sessions) {
|
|
189
|
+
try {
|
|
190
|
+
const files = await fs.readdir(dirPath);
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (file.endsWith('.json')) {
|
|
193
|
+
try {
|
|
194
|
+
const sessionPath = path.join(dirPath, file);
|
|
195
|
+
const data = await fs.readFile(sessionPath, 'utf-8');
|
|
196
|
+
const session = JSON.parse(data);
|
|
197
|
+
sessions.push({
|
|
198
|
+
id: session.id,
|
|
199
|
+
title: session.title,
|
|
200
|
+
summary: session.summary,
|
|
201
|
+
createdAt: session.createdAt,
|
|
202
|
+
updatedAt: session.updatedAt,
|
|
203
|
+
messageCount: session.messageCount,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// Skip invalid session files
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
// Skip directory if it can't be read
|
|
215
|
+
}
|
|
216
|
+
}
|
|
99
217
|
async addMessage(message) {
|
|
100
218
|
if (!this.currentSession) {
|
|
101
219
|
this.currentSession = await this.createNewSession();
|
|
@@ -142,10 +260,59 @@ class SessionManager {
|
|
|
142
260
|
this.currentSession.messages.push(message);
|
|
143
261
|
this.currentSession.messageCount = this.currentSession.messages.length;
|
|
144
262
|
this.currentSession.updatedAt = Date.now();
|
|
145
|
-
//
|
|
263
|
+
// Generate summary from first user message using summaryAgent (parallel, non-blocking)
|
|
146
264
|
if (this.currentSession.messageCount === 1 && message.role === 'user') {
|
|
265
|
+
// Set temporary title immediately (synchronous)
|
|
147
266
|
this.currentSession.title = message.content.slice(0, 50);
|
|
148
267
|
this.currentSession.summary = message.content.slice(0, 100);
|
|
268
|
+
// Cancel any previous summary generation (防呆机制)
|
|
269
|
+
this.cancelOngoingSummaryGeneration();
|
|
270
|
+
// Create new AbortController for this summary generation
|
|
271
|
+
this.summaryAbortController = new AbortController();
|
|
272
|
+
const currentSessionId = this.currentSession.id;
|
|
273
|
+
const abortSignal = this.summaryAbortController.signal;
|
|
274
|
+
// Set timeout to cancel summary generation after 30 seconds (防呆机制)
|
|
275
|
+
this.summaryTimeoutId = setTimeout(() => {
|
|
276
|
+
if (this.summaryAbortController) {
|
|
277
|
+
console.warn('Summary generation timeout after 30s, aborting...');
|
|
278
|
+
this.summaryAbortController.abort();
|
|
279
|
+
this.summaryAbortController = null;
|
|
280
|
+
}
|
|
281
|
+
}, 30000);
|
|
282
|
+
// Generate better summary in parallel (non-blocking)
|
|
283
|
+
// This won't delay the main conversation flow
|
|
284
|
+
summaryAgent
|
|
285
|
+
.generateSummary(message.content, abortSignal)
|
|
286
|
+
.then(summary => {
|
|
287
|
+
// 防呆检查:确保会话没有被切换,且仍然是第一条消息
|
|
288
|
+
if (this.currentSession &&
|
|
289
|
+
this.currentSession.id === currentSessionId &&
|
|
290
|
+
this.currentSession.messageCount === 1) {
|
|
291
|
+
// Only update if this is still the first message in the same session
|
|
292
|
+
this.currentSession.title = summary;
|
|
293
|
+
this.currentSession.summary = summary;
|
|
294
|
+
this.saveSession(this.currentSession).catch(error => {
|
|
295
|
+
console.error('Failed to save session with generated summary:', error);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// Clean up
|
|
299
|
+
this.cancelOngoingSummaryGeneration();
|
|
300
|
+
})
|
|
301
|
+
.catch(error => {
|
|
302
|
+
// Clean up on error
|
|
303
|
+
this.cancelOngoingSummaryGeneration();
|
|
304
|
+
// Silently fail if aborted (expected behavior)
|
|
305
|
+
if (error.name === 'AbortError' || abortSignal.aborted) {
|
|
306
|
+
console.log('Summary generation cancelled (expected)');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// Log other errors - we already have a fallback title/summary
|
|
310
|
+
console.warn('Summary generation failed, using fallback:', error);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
else if (this.currentSession.messageCount > 1) {
|
|
314
|
+
// 防呆机制:如果不是第一条消息,取消任何正在进行的摘要生成
|
|
315
|
+
this.cancelOngoingSummaryGeneration();
|
|
149
316
|
}
|
|
150
317
|
await this.saveSession(this.currentSession);
|
|
151
318
|
}
|
|
@@ -153,20 +320,64 @@ class SessionManager {
|
|
|
153
320
|
return this.currentSession;
|
|
154
321
|
}
|
|
155
322
|
setCurrentSession(session) {
|
|
323
|
+
// 防呆机制:切换会话时取消正在进行的摘要生成
|
|
324
|
+
this.cancelOngoingSummaryGeneration();
|
|
156
325
|
this.currentSession = session;
|
|
157
326
|
}
|
|
158
327
|
clearCurrentSession() {
|
|
328
|
+
// 防呆机制:清除会话时取消正在进行的摘要生成
|
|
329
|
+
this.cancelOngoingSummaryGeneration();
|
|
159
330
|
this.currentSession = null;
|
|
160
331
|
}
|
|
161
332
|
async deleteSession(sessionId) {
|
|
333
|
+
let sessionDeleted = false;
|
|
334
|
+
// 首先尝试删除旧格式(向下兼容)
|
|
162
335
|
try {
|
|
163
|
-
const
|
|
164
|
-
await fs.unlink(
|
|
165
|
-
|
|
336
|
+
const oldSessionPath = path.join(this.sessionsDir, `${sessionId}.json`);
|
|
337
|
+
await fs.unlink(oldSessionPath);
|
|
338
|
+
sessionDeleted = true;
|
|
166
339
|
}
|
|
167
340
|
catch (error) {
|
|
168
|
-
|
|
341
|
+
// 旧格式不存在,搜索日期文件夹
|
|
342
|
+
}
|
|
343
|
+
// 在日期文件夹中查找并删除会话
|
|
344
|
+
if (!sessionDeleted) {
|
|
345
|
+
try {
|
|
346
|
+
const files = await fs.readdir(this.sessionsDir);
|
|
347
|
+
for (const file of files) {
|
|
348
|
+
const filePath = path.join(this.sessionsDir, file);
|
|
349
|
+
const stat = await fs.stat(filePath);
|
|
350
|
+
if (stat.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(file)) {
|
|
351
|
+
// 这是日期文件夹,查找会话文件
|
|
352
|
+
const sessionPath = path.join(filePath, `${sessionId}.json`);
|
|
353
|
+
try {
|
|
354
|
+
await fs.unlink(sessionPath);
|
|
355
|
+
sessionDeleted = true;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
// 文件不存在,继续搜索
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
// 目录读取失败
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// 如果会话删除成功,同时删除对应的TODO列表
|
|
370
|
+
if (sessionDeleted) {
|
|
371
|
+
try {
|
|
372
|
+
const todoService = getTodoService();
|
|
373
|
+
await todoService.deleteTodoList(sessionId);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
// TODO删除失败不影响会话删除结果
|
|
377
|
+
console.warn(`Failed to delete TODO list for session ${sessionId}:`, error);
|
|
378
|
+
}
|
|
169
379
|
}
|
|
380
|
+
return sessionDeleted;
|
|
170
381
|
}
|
|
171
382
|
async truncateMessages(messageCount) {
|
|
172
383
|
if (!this.currentSession) {
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -6,97 +6,118 @@
|
|
|
6
6
|
|
|
7
7
|
**English** | [中文](readme_zh.md)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
_An intelligent AI-powered CLI tool for developers_
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
## Install
|
|
15
|
+
## Installation
|
|
17
16
|
|
|
18
17
|
```bash
|
|
19
|
-
$ npm install
|
|
18
|
+
$ npm install -g snow-ai
|
|
20
19
|
```
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
$ snow
|
|
25
|
-
```
|
|
21
|
+
You can also clone and build from source: https://github.com/MayDay-wpf/snow-cli
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
$ snow --update
|
|
30
|
-
```
|
|
23
|
+
### Install VSCode Extension
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
```json
|
|
34
|
-
{
|
|
35
|
-
"snowcfg": {
|
|
36
|
-
"baseUrl": "https://api.openai.com/v1",//Gemini:https://generativelanguage.googleapis.com Anthropic:https://api.anthropic.com
|
|
37
|
-
"apiKey": "your-api-key",
|
|
38
|
-
"requestMethod": "responses",
|
|
39
|
-
"advancedModel": "gpt-5-codex",
|
|
40
|
-
"basicModel": "gpt-5-codex",
|
|
41
|
-
"maxContextTokens": 32000, //The maximum context length of the model
|
|
42
|
-
"maxTokens": 4096, // The maximum generation length of the model
|
|
43
|
-
"anthropicBeta": false,
|
|
44
|
-
"compactModel": {
|
|
45
|
-
"baseUrl": "https://api.opeai.com/v1",
|
|
46
|
-
"apiKey": "your-api-key",
|
|
47
|
-
"modelName": "gpt-4.1-mini"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
25
|
+
- Download [VSIX/snow-cli-x.x.x.vsix](https://github.com/MayDay-wpf/snow-cli/blob/main/VSIX/)
|
|
52
26
|
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
$ npm uninstall --global snow-ai
|
|
56
|
-
```
|
|
27
|
+
- Open VSCode, click `Extensions` -> `Install from VSIX...` -> select `snow-cli-0.2.6.vsix`
|
|
57
28
|
|
|
58
|
-
|
|
29
|
+
### Install JetBrains Plugin
|
|
59
30
|
|
|
60
|
-
|
|
31
|
+
- Download [JetBrains/build/distributions](https://github.com/MayDay-wpf/snow-cli/tree/main/JetBrains/build/distributions)
|
|
61
32
|
|
|
62
|
-
|
|
33
|
+
## Available Commands
|
|
63
34
|
|
|
64
|
-
|
|
35
|
+
- **Start**: `$ snow`
|
|
36
|
+
- **Update**: `$ snow --update`
|
|
37
|
+
- **Version**: `$ snow --version`
|
|
38
|
+
- **Resume**: `$ snow -c` - Restore the latest conversation history (fully compatible with Claude Code)
|
|
65
39
|
|
|
66
|
-
|
|
40
|
+
## API & Model Settings
|
|
67
41
|
|
|
68
|
-
|
|
42
|
+
In version `v0.3.2` and later, all official SDKs have been removed (they were too heavy), so the configuration is slightly different. After starting, enter `API & Model Settings` to see the following options:
|
|
69
43
|
|
|
70
|
-
|
|
71
|
-
|
|
44
|
+
- **Profile** - Switch or create new configurations. Snow now supports saving multiple API and model schemes
|
|
45
|
+
- **Base URL** - Request endpoint. Since official SDKs were removed, OpenAI and Anthropic require `/v1` suffix, Gemini requires `/v1beta`
|
|
46
|
+
- **API Key** - Your API key
|
|
47
|
+
- **Request Method** - Choose based on your needs: `Chat Completions`, `Responses`, `Gemini`, or `Anthropic`
|
|
48
|
+
- **Anthropic Beta** - When checked, Anthropic requests will automatically include `beta=true` parameter
|
|
49
|
+
- **Advanced Model**, **Basic Model**, **Compact Model** - Set the high-performance model for tasks, small model for summarization, and compact model for context compression. All three models use the configured `BaseURL` and `API Key`. The system automatically fetches available models from the `/models` endpoint with filtering support. For APIs with incomplete model lists, use `Manual Input (Enter model name)` to specify the model name
|
|
50
|
+
- **Max Context Tokens** - The model's maximum context window, used for calculating context percentage. For example, Gemini typically has 1M context, so enter `1000000`. This parameter only affects UI calculations, not actual model context
|
|
51
|
+
- **Max Tokens** - This is critical and will be directly added to API requests as the `max_tokens` parameter
|
|
72
52
|
|
|
73
53
|

|
|
74
54
|
|
|
75
|
-
|
|
55
|
+
## Proxy & Browser Settings
|
|
56
|
+
|
|
57
|
+
Configure system proxy port and search engine for web search. In most cases, this doesn't need modification as the app will automatically use system proxy. The app automatically detects available search engines (Edge/Chrome) unless you've manually changed their installation paths.
|
|
76
58
|
|
|
77
59
|

|
|
78
|
-
* In the middle of the conversation: click ESC to stop AI generation
|
|
79
60
|
|
|
80
|
-
|
|
61
|
+
## System Prompt Settings
|
|
81
62
|
|
|
82
|
-
|
|
83
|
-
* Windows:`alt + v` Paste image
|
|
63
|
+
Customize your system prompt. Note that this supplements Snow's built-in system prompt rather than replacing it. When you set a custom system prompt, Snow's default prompt is downgraded to a user message and appended to the first user message. On Windows, the app automatically opens Notepad; on macOS/Linux, it uses the system's default terminal text editor. After editing and saving, Snow will close and prompt you to restart: `Custom system prompt saved successfully! Please use 'snow' to restart!`
|
|
84
64
|
|
|
65
|
+
## Custom Headers Settings
|
|
85
66
|
|
|
86
|
-
|
|
67
|
+
Add custom request headers. Note that you can only add headers, not override Snow's built-in headers.
|
|
68
|
+
|
|
69
|
+
## MCP Settings
|
|
70
|
+
|
|
71
|
+
Configure MCP services. The method is identical to setting system prompts, and the JSON format matches Cursor's format.
|
|
72
|
+
|
|
73
|
+
## Getting Started - Start Conversation
|
|
74
|
+
|
|
75
|
+
Once everything is configured, enter the conversation page by clicking `Start`.
|
|
76
|
+
|
|
77
|
+
- If you launch Snow from VSCode or other editors, Snow will automatically connect to the IDE using the `Snow CLI` plugin. You'll see a connection message. The plugins are published online - search for `Snow CLI` in the plugin marketplace to install.
|
|
87
78
|
|
|
88
79
|

|
|
89
|
-
- /clear - Create a new session
|
|
90
80
|
|
|
91
|
-
|
|
81
|
+
### File Selection & Commands
|
|
82
|
+
|
|
83
|
+
- Use `@` to select files. In VSCode, you can also hold `Shift` and drag files for the same effect
|
|
84
|
+
- Use `/` to view available commands:
|
|
85
|
+
- `/init` - Build project documentation `SNOW.md`
|
|
86
|
+
- `/clear` - Create a new session
|
|
87
|
+
- `/resume` - Restore conversation history
|
|
88
|
+
- `/mcp` - Check MCP connection status and reconnect
|
|
89
|
+
- `/yolo` - Unattended mode (all tool calls execute without confirmation - use with caution)
|
|
90
|
+
- `/ide` - Manually connect to IDE (usually automatic if plugin is installed)
|
|
91
|
+
- `/compact` - Compress context (rarely used as compression reduces AI quality)
|
|
92
|
+
|
|
93
|
+
### Keyboard Shortcuts
|
|
94
|
+
|
|
95
|
+
- **Windows**: `Alt+V` - Paste image; **macOS/Linux**: `Ctrl+V` - Paste image (with prompt)
|
|
96
|
+
- `Ctrl+L` - Clear input from cursor position to the left
|
|
97
|
+
- `Ctrl+R` - Clear input from cursor position to the right
|
|
98
|
+
- `Shift+Tab` - Toggle Yolo mode on/off
|
|
99
|
+
- `ESC` - Stop AI generation
|
|
100
|
+
- **Double-click `ESC`** - Rollback conversation (with file checkpoints)
|
|
101
|
+
|
|
102
|
+
### Token Usage
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
The input area displays context usage percentage, token count, cache hit tokens, and cache creation tokens.
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+

|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
## Snow System Files
|
|
98
109
|
|
|
99
|
-
|
|
110
|
+
All Snow files are stored in the `.snow` folder in your user directory. Here's what each file/folder contains:
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+

|
|
102
113
|
|
|
114
|
+
- **log** - Runtime logs (not uploaded anywhere, kept locally for debugging). Safe to delete
|
|
115
|
+
- **profiles** - Multiple configuration files for switching between different API/model setups
|
|
116
|
+
- **sessions** - All conversation history (required for `/resume` and other features, not uploaded)
|
|
117
|
+
- **snapshots** - File snapshots before AI edits (used for rollback). Automatic management, no manual intervention needed
|
|
118
|
+
- **todo** - Persisted todo lists from each conversation (prevents AI from forgetting tasks if app exits unexpectedly)
|
|
119
|
+
- **active-profile.txt** - Identifies the currently active profile (for backward compatibility with early versions)
|
|
120
|
+
- **config.json** - Main API configuration file
|
|
121
|
+
- **custom-headers.json** - Custom request headers
|
|
122
|
+
- **mcp-config.json** - MCP service configuration
|
|
123
|
+
- **system-prompt.txt** - Custom system prompt content
|