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.
@@ -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
@@ -3,8 +3,8 @@
3
3
  * Foliko CLI 入口
4
4
  * Usage: foliko <command> [options]
5
5
  */
6
- // 加载 dotenv
7
- require('dotenv').config({ quiet: true });
6
+ // 加载 .env 文件(覆盖系统环境变量,确保 .env 优先级最高)
7
+ require('dotenv').config({ override: true });
8
8
 
9
9
  const { cli } = require('../src');
10
10
 
@@ -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 DEFAULT_CONFIG = {
16
- model: 'MiniMax-M2.7',
17
- provider: 'minimax',
18
- baseURL: 'https://api.minimaxi.com/v1',
19
- apiKey: null,
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 || DEFAULT_CONFIG.provider;
39
- const providerDefaults = PROVIDER_DEFAULTS[provider] || PROVIDER_DEFAULTS.minimax;
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
- // 支持多种 API key 环境变量名
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 || providerDefaults.model,
51
- provider: provider,
52
- baseURL: process.env.FOLIKO_BASE_URL || providerDefaults.baseURL,
53
- apiKey: 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
 
@@ -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, Markdown, Loader, Text, Spacer, CombinedAutocompleteProvider, matchesKey, Container, TruncatedText, visibleWidth } = require('@earendil-works/pi-tui');
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
- this.notifier = {
159
- dom: null,
160
- spacer: notifierSpacer,
161
- showed: false,
162
- _timeout: null,
163
- index:0,
164
- show(text, duration = 5000) {
165
- if (self.notifier._timeout) {
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
- this.loader = {
196
- dom: loader,
197
- spacer: loaderSpacer,
198
- startTime: null,
199
- timer: null,
200
- index:2,
201
- show() {
202
- self.statusContainer.children[this.index] = self.loader.dom;
203
- editor.disableSubmit = true;
204
- self.startTime = Date.now();
205
- self.loader.dom.setMessage("正在思考中... (0)");
206
- self.loader.dom.start();
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
- self.loader.dom.stop();
225
- self.statusContainer.children[this.index] = self.loader.spacer;
226
- tui.requestRender();
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.loader.hide();
310
- this.tooler.hide();
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
- self.agent.sendMessage(message, { sessionId:self.sessionId }).then(this.next).catch(self.clear_message_done.bind(self));
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
- // 使用 MessageBubble 组件实现左侧图标 + 右侧 markdown 布局
375
- // 无额外 padding,Markdown 自身的段落间距已经足够
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 msgChildren = this.messageContainer.children;
423
- while (msgChildren.length > 1) {
424
- msgChildren.splice(1, 1);
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
- if(!this._currentBotMessage){
506
- // AI 回复,让 create_message 添加前缀
507
- this._currentBotMessage = this.create_message(renderLine(this._lineBuffer,renderState,true), colored('', GREEN),true);
508
- }else{
509
- // 已经有前缀,直接更新内容
510
- this._currentBotMessage.setText(renderLine(this._lineBuffer,renderState,true));
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 === 'error') {
521
- this.tooler.show(chalk.red(`[Error] ${chunk.error}`))
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 })