snow-ai 0.3.11 → 0.3.13
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/compactAgent.js +6 -5
- package/dist/agents/reviewAgent.js +1 -0
- package/dist/agents/summaryAgent.js +1 -0
- package/dist/api/anthropic.d.ts +7 -1
- package/dist/api/anthropic.js +74 -7
- package/dist/api/chat.js +1 -1
- package/dist/api/responses.js +2 -2
- package/dist/api/systemPrompt.js +4 -5
- package/dist/api/types.d.ts +5 -0
- package/dist/hooks/useConversation.js +13 -3
- package/dist/hooks/useFilePicker.d.ts +4 -4
- package/dist/hooks/useFilePicker.js +88 -40
- package/dist/hooks/useHistoryNavigation.d.ts +5 -0
- package/dist/hooks/useHistoryNavigation.js +78 -1
- package/dist/hooks/useInputBuffer.js +1 -1
- package/dist/hooks/useKeyboardInput.d.ts +5 -0
- package/dist/hooks/useKeyboardInput.js +51 -9
- package/dist/hooks/useStreamingState.js +1 -1
- package/dist/mcp/aceCodeSearch.d.ts +4 -0
- package/dist/mcp/aceCodeSearch.js +17 -1
- package/dist/mcp/filesystem.js +2 -2
- package/dist/mcp/utils/filesystem/match-finder.utils.js +1 -1
- package/dist/ui/components/ChatInput.js +11 -4
- package/dist/ui/pages/ChatScreen.js +6 -2
- package/dist/ui/pages/ConfigScreen.js +245 -110
- package/dist/ui/pages/HeadlessModeScreen.js +3 -1
- package/dist/utils/apiConfig.d.ts +5 -0
- package/dist/utils/apiConfig.js +9 -3
- package/dist/utils/contextCompressor.js +7 -2
- package/dist/utils/historyManager.d.ts +45 -0
- package/dist/utils/historyManager.js +159 -0
- package/dist/utils/subAgentExecutor.js +2 -1
- package/dist/utils/textBuffer.js +24 -5
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
class HistoryManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
Object.defineProperty(this, "historyFile", {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true,
|
|
11
|
+
value: void 0
|
|
12
|
+
});
|
|
13
|
+
Object.defineProperty(this, "maxAge", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: 24 * 60 * 60 * 1000
|
|
18
|
+
}); // 1 day in milliseconds
|
|
19
|
+
Object.defineProperty(this, "maxEntries", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: 1000
|
|
24
|
+
}); // Maximum number of entries to keep
|
|
25
|
+
Object.defineProperty(this, "historyData", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: null
|
|
30
|
+
});
|
|
31
|
+
const snowDir = path.join(os.homedir(), '.snow');
|
|
32
|
+
this.historyFile = path.join(snowDir, 'history.json');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Ensure the .snow directory exists
|
|
36
|
+
*/
|
|
37
|
+
async ensureSnowDir() {
|
|
38
|
+
try {
|
|
39
|
+
const snowDir = path.dirname(this.historyFile);
|
|
40
|
+
await fs.mkdir(snowDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// Directory already exists or other error
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Load history from file
|
|
48
|
+
*/
|
|
49
|
+
async loadHistory() {
|
|
50
|
+
try {
|
|
51
|
+
await this.ensureSnowDir();
|
|
52
|
+
// Try to read existing history file
|
|
53
|
+
const data = await fs.readFile(this.historyFile, 'utf-8');
|
|
54
|
+
this.historyData = JSON.parse(data);
|
|
55
|
+
// Clean up old entries if needed
|
|
56
|
+
await this.cleanupOldEntries();
|
|
57
|
+
return this.historyData.entries;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// File doesn't exist or is corrupted, start fresh
|
|
61
|
+
this.historyData = {
|
|
62
|
+
entries: [],
|
|
63
|
+
lastCleanup: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Add a new entry to history
|
|
70
|
+
*/
|
|
71
|
+
async addEntry(content) {
|
|
72
|
+
// Don't add empty or whitespace-only entries
|
|
73
|
+
if (!content || !content.trim()) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Load history if not already loaded
|
|
77
|
+
if (!this.historyData) {
|
|
78
|
+
await this.loadHistory();
|
|
79
|
+
}
|
|
80
|
+
// Don't add duplicate of the last entry
|
|
81
|
+
const lastEntry = this.historyData.entries[this.historyData.entries.length - 1];
|
|
82
|
+
if (lastEntry && lastEntry.content === content) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Add new entry
|
|
86
|
+
const newEntry = {
|
|
87
|
+
content,
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
};
|
|
90
|
+
this.historyData.entries.push(newEntry);
|
|
91
|
+
// Limit the number of entries
|
|
92
|
+
if (this.historyData.entries.length > this.maxEntries) {
|
|
93
|
+
this.historyData.entries = this.historyData.entries.slice(-this.maxEntries);
|
|
94
|
+
}
|
|
95
|
+
// Save to file
|
|
96
|
+
await this.saveHistory();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get all history entries (newest first)
|
|
100
|
+
*/
|
|
101
|
+
async getEntries() {
|
|
102
|
+
if (!this.historyData) {
|
|
103
|
+
await this.loadHistory();
|
|
104
|
+
}
|
|
105
|
+
// Return a copy in reverse order (newest first)
|
|
106
|
+
return [...this.historyData.entries].reverse();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Clean up entries older than maxAge
|
|
110
|
+
*/
|
|
111
|
+
async cleanupOldEntries() {
|
|
112
|
+
if (!this.historyData) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const cutoffTime = now - this.maxAge;
|
|
117
|
+
// Only cleanup once per hour to avoid excessive file writes
|
|
118
|
+
if (now - this.historyData.lastCleanup < 60 * 60 * 1000) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Filter out old entries
|
|
122
|
+
const originalLength = this.historyData.entries.length;
|
|
123
|
+
this.historyData.entries = this.historyData.entries.filter(entry => entry.timestamp > cutoffTime);
|
|
124
|
+
// Update last cleanup time
|
|
125
|
+
this.historyData.lastCleanup = now;
|
|
126
|
+
// Save if we removed any entries
|
|
127
|
+
if (this.historyData.entries.length < originalLength) {
|
|
128
|
+
await this.saveHistory();
|
|
129
|
+
logger.debug(`Cleaned up ${originalLength - this.historyData.entries.length} old history entries`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Save history to file
|
|
134
|
+
*/
|
|
135
|
+
async saveHistory() {
|
|
136
|
+
if (!this.historyData) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
await this.ensureSnowDir();
|
|
141
|
+
await fs.writeFile(this.historyFile, JSON.stringify(this.historyData, null, 2), 'utf-8');
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
logger.error('Failed to save history:', error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Clear all history
|
|
149
|
+
*/
|
|
150
|
+
async clearHistory() {
|
|
151
|
+
this.historyData = {
|
|
152
|
+
entries: [],
|
|
153
|
+
lastCleanup: Date.now(),
|
|
154
|
+
};
|
|
155
|
+
await this.saveHistory();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Export singleton instance
|
|
159
|
+
export const historyManager = new HistoryManager();
|
|
@@ -75,7 +75,7 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
|
|
|
75
75
|
// Get API configuration
|
|
76
76
|
const config = getOpenAiConfig();
|
|
77
77
|
const currentSession = sessionManager.getCurrentSession();
|
|
78
|
-
const model = config.advancedModel || '
|
|
78
|
+
const model = config.advancedModel || 'gpt-5';
|
|
79
79
|
// Call API with sub-agent's tools - choose API based on config
|
|
80
80
|
const stream = config.requestMethod === 'anthropic'
|
|
81
81
|
? createStreamingAnthropicCompletion({
|
|
@@ -85,6 +85,7 @@ export async function executeSubAgent(agentId, prompt, onMessage, abortSignal, r
|
|
|
85
85
|
max_tokens: config.maxTokens || 4096,
|
|
86
86
|
tools: allowedTools,
|
|
87
87
|
sessionId: currentSession?.id,
|
|
88
|
+
disableThinking: true, // Sub-agents 不使用 Extended Thinking
|
|
88
89
|
}, abortSignal)
|
|
89
90
|
: config.requestMethod === 'gemini'
|
|
90
91
|
? createStreamingGeminiCompletion({
|
package/dist/utils/textBuffer.js
CHANGED
|
@@ -196,16 +196,19 @@ export class TextBuffer {
|
|
|
196
196
|
if (this.pasteTimer) {
|
|
197
197
|
clearTimeout(this.pasteTimer);
|
|
198
198
|
}
|
|
199
|
-
//
|
|
199
|
+
// 如果是第一批数据,记录插入位置并清空内容
|
|
200
200
|
const isFirstBatch = !this.pasteAccumulator;
|
|
201
201
|
if (isFirstBatch) {
|
|
202
202
|
this.pastePlaceholderPosition = this.cursorIndex;
|
|
203
|
+
// 保存粘贴位置前后的内容,避免后续计算错误
|
|
204
|
+
this.content = cpSlice(this.content, 0, this.pastePlaceholderPosition) +
|
|
205
|
+
cpSlice(this.content, this.pastePlaceholderPosition);
|
|
203
206
|
}
|
|
204
207
|
// 累积数据
|
|
205
208
|
this.pasteAccumulator += sanitized;
|
|
206
|
-
//
|
|
209
|
+
// 移除所有旧的临时占位符(使用全局替换)
|
|
207
210
|
if (!isFirstBatch) {
|
|
208
|
-
const tempPlaceholderPattern = /\[Pasting\.\.\. \d+ chars\]
|
|
211
|
+
const tempPlaceholderPattern = /\[Pasting\.\.\. \d+ chars\]/g;
|
|
209
212
|
this.content = this.content.replace(tempPlaceholderPattern, '');
|
|
210
213
|
}
|
|
211
214
|
// 显示更新后的临时占位符
|
|
@@ -241,9 +244,9 @@ export class TextBuffer {
|
|
|
241
244
|
return;
|
|
242
245
|
}
|
|
243
246
|
const totalChars = this.pasteAccumulator.length;
|
|
244
|
-
//
|
|
247
|
+
// 移除所有临时占位符(使用全局替换)
|
|
245
248
|
// 临时占位符格式: [Pasting... XXX chars]
|
|
246
|
-
const tempPlaceholderPattern = /\[Pasting\.\.\. \d+ chars\]
|
|
249
|
+
const tempPlaceholderPattern = /\[Pasting\.\.\. \d+ chars\]/g;
|
|
247
250
|
this.content = this.content.replace(tempPlaceholderPattern, '');
|
|
248
251
|
// 只有当累积的字符数超过300时才创建占位符
|
|
249
252
|
if (totalChars > 300) {
|
|
@@ -331,6 +334,14 @@ export class TextBuffer {
|
|
|
331
334
|
if (this.visualLines.length === 0) {
|
|
332
335
|
return;
|
|
333
336
|
}
|
|
337
|
+
// 检查是否只有单行(没有换行符)
|
|
338
|
+
const hasNewline = this.content.includes('\n');
|
|
339
|
+
if (!hasNewline && this.visualLines.length === 1) {
|
|
340
|
+
// 单行模式:移动到行首
|
|
341
|
+
this.cursorIndex = 0;
|
|
342
|
+
this.recomputeVisualCursorOnly();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
334
345
|
const currentRow = this.visualCursorPos[0];
|
|
335
346
|
if (currentRow <= 0) {
|
|
336
347
|
return;
|
|
@@ -341,6 +352,14 @@ export class TextBuffer {
|
|
|
341
352
|
if (this.visualLines.length === 0) {
|
|
342
353
|
return;
|
|
343
354
|
}
|
|
355
|
+
// 检查是否只有单行(没有换行符)
|
|
356
|
+
const hasNewline = this.content.includes('\n');
|
|
357
|
+
if (!hasNewline && this.visualLines.length === 1) {
|
|
358
|
+
// 单行模式:移动到行尾
|
|
359
|
+
this.cursorIndex = cpLen(this.content);
|
|
360
|
+
this.recomputeVisualCursorOnly();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
344
363
|
const currentRow = this.visualCursorPos[0];
|
|
345
364
|
if (currentRow >= this.visualLines.length - 1) {
|
|
346
365
|
return;
|