snow-ai 0.3.2 → 0.3.3

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.
@@ -0,0 +1,31 @@
1
+ export declare class SummaryAgent {
2
+ private modelName;
3
+ private requestMethod;
4
+ private initialized;
5
+ /**
6
+ * Initialize the summary agent with current configuration
7
+ * @returns true if initialized successfully, false otherwise
8
+ */
9
+ private initialize;
10
+ /**
11
+ * Check if summary agent is available
12
+ */
13
+ isAvailable(): Promise<boolean>;
14
+ /**
15
+ * Call the basic model with the same routing as main flow
16
+ * Uses streaming APIs and intercepts to assemble complete response
17
+ * This ensures 100% consistency with main flow routing
18
+ * @param messages - Chat messages
19
+ * @param abortSignal - Optional abort signal to cancel the request
20
+ */
21
+ private callBasicModel;
22
+ /**
23
+ * Generate a concise summary from the first user message
24
+ *
25
+ * @param userMessage - The first user message in the conversation
26
+ * @param abortSignal - Optional abort signal to cancel generation
27
+ * @returns A concise summary (10-20 words) suitable for session title
28
+ */
29
+ generateSummary(userMessage: string, abortSignal?: AbortSignal): Promise<string>;
30
+ }
31
+ export declare const summaryAgent: SummaryAgent;
@@ -0,0 +1,256 @@
1
+ import { getOpenAiConfig, getCustomSystemPrompt } from '../utils/apiConfig.js';
2
+ import { logger } from '../utils/logger.js';
3
+ import { createStreamingChatCompletion } from '../api/chat.js';
4
+ import { createStreamingResponse } from '../api/responses.js';
5
+ import { createStreamingGeminiCompletion } from '../api/gemini.js';
6
+ import { createStreamingAnthropicCompletion } from '../api/anthropic.js';
7
+ export class SummaryAgent {
8
+ constructor() {
9
+ Object.defineProperty(this, "modelName", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: ''
14
+ });
15
+ Object.defineProperty(this, "requestMethod", {
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true,
19
+ value: 'chat'
20
+ });
21
+ Object.defineProperty(this, "initialized", {
22
+ enumerable: true,
23
+ configurable: true,
24
+ writable: true,
25
+ value: false
26
+ });
27
+ }
28
+ /**
29
+ * Initialize the summary agent with current configuration
30
+ * @returns true if initialized successfully, false otherwise
31
+ */
32
+ async initialize() {
33
+ try {
34
+ const config = getOpenAiConfig();
35
+ // Check if basic model is configured
36
+ if (!config.basicModel) {
37
+ return false;
38
+ }
39
+ this.modelName = config.basicModel;
40
+ this.requestMethod = config.requestMethod; // Follow main flow's request method
41
+ this.initialized = true;
42
+ return true;
43
+ }
44
+ catch (error) {
45
+ logger.warn('Failed to initialize summary agent:', error);
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * Check if summary agent is available
51
+ */
52
+ async isAvailable() {
53
+ if (!this.initialized) {
54
+ return await this.initialize();
55
+ }
56
+ return true;
57
+ }
58
+ /**
59
+ * Call the basic model with the same routing as main flow
60
+ * Uses streaming APIs and intercepts to assemble complete response
61
+ * This ensures 100% consistency with main flow routing
62
+ * @param messages - Chat messages
63
+ * @param abortSignal - Optional abort signal to cancel the request
64
+ */
65
+ async callBasicModel(messages, abortSignal) {
66
+ const config = getOpenAiConfig();
67
+ if (!config.basicModel) {
68
+ throw new Error('Basic model not configured');
69
+ }
70
+ // Get custom system prompt if configured
71
+ const customSystemPrompt = getCustomSystemPrompt();
72
+ // If custom system prompt exists, prepend it to messages
73
+ // This ensures summary agent respects user's custom system configuration
74
+ let processedMessages = messages;
75
+ if (customSystemPrompt) {
76
+ processedMessages = [
77
+ {
78
+ role: 'system',
79
+ content: customSystemPrompt,
80
+ },
81
+ ...messages,
82
+ ];
83
+ }
84
+ // Temporarily override advancedModel with basicModel
85
+ const originalAdvancedModel = config.advancedModel;
86
+ try {
87
+ // Override config to use basicModel
88
+ config.advancedModel = config.basicModel;
89
+ let streamGenerator;
90
+ // Route to appropriate streaming API based on request method (follows main flow exactly)
91
+ switch (this.requestMethod) {
92
+ case 'anthropic':
93
+ streamGenerator = createStreamingAnthropicCompletion({
94
+ model: this.modelName,
95
+ messages: processedMessages,
96
+ max_tokens: 1024, // Summaries are short
97
+ }, abortSignal);
98
+ break;
99
+ case 'gemini':
100
+ streamGenerator = createStreamingGeminiCompletion({
101
+ model: this.modelName,
102
+ messages: processedMessages,
103
+ }, abortSignal);
104
+ break;
105
+ case 'responses':
106
+ streamGenerator = createStreamingResponse({
107
+ model: this.modelName,
108
+ messages: processedMessages,
109
+ stream: true,
110
+ }, abortSignal);
111
+ break;
112
+ case 'chat':
113
+ default:
114
+ streamGenerator = createStreamingChatCompletion({
115
+ model: this.modelName,
116
+ messages: processedMessages,
117
+ stream: true,
118
+ }, abortSignal);
119
+ break;
120
+ }
121
+ // Intercept streaming response and assemble complete content
122
+ let completeContent = '';
123
+ let chunkCount = 0;
124
+ try {
125
+ for await (const chunk of streamGenerator) {
126
+ chunkCount++;
127
+ // Check abort signal
128
+ if (abortSignal?.aborted) {
129
+ throw new Error('Request aborted');
130
+ }
131
+ // Handle different chunk formats based on request method
132
+ if (this.requestMethod === 'chat') {
133
+ // Chat API uses standard OpenAI format: {choices: [{delta: {content}}]}
134
+ if (chunk.choices && chunk.choices[0]?.delta?.content) {
135
+ completeContent += chunk.choices[0].delta.content;
136
+ }
137
+ }
138
+ else {
139
+ // Responses, Gemini, and Anthropic APIs all use: {type: 'content', content: string}
140
+ if (chunk.type === 'content' && chunk.content) {
141
+ completeContent += chunk.content;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ catch (streamError) {
147
+ // Log streaming error with details
148
+ if (streamError instanceof Error) {
149
+ logger.error('Summary agent: Streaming error:', {
150
+ error: streamError.message,
151
+ stack: streamError.stack,
152
+ name: streamError.name,
153
+ chunkCount,
154
+ contentLength: completeContent.length,
155
+ });
156
+ }
157
+ else {
158
+ logger.error('Summary agent: Unknown streaming error:', {
159
+ error: streamError,
160
+ chunkCount,
161
+ contentLength: completeContent.length,
162
+ });
163
+ }
164
+ throw streamError;
165
+ }
166
+ return completeContent;
167
+ }
168
+ catch (error) {
169
+ // Log detailed error from API call setup or streaming
170
+ if (error instanceof Error) {
171
+ logger.error('Summary agent: API call failed:', {
172
+ error: error.message,
173
+ stack: error.stack,
174
+ name: error.name,
175
+ requestMethod: this.requestMethod,
176
+ modelName: this.modelName,
177
+ });
178
+ }
179
+ else {
180
+ logger.error('Summary agent: Unknown API error:', {
181
+ error,
182
+ requestMethod: this.requestMethod,
183
+ modelName: this.modelName,
184
+ });
185
+ }
186
+ throw error;
187
+ }
188
+ finally {
189
+ // Restore original config
190
+ config.advancedModel = originalAdvancedModel;
191
+ }
192
+ }
193
+ /**
194
+ * Generate a concise summary from the first user message
195
+ *
196
+ * @param userMessage - The first user message in the conversation
197
+ * @param abortSignal - Optional abort signal to cancel generation
198
+ * @returns A concise summary (10-20 words) suitable for session title
199
+ */
200
+ async generateSummary(userMessage, abortSignal) {
201
+ const available = await this.isAvailable();
202
+ if (!available) {
203
+ // If summary agent is not available, return a truncated version of the message
204
+ return userMessage.slice(0, 50) + (userMessage.length > 50 ? '...' : '');
205
+ }
206
+ try {
207
+ const summaryPrompt = `Generate a concise summary (10-20 words) for the following user message. The summary should capture the main topic or intent.
208
+
209
+ User Message: ${userMessage}
210
+
211
+ Instructions:
212
+ 1. Keep it under 20 words
213
+ 2. Focus on the main topic or question
214
+ 3. Use clear, simple language
215
+ 4. Do not include quotes or special formatting
216
+ 5. Make it suitable as a conversation title
217
+
218
+ Summary:`;
219
+ const messages = [
220
+ {
221
+ role: 'user',
222
+ content: summaryPrompt,
223
+ },
224
+ ];
225
+ const summary = await this.callBasicModel(messages, abortSignal);
226
+ if (!summary || summary.trim().length === 0) {
227
+ logger.warn('Summary agent returned empty response, using truncated message');
228
+ return (userMessage.slice(0, 50) + (userMessage.length > 50 ? '...' : ''));
229
+ }
230
+ // Clean up the summary (remove quotes, trim whitespace)
231
+ const cleanedSummary = summary
232
+ .trim()
233
+ .replace(/^["']|["']$/g, '') // Remove leading/trailing quotes
234
+ .replace(/\n/g, ' ') // Replace newlines with spaces
235
+ .slice(0, 100); // Limit to 100 characters max
236
+ return cleanedSummary;
237
+ }
238
+ catch (error) {
239
+ // Log detailed error information
240
+ if (error instanceof Error) {
241
+ logger.warn('Summary agent generation failed, using truncated message:', {
242
+ error: error.message,
243
+ stack: error.stack,
244
+ name: error.name,
245
+ });
246
+ }
247
+ else {
248
+ logger.warn('Summary agent generation failed with unknown error:', error);
249
+ }
250
+ // Fallback to truncated message
251
+ return userMessage.slice(0, 50) + (userMessage.length > 50 ? '...' : '');
252
+ }
253
+ }
254
+ }
255
+ // Export singleton instance
256
+ export const summaryAgent = new SummaryAgent();
@@ -2,9 +2,20 @@ import { useCallback, useRef } from 'react';
2
2
  import { sessionManager } from '../utils/sessionManager.js';
3
3
  export function useSessionSave() {
4
4
  const savedMessagesRef = useRef(new Set());
5
- // Generate a unique ID for a message (based on role + content + timestamp window)
5
+ // Generate a unique ID for a message (based on role + content + timestamp window + tool identifiers)
6
6
  const generateMessageId = useCallback((message, timestamp) => {
7
- return `${message.role}-${message.content.length}-${Math.floor(timestamp / 5000)}`;
7
+ // Base ID with role, content length, and time window
8
+ let id = `${message.role}-${message.content.length}-${Math.floor(timestamp / 5000)}`;
9
+ // For assistant messages with tool_calls, include tool call IDs to ensure uniqueness
10
+ if (message.role === 'assistant' && message.tool_calls && message.tool_calls.length > 0) {
11
+ const toolCallIds = message.tool_calls.map(tc => tc.id).sort().join(',');
12
+ id += `-tools:${toolCallIds}`;
13
+ }
14
+ // For tool result messages, include the tool_call_id to ensure uniqueness
15
+ if (message.role === 'tool' && message.tool_call_id) {
16
+ id += `-toolcall:${message.tool_call_id}`;
17
+ }
18
+ return id;
8
19
  }, []);
9
20
  // Save API message directly - 直接保存 API 格式的消息
10
21
  const saveMessage = useCallback(async (message) => {
@@ -674,11 +674,18 @@ export default function ChatScreen({ skipWelcome }) {
674
674
  .filter(m => m.toolPending)
675
675
  .map((message, index) => (React.createElement(Box, { key: `pending-tool-${index}`, marginBottom: 1, paddingX: 1, width: terminalWidth },
676
676
  React.createElement(Text, { color: "yellowBright", bold: true }, "\u2746"),
677
- React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "row" },
678
- React.createElement(MarkdownRenderer, { content: message.content || ' ', color: "yellow" }),
679
- React.createElement(Box, { marginLeft: 1 },
680
- React.createElement(Text, { color: "yellow" },
681
- React.createElement(Spinner, { type: "dots" }))))))),
677
+ React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" },
678
+ React.createElement(Box, { flexDirection: "row" },
679
+ React.createElement(MarkdownRenderer, { content: message.content || ' ', color: "yellow" }),
680
+ React.createElement(Box, { marginLeft: 1 },
681
+ React.createElement(Text, { color: "yellow" },
682
+ React.createElement(Spinner, { type: "dots" })))),
683
+ message.toolDisplay && message.toolDisplay.args.length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 }, message.toolDisplay.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: "gray", dimColor: true },
684
+ arg.isLast ? '└─' : '├─',
685
+ " ",
686
+ arg.key,
687
+ ": ",
688
+ arg.value))))))))),
682
689
  (streamingState.isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1, paddingX: 1, width: terminalWidth },
683
690
  React.createElement(Text, { color: ['#FF6EBF', 'green', 'blue', 'cyan', '#B588F8'][streamingState.animationFrame], bold: true }, "\u2746"),
684
691
  React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, streamingState.isStreaming ? (React.createElement(React.Fragment, null, streamingState.retryStatus &&
@@ -22,9 +22,16 @@ export interface SessionListItem {
22
22
  declare class SessionManager {
23
23
  private readonly sessionsDir;
24
24
  private currentSession;
25
+ private summaryAbortController;
26
+ private summaryTimeoutId;
25
27
  constructor();
26
28
  private ensureSessionsDir;
27
29
  private getSessionPath;
30
+ /**
31
+ * Cancel any ongoing summary generation
32
+ * This prevents wasted resources and race conditions
33
+ */
34
+ private cancelOngoingSummaryGeneration;
28
35
  createNewSession(): Promise<Session>;
29
36
  saveSession(session: Session): Promise<void>;
30
37
  loadSession(sessionId: string): Promise<Session | null>;
@@ -2,6 +2,7 @@ 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';
5
6
  class SessionManager {
6
7
  constructor() {
7
8
  Object.defineProperty(this, "sessionsDir", {
@@ -16,6 +17,18 @@ class SessionManager {
16
17
  writable: true,
17
18
  value: null
18
19
  });
20
+ Object.defineProperty(this, "summaryAbortController", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: null
25
+ });
26
+ Object.defineProperty(this, "summaryTimeoutId", {
27
+ enumerable: true,
28
+ configurable: true,
29
+ writable: true,
30
+ value: null
31
+ });
19
32
  this.sessionsDir = path.join(os.homedir(), '.snow', 'sessions');
20
33
  }
21
34
  async ensureSessionsDir() {
@@ -29,6 +42,20 @@ class SessionManager {
29
42
  getSessionPath(sessionId) {
30
43
  return path.join(this.sessionsDir, `${sessionId}.json`);
31
44
  }
45
+ /**
46
+ * Cancel any ongoing summary generation
47
+ * This prevents wasted resources and race conditions
48
+ */
49
+ cancelOngoingSummaryGeneration() {
50
+ if (this.summaryAbortController) {
51
+ this.summaryAbortController.abort();
52
+ this.summaryAbortController = null;
53
+ }
54
+ if (this.summaryTimeoutId) {
55
+ clearTimeout(this.summaryTimeoutId);
56
+ this.summaryTimeoutId = null;
57
+ }
58
+ }
32
59
  async createNewSession() {
33
60
  await this.ensureSessionsDir();
34
61
  // 使用 UUID v4 生成唯一会话 ID,避免并发冲突
@@ -40,7 +67,7 @@ class SessionManager {
40
67
  createdAt: Date.now(),
41
68
  updatedAt: Date.now(),
42
69
  messages: [],
43
- messageCount: 0
70
+ messageCount: 0,
44
71
  };
45
72
  this.currentSession = session;
46
73
  await this.saveSession(session);
@@ -80,7 +107,7 @@ class SessionManager {
80
107
  summary: session.summary,
81
108
  createdAt: session.createdAt,
82
109
  updatedAt: session.updatedAt,
83
- messageCount: session.messageCount
110
+ messageCount: session.messageCount,
84
111
  });
85
112
  }
86
113
  catch (error) {
@@ -142,10 +169,59 @@ class SessionManager {
142
169
  this.currentSession.messages.push(message);
143
170
  this.currentSession.messageCount = this.currentSession.messages.length;
144
171
  this.currentSession.updatedAt = Date.now();
145
- // Simple title generation from first user message (no API call)
172
+ // Generate summary from first user message using summaryAgent (parallel, non-blocking)
146
173
  if (this.currentSession.messageCount === 1 && message.role === 'user') {
174
+ // Set temporary title immediately (synchronous)
147
175
  this.currentSession.title = message.content.slice(0, 50);
148
176
  this.currentSession.summary = message.content.slice(0, 100);
177
+ // Cancel any previous summary generation (防呆机制)
178
+ this.cancelOngoingSummaryGeneration();
179
+ // Create new AbortController for this summary generation
180
+ this.summaryAbortController = new AbortController();
181
+ const currentSessionId = this.currentSession.id;
182
+ const abortSignal = this.summaryAbortController.signal;
183
+ // Set timeout to cancel summary generation after 30 seconds (防呆机制)
184
+ this.summaryTimeoutId = setTimeout(() => {
185
+ if (this.summaryAbortController) {
186
+ console.warn('Summary generation timeout after 30s, aborting...');
187
+ this.summaryAbortController.abort();
188
+ this.summaryAbortController = null;
189
+ }
190
+ }, 30000);
191
+ // Generate better summary in parallel (non-blocking)
192
+ // This won't delay the main conversation flow
193
+ summaryAgent
194
+ .generateSummary(message.content, abortSignal)
195
+ .then(summary => {
196
+ // 防呆检查:确保会话没有被切换,且仍然是第一条消息
197
+ if (this.currentSession &&
198
+ this.currentSession.id === currentSessionId &&
199
+ this.currentSession.messageCount === 1) {
200
+ // Only update if this is still the first message in the same session
201
+ this.currentSession.title = summary;
202
+ this.currentSession.summary = summary;
203
+ this.saveSession(this.currentSession).catch(error => {
204
+ console.error('Failed to save session with generated summary:', error);
205
+ });
206
+ }
207
+ // Clean up
208
+ this.cancelOngoingSummaryGeneration();
209
+ })
210
+ .catch(error => {
211
+ // Clean up on error
212
+ this.cancelOngoingSummaryGeneration();
213
+ // Silently fail if aborted (expected behavior)
214
+ if (error.name === 'AbortError' || abortSignal.aborted) {
215
+ console.log('Summary generation cancelled (expected)');
216
+ return;
217
+ }
218
+ // Log other errors - we already have a fallback title/summary
219
+ console.warn('Summary generation failed, using fallback:', error);
220
+ });
221
+ }
222
+ else if (this.currentSession.messageCount > 1) {
223
+ // 防呆机制:如果不是第一条消息,取消任何正在进行的摘要生成
224
+ this.cancelOngoingSummaryGeneration();
149
225
  }
150
226
  await this.saveSession(this.currentSession);
151
227
  }
@@ -153,9 +229,13 @@ class SessionManager {
153
229
  return this.currentSession;
154
230
  }
155
231
  setCurrentSession(session) {
232
+ // 防呆机制:切换会话时取消正在进行的摘要生成
233
+ this.cancelOngoingSummaryGeneration();
156
234
  this.currentSession = session;
157
235
  }
158
236
  clearCurrentSession() {
237
+ // 防呆机制:清除会话时取消正在进行的摘要生成
238
+ this.cancelOngoingSummaryGeneration();
159
239
  this.currentSession = null;
160
240
  }
161
241
  async deleteSession(sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {
package/readme.md CHANGED
@@ -6,97 +6,118 @@
6
6
 
7
7
  **English** | [中文](readme_zh.md)
8
8
 
9
- *An intelligent AI-powered CLI tool for developers*
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 --global snow-ai
18
+ $ npm install -g snow-ai
20
19
  ```
21
20
 
22
- ## Start
23
- ```bash
24
- $ snow
25
- ```
21
+ You can also clone and build from source: https://github.com/MayDay-wpf/snow-cli
26
22
 
27
- ## Update
28
- ```bash
29
- $ snow --update
30
- ```
23
+ ### Install VSCode Extension
31
24
 
32
- ## Config example `./User/.snow/config.json`
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
- ## Uninstall
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
- ## Install VSCode Extension
29
+ ### Install JetBrains Plugin
59
30
 
60
- * download [VSIX/snow-cli-x.x.x.vsix](https://github.com/MayDay-wpf/snow-cli/blob/main/VSIX/)
31
+ - Download [JetBrains/build/distributions](https://github.com/MayDay-wpf/snow-cli/tree/main/JetBrains/build/distributions)
61
32
 
62
- * open VSCode, click `Extensions` -> `Install from VSIX...` -> select `snow-cli-0.2.6.vsix`
33
+ ## Available Commands
63
34
 
64
- ## Install JetBrains plugin
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
- * download [JetBrains/build/distributions](https://github.com/MayDay-wpf/snow-cli/tree/main/JetBrains/build/distributions)
40
+ ## API & Model Settings
67
41
 
68
- * File > Settings > Plugins
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
- ## Live View
71
- * **Welcome & Settings**
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
  ![alt text](image.png)
74
54
 
75
- * **Agent**
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
  ![alt text](image-1.png)
78
- * In the middle of the conversation: click ESC to stop AI generation
79
60
 
80
- * When mounting: double-click ESC, view the dialogue recorder, select rollback, including file checkpoints
61
+ ## System Prompt Settings
81
62
 
82
- * MacOS:`ctrl + v` Paste image
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
- * **Commands**
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
  ![alt text](image-2.png)
89
- - /clear - Create a new session
90
80
 
91
- - /resume - The recovery history has
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
- - /mcp - Check the status of MCP service
104
+ The input area displays context usage percentage, token count, cache hit tokens, and cache creation tokens.
94
105
 
95
- - /yolo - Unattended mode, all tools automatically agree to execute
106
+ ![alt text](image-3.png)
96
107
 
97
- - /init - Initialize the project and generate the SNOW.md description document
108
+ ## Snow System Files
98
109
 
99
- - /ide - Connect to VSCode, you need to install the plug-in
110
+ All Snow files are stored in the `.snow` folder in your user directory. Here's what each file/folder contains:
100
111
 
101
- - /compact - compress the context into a sentence
112
+ ![alt text](image-4.png)
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