foliko 1.1.67 → 1.1.69

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.
Files changed (157) hide show
  1. package/.claude/settings.local.json +19 -10
  2. package/.dockerignore +45 -45
  3. package/.env.example +56 -56
  4. package/CLAUDE.md +2 -2
  5. package/README.md +13 -13
  6. package/SPEC.md +3 -3
  7. package/cli/src/commands/chat.js +2 -20
  8. package/cli/src/commands/list.js +7 -6
  9. package/cli/src/commands/plugin.js +3 -2
  10. package/cli/src/daemon.js +2 -2
  11. package/cli/src/ui/chat-ui-old.js +15 -4
  12. package/cli/src/ui/chat-ui.js +236 -203
  13. package/cli/src/ui/footer-bar.js +20 -46
  14. package/cli/src/ui/message-bubble.js +24 -2
  15. package/cli/src/ui/status-bar.js +177 -0
  16. package/cli/src/utils/config.js +29 -0
  17. package/cli/src/utils/plugin-config.js +1 -1
  18. package/docker-compose.yml +33 -33
  19. package/docs/features.md +120 -120
  20. package/docs/quick-reference.md +160 -160
  21. package/docs/user-manual.md +1391 -1391
  22. package/examples/ambient-example.js +2 -2
  23. package/examples/bootstrap.js +3 -3
  24. package/examples/test-chat.js +1 -1
  25. package/examples/test-reload.js +1 -1
  26. package/examples/test-telegram.js +1 -1
  27. package/examples/test-tg-bot.js +1 -1
  28. package/examples/test-tg-simple.js +2 -2
  29. package/examples/test-tg.js +1 -1
  30. package/examples/test-think.js +1 -1
  31. package/examples/test-weixin-feishu.js +3 -3
  32. package/package.json +3 -1
  33. package/plugins/ambient-agent/index.js +1 -1
  34. package/plugins/audit-plugin.js +84 -29
  35. package/plugins/coordinator-plugin.js +14 -12
  36. package/plugins/data-splitter-plugin.js +323 -0
  37. package/plugins/default-plugins.js +23 -12
  38. package/plugins/email/index.js +1 -1
  39. package/plugins/extension-executor-plugin.js +87 -9
  40. package/plugins/feishu-plugin.js +118 -16
  41. package/plugins/file-system-plugin.js +68 -50
  42. package/plugins/gate-trading.js +10 -10
  43. package/plugins/install-plugin.js +7 -7
  44. package/plugins/memory-plugin.js +9 -12
  45. package/plugins/plugin-manager-plugin.js +12 -14
  46. package/plugins/python-executor-plugin.js +1 -1
  47. package/plugins/python-plugin-loader.js +1 -1
  48. package/plugins/qq-plugin.js +151 -24
  49. package/plugins/rules-plugin.js +8 -8
  50. package/plugins/scheduler-plugin.js +24 -20
  51. package/plugins/session-plugin.js +313 -397
  52. package/plugins/storage-plugin.js +235 -175
  53. package/plugins/subagent-plugin.js +17 -13
  54. package/plugins/telegram-plugin.js +116 -17
  55. package/plugins/think-plugin.js +64 -60
  56. package/plugins/tools-plugin.js +8 -8
  57. package/plugins/web-plugin.js +2 -2
  58. package/plugins/weixin-plugin.js +107 -24
  59. package/skills/find-skills/AGENTS.md +2 -2
  60. package/skills/find-skills/SKILL.md +133 -133
  61. package/skills/foliko-dev/AGENTS.md +236 -236
  62. package/skills/foliko-dev/SKILL.md +19 -19
  63. package/skills/mcp-usage/SKILL.md +200 -200
  64. package/skills/plugin-guide/SKILL.md +4 -4
  65. package/skills/python-plugin-dev/SKILL.md +5 -5
  66. package/skills/skill-guide/SKILL.md +104 -6
  67. package/skills/subagent-guide/SKILL.md +237 -237
  68. package/skills/workflow-guide/SKILL.md +646 -646
  69. package/src/capabilities/skill-manager.js +124 -17
  70. package/src/capabilities/workflow-engine.js +3 -3
  71. package/src/core/agent-chat.js +72 -26
  72. package/src/core/agent.js +17 -27
  73. package/src/core/branch-summary-auto.js +206 -0
  74. package/src/core/chat-session.js +45 -169
  75. package/src/core/command-registry.js +200 -0
  76. package/src/core/constants.js +198 -0
  77. package/src/core/context-compressor.js +702 -326
  78. package/src/core/context-manager.js +0 -1
  79. package/src/core/enhanced-context-compressor.js +210 -0
  80. package/src/core/framework.js +260 -84
  81. package/src/core/jsonl-storage.js +253 -0
  82. package/src/core/plugin-base.js +7 -5
  83. package/src/core/plugin-manager.js +15 -10
  84. package/src/core/provider-registry.js +159 -0
  85. package/src/core/provider.js +2 -0
  86. package/src/core/session-entry.js +225 -0
  87. package/src/core/session-manager.js +701 -0
  88. package/src/core/storage-manager.js +494 -0
  89. package/src/core/sub-agent-config.js +1 -1
  90. package/src/core/subagent.js +16 -135
  91. package/src/core/token-counter.js +177 -58
  92. package/src/core/tool-executor.js +2 -70
  93. package/src/core/ui-extension-context.js +174 -0
  94. package/src/executors/mcp-executor.js +27 -16
  95. package/src/utils/chat-queue.js +11 -22
  96. package/src/utils/data-splitter.js +345 -0
  97. package/src/utils/logger.js +152 -180
  98. package/src/utils/message-validator.js +283 -0
  99. package/src/utils/plugin-helpers.js +2 -2
  100. package/src/utils/retry.js +168 -22
  101. package/website_v2/docs/api.html +1 -1
  102. package/website_v2/docs/configuration.html +2 -2
  103. package/website_v2/docs/plugin-development.html +4 -4
  104. package/website_v2/docs/project-structure.html +2 -2
  105. package/website_v2/docs/skill-development.html +2 -2
  106. package/website_v2/index.html +1 -1
  107. package/website_v2/styles/animations.css +7 -7
  108. package/.agent/agents/backend-dev.md +0 -102
  109. package/.agent/agents/data-analyst.md +0 -117
  110. package/.agent/agents/devops.md +0 -115
  111. package/.agent/agents/frontend-dev.md +0 -94
  112. package/.agent/agents/network-requester.md +0 -44
  113. package/.agent/agents/poster-designer.md +0 -52
  114. package/.agent/agents/product-manager.md +0 -85
  115. package/.agent/agents/qa-engineer.md +0 -100
  116. package/.agent/agents/security-engineer.md +0 -99
  117. package/.agent/agents/team-lead.md +0 -137
  118. package/.agent/agents/ui-designer.md +0 -116
  119. package/.agent/data/default.json +0 -58
  120. package/.agent/data/email/processed-emails.json +0 -1
  121. package/.agent/data/plugins-state.json +0 -199
  122. package/.agent/data/scheduler/tasks.json +0 -1
  123. package/.agent/data/web/web-config.json +0 -5
  124. package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
  125. package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
  126. package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
  127. package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
  128. package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
  129. package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
  130. package/.agent/data/weixin.json +0 -6
  131. package/.agent/mcp_config.json +0 -14
  132. package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
  133. package/.agent/package.json +0 -8
  134. package/.agent/plugins/marknative/README.md +0 -134
  135. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  136. package/.agent/plugins/marknative/fonts.zip +0 -0
  137. package/.agent/plugins/marknative/index.js +0 -256
  138. package/.agent/plugins/marknative/package.json +0 -12
  139. package/.agent/plugins/test-plugin.py +0 -99
  140. package/.agent/plugins.json +0 -14
  141. package/.agent/python-scripts/test_sample.py +0 -24
  142. package/.agent/sessions/cli_default.json +0 -247
  143. package/.agent/skills/agent-browser/SKILL.md +0 -311
  144. package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
  145. package/.agent/skills/sysinfo/SKILL.md +0 -38
  146. package/.agent/skills/sysinfo/system-info.sh +0 -130
  147. package/.agent/skills/workflow/SKILL.md +0 -324
  148. package/.agent/test-agent.js +0 -35
  149. package/.agent/weixin.json +0 -6
  150. package/.agent/workflows/email-digest.json +0 -50
  151. package/.agent/workflows/file-backup.json +0 -21
  152. package/.agent/workflows/get-ip-notify.json +0 -32
  153. package/.agent/workflows/news-aggregator.json +0 -93
  154. package/.agent/workflows/news-dashboard-v2.json +0 -94
  155. package/.agent/workflows/notification-batch.json +0 -32
  156. package/src/core/session-context.js +0 -346
  157. package/src/core/session-storage.js +0 -295
@@ -5,8 +5,8 @@
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');
9
- const { cleanResponse } = require('../../../src/utils');
8
+ const { TUI, ProcessTerminal, Editor, Text, CombinedAutocompleteProvider, matchesKey, Container } = require('@earendil-works/pi-tui');
9
+ const { cleanResponse, logger } = 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');
@@ -14,6 +14,7 @@ const Queue=require('js-queue');
14
14
  const hl = require('cli-highlight');
15
15
  const { renderDiffWithHeader } = require('../utils/render-diff');
16
16
  const { FooterBar } = require('./footer-bar');
17
+ const { StatusBar } = require('./status-bar');
17
18
  const queue=new Queue();
18
19
  // Foliko 主色(蓝绿)
19
20
  const folikoPrimary = chalk.hex('#2A9D8F');
@@ -70,15 +71,15 @@ class ChatUI {
70
71
  const terminal = new ProcessTerminal();
71
72
  const tui=this.tui = new TUI(terminal);
72
73
  this.messageContainer= new Container(0)
73
- this.statusContainer= new Container(0)
74
74
  const text=`${folikoTerracotta("Ctrl+C")} 退出 | ${folikoTerracotta("/")} 指令列表 | ${folikoTerracotta("Enter")} 发送 | ${folikoTerracotta("Shift+Enter")} 换行`
75
75
  this.messageContainer.addChild(
76
76
  new Text(FOLIKO_LOGO + folikoGold(`\n欢迎使用Foliko!\n`)+folikoSand(text),3,2),
77
77
  );
78
78
  this.tui.addChild(this.messageContainer)
79
- this.tui.addChild(this.statusContainer)
80
-
81
79
 
80
+ // 状态栏组件(通知/工具调用/思考中),插在消息区和编辑器之间
81
+ this.statusBar = new StatusBar(tui, this.editor);
82
+ this.tui.addChild(this.statusBar.container);
82
83
 
83
84
  this._currentBotMessage=null
84
85
 
@@ -88,12 +89,24 @@ class ChatUI {
88
89
  // 会话管理
89
90
  this.sessionId = options.sessionId || 'cli_default';
90
91
  this.sessionPlugin = null;
92
+
93
+ // 获取 skill 命令列表
94
+ const skillCommands = [];
95
+ if (agent.framework) {
96
+ const extExecutor = agent.framework.pluginManager?.get('extension-executor');
97
+ if (extExecutor && typeof extExecutor.getSkillCommands === 'function') {
98
+ skillCommands.push(...extExecutor.getSkillCommands());
99
+ }
100
+ }
101
+
102
+ const baseCommands = [
103
+ { name: "compress", description: "压缩记录" },
104
+ { name: "clear", description: "清除记录" },
105
+ { name: "exit", description: "退出" },
106
+ ];
107
+
91
108
  const autocompleteProvider = new CombinedAutocompleteProvider(
92
- [
93
- { name: "compress", description: "压缩记录" },
94
- { name: "clear", description: "清除记录" },
95
- { name: "exit", description: "退出" },
96
- ],
109
+ [...baseCommands, ...skillCommands],
97
110
  process.cwd(),
98
111
  );
99
112
 
@@ -135,132 +148,6 @@ class ChatUI {
135
148
  if (initialFooter.length > 0) {
136
149
  this._footerText.setText(initialFooter[0]);
137
150
  }
138
- const tooler=new Loader(
139
- tui,
140
- (s) => chalk.green(s), // indicatorColorFn
141
- (s) => chalk.yellow(s), // messageColorFn
142
- "工具调用...",
143
- {
144
- frames:["|", "/", "-", "\\"].map(str=>folikoTerracotta(str))
145
- }
146
- )
147
- const loader=new Loader(
148
- tui,
149
- (s) => chalk.cyan(s), // indicatorColorFn
150
- (s) => chalk.dim(s), // messageColorFn
151
- "正在思考中...",
152
- {
153
- frames:["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"].map(str=>folikoPrimary(str))
154
- }
155
- )
156
- // Spacer 占位,用于隐藏时保持布局
157
- const toolerSpacer = new Spacer(0);
158
- const loaderSpacer = new Spacer(0);
159
- const notifierSpacer = new Spacer(0);
160
-
161
- this.statusContainer.addChild(toolerSpacer); // tooler 位置
162
- this.statusContainer.addChild(loaderSpacer); // loader 位置
163
- this.statusContainer.addChild(notifierSpacer); // notifier 位置
164
-
165
- const self = this;
166
-
167
- this.tooler = {
168
- dom: tooler,
169
- spacer: toolerSpacer,
170
- showed:false,
171
- index:1,
172
- show(text) {
173
- this.showed = true;
174
- self.statusContainer.children[this.index] = self.tooler.dom;
175
- this.dom.setMessage(text);
176
- tui.requestRender();
177
- return self.tooler.dom;
178
- },
179
- hide() {
180
- this.showed = false;
181
- self.statusContainer.children[this.index] = self.tooler.spacer;
182
- tui.requestRender();
183
- },
184
- setText(text) {
185
- if(!this.showed){
186
- this.show(text);
187
- } else {
188
- this.dom.setMessage(text);
189
- }
190
- }
191
- };
192
-
193
- this.notifier = {
194
- dom: null,
195
- spacer: notifierSpacer,
196
- showed: false,
197
- _timeout: null,
198
- index:0,
199
- show(text, duration = 5000) {
200
- if (self.notifier._timeout) {
201
- clearTimeout(self.notifier._timeout);
202
- }
203
- if (!self.notifier.dom) {
204
- self.notifier.dom = new Loader(
205
- tui,
206
- (s) => chalk.cyan(s),
207
- (s) => chalk.yellow(s),
208
- text,
209
- );
210
- }
211
- self.notifier.dom.setMessage(text);
212
- self.statusContainer.children[this.index] = self.notifier.dom;
213
- self.notifier.showed = true;
214
- tui.requestRender();
215
- self.notifier._timeout = setTimeout(() => {
216
- self.notifier.hide();
217
- }, duration);
218
- },
219
- hide() {
220
- if (self.notifier._timeout) {
221
- clearTimeout(self.notifier._timeout);
222
- self.notifier._timeout = null;
223
- }
224
- self.notifier.showed = false;
225
- self.statusContainer.children[this.index] = self.notifier.spacer;
226
- tui.requestRender();
227
- },
228
- };
229
-
230
- this.loader = {
231
- dom: loader,
232
- spacer: loaderSpacer,
233
- startTime: null,
234
- timer: null,
235
- index:2,
236
- show() {
237
- self.statusContainer.children[this.index] = self.loader.dom;
238
- editor.disableSubmit = true;
239
- self.startTime = Date.now();
240
- self.loader.dom.setMessage("正在思考中... (0秒)");
241
- self.loader.dom.start();
242
- // 每秒更新计时
243
- self.loader.timer = setInterval(() => {
244
- const elapsed = Math.floor((Date.now() - self.startTime) / 1000);
245
- const seconds = elapsed % 60;
246
- const minutes = Math.floor(elapsed / 60);
247
- const timeStr = minutes > 0 ? `${minutes}分${seconds}秒` : `${seconds}秒`;
248
- self.loader.dom.setMessage(`正在思考中... (${timeStr})`);
249
- }, 1000);
250
- tui.requestRender();
251
- return self.loader.dom;
252
- },
253
- hide() {
254
- editor.disableSubmit = false;
255
- if (self.loader.timer) {
256
- clearInterval(self.loader.timer);
257
- self.loader.timer = null;
258
- }
259
- self.loader.dom.stop();
260
- self.statusContainer.children[this.index] = self.loader.spacer;
261
- tui.requestRender();
262
- }
263
- };
264
151
  tui.requestRender();
265
152
  // 中断状态(供监听器访问)
266
153
  this.interrupted = false;
@@ -270,6 +157,11 @@ class ChatUI {
270
157
 
271
158
  // 流式输出缓冲区(可重置)
272
159
  this._lineBuffer = '';
160
+ // 流式渲染缓冲区:合并高频 chunk 后统一刷新 Markdown,减少重复解析
161
+ this._streamBufferTimer = null;
162
+ this._pendingTextUpdate = false;
163
+ // renderLine 的共享状态(引用传递,跨调用保持 codeBlock/think 状态)
164
+ this._renderState = { inThink: true, inCodeBlock: false };
273
165
  // agent.framework.on("console:log",async (...args)=>{
274
166
  // this.create_message(args.join(' '),chalk.dim('● '),true);
275
167
  // })
@@ -296,18 +188,19 @@ class ChatUI {
296
188
  this._setupSessionListeners();
297
189
  const level_dict={
298
190
  DEBUG: 'cyan',
299
- // INFO: 'dim',
191
+ INFO: 'dim',
192
+ LOG: 'dim',
300
193
  WARN: 'yellow',
301
194
  ERROR: 'red',
302
195
  }
303
196
  // 监听错误日志,显示到 tooler
304
- // this._logHandler = (data) => {
305
- // if(!Object.keys(level_dict).includes(data.level))return;
306
- // const level=folikoTerracotta(`[${data.level}]`)
307
- // const level_key=level_dict[data.level]||'dim'
308
- // this.notifier.show(`${level} ${chalk[level_key](data.message)}`);
309
- // };
310
- // logEmitter.on('log', this._logHandler);
197
+ this._logHandler = (data) => {
198
+ if(!Object.keys(level_dict).includes(data.level))return;
199
+ const level=folikoTerracotta(`[${data.level}]`)
200
+ const level_key=level_dict[data.level]||'dim'
201
+ this.statusBar.notifier.show(`${level} ${chalk[level_key](data.message)}`)
202
+ };
203
+ logger.on('log', this._logHandler);
311
204
 
312
205
  // 监听通知事件,显示到通知区域
313
206
  this._notificationHandler = (data) => {
@@ -318,7 +211,7 @@ class ChatUI {
318
211
  }
319
212
  const time = timestamp ? new Date(timestamp).toLocaleTimeString('zh-CN') : '';
320
213
  const notificationText = `🔔 [${source}] ${title}${time ? ` (${time})` : ''}`;
321
- this.notifier.show(notificationText);
214
+ this.statusBar.notifier.show(notificationText);
322
215
  // 显示消息内容
323
216
  this.create_message(`**${title}**\n${message}`, "🔔 ");
324
217
  };
@@ -344,8 +237,13 @@ class ChatUI {
344
237
  * 清理监听器
345
238
  */
346
239
  dispose() {
240
+ // 清理流式缓冲区定时器
241
+ if (this._streamBufferTimer) {
242
+ clearTimeout(this._streamBufferTimer);
243
+ this._streamBufferTimer = null;
244
+ }
347
245
  if (this._logHandler && this.agent.framework) {
348
- logEmitter.off('log', this._logHandler);
246
+ logger.off('log', this._logHandler);
349
247
  this._logHandler = null;
350
248
  }
351
249
  if (this._notificationHandler && this.agent.framework) {
@@ -359,8 +257,14 @@ class ChatUI {
359
257
  }
360
258
 
361
259
  clear_message_done(){
362
- this.loader.hide();
363
- this.tooler.hide();
260
+ this._flushStreamBuffer(); // 确保最后的文本被刷新
261
+ if (this._streamBufferTimer) {
262
+ clearTimeout(this._streamBufferTimer);
263
+ this._streamBufferTimer = null;
264
+ }
265
+ this._pendingTextUpdate = false;
266
+ this.statusBar.loader.hide();
267
+ this.statusBar.tooler.hide();
364
268
  this._lineBuffer=""
365
269
  this.isResponding=false
366
270
  this._currentBotMessage=null
@@ -379,6 +283,106 @@ class ChatUI {
379
283
  return
380
284
  }
381
285
 
286
+ /**
287
+ * 统一命令执行入口
288
+ */
289
+ async _executeCommand(trimmed) {
290
+ // 基础命令
291
+ const cmd = trimmed.toLowerCase();
292
+ if (cmd === '/clear') {
293
+ this._clearContext()
294
+ this.create_message(`${colored('[提示]', CYAN)} 对话上下文已清除\n`,chalk.dim('● '))
295
+ return true;
296
+ }
297
+ if (cmd === '/compress') {
298
+ this._compressContext()
299
+ return true;
300
+ }
301
+ if (cmd === '/exit') {
302
+ this.create_message(`${colored('[再见]', CYAN)} 感谢使用 Foliko!\n`,chalk.dim('● '))
303
+ this.tui.stop();
304
+ setTimeout(() => process.exit(0), 500);
305
+ return true;
306
+ }
307
+
308
+ // /help 命令显示所有注册的命令
309
+ if (cmd === '/help') {
310
+ const { getCommandRegistry } = require('../../../src/core/command-registry');
311
+ const registry = getCommandRegistry();
312
+ const commands = registry.getAllCommands();
313
+
314
+ let helpText = `${colored('[帮助]', CYAN)} 可用命令:\n`;
315
+ helpText += ` ${colored('/clear', CYAN)} - 清除对话上下文\n`;
316
+ helpText += ` ${colored('/compress', CYAN)} - 压缩上下文\n`;
317
+ helpText += ` ${colored('/exit', CYAN)} - 退出\n`;
318
+
319
+ // 添加 skill 命令
320
+ const extExecutor = this.agent.framework?.pluginManager?.get('extension-executor');
321
+ if (extExecutor && typeof extExecutor.getSkillCommandsHelp === 'function') {
322
+ const skillHelp = extExecutor.getSkillCommandsHelp();
323
+ if (skillHelp) {
324
+ helpText += `\n${colored('[技能命令]', CYAN)}\n`;
325
+ helpText += skillHelp.split('\n').map(line => ` ${line}`).join('\n') + '\n';
326
+ }
327
+ }
328
+
329
+ if (commands.length > 0) {
330
+ helpText += `\n${colored('[扩展命令]', CYAN)}\n`;
331
+ for (const cmd of commands) {
332
+ const pluginTag = cmd.registeredBy ? ` [${cmd.registeredBy}]` : '';
333
+ helpText += ` ${colored('/' + cmd.name, CYAN)}${pluginTag} - ${cmd.description || '无描述'}\n`;
334
+ }
335
+ }
336
+ this.create_message(helpText, chalk.dim('● '));
337
+ return true;
338
+ }
339
+
340
+ // 尝试通过 CommandRegistry 执行命令
341
+ const cmdName = trimmed.slice(1).split(' ')[0];
342
+ const cmdArgs = trimmed.slice(cmdName.length + 2);
343
+ if (trimmed.startsWith('/') && cmdName) {
344
+ // 检查 skill 命令 (格式: skillname:cmdname)
345
+ if (cmdName.includes(':')) {
346
+ try {
347
+ const result = await this.agent.framework.executeTool('ext_call', {
348
+ plugin: 'skill',
349
+ tool: cmdName,
350
+ args: { args: cmdArgs }
351
+ });
352
+ if (result.success !== false) {
353
+ this.create_message(`${colored('[结果]', CYAN)} ${result.data || result}\n`, chalk.dim('● '));
354
+ return true;
355
+ }
356
+ } catch (err) {
357
+ log.warn('Skill command failed:', err.message);
358
+ }
359
+ }
360
+
361
+ // 检查 CommandRegistry 中的命令
362
+ const { getCommandRegistry } = require('../../../src/core/command-registry');
363
+ const registry = getCommandRegistry();
364
+ const cmd = registry.getCommand(cmdName);
365
+ if (cmd) {
366
+ try {
367
+ const context = {
368
+ sessionId: this.sessionId,
369
+ agent: this.agent,
370
+ ui: this,
371
+ framework: this.agent.framework,
372
+ };
373
+ registry.execute(cmdName, cmdArgs, context).catch(err => {
374
+ this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.dim('● '));
375
+ });
376
+ } catch (err) {
377
+ this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.dim('● '));
378
+ }
379
+ return true;
380
+ }
381
+ }
382
+
383
+ return false;
384
+ }
385
+
382
386
  async handleOnSubmit(value){
383
387
  try{
384
388
  if (this.isResponding) {
@@ -389,21 +393,8 @@ class ChatUI {
389
393
  return;
390
394
  }
391
395
 
392
- // 处理快捷命令
393
- const cmd = trimmed.toLowerCase();
394
- if (cmd === '/clear') {
395
- this._clearContext()
396
- this.create_message(`${colored('[提示]', CYAN)} 对话上下文已清除\n`,chalk.dim('● '))
397
- return;
398
- }
399
- if (cmd === '/compress') {
400
- this._compressContext()
401
- return;
402
- }
403
- if (cmd === '/exit') {
404
- this.create_message(`${colored('[再见]', CYAN)} 感谢使用 Foliko!\n`,chalk.dim('● '))
405
- this.tui.stop();
406
- setTimeout(() => process.exit(0), 500);
396
+ // 尝试执行命令
397
+ if (await this._executeCommand(trimmed)) {
407
398
  return;
408
399
  }
409
400
 
@@ -427,11 +418,8 @@ class ChatUI {
427
418
  }
428
419
 
429
420
  create_message(text, icon="", isBot=false, bgFn=null){
430
- // 使用 MessageBubble 组件实现左侧图标 + 右侧 markdown 布局
431
- // bgFn: 可选背景色函数 (line) => coloredLine,用于代码块等整块背景
432
421
  const bubble = new MessageBubble(icon, text, isBot, markdownTheme, 0, 0, 1, bgFn);
433
- const children = this.messageContainer.children;
434
- children.splice(children.length, 0, bubble);
422
+ this.messageContainer.addChild(bubble);
435
423
  this.tui.requestRender();
436
424
  return bubble
437
425
  }
@@ -440,11 +428,16 @@ class ChatUI {
440
428
 
441
429
  _clearContext() {
442
430
  // 隐藏 loader 和 tooler
443
- this.tooler.hide();
444
- this.loader.hide();
431
+ this.statusBar.tooler.hide();
432
+ this.statusBar.loader.hide();
445
433
 
446
434
  const { sessionId } = this;
447
435
 
436
+ // Clear ChatSession message store (memory + file)
437
+ if (this.agent._chatSession) {
438
+ this.agent._chatSession.clearSessionMessages(sessionId, true);
439
+ }
440
+
448
441
  // 1. 清除 AgentChatHandler 的消息存储(需要传 sessionId)
449
442
  if (this.agent._chatHandler) {
450
443
  this.agent._chatHandler.clearHistory(sessionId);
@@ -455,29 +448,38 @@ class ChatUI {
455
448
  const sessionCtx = this.agent.framework.getSessionContext(sessionId);
456
449
  if (sessionCtx) {
457
450
  sessionCtx.clearMessages();
458
- sessionCtx.compressionState.count = 0;
459
- sessionCtx.metadata.compressionCount = 0;
451
+ if (sessionCtx.compressionState) {
452
+ sessionCtx.compressionState.count = 0;
453
+ }
454
+ if (sessionCtx.metadata) {
455
+ sessionCtx.metadata.compressionCount = 0;
456
+ }
460
457
  }
461
458
  // 同步清除 framework 缓存的 sessionContexts
462
459
  const cachedCtx = this.agent.framework._sessionContexts?.get(sessionId);
463
460
  if (cachedCtx) {
464
461
  cachedCtx.clearMessages();
465
- cachedCtx.compressionState.count = 0;
466
- cachedCtx.metadata.compressionCount = 0;
462
+ if (cachedCtx.compressionState) {
463
+ cachedCtx.compressionState.count = 0;
464
+ }
465
+ if (cachedCtx.metadata) {
466
+ cachedCtx.metadata.compressionCount = 0;
467
+ }
467
468
  }
468
469
  // 清除 SessionPlugin 中的消息
469
470
  if (this.sessionPlugin) {
470
- const session = this.sessionPlugin.getSession(sessionId);
471
- if (session) {
472
- session.messages = [];
471
+ const manager = this.sessionPlugin._getSessionManager(sessionId);
472
+ if (manager) {
473
+ manager.clearMessages();
473
474
  }
474
475
  }
475
476
  }
476
477
 
477
478
  // 清空 messageContainer 中的消息,保留 welcome
478
- const msgChildren = this.messageContainer.children;
479
- while (msgChildren.length > 1) {
480
- msgChildren.splice(1, 1);
479
+ const welcomeChild = this.messageContainer.children[0];
480
+ this.messageContainer.clear();
481
+ if (welcomeChild) {
482
+ this.messageContainer.addChild(welcomeChild);
481
483
  }
482
484
 
483
485
  // 重置 footer 统计
@@ -493,8 +495,8 @@ class ChatUI {
493
495
 
494
496
  _compressContext() {
495
497
  // 隐藏 loader 和 tooler
496
- this.tooler.hide();
497
- this.loader.hide();
498
+ this.statusBar.tooler.hide();
499
+ this.statusBar.loader.hide();
498
500
 
499
501
  const { sessionId } = this;
500
502
 
@@ -522,14 +524,14 @@ class ChatUI {
522
524
  }
523
525
 
524
526
  const beforeCount = messages ? messages.length : 0;
525
- this.tooler.show(`${colored('[压缩]', YELLOW)} 压缩前: ${beforeCount} 条`);
527
+ this.statusBar.tooler.show(`${colored('[压缩]', YELLOW)} 压缩前: ${beforeCount} 条`);
526
528
 
527
529
  if (messages && messages.length > 0) {
528
530
  // 使用 ContextCompressor 压缩
529
531
  if (chatHandler._contextCompressor) {
530
532
  chatHandler._contextCompressor.compress(sessionId, messages, messageStore).then(() => {
531
533
  const afterCount = messages.length;
532
- this.tooler.setText(`${colored('[压缩]', YELLOW)} 压缩后: ${afterCount} 条 (保留${chatHandler._contextCompressor._keepRecentMessages}条)`);
534
+ this.statusBar.tooler.setText(`${colored('[压缩]', YELLOW)} 压缩后: ${afterCount} 条 (保留${chatHandler._contextCompressor._keepRecentMessages}条)`);
533
535
  // 同步回 SessionContext
534
536
  if (this.agent.framework) {
535
537
  const sessionCtx = this.agent.framework.getSessionContext(sessionId);
@@ -538,49 +540,79 @@ class ChatUI {
538
540
  }
539
541
  }
540
542
  setTimeout(() => {
541
- this.tooler.hide();
543
+ this.statusBar.tooler.hide();
542
544
  this.create_message(`${colored('[提示]', CYAN)} 上下文已压缩`,chalk.dim('● '))
543
545
  this.tui.requestRender();
544
546
  }, 2000);
545
547
  }).catch(err => {
546
- this.tooler.setText(`${colored('[错误]', RED)} 压缩失败: ${err.message}`);
548
+ this.statusBar.tooler.setText(`${colored('[错误]', RED)} 压缩失败: ${err.message}`);
547
549
  });
548
550
  } else {
549
- this.tooler.setText(`${colored('[错误]', RED)} 无 ContextCompressor`);
551
+ this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 ContextCompressor`);
550
552
  }
551
553
  } else {
552
- this.tooler.setText(`${colored('[提示]', CYAN)} 无消息可压缩`);
554
+ this.statusBar.tooler.setText(`${colored('[提示]', CYAN)} 无消息可压缩`);
553
555
  }
554
556
  } else {
555
- this.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
557
+ this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
556
558
  }
557
559
  }
560
+ /**
561
+ * 刷新流式输出缓冲区:合并高频 chunk 后统一渲染,减少 Markdown 重复解析
562
+ */
563
+ _flushStreamBuffer() {
564
+ this._streamBufferTimer = null;
565
+ if (!this._lineBuffer) return;
566
+
567
+ // 渲染剩余的 partial line(没有 \n 结尾的尾部文字)
568
+ const rendered = renderLine(this._lineBuffer, this._renderState, true);
569
+ if (!this._currentBotMessage) {
570
+ this._currentBotMessage = this.create_message(rendered, colored('● ', GREEN), true);
571
+ } else {
572
+ this._currentBotMessage.appendContent(rendered);
573
+ }
574
+ this._lineBuffer = '';
575
+ }
576
+
558
577
  /**
559
578
  * 设置 session scope 事件监听
560
579
  */
561
580
  _setupSessionListeners() {
562
- const renderState = { inThink: true, inCodeBlock: false };
563
-
564
581
  this.sessionScope.on('stream:chunk', ({ chunk }) => {
565
582
  if (this.interrupted) return;
566
583
 
567
584
  if (chunk.type === 'text') {
568
- this._lineBuffer += chunk.text;
569
-
570
- if(!this._currentBotMessage){
571
- // AI 回复,让 create_message 添加前缀
572
- this._currentBotMessage = this.create_message(renderLine(this._lineBuffer,renderState,true), colored('', GREEN),true);
573
- }else{
574
- // 已经有前缀,直接更新内容
575
- this._currentBotMessage.setText(renderLine(this._lineBuffer,renderState,true));
585
+ this._lineBuffer += chunk.text
586
+
587
+ // 逐行处理:遇到 \n 立即渲染完整行,实现即时反馈
588
+ // 行尾补回 \n 确保 TUI 正确换行(空行也要渲染,保留段落分隔)
589
+ // while (this._lineBuffer.includes('\n')) {
590
+ // if (this.interrupted) break;
591
+
592
+ // const nlIndex = this._lineBuffer.indexOf('\n');
593
+ // const line = this._lineBuffer.substring(0, nlIndex);
594
+ // this._lineBuffer = this._lineBuffer.substring(nlIndex + 1);
595
+
596
+ // const rendered = line;
597
+ // if (!this._currentBotMessage) {
598
+ // this._currentBotMessage = this.create_message(rendered, colored('● ', GREEN), true);
599
+ // } else {
600
+ // this._currentBotMessage.appendContent(rendered);
601
+ // }
602
+
603
+ // }
604
+
605
+ // 兜底:partial line(无 \n 结尾)用 timer 刷新,防止尾部文字卡住
606
+ if (this._lineBuffer && !this._streamBufferTimer) {
607
+ this._streamBufferTimer = setTimeout(() => this._flushStreamBuffer(), 0);
576
608
  }
577
609
  } else if (chunk.type === 'tool-call') {
578
610
  const args = chunk.input ? JSON.stringify(chunk.input).slice(0, 50) : '';
579
- this.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
611
+ this.statusBar.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
580
612
  } else if(chunk.type==='tool-result'){
581
613
  const result = chunk.result;
582
614
  const shortResult = typeof result === 'string' ? result.slice(0, 30) : JSON.stringify(result).slice(0, 30);
583
- this.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
615
+ this.statusBar.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
584
616
 
585
617
  // 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
586
618
  if (chunk.toolName === 'edit' || chunk.toolName === 'edit_file') {
@@ -592,8 +624,9 @@ class ChatUI {
592
624
  if (resultObj && resultObj.diff) {
593
625
  const diffContent = renderDiffWithHeader(resultObj.diff, resultObj.filePath);
594
626
  const bgColor = chalk.bgHex('#1a1a2e');
595
- this.create_message(diffContent, chalk.yellow(''), true, (line) => bgColor(line));
596
- this.tooler.show(`${chalk.green('[Diff]')} ${folikoGold(resultObj.filePath)}`);
627
+ this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
628
+ // this.create_message(diffContent, chalk.yellow(''), true, (line) => bgColor(line));
629
+ this.statusBar.tooler.show(`${chalk.green('[Diff]')} ${folikoGold(resultObj.filePath)}`);
597
630
  }
598
631
  }
599
632
  // setTimeout(() => this.tooler.hide(), 3000);
@@ -610,7 +643,7 @@ class ChatUI {
610
643
  }
611
644
  });
612
645
  this.sessionScope.on('message:start', async () => {
613
- this.loader.show();
646
+ this.statusBar.loader.show();
614
647
  })
615
648
  this.sessionScope.on('message:complete', async ({ content, requestId }) => {
616
649
  try{