foliko 1.1.62 → 1.1.64
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/.agent/sessions/cli_default.json +124 -228
- package/.agent/sessions/test-session.json +16 -0
- package/cli/bin/foliko.js +2 -2
- package/cli/src/commands/chat.js +15 -26
- package/cli/src/ui/chat-ui.js +184 -167
- package/cli/src/ui/footer-bar.js +238 -0
- package/cli/src/ui/message-bubble.js +33 -4
- package/cli/src/ui/status-bar.js +177 -0
- package/cli/src/utils/render-diff.js +149 -0
- package/package.json +2 -2
- package/plugins/file-system-plugin.js +179 -131
- package/plugins/qq-plugin.js +1 -1
- package/src/core/agent-chat.js +103 -244
- package/src/core/agent.js +25 -286
- package/src/core/chat-session.js +7 -161
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +12 -238
- package/src/core/context-manager.js +1 -0
- package/src/core/framework.js +163 -94
- package/src/core/plugin-base.js +7 -5
- package/src/core/provider.js +6 -0
- package/src/core/subagent.js +16 -135
- package/src/core/tool-executor.js +2 -70
- package/src/executors/mcp-executor.js +1 -1
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/download.js +5 -4
- package/src/utils/edit-diff.js +516 -0
- package/src/utils/event-emitter.js +1 -1
- package/src/utils/index.js +14 -14
- package/src/utils/logger.js +16 -15
- package/src/utils/message-validator.js +283 -0
- package/src/utils/retry.js +168 -22
- package/src/utils/sandbox.js +60 -207
- package/cli/src/utils/debounce.js +0 -106
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sessionId": "test-session",
|
|
3
|
+
"messages": [
|
|
4
|
+
{
|
|
5
|
+
"role": "user",
|
|
6
|
+
"content": "测试消息"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"variables": {},
|
|
10
|
+
"metadata": {
|
|
11
|
+
"createdAt": 1779443838550,
|
|
12
|
+
"lastActive": 1779443838550,
|
|
13
|
+
"messageCount": 0,
|
|
14
|
+
"compressionCount": 0
|
|
15
|
+
}
|
|
16
|
+
}
|
package/cli/bin/foliko.js
CHANGED
package/cli/src/commands/chat.js
CHANGED
|
@@ -7,50 +7,39 @@ const path = require('path');
|
|
|
7
7
|
const { Framework } = require('../../../src');
|
|
8
8
|
const { ChatUI } = require('../ui/chat-ui');
|
|
9
9
|
const { logger, LOG_LEVELS } = require('../../../src/utils/logger');
|
|
10
|
+
const { DEFAULT_PROVIDERS } = require('../../../src/core/provider');
|
|
10
11
|
|
|
11
12
|
// // 加载 .env 文件
|
|
12
13
|
// dotenv.config({ override: true,quiet: true});
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Provider 默认配置
|
|
23
|
-
const PROVIDER_DEFAULTS = {
|
|
24
|
-
minimax: {
|
|
25
|
-
model: 'MiniMax-M2.7',
|
|
26
|
-
baseURL: 'https://api.minimaxi.com/v1',
|
|
27
|
-
},
|
|
28
|
-
deepseek: {
|
|
29
|
-
model: 'deepseek-chat',
|
|
30
|
-
baseURL: 'https://api.deepseek.com/v1',
|
|
31
|
-
},
|
|
15
|
+
// 各 provider 默认使用的模型
|
|
16
|
+
const PROVIDER_MODELS = {
|
|
17
|
+
minimax: 'MiniMax-M2.7',
|
|
18
|
+
deepseek: 'deepseek-chat',
|
|
19
|
+
openai: 'gpt-4o',
|
|
20
|
+
anthropic: 'claude-sonnet-4-20250514',
|
|
32
21
|
};
|
|
33
22
|
|
|
34
23
|
/**
|
|
35
24
|
* 获取环境变量配置
|
|
36
25
|
*/
|
|
37
26
|
function getEnvConfig() {
|
|
38
|
-
const provider = process.env.FOLIKO_PROVIDER ||
|
|
39
|
-
const
|
|
27
|
+
const provider = process.env.FOLIKO_PROVIDER || 'minimax';
|
|
28
|
+
const providerInfo = DEFAULT_PROVIDERS[provider];
|
|
29
|
+
const defaultModel = PROVIDER_MODELS[provider] || 'deepseek-chat';
|
|
40
30
|
|
|
41
|
-
//
|
|
31
|
+
// API key: 优先 FOLIKO_API_KEY,否则找 {PROVIDER}_API_KEY
|
|
42
32
|
let apiKey = process.env.FOLIKO_API_KEY || null;
|
|
43
33
|
if (!apiKey) {
|
|
44
|
-
// 根据 provider 查找对应的 API key
|
|
45
34
|
const upperProvider = provider.toUpperCase().replace(/-/g, '_');
|
|
46
35
|
apiKey = process.env[`${upperProvider}_API_KEY`] || null;
|
|
47
36
|
}
|
|
48
37
|
|
|
49
38
|
return {
|
|
50
|
-
model: process.env.FOLIKO_MODEL ||
|
|
51
|
-
provider
|
|
52
|
-
baseURL: process.env.FOLIKO_BASE_URL ||
|
|
53
|
-
apiKey
|
|
39
|
+
model: process.env.FOLIKO_MODEL || defaultModel,
|
|
40
|
+
provider,
|
|
41
|
+
baseURL: process.env.FOLIKO_BASE_URL || (providerInfo ? providerInfo.baseURL : null),
|
|
42
|
+
apiKey,
|
|
54
43
|
};
|
|
55
44
|
}
|
|
56
45
|
|
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
const chalk = require('chalk').default;
|
|
6
6
|
const figlet = require('figlet');
|
|
7
7
|
const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils/ansi');
|
|
8
|
-
const { TUI, ProcessTerminal, Editor,
|
|
8
|
+
const { TUI, ProcessTerminal, Editor, Text, CombinedAutocompleteProvider, matchesKey, Container } = require('@earendil-works/pi-tui');
|
|
9
9
|
const { cleanResponse } = require('../../../src/utils');
|
|
10
10
|
const { logEmitter } = require('../../../src/utils/logger');
|
|
11
11
|
const { renderLine } = require('../utils/markdown');
|
|
12
12
|
const { MessageBubble } = require('./message-bubble');
|
|
13
13
|
const Queue=require('js-queue');
|
|
14
14
|
const hl = require('cli-highlight');
|
|
15
|
+
const { renderDiffWithHeader } = require('../utils/render-diff');
|
|
16
|
+
const { FooterBar } = require('./footer-bar');
|
|
17
|
+
const { StatusBar } = require('./status-bar');
|
|
15
18
|
const queue=new Queue();
|
|
16
19
|
// Foliko 主色(蓝绿)
|
|
17
20
|
const folikoPrimary = chalk.hex('#2A9D8F');
|
|
@@ -68,15 +71,15 @@ class ChatUI {
|
|
|
68
71
|
const terminal = new ProcessTerminal();
|
|
69
72
|
const tui=this.tui = new TUI(terminal);
|
|
70
73
|
this.messageContainer= new Container(0)
|
|
71
|
-
this.statusContainer= new Container(0)
|
|
72
74
|
const text=`${folikoTerracotta("Ctrl+C")} 退出 | ${folikoTerracotta("/")} 指令列表 | ${folikoTerracotta("Enter")} 发送 | ${folikoTerracotta("Shift+Enter")} 换行`
|
|
73
75
|
this.messageContainer.addChild(
|
|
74
76
|
new Text(FOLIKO_LOGO + folikoGold(`\n欢迎使用Foliko!\n`)+folikoSand(text),3,2),
|
|
75
77
|
);
|
|
76
78
|
this.tui.addChild(this.messageContainer)
|
|
77
|
-
this.tui.addChild(this.statusContainer)
|
|
78
|
-
|
|
79
79
|
|
|
80
|
+
// 状态栏组件(通知/工具调用/思考中),插在消息区和编辑器之间
|
|
81
|
+
this.statusBar = new StatusBar(tui, this.editor);
|
|
82
|
+
this.tui.addChild(this.statusBar.container);
|
|
80
83
|
|
|
81
84
|
this._currentBotMessage=null
|
|
82
85
|
|
|
@@ -100,132 +103,39 @@ class ChatUI {
|
|
|
100
103
|
this.editor.onSubmit=this.handleOnSubmit.bind(this)
|
|
101
104
|
this.tui.addChild(this.editor);
|
|
102
105
|
this.tui.setFocus(this.editor);
|
|
103
|
-
const tooler=new Loader(
|
|
104
|
-
tui,
|
|
105
|
-
(s) => chalk.green(s), // indicatorColorFn
|
|
106
|
-
(s) => chalk.yellow(s), // messageColorFn
|
|
107
|
-
"工具调用...",
|
|
108
|
-
{
|
|
109
|
-
frames:["|", "/", "-", "\\"].map(str=>folikoTerracotta(str))
|
|
110
|
-
}
|
|
111
|
-
)
|
|
112
|
-
const loader=new Loader(
|
|
113
|
-
tui,
|
|
114
|
-
(s) => chalk.cyan(s), // indicatorColorFn
|
|
115
|
-
(s) => chalk.dim(s), // messageColorFn
|
|
116
|
-
"正在思考中...",
|
|
117
|
-
{
|
|
118
|
-
frames:["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"].map(str=>folikoPrimary(str))
|
|
119
|
-
}
|
|
120
|
-
)
|
|
121
|
-
// Spacer 占位,用于隐藏时保持布局
|
|
122
|
-
const toolerSpacer = new Spacer(0);
|
|
123
|
-
const loaderSpacer = new Spacer(0);
|
|
124
|
-
const notifierSpacer = new Spacer(0);
|
|
125
|
-
|
|
126
|
-
this.statusContainer.addChild(toolerSpacer); // tooler 位置
|
|
127
|
-
this.statusContainer.addChild(loaderSpacer); // loader 位置
|
|
128
|
-
this.statusContainer.addChild(notifierSpacer); // notifier 位置
|
|
129
|
-
|
|
130
|
-
const self = this;
|
|
131
|
-
|
|
132
|
-
this.tooler = {
|
|
133
|
-
dom: tooler,
|
|
134
|
-
spacer: toolerSpacer,
|
|
135
|
-
showed:false,
|
|
136
|
-
index:1,
|
|
137
|
-
show(text) {
|
|
138
|
-
this.showed = true;
|
|
139
|
-
self.statusContainer.children[this.index] = self.tooler.dom;
|
|
140
|
-
this.dom.setMessage(text);
|
|
141
|
-
tui.requestRender();
|
|
142
|
-
return self.tooler.dom;
|
|
143
|
-
},
|
|
144
|
-
hide() {
|
|
145
|
-
this.showed = false;
|
|
146
|
-
self.statusContainer.children[this.index] = self.tooler.spacer;
|
|
147
|
-
tui.requestRender();
|
|
148
|
-
},
|
|
149
|
-
setText(text) {
|
|
150
|
-
if(!this.showed){
|
|
151
|
-
this.show(text);
|
|
152
|
-
} else {
|
|
153
|
-
this.dom.setMessage(text);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
106
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
clearTimeout(self.notifier._timeout);
|
|
167
|
-
}
|
|
168
|
-
if (!self.notifier.dom) {
|
|
169
|
-
self.notifier.dom = new Loader(
|
|
170
|
-
tui,
|
|
171
|
-
(s) => chalk.cyan(s),
|
|
172
|
-
(s) => chalk.yellow(s),
|
|
173
|
-
text,
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
self.notifier.dom.setMessage(text);
|
|
177
|
-
self.statusContainer.children[this.index] = self.notifier.dom;
|
|
178
|
-
self.notifier.showed = true;
|
|
179
|
-
tui.requestRender();
|
|
180
|
-
self.notifier._timeout = setTimeout(() => {
|
|
181
|
-
self.notifier.hide();
|
|
182
|
-
}, duration);
|
|
183
|
-
},
|
|
184
|
-
hide() {
|
|
185
|
-
if (self.notifier._timeout) {
|
|
186
|
-
clearTimeout(self.notifier._timeout);
|
|
187
|
-
self.notifier._timeout = null;
|
|
188
|
-
}
|
|
189
|
-
self.notifier.showed = false;
|
|
190
|
-
self.statusContainer.children[this.index] = self.notifier.spacer;
|
|
191
|
-
tui.requestRender();
|
|
192
|
-
},
|
|
193
|
-
};
|
|
107
|
+
// 底部状态栏(使用 Text 组件,动态更新内容)
|
|
108
|
+
this.footerBar = new FooterBar(agent, {
|
|
109
|
+
maxContextTokens: agent._chatHandler?._maxContextTokens || 100000,
|
|
110
|
+
});
|
|
111
|
+
this._footerText = new Text('', 0, 0);
|
|
112
|
+
this.footerContainer = new Container(1); // 固定高度 1 行
|
|
113
|
+
this.footerContainer.addChild(this._footerText);
|
|
114
|
+
this.tui.addChild(this.footerContainer);
|
|
194
115
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
self.loader.timer = setInterval(() => {
|
|
209
|
-
const elapsed = Math.floor((Date.now() - self.startTime) / 1000);
|
|
210
|
-
const seconds = elapsed % 60;
|
|
211
|
-
const minutes = Math.floor(elapsed / 60);
|
|
212
|
-
const timeStr = minutes > 0 ? `${minutes}分${seconds}秒` : `${seconds}秒`;
|
|
213
|
-
self.loader.dom.setMessage(`正在思考中... (${timeStr})`);
|
|
214
|
-
}, 1000);
|
|
215
|
-
tui.requestRender();
|
|
216
|
-
return self.loader.dom;
|
|
217
|
-
},
|
|
218
|
-
hide() {
|
|
219
|
-
editor.disableSubmit = false;
|
|
220
|
-
if (self.loader.timer) {
|
|
221
|
-
clearInterval(self.loader.timer);
|
|
222
|
-
self.loader.timer = null;
|
|
116
|
+
// 监听 framework 的 agent:usage 事件(绕过 queue,更可靠)
|
|
117
|
+
if (agent.framework) {
|
|
118
|
+
agent.framework.on('agent:usage', (data) => {
|
|
119
|
+
if (this.footerBar && data.usage) {
|
|
120
|
+
const u = data.usage;
|
|
121
|
+
const input = u.promptTokens || u.prompt_tokens || 0;
|
|
122
|
+
const output = u.completionTokens || u.completion_tokens || 0;
|
|
123
|
+
this.footerBar.updateUsage(input, output);
|
|
124
|
+
const footerLines = this.footerBar.render(this.tui.width || process.stdout.columns || 80);
|
|
125
|
+
if (footerLines.length > 0 && this._footerText) {
|
|
126
|
+
this._footerText.setText(footerLines[0]);
|
|
127
|
+
}
|
|
128
|
+
this.tui.requestRender();
|
|
223
129
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 初始渲染 footer(refreshFromAgent 内部会主动加载历史消息)
|
|
134
|
+
this.footerBar.refreshFromAgent();
|
|
135
|
+
const initialFooter = this.footerBar.render(this.tui.width || 80);
|
|
136
|
+
if (initialFooter.length > 0) {
|
|
137
|
+
this._footerText.setText(initialFooter[0]);
|
|
138
|
+
}
|
|
229
139
|
tui.requestRender();
|
|
230
140
|
// 中断状态(供监听器访问)
|
|
231
141
|
this.interrupted = false;
|
|
@@ -235,6 +145,11 @@ class ChatUI {
|
|
|
235
145
|
|
|
236
146
|
// 流式输出缓冲区(可重置)
|
|
237
147
|
this._lineBuffer = '';
|
|
148
|
+
// 流式渲染缓冲区:合并高频 chunk 后统一刷新 Markdown,减少重复解析
|
|
149
|
+
this._streamBufferTimer = null;
|
|
150
|
+
this._pendingTextUpdate = false;
|
|
151
|
+
// renderLine 的共享状态(引用传递,跨调用保持 codeBlock/think 状态)
|
|
152
|
+
this._renderState = { inThink: true, inCodeBlock: false };
|
|
238
153
|
// agent.framework.on("console:log",async (...args)=>{
|
|
239
154
|
// this.create_message(args.join(' '),chalk.dim('● '),true);
|
|
240
155
|
// })
|
|
@@ -283,11 +198,25 @@ class ChatUI {
|
|
|
283
198
|
}
|
|
284
199
|
const time = timestamp ? new Date(timestamp).toLocaleTimeString('zh-CN') : '';
|
|
285
200
|
const notificationText = `🔔 [${source}] ${title}${time ? ` (${time})` : ''}`;
|
|
286
|
-
this.notifier.show(notificationText);
|
|
201
|
+
this.statusBar.notifier.show(notificationText);
|
|
287
202
|
// 显示消息内容
|
|
288
203
|
this.create_message(`**${title}**\n${message}`, "🔔 ");
|
|
289
204
|
};
|
|
290
205
|
this.agent.framework.on('notification', this._notificationHandler);
|
|
206
|
+
|
|
207
|
+
// 监听 usage 事件(用于 footer 更新)
|
|
208
|
+
this._usageHandler = (data) => {
|
|
209
|
+
if (this.footerBar && data.usage) {
|
|
210
|
+
const u = data.usage;
|
|
211
|
+
this.footerBar.updateUsage(u.promptTokens || 0, u.completionTokens || 0);
|
|
212
|
+
const footerLines = this.footerBar.render(this.tui.width || process.stdout.columns || 80);
|
|
213
|
+
if (footerLines.length > 0 && this._footerText) {
|
|
214
|
+
this._footerText.setText(footerLines[0]);
|
|
215
|
+
}
|
|
216
|
+
this.tui.requestRender();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
agent.framework.on('agent:usage', this._usageHandler);
|
|
291
220
|
}
|
|
292
221
|
}
|
|
293
222
|
|
|
@@ -295,6 +224,11 @@ class ChatUI {
|
|
|
295
224
|
* 清理监听器
|
|
296
225
|
*/
|
|
297
226
|
dispose() {
|
|
227
|
+
// 清理流式缓冲区定时器
|
|
228
|
+
if (this._streamBufferTimer) {
|
|
229
|
+
clearTimeout(this._streamBufferTimer);
|
|
230
|
+
this._streamBufferTimer = null;
|
|
231
|
+
}
|
|
298
232
|
if (this._logHandler && this.agent.framework) {
|
|
299
233
|
logEmitter.off('log', this._logHandler);
|
|
300
234
|
this._logHandler = null;
|
|
@@ -303,11 +237,21 @@ class ChatUI {
|
|
|
303
237
|
this.agent.framework.off('notification', this._notificationHandler);
|
|
304
238
|
this._notificationHandler = null;
|
|
305
239
|
}
|
|
240
|
+
if (this._usageHandler && this.agent.framework) {
|
|
241
|
+
this.agent.framework.off('agent:usage', this._usageHandler);
|
|
242
|
+
this._usageHandler = null;
|
|
243
|
+
}
|
|
306
244
|
}
|
|
307
245
|
|
|
308
246
|
clear_message_done(){
|
|
309
|
-
this.
|
|
310
|
-
this.
|
|
247
|
+
this._flushStreamBuffer(); // 确保最后的文本被刷新
|
|
248
|
+
if (this._streamBufferTimer) {
|
|
249
|
+
clearTimeout(this._streamBufferTimer);
|
|
250
|
+
this._streamBufferTimer = null;
|
|
251
|
+
}
|
|
252
|
+
this._pendingTextUpdate = false;
|
|
253
|
+
this.statusBar.loader.hide();
|
|
254
|
+
this.statusBar.tooler.hide();
|
|
311
255
|
this._lineBuffer=""
|
|
312
256
|
this.isResponding=false
|
|
313
257
|
this._currentBotMessage=null
|
|
@@ -316,7 +260,10 @@ class ChatUI {
|
|
|
316
260
|
sendMessage(message){
|
|
317
261
|
const self=this;
|
|
318
262
|
queue.add(function(){
|
|
319
|
-
|
|
263
|
+
var next = this.next;
|
|
264
|
+
self.agent.sendMessage(message, { sessionId:self.sessionId }).then(next).catch(function(e){
|
|
265
|
+
self.clear_message_done();
|
|
266
|
+
});
|
|
320
267
|
})
|
|
321
268
|
|
|
322
269
|
this.tui.setFocus(this.editor);
|
|
@@ -370,12 +317,9 @@ class ChatUI {
|
|
|
370
317
|
}
|
|
371
318
|
}
|
|
372
319
|
|
|
373
|
-
create_message(text, icon="",isBot=false){
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const bubble = new MessageBubble(icon, text, isBot, markdownTheme, 0, 0, 1);
|
|
377
|
-
const children = this.messageContainer.children;
|
|
378
|
-
children.splice(children.length, 0, bubble);
|
|
320
|
+
create_message(text, icon="", isBot=false, bgFn=null){
|
|
321
|
+
const bubble = new MessageBubble(icon, text, isBot, markdownTheme, 0, 0, 1, bgFn);
|
|
322
|
+
this.messageContainer.addChild(bubble);
|
|
379
323
|
this.tui.requestRender();
|
|
380
324
|
return bubble
|
|
381
325
|
}
|
|
@@ -384,8 +328,8 @@ class ChatUI {
|
|
|
384
328
|
|
|
385
329
|
_clearContext() {
|
|
386
330
|
// 隐藏 loader 和 tooler
|
|
387
|
-
this.tooler.hide();
|
|
388
|
-
this.loader.hide();
|
|
331
|
+
this.statusBar.tooler.hide();
|
|
332
|
+
this.statusBar.loader.hide();
|
|
389
333
|
|
|
390
334
|
const { sessionId } = this;
|
|
391
335
|
|
|
@@ -419,17 +363,27 @@ class ChatUI {
|
|
|
419
363
|
}
|
|
420
364
|
|
|
421
365
|
// 清空 messageContainer 中的消息,保留 welcome
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
366
|
+
const welcomeChild = this.messageContainer.children[0];
|
|
367
|
+
this.messageContainer.clear();
|
|
368
|
+
if (welcomeChild) {
|
|
369
|
+
this.messageContainer.addChild(welcomeChild);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 重置 footer 统计
|
|
373
|
+
if (this.footerBar) {
|
|
374
|
+
this.footerBar.reset();
|
|
375
|
+
const footerLines = this.footerBar.render(this.tui.width || 80);
|
|
376
|
+
if (footerLines.length > 0 && this._footerText) {
|
|
377
|
+
this._footerText.setText(footerLines[0]);
|
|
378
|
+
}
|
|
425
379
|
}
|
|
426
380
|
this.tui.requestRender();
|
|
427
381
|
}
|
|
428
382
|
|
|
429
383
|
_compressContext() {
|
|
430
384
|
// 隐藏 loader 和 tooler
|
|
431
|
-
this.tooler.hide();
|
|
432
|
-
this.loader.hide();
|
|
385
|
+
this.statusBar.tooler.hide();
|
|
386
|
+
this.statusBar.loader.hide();
|
|
433
387
|
|
|
434
388
|
const { sessionId } = this;
|
|
435
389
|
|
|
@@ -457,14 +411,14 @@ class ChatUI {
|
|
|
457
411
|
}
|
|
458
412
|
|
|
459
413
|
const beforeCount = messages ? messages.length : 0;
|
|
460
|
-
this.tooler.show(`${colored('[压缩]', YELLOW)} 压缩前: ${beforeCount} 条`);
|
|
414
|
+
this.statusBar.tooler.show(`${colored('[压缩]', YELLOW)} 压缩前: ${beforeCount} 条`);
|
|
461
415
|
|
|
462
416
|
if (messages && messages.length > 0) {
|
|
463
417
|
// 使用 ContextCompressor 压缩
|
|
464
418
|
if (chatHandler._contextCompressor) {
|
|
465
419
|
chatHandler._contextCompressor.compress(sessionId, messages, messageStore).then(() => {
|
|
466
420
|
const afterCount = messages.length;
|
|
467
|
-
this.tooler.setText(`${colored('[压缩]', YELLOW)} 压缩后: ${afterCount} 条 (保留${chatHandler._contextCompressor._keepRecentMessages}条)`);
|
|
421
|
+
this.statusBar.tooler.setText(`${colored('[压缩]', YELLOW)} 压缩后: ${afterCount} 条 (保留${chatHandler._contextCompressor._keepRecentMessages}条)`);
|
|
468
422
|
// 同步回 SessionContext
|
|
469
423
|
if (this.agent.framework) {
|
|
470
424
|
const sessionCtx = this.agent.framework.getSessionContext(sessionId);
|
|
@@ -473,59 +427,122 @@ class ChatUI {
|
|
|
473
427
|
}
|
|
474
428
|
}
|
|
475
429
|
setTimeout(() => {
|
|
476
|
-
this.tooler.hide();
|
|
430
|
+
this.statusBar.tooler.hide();
|
|
477
431
|
this.create_message(`${colored('[提示]', CYAN)} 上下文已压缩`,chalk.dim('● '))
|
|
478
432
|
this.tui.requestRender();
|
|
479
433
|
}, 2000);
|
|
480
434
|
}).catch(err => {
|
|
481
|
-
this.tooler.setText(`${colored('[错误]', RED)} 压缩失败: ${err.message}`);
|
|
435
|
+
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 压缩失败: ${err.message}`);
|
|
482
436
|
});
|
|
483
437
|
} else {
|
|
484
|
-
this.tooler.setText(`${colored('[错误]', RED)} 无 ContextCompressor`);
|
|
438
|
+
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 ContextCompressor`);
|
|
485
439
|
}
|
|
486
440
|
} else {
|
|
487
|
-
this.tooler.setText(`${colored('[提示]', CYAN)} 无消息可压缩`);
|
|
441
|
+
this.statusBar.tooler.setText(`${colored('[提示]', CYAN)} 无消息可压缩`);
|
|
488
442
|
}
|
|
489
443
|
} else {
|
|
490
|
-
this.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
|
|
444
|
+
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 刷新流式输出缓冲区:合并高频 chunk 后统一渲染,减少 Markdown 重复解析
|
|
449
|
+
*/
|
|
450
|
+
_flushStreamBuffer() {
|
|
451
|
+
this._streamBufferTimer = null;
|
|
452
|
+
if (!this._lineBuffer) return;
|
|
453
|
+
|
|
454
|
+
// 渲染剩余的 partial line(没有 \n 结尾的尾部文字)
|
|
455
|
+
const rendered = renderLine(this._lineBuffer, this._renderState, true);
|
|
456
|
+
if (!this._currentBotMessage) {
|
|
457
|
+
this._currentBotMessage = this.create_message(rendered, colored('● ', GREEN), true);
|
|
458
|
+
} else {
|
|
459
|
+
this._currentBotMessage.appendContent(rendered);
|
|
491
460
|
}
|
|
461
|
+
this._lineBuffer = '';
|
|
492
462
|
}
|
|
463
|
+
|
|
493
464
|
/**
|
|
494
465
|
* 设置 session scope 事件监听
|
|
495
466
|
*/
|
|
496
467
|
_setupSessionListeners() {
|
|
497
|
-
const renderState = { inThink: true, inCodeBlock: false };
|
|
498
|
-
|
|
499
468
|
this.sessionScope.on('stream:chunk', ({ chunk }) => {
|
|
500
469
|
if (this.interrupted) return;
|
|
501
470
|
|
|
502
471
|
if (chunk.type === 'text') {
|
|
503
|
-
this._lineBuffer += chunk.text
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
472
|
+
this._lineBuffer += chunk.text
|
|
473
|
+
|
|
474
|
+
// 逐行处理:遇到 \n 立即渲染完整行,实现即时反馈
|
|
475
|
+
// 行尾补回 \n 确保 TUI 正确换行(空行也要渲染,保留段落分隔)
|
|
476
|
+
// while (this._lineBuffer.includes('\n')) {
|
|
477
|
+
// if (this.interrupted) break;
|
|
478
|
+
|
|
479
|
+
// const nlIndex = this._lineBuffer.indexOf('\n');
|
|
480
|
+
// const line = this._lineBuffer.substring(0, nlIndex);
|
|
481
|
+
// this._lineBuffer = this._lineBuffer.substring(nlIndex + 1);
|
|
482
|
+
|
|
483
|
+
// const rendered = line;
|
|
484
|
+
// if (!this._currentBotMessage) {
|
|
485
|
+
// this._currentBotMessage = this.create_message(rendered, colored('● ', GREEN), true);
|
|
486
|
+
// } else {
|
|
487
|
+
// this._currentBotMessage.appendContent(rendered);
|
|
488
|
+
// }
|
|
489
|
+
|
|
490
|
+
// }
|
|
491
|
+
|
|
492
|
+
// 兜底:partial line(无 \n 结尾)用 timer 刷新,防止尾部文字卡住
|
|
493
|
+
if (this._lineBuffer && !this._streamBufferTimer) {
|
|
494
|
+
this._streamBufferTimer = setTimeout(() => this._flushStreamBuffer(), 0);
|
|
511
495
|
}
|
|
512
496
|
} else if (chunk.type === 'tool-call') {
|
|
513
497
|
const args = chunk.input ? JSON.stringify(chunk.input).slice(0, 50) : '';
|
|
514
|
-
this.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
|
|
498
|
+
this.statusBar.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
|
|
515
499
|
} else if(chunk.type==='tool-result'){
|
|
516
500
|
const result = chunk.result;
|
|
517
501
|
const shortResult = typeof result === 'string' ? result.slice(0, 30) : JSON.stringify(result).slice(0, 30);
|
|
518
|
-
this.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
|
|
502
|
+
this.statusBar.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
|
|
503
|
+
|
|
504
|
+
// 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
|
|
505
|
+
if (chunk.toolName === 'edit' || chunk.toolName === 'edit_file') {
|
|
506
|
+
let resultObj = result;
|
|
507
|
+
// 字符串需要解析
|
|
508
|
+
if (typeof result === 'string') {
|
|
509
|
+
try { resultObj = JSON.parse(result); } catch (e) { resultObj = null; }
|
|
510
|
+
}
|
|
511
|
+
if (resultObj && resultObj.diff) {
|
|
512
|
+
const diffContent = renderDiffWithHeader(resultObj.diff, resultObj.filePath);
|
|
513
|
+
const bgColor = chalk.bgHex('#1a1a2e');
|
|
514
|
+
this.create_message(diffContent, chalk.yellow('▸ '), true, (line) => bgColor(line));
|
|
515
|
+
this.statusBar.tooler.show(`${chalk.green('[Diff]')} ${folikoGold(resultObj.filePath)}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
519
518
|
// setTimeout(() => this.tooler.hide(), 3000);
|
|
520
|
-
}else if (chunk.type === '
|
|
521
|
-
|
|
519
|
+
} else if (chunk.type === 'usage') {
|
|
520
|
+
// 直接从 stream 获取用量数据,立即更新 footer
|
|
521
|
+
if (this.footerBar) {
|
|
522
|
+
this.footerBar.updateUsage(chunk.inputTokens || 0, chunk.outputTokens || 0);
|
|
523
|
+
const footerLines = this.footerBar.render(this.tui.width || process.stdout.columns || 80);
|
|
524
|
+
if (footerLines.length > 0 && this._footerText) {
|
|
525
|
+
this._footerText.setText(footerLines[0]);
|
|
526
|
+
}
|
|
527
|
+
this.tui.requestRender();
|
|
528
|
+
}
|
|
522
529
|
}
|
|
523
530
|
});
|
|
524
531
|
this.sessionScope.on('message:start', async () => {
|
|
525
|
-
this.loader.show();
|
|
532
|
+
this.statusBar.loader.show();
|
|
526
533
|
})
|
|
527
|
-
this.sessionScope.on('message:complete', async ({ content }) => {
|
|
534
|
+
this.sessionScope.on('message:complete', async ({ content, requestId }) => {
|
|
528
535
|
try{
|
|
536
|
+
// 更新 footer 中的 token 用量和模型信息
|
|
537
|
+
if (this.footerBar) {
|
|
538
|
+
this.footerBar.refreshFromAgent();
|
|
539
|
+
const footerLines = this.footerBar.render(this.tui.width || process.stdout.columns || 80);
|
|
540
|
+
if (footerLines.length > 0 && this._footerText) {
|
|
541
|
+
this._footerText.setText(footerLines[0]);
|
|
542
|
+
}
|
|
543
|
+
this.tui.requestRender();
|
|
544
|
+
}
|
|
545
|
+
|
|
529
546
|
if (!content) {
|
|
530
547
|
const msg = '继续'
|
|
531
548
|
await this.agent.sendMessage(msg, { sessionId:this.sessionId, priority: 1 })
|