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.
- package/dist/agents/summaryAgent.d.ts +31 -0
- package/dist/agents/summaryAgent.js +256 -0
- package/dist/hooks/useSessionSave.js +13 -2
- package/dist/ui/pages/ChatScreen.js +12 -5
- package/dist/utils/sessionManager.d.ts +7 -0
- package/dist/utils/sessionManager.js +83 -3
- package/package.json +1 -1
- package/readme.md +78 -57
|
@@ -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
|
-
|
|
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: "
|
|
678
|
-
React.createElement(
|
|
679
|
-
|
|
680
|
-
React.createElement(
|
|
681
|
-
React.createElement(
|
|
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
|
-
//
|
|
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
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
|