foliko 1.1.62 → 1.1.63

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
+ }
@@ -12,6 +12,8 @@ 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');
15
17
  const queue=new Queue();
16
18
  // Foliko 主色(蓝绿)
17
19
  const folikoPrimary = chalk.hex('#2A9D8F');
@@ -100,6 +102,39 @@ class ChatUI {
100
102
  this.editor.onSubmit=this.handleOnSubmit.bind(this)
101
103
  this.tui.addChild(this.editor);
102
104
  this.tui.setFocus(this.editor);
105
+
106
+ // 底部状态栏(使用 Text 组件,动态更新内容)
107
+ this.footerBar = new FooterBar(agent, {
108
+ maxContextTokens: agent._chatHandler?._maxContextTokens || 100000,
109
+ });
110
+ this._footerText = new Text('', 0, 0);
111
+ this.footerContainer = new Container(1); // 固定高度 1 行
112
+ this.footerContainer.addChild(this._footerText);
113
+ this.tui.addChild(this.footerContainer);
114
+
115
+ // 监听 framework 的 agent:usage 事件(绕过 queue,更可靠)
116
+ if (agent.framework) {
117
+ agent.framework.on('agent:usage', (data) => {
118
+ if (this.footerBar && data.usage) {
119
+ const u = data.usage;
120
+ const input = u.promptTokens || u.prompt_tokens || 0;
121
+ const output = u.completionTokens || u.completion_tokens || 0;
122
+ this.footerBar.updateUsage(input, output);
123
+ const footerLines = this.footerBar.render(this.tui.width || process.stdout.columns || 80);
124
+ if (footerLines.length > 0 && this._footerText) {
125
+ this._footerText.setText(footerLines[0]);
126
+ }
127
+ this.tui.requestRender();
128
+ }
129
+ });
130
+ }
131
+
132
+ // 初始渲染 footer(refreshFromAgent 内部会主动加载历史消息)
133
+ this.footerBar.refreshFromAgent();
134
+ const initialFooter = this.footerBar.render(this.tui.width || 80);
135
+ if (initialFooter.length > 0) {
136
+ this._footerText.setText(initialFooter[0]);
137
+ }
103
138
  const tooler=new Loader(
104
139
  tui,
105
140
  (s) => chalk.green(s), // indicatorColorFn
@@ -316,7 +351,10 @@ class ChatUI {
316
351
  sendMessage(message){
317
352
  const self=this;
318
353
  queue.add(function(){
319
- self.agent.sendMessage(message, { sessionId:self.sessionId }).then(this.next).catch(self.clear_message_done.bind(self));
354
+ var next = this.next;
355
+ self.agent.sendMessage(message, { sessionId:self.sessionId }).then(next).catch(function(e){
356
+ self.clear_message_done();
357
+ });
320
358
  })
321
359
 
322
360
  this.tui.setFocus(this.editor);
@@ -370,10 +408,10 @@ class ChatUI {
370
408
  }
371
409
  }
372
410
 
373
- create_message(text, icon="",isBot=false){
411
+ create_message(text, icon="", isBot=false, bgFn=null){
374
412
  // 使用 MessageBubble 组件实现左侧图标 + 右侧 markdown 布局
375
- // 无额外 padding,Markdown 自身的段落间距已经足够
376
- const bubble = new MessageBubble(icon, text, isBot, markdownTheme, 0, 0, 1);
413
+ // bgFn: 可选背景色函数 (line) => coloredLine,用于代码块等整块背景
414
+ const bubble = new MessageBubble(icon, text, isBot, markdownTheme, 0, 0, 1, bgFn);
377
415
  const children = this.messageContainer.children;
378
416
  children.splice(children.length, 0, bubble);
379
417
  this.tui.requestRender();
@@ -423,6 +461,15 @@ class ChatUI {
423
461
  while (msgChildren.length > 1) {
424
462
  msgChildren.splice(1, 1);
425
463
  }
464
+
465
+ // 重置 footer 统计
466
+ if (this.footerBar) {
467
+ this.footerBar.reset();
468
+ const footerLines = this.footerBar.render(this.tui.width || 80);
469
+ if (footerLines.length > 0 && this._footerText) {
470
+ this._footerText.setText(footerLines[0]);
471
+ }
472
+ }
426
473
  this.tui.requestRender();
427
474
  }
428
475
 
@@ -516,16 +563,49 @@ class ChatUI {
516
563
  const result = chunk.result;
517
564
  const shortResult = typeof result === 'string' ? result.slice(0, 30) : JSON.stringify(result).slice(0, 30);
518
565
  this.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
566
+
567
+ // 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
568
+ if (chunk.toolName === 'edit' || chunk.toolName === 'edit_file') {
569
+ let resultObj = result;
570
+ // 字符串需要解析
571
+ if (typeof result === 'string') {
572
+ try { resultObj = JSON.parse(result); } catch (e) { resultObj = null; }
573
+ }
574
+ if (resultObj && resultObj.diff) {
575
+ const diffContent = renderDiffWithHeader(resultObj.diff, resultObj.filePath);
576
+ const bgColor = chalk.bgHex('#1a1a2e');
577
+ this.create_message(diffContent, chalk.yellow('▸ '), true, (line) => bgColor(line));
578
+ this.tooler.show(`${chalk.green('[Diff]')} ${folikoGold(resultObj.filePath)}`);
579
+ }
580
+ }
519
581
  // setTimeout(() => this.tooler.hide(), 3000);
520
- }else if (chunk.type === 'error') {
521
- this.tooler.show(chalk.red(`[Error] ${chunk.error}`))
582
+ } else if (chunk.type === 'usage') {
583
+ // 直接从 stream 获取用量数据,立即更新 footer
584
+ if (this.footerBar) {
585
+ this.footerBar.updateUsage(chunk.inputTokens || 0, chunk.outputTokens || 0);
586
+ const footerLines = this.footerBar.render(this.tui.width || process.stdout.columns || 80);
587
+ if (footerLines.length > 0 && this._footerText) {
588
+ this._footerText.setText(footerLines[0]);
589
+ }
590
+ this.tui.requestRender();
591
+ }
522
592
  }
523
593
  });
524
594
  this.sessionScope.on('message:start', async () => {
525
595
  this.loader.show();
526
596
  })
527
- this.sessionScope.on('message:complete', async ({ content }) => {
597
+ this.sessionScope.on('message:complete', async ({ content, requestId }) => {
528
598
  try{
599
+ // 更新 footer 中的 token 用量和模型信息
600
+ if (this.footerBar) {
601
+ this.footerBar.refreshFromAgent();
602
+ const footerLines = this.footerBar.render(this.tui.width || process.stdout.columns || 80);
603
+ if (footerLines.length > 0 && this._footerText) {
604
+ this._footerText.setText(footerLines[0]);
605
+ }
606
+ this.tui.requestRender();
607
+ }
608
+
529
609
  if (!content) {
530
610
  const msg = '继续'
531
611
  await this.agent.sendMessage(msg, { sessionId:this.sessionId, priority: 1 })
@@ -0,0 +1,263 @@
1
+ /**
2
+ * FooterBar — 底部状态栏组件
3
+ *
4
+ * 类似 pi 的底部栏,显示 token 用量、费用、上下文占比、模型名等信息。
5
+ * 格式: ↑input ↓output $cost context%/max (model-name)
6
+ */
7
+
8
+ const chalk = require('chalk').default;
9
+ const { TokenCounter } = require('../../../src/core/token-counter');
10
+
11
+ // Foliko 配色
12
+ const folikoDim = chalk.hex('#6B7280');
13
+ const folikoAccent = chalk.hex('#2A9D8F');
14
+ const folikoGold = chalk.hex('#E9C46A');
15
+
16
+ class FooterBar {
17
+ /**
18
+ * @param {Object} agent - Agent 实例
19
+ * @param {Object} options
20
+ */
21
+ constructor(agent, options = {}) {
22
+ this.agent = agent;
23
+
24
+ // Token 计算器
25
+ this._tokenCounter = new TokenCounter();
26
+
27
+ // 累积统计
28
+ this._stats = {
29
+ inputTokens: 0,
30
+ outputTokens: 0,
31
+ cost: 0,
32
+ contextTokens: 0,
33
+ maxContextTokens: options.maxContextTokens || 100000,
34
+ contextPercent: 0,
35
+ };
36
+
37
+ // 上次看到的 usage(用于计算增量)
38
+ this._lastUsage = null;
39
+
40
+ // 模型信息
41
+ this._modelName = agent.model || 'deepseek-chat';
42
+ this._thinkingLevel = this._getThinkingLevel();
43
+
44
+ // 缓存行
45
+ this._cachedLine = '';
46
+ }
47
+
48
+ /**
49
+ * 获取思考级别
50
+ */
51
+ _getThinkingLevel() {
52
+ const chatHandler = this.agent._chatHandler;
53
+ if (chatHandler && chatHandler._thinkingMode) {
54
+ return 'thinking';
55
+ }
56
+ return 'normal';
57
+ }
58
+
59
+ /**
60
+ * 格式化数字(千分位)
61
+ */
62
+ _formatNum(n) {
63
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
64
+ if (n >= 1000) return (n / 1000).toFixed(0) + 'K';
65
+ return String(n);
66
+ }
67
+
68
+ /**
69
+ * 更新 token 用量
70
+ * @param {number} inputDelta - 本次输入 token
71
+ * @param {number} outputDelta - 本次输出 token
72
+ * @param {number} costDelta - 本次费用
73
+ * @param {Object} contextInfo - 上下文信息 { current, max }
74
+ */
75
+ updateUsage(inputDelta = 0, outputDelta = 0, costDelta = 0, contextInfo = null) {
76
+ this._stats.inputTokens += inputDelta;
77
+ this._stats.outputTokens += outputDelta;
78
+ this._stats.cost += costDelta;
79
+
80
+ if (contextInfo) {
81
+ this._stats.contextTokens = contextInfo.current || this._stats.contextTokens;
82
+ this._stats.maxContextTokens = contextInfo.max || this._stats.maxContextTokens;
83
+ }
84
+
85
+ if (this._stats.maxContextTokens > 0) {
86
+ this._stats.contextPercent = (this._stats.contextTokens / this._stats.maxContextTokens) * 100;
87
+ }
88
+
89
+ this._cachedLine = '';
90
+ }
91
+
92
+ /**
93
+ * 更新模型名
94
+ */
95
+ updateModel(modelName) {
96
+ if (modelName) {
97
+ this._modelName = modelName;
98
+ this._cachedLine = '';
99
+ }
100
+ }
101
+
102
+ /**
103
+ * 重置统计
104
+ */
105
+ reset() {
106
+ this._stats = {
107
+ inputTokens: 0,
108
+ outputTokens: 0,
109
+ cost: 0,
110
+ contextTokens: 0,
111
+ maxContextTokens: 100000,
112
+ contextPercent: 0,
113
+ };
114
+ this._cachedLine = '';
115
+ }
116
+
117
+ /**
118
+ * 从 agent 刷新数据
119
+ * 从 messageStore 获取最新一次调用的用量,计算增量后累加
120
+ */
121
+ refreshFromAgent() {
122
+ try {
123
+ const chatHandler = this.agent._chatHandler;
124
+ if (!chatHandler) return;
125
+
126
+ const sessionId = 'cli_default';
127
+
128
+ // 主动加载历史记录(确保从 SessionContext/文件加载历史消息)
129
+ chatHandler._chatSession.loadHistory(sessionId);
130
+
131
+ const messageStore = chatHandler._chatSession.getSessionMessageStore(sessionId);
132
+ if (!messageStore) return;
133
+
134
+ // 1. Token 用量:计算与上次记录的增量
135
+ if (messageStore.usage) {
136
+ const u = messageStore.usage;
137
+ const inputTokens = u.promptTokens || u.prompt_tokens || 0;
138
+ const outputTokens = u.completionTokens || u.completion_tokens || 0;
139
+
140
+ if (this._lastUsage) {
141
+ // 有上一次记录,计算增量
142
+ const inputDelta = Math.max(0, inputTokens - this._lastUsage.inputTokens);
143
+ const outputDelta = Math.max(0, outputTokens - this._lastUsage.outputTokens);
144
+ if (inputDelta > 0 || outputDelta > 0) {
145
+ this.updateUsage(inputDelta, outputDelta);
146
+ }
147
+ } else {
148
+ // 第一次,直接设置
149
+ this._stats.inputTokens = inputTokens;
150
+ this._stats.outputTokens = outputTokens;
151
+ }
152
+ this._lastUsage = { inputTokens, outputTokens };
153
+ }
154
+
155
+ // 2. 上下文 token 估算(使用 TokenCounter)
156
+ if (messageStore.messages && messageStore.messages.length > 0) {
157
+ const msgTokens = this._tokenCounter.countMessages(messageStore.messages);
158
+ this._stats.contextTokens = msgTokens;
159
+ if (this._stats.maxContextTokens > 0) {
160
+ this._stats.contextPercent = Math.min(100, (msgTokens / this._stats.maxContextTokens) * 100);
161
+ }
162
+ }
163
+
164
+ // 3. 模型名
165
+ this._modelName = chatHandler.model || this._modelName;
166
+ } catch (e) {
167
+ // 静默忽略
168
+ }
169
+ this._cachedLine = '';
170
+ }
171
+
172
+ /**
173
+ * 渲染组件
174
+ * @param {number} width - 终端宽度
175
+ * @returns {string[]} 行数组
176
+ */
177
+ render(width) {
178
+ if (width <= 0) return [''];
179
+
180
+ // 构建状态行
181
+ const s = this._stats;
182
+ const parts = [];
183
+
184
+ // ↑ input tokens
185
+ parts.push(folikoGold(`↑${this._formatNum(s.inputTokens)}`));
186
+
187
+ // ↓ output tokens
188
+ parts.push(folikoGold(`↓${this._formatNum(s.outputTokens)}`));
189
+
190
+ // $ cost
191
+ if (s.cost > 0) {
192
+ parts.push(chalk.white(`$${s.cost.toFixed(3)}`));
193
+ }
194
+
195
+ // context% / max
196
+ const ctxPercent = Math.round(s.contextPercent);
197
+ const ctxMax = this._formatNum(s.maxContextTokens);
198
+ parts.push(chalk.dim(`${ctxPercent}%/${ctxMax}`));
199
+
200
+ // 模型名
201
+ const modelShort = this._modelName.length > 20
202
+ ? this._modelName.slice(0, 18) + '..'
203
+ : this._modelName;
204
+ parts.push(folikoAccent(`(${modelShort})`));
205
+
206
+ // 思考级别指示
207
+ if (this._thinkingLevel === 'thinking') {
208
+ parts.push(chalk.magenta('🧠'));
209
+ }
210
+
211
+ // 组装
212
+ let line = parts.join(' ');
213
+
214
+ // 计算实际显示宽度(去除 ANSI 编码)
215
+ const visibleLen = visibleWidth ? visibleWidth(line) : line.length;
216
+
217
+ // 如果太长,缩短模型名
218
+ if (visibleLen > width) {
219
+ const modelLen = this._modelName.length > 12 ? 10 : this._modelName.length;
220
+ const shortModel = this._modelName.length > 12
221
+ ? this._modelName.slice(0, modelLen) + '..'
222
+ : this._modelName;
223
+ const shortAccent = folikoAccent(`(${shortModel})`);
224
+ const shortLine = parts.slice(0, 3).join(' ') + ' ' + chalk.dim(`${ctxPercent}%`) + ' ' + shortAccent;
225
+ line = shortLine;
226
+ }
227
+
228
+ // 截断到可用宽度
229
+ if (visibleWidth && visibleWidth(line) > width) {
230
+ line = truncateToWidth ? truncateToWidth(line, width) : line.slice(0, width);
231
+ }
232
+
233
+ return [line];
234
+ }
235
+ }
236
+
237
+ function visibleWidth(text) {
238
+ // 简易 ANSI 不可见宽度计算
239
+ const stripped = text.replace(/\x1B\[[0-9;]*m/g, '');
240
+ return stripped.length;
241
+ }
242
+
243
+ function truncateToWidth(text, maxWidth) {
244
+ const stripped = text.replace(/\x1B\[[0-9;]*m/g, '');
245
+ if (stripped.length <= maxWidth) return text;
246
+
247
+ // 需要截断,保留 ANSI 编码不变
248
+ let result = '';
249
+ let visLen = 0;
250
+ const regex = /(\x1B\[[0-9;]*m)|(.)/g;
251
+ let match;
252
+ while ((match = regex.exec(text)) !== null && visLen < maxWidth) {
253
+ if (match[1]) {
254
+ result += match[1];
255
+ } else {
256
+ result += match[2];
257
+ visLen++;
258
+ }
259
+ }
260
+ return result;
261
+ }
262
+
263
+ module.exports = { FooterBar };
@@ -12,8 +12,9 @@ class MessageBubble {
12
12
  * @param {number} paddingX - 左右内边距
13
13
  * @param {number} paddingTop - 上方内边距
14
14
  * @param {number} paddingBottom - 下方内边距
15
+ * @param {Function} [bgFn] - 背景色函数 (line) => coloredLine,每行渲染后调用
15
16
  */
16
- constructor(icon, content, isBot, markdownTheme, paddingX = 0, paddingTop = 0, paddingBottom = 0) {
17
+ constructor(icon, content, isBot, markdownTheme, paddingX = 0, paddingTop = 0, paddingBottom = 0, bgFn = null) {
17
18
  this.icon = icon;
18
19
  this.content = content;
19
20
  this.isBot = isBot;
@@ -21,6 +22,7 @@ class MessageBubble {
21
22
  this.paddingX = paddingX;
22
23
  this.paddingTop = paddingTop;
23
24
  this.paddingBottom = paddingBottom;
25
+ this._bgFn = bgFn;
24
26
  this._markdown = null;
25
27
  this._cacheKey = null;
26
28
  this._cachedLines = null;
@@ -78,9 +80,14 @@ class MessageBubble {
78
80
  // 确保最终输出不超过 width
79
81
  const truncatedResult = result.map(line => truncateToWidth(line, width));
80
82
 
83
+ // 应用背景色(如果有 bgFn)
84
+ const finalLines = this._bgFn
85
+ ? truncatedResult.map(line => this._bgFn(line))
86
+ : truncatedResult;
87
+
81
88
  // 更新缓存
82
89
  this._cacheKey = cacheKey;
83
- this._cachedLines = [...topPadding, ...truncatedResult, ...bottomPadding];
90
+ this._cachedLines = [...topPadding, ...finalLines, ...bottomPadding];
84
91
 
85
92
  return this._cachedLines;
86
93
  }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * render-diff.js — TUI diff 渲染
3
+ *
4
+ * 只负责文字颜色(红/绿/灰/白),不处理背景色。
5
+ * 背景色由 MessageBubble 的 bgFn 统一提供。
6
+ * 使用 \x1b[39m(仅重置前景色),避免破坏 bgFn 的背景。
7
+ */
8
+
9
+ const Diff = require('diff');
10
+
11
+ const A = {
12
+ RED: '\x1b[38;2;248;113;113m', // 删除文字色
13
+ GREEN: '\x1b[38;2;74;222;128m', // 新增文字色
14
+ GRAY: '\x1b[38;2;156;163;175m', // 上下文文字色
15
+ WHITE: '\x1b[97m', // 变化部分突出
16
+ HFG: '\x1b[38;2;229;231;235m', // 文件头文字色
17
+ BOLD: '\x1b[1m',
18
+ FGRST: '\x1b[39m', // 仅重置前景色(不碰背景)
19
+ };
20
+
21
+ function parseDiffLine(line) {
22
+ const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/);
23
+ if (!match) return null;
24
+ return { prefix: match[1], lineNum: match[2], content: match[3] };
25
+ }
26
+
27
+ function replaceTabs(text) {
28
+ return text.replace(/\t/g, ' ');
29
+ }
30
+
31
+ /**
32
+ * 单词级 diff,变化部分用白色突出
33
+ */
34
+ function renderIntraLineDiff(oldContent, newContent) {
35
+ const wordDiff = Diff.diffWords(oldContent, newContent);
36
+ let removedLine = '';
37
+ let addedLine = '';
38
+ let skippedRemovedWs = false;
39
+ let skippedAddedWs = false;
40
+
41
+ for (const part of wordDiff) {
42
+ if (part.removed) {
43
+ let value = part.value;
44
+ if (!skippedRemovedWs) {
45
+ const ws = value.match(/^(\s*)/)?.[1] || '';
46
+ removedLine += ws;
47
+ value = value.slice(ws.length);
48
+ skippedRemovedWs = true;
49
+ }
50
+ if (value) removedLine += A.WHITE + value + A.FGRST;
51
+ } else if (part.added) {
52
+ let value = part.value;
53
+ if (!skippedAddedWs) {
54
+ const ws = value.match(/^(\s*)/)?.[1] || '';
55
+ addedLine += ws;
56
+ value = value.slice(ws.length);
57
+ skippedAddedWs = true;
58
+ }
59
+ if (value) addedLine += A.WHITE + value + A.FGRST;
60
+ } else {
61
+ removedLine += part.value;
62
+ addedLine += part.value;
63
+ }
64
+ }
65
+ return { removedLine, addedLine };
66
+ }
67
+
68
+ /**
69
+ * 渲染 diff 文本(只有文字颜色,无背景色)
70
+ */
71
+ function renderDiff(diffText) {
72
+ if (!diffText) return '';
73
+
74
+ const lines = diffText.split('\n');
75
+ const result = [];
76
+ let i = 0;
77
+
78
+ while (i < lines.length) {
79
+ const line = lines[i];
80
+ const parsed = parseDiffLine(line);
81
+
82
+ if (!parsed) {
83
+ result.push(A.GRAY + line + A.FGRST);
84
+ i++;
85
+ continue;
86
+ }
87
+
88
+ if (parsed.prefix === '-') {
89
+ const removedLines = [];
90
+ while (i < lines.length) {
91
+ const p = parseDiffLine(lines[i]);
92
+ if (!p || p.prefix !== '-') break;
93
+ removedLines.push({ lineNum: p.lineNum, content: p.content });
94
+ i++;
95
+ }
96
+ const addedLines = [];
97
+ while (i < lines.length) {
98
+ const p = parseDiffLine(lines[i]);
99
+ if (!p || p.prefix !== '+') break;
100
+ addedLines.push({ lineNum: p.lineNum, content: p.content });
101
+ i++;
102
+ }
103
+
104
+ if (removedLines.length === 1 && addedLines.length === 1) {
105
+ const r = removedLines[0];
106
+ const a = addedLines[0];
107
+ const { removedLine, addedLine } = renderIntraLineDiff(
108
+ replaceTabs(r.content),
109
+ replaceTabs(a.content)
110
+ );
111
+ result.push(A.RED + `-${r.lineNum} ${removedLine}` + A.FGRST);
112
+ result.push(A.GREEN + `+${a.lineNum} ${addedLine}` + A.FGRST);
113
+ } else {
114
+ for (const r of removedLines) {
115
+ result.push(A.RED + `-${r.lineNum} ${replaceTabs(r.content)}` + A.FGRST);
116
+ }
117
+ for (const a of addedLines) {
118
+ result.push(A.GREEN + `+${a.lineNum} ${replaceTabs(a.content)}` + A.FGRST);
119
+ }
120
+ }
121
+ } else if (parsed.prefix === '+') {
122
+ result.push(A.GREEN + `+${parsed.lineNum} ${replaceTabs(parsed.content)}` + A.FGRST);
123
+ i++;
124
+ } else {
125
+ result.push(A.GRAY + ` ${parsed.lineNum} ${replaceTabs(parsed.content)}` + A.FGRST);
126
+ i++;
127
+ }
128
+ }
129
+
130
+ return result.join('\n');
131
+ }
132
+
133
+ /**
134
+ * 带文件路径头的 diff 渲染
135
+ */
136
+ function renderDiffWithHeader(diffText, filePath, prefix='Edit') {
137
+ if (!diffText) return '';
138
+ const parts = [];
139
+ if (filePath) {
140
+ parts.push(A.HFG + A.BOLD + ` ${prefix} ${filePath} ` + A.FGRST);
141
+ }
142
+ parts.push(renderDiff(diffText));
143
+ return parts.join('\n');
144
+ }
145
+
146
+ module.exports = {
147
+ renderDiff,
148
+ renderDiffWithHeader,
149
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.1.62",
3
+ "version": "1.1.63",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
@@ -65,6 +65,7 @@
65
65
  "chalk": "^5.6.2",
66
66
  "cli-highlight": "^2.1.11",
67
67
  "crypto": "^1.0.1",
68
+ "diff": "^9.0.0",
68
69
  "dotenv": "^17.3.1",
69
70
  "figlet": "^1.11.0",
70
71
  "file-type": "^22.0.1",