mocode-ai 0.6.7 → 0.6.9

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.
@@ -1,51 +1,87 @@
1
1
  import { builtinTools } from './builtins/index.js';
2
- import { recordMutation } from '../rollback/index.js';
2
+ import { beginPathMutation, beginWorkspaceMutation, endPathMutation, endWorkspaceMutation, } from '../rollback/index.js';
3
3
  import { enforceSandbox } from '../sandbox/index.js';
4
+ import { t } from '../i18n/index.js';
4
5
  /**
5
- * 工具注册表。当前 = 内置工具;
6
- * 未来可在此合并 MCP 工具、用户自定义工具等(见 src/mcp/)。
6
+ * 可扩展工具注册表。数组实例始终稳定,使已经持有 tools 引用的 agent/LLM 能看到运行时新增工具。
7
+ * 扩展按 source 替换,MCP 重连或配置刷新不会累积旧工具。
7
8
  */
8
- export const tools = builtinTools;
9
+ const extensions = new Map();
10
+ export const tools = [...builtinTools];
11
+ export function registerToolsExtension(sourceOrAdditions, maybeAdditions) {
12
+ const source = typeof sourceOrAdditions === 'string' ? sourceOrAdditions : 'external';
13
+ const additions = typeof sourceOrAdditions === 'string' ? (maybeAdditions ?? []) : sourceOrAdditions;
14
+ extensions.set(source, additions);
15
+ const rejected = additions
16
+ .filter((tool) => builtinTools.some((builtin) => builtin.name === tool.name))
17
+ .map((tool) => tool.name);
18
+ rebuildTools();
19
+ return rejected;
20
+ }
21
+ export function clearToolsExtension(source) {
22
+ if (extensions.delete(source))
23
+ rebuildTools();
24
+ }
25
+ function rebuildTools() {
26
+ const names = new Set();
27
+ const next = [];
28
+ for (const tool of [...builtinTools, ...Array.from(extensions.values()).flat()]) {
29
+ if (names.has(tool.name))
30
+ continue;
31
+ names.add(tool.name);
32
+ next.push(tool);
33
+ }
34
+ tools.splice(0, tools.length, ...next);
35
+ }
9
36
  /**
10
37
  * 按名调度工具,统一 try/catch + JSON 解析,返回字符串而非抛错。
11
38
  * signal 透传给 tool.execute(经 ctx):长任务工具(run_command/web_fetch)abort 即时取消,
12
39
  * 让用户 Ctrl+C 能跟手中断工具执行(而非等命令跑完 / 超时)。
13
- * opts.skipRollback:子 agent 逻辑隔离用——跳过 recordMutation,子 agent 改动不进主回滚快照链。
14
40
  * opts.dropContext:上下文剔除回调(drop_context 工具用),透传给 tool.execute 经 ctx。
15
41
  */
16
42
  export async function executeTool(name, argsRaw, signal, opts) {
17
43
  const tool = tools.find((t) => t.name === name);
18
44
  if (!tool)
19
- return `错误:未知工具 "${name}"`;
45
+ return t('toolError.unknown', { name });
20
46
  let args;
21
47
  try {
22
48
  args = argsRaw.trim() ? JSON.parse(argsRaw) : {};
23
49
  }
24
50
  catch {
25
- return `错误:工具 ${name} 的 arguments 不是合法 JSON: ${argsRaw}`;
51
+ return t('toolError.invalidJson', { name, arguments: argsRaw });
26
52
  }
27
53
  try {
28
- // 沙箱:路径类工具(读/写/改)越界拒绝 + args.path 重写为牢内绝对;glob/grep pattern 校验。
29
- // 返 string = 拒绝(直接喂 LLM,不执行);返 null = 放行(可能已重写 args.path)。
30
- // 须在 recordMutation 前:让快照路径 = 牢内绝对路径,与回滚一致。不抛(契约:调度器永不抛错)。
54
+ // 沙箱先重写/校验路径,保证快照与实际执行目标完全一致。
31
55
  const sbErr = enforceSandbox(name, args);
32
56
  if (sbErr)
33
57
  return sbErr;
34
- // 撤销回滚用:write_file/edit_file 改动前记 before 快照(回滚时恢复到轮末状态)。
35
- // 子 agent(skipRollback)跳过:其改动不进主回滚链,主 /rollback 不撤销(靠 git 兜底)。
36
- if (!opts?.skipRollback &&
37
- (name === 'write_file' || name === 'edit_file') &&
58
+ const pathCapture = (name === 'write_file' || name === 'edit_file') &&
38
59
  typeof args.path === 'string' &&
39
- args.path) {
40
- recordMutation(args.path);
60
+ args.path
61
+ ? beginPathMutation(args.path)
62
+ : null;
63
+ // shell 与 MCP 的副作用无法从参数可靠推断:以工作区前后状态识别实际改动。
64
+ // task 本身不扫描;其子 agent 共享当前轮,并在各自真实写工具处记账。
65
+ const workspaceCapture = name === 'run_command' || name.startsWith('mcp__')
66
+ ? beginWorkspaceMutation()
67
+ : null;
68
+ try {
69
+ return await tool.execute(args, {
70
+ signal,
71
+ dropContext: opts?.dropContext,
72
+ });
73
+ }
74
+ finally {
75
+ if (pathCapture)
76
+ endPathMutation(pathCapture, name);
77
+ if (workspaceCapture)
78
+ endWorkspaceMutation(workspaceCapture, name);
41
79
  }
42
- return await tool.execute(args, {
43
- signal,
44
- skipRollback: opts?.skipRollback,
45
- dropContext: opts?.dropContext,
46
- });
47
80
  }
48
81
  catch (e) {
49
- return `错误:工具 ${name} 执行失败: ${e instanceof Error ? e.message : String(e)}`;
82
+ return t('toolError.execution', {
83
+ name,
84
+ message: e instanceof Error ? e.message : String(e),
85
+ });
50
86
  }
51
87
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Tool results are still strings for LLM compatibility. Keep error detection
3
+ * language-independent while legacy built-ins are migrated to structured results.
4
+ */
5
+ export function isToolErrorOutput(output) {
6
+ return /^(?:错误|Error):/.test(output.trimStart());
7
+ }
@@ -5,15 +5,18 @@ import { displayWidth, truncateDisplay, visColToCharCol, wrapByDisplayWidth } fr
5
5
  import * as layout from './layout.js';
6
6
  import * as mouse from './mouse.js';
7
7
  import { Spinner } from './spinner.js';
8
+ import { t } from '../i18n/index.js';
8
9
  const emitter = stdin;
9
- /** choice 末尾的"自定义输入"项标签(纯 ASCII,宽度安全)。选中后切到 input 子态。 */
10
- const CUSTOM_LABEL = '其他(自定义输入)';
10
+ /** choice 末尾的本地化“自定义输入”项标签。 */
11
+ const customLabel = () => t('intervention.custom');
11
12
  /** 弹出介入面板,阻塞直到用户完成选择。 */
12
13
  export async function promptIntervention(req) {
13
14
  // 非 TTY 降级:不阻塞,自动选默认。日志走 stderr(不污染 stdout 内容流)。
14
15
  if (!layout.isActive()) {
15
- const kind = req.type === 'choice' ? '自动选默认项' : '自动返回空输入';
16
- stderr.write(`[介入] ${req.title}(非交互环境,${kind})\n`);
16
+ const kind = req.type === 'choice'
17
+ ? t('intervention.defaultChoice')
18
+ : t('intervention.emptyInput');
19
+ stderr.write(`${t('intervention.nonInteractive', { title: req.title, kind })}\n`);
17
20
  if (req.type === 'choice') {
18
21
  const first = req.options?.[0];
19
22
  const value = typeof first === 'string' ? first : first?.label ?? '';
@@ -45,7 +48,7 @@ export async function promptIntervention(req) {
45
48
  const g = layout.getGeo();
46
49
  const cols = g.cols;
47
50
  const allowCustom = req.allowCustom !== false;
48
- const items = allowCustom ? [...options, { label: CUSTOM_LABEL }] : [...options];
51
+ const items = allowCustom ? [...options, { label: customLabel() }] : [...options];
49
52
  const optionCount = items.length;
50
53
  // 每项占行数 = label + (detail)整体 wrap 到可用宽度后的实际行数(超长换行,不再省略)。
51
54
  // 短 label 仍只占 1 行;长 detail 自然占用多行,菜单总高度随之动态增长。
@@ -129,7 +132,9 @@ export async function promptIntervention(req) {
129
132
  if (dl.length > detailCap)
130
133
  lines.push(`${ui.dim}…${ui.reset}`);
131
134
  }
132
- lines.push(`${ui.dim}Enter 提交 · Esc ${cameFromChoice ? '返回选项' : '取消'} · Ctrl+C 取消${ui.reset}`);
135
+ lines.push(`${ui.dim}${t('prompt.submit', {
136
+ escape: cameFromChoice ? t('prompt.back') : t('prompt.cancel'),
137
+ })}${ui.reset}`);
133
138
  return lines;
134
139
  }
135
140
  /** 鼠标左键点击输入框 → 把 cursor 移到点击位置(与 prompt.ts 的 applyExternalCursor 同源逻辑)。
@@ -156,7 +161,7 @@ export async function promptIntervention(req) {
156
161
  }
157
162
  function redraw() {
158
163
  if (mode === 'choice') {
159
- const hint = '↑↓ 选择 · Enter 确认 · 数字键直选 · Esc 取消';
164
+ const hint = t('prompt.chooseConfirm');
160
165
  layout.paintInput({
161
166
  prompt: '❯ ',
162
167
  lines: [hint],
package/dist/ui/layout.js CHANGED
@@ -6,6 +6,7 @@ import * as mouse from './mouse.js';
6
6
  import { shiftBatchesAfter } from './batch.js';
7
7
  import { copyToClipboard, readClipboard } from './clipboard.js';
8
8
  import { renderMarkdown } from './markdown.js';
9
+ import { t } from '../i18n/index.js';
9
10
  // ── 内部状态 ──
10
11
  let active = false;
11
12
  let mode = 'input';
@@ -1284,7 +1285,7 @@ function composeSpinnerLine(status, cols) {
1284
1285
  let tail = '';
1285
1286
  let tailW = 0;
1286
1287
  if (scrolled) {
1287
- tail = `历史 ↑${scrollOffset} (PgDn 回底)`;
1288
+ tail = t('status.history', { count: scrollOffset });
1288
1289
  tailW = displayWidth(tail);
1289
1290
  }
1290
1291
  const rightStr = tail ? `${ui.yellow}${tail}${ui.reset}` : '';
@@ -1302,7 +1303,7 @@ function composeModelLine(status, cols) {
1302
1303
  const modePart = modeTag ? `${modeColor}${modeTag}${ui.reset}` : '';
1303
1304
  const modeW = modeTag ? displayWidth(modeTag) : 0;
1304
1305
  // 切换提示:告诉用户怎么切模式。灰(dim)降优先级,不与 modeTag 抢色;只在有 modeTag 时出现。
1305
- const HINT = 'Shift+Tab 切换';
1306
+ const HINT = t('status.modeSwitch');
1306
1307
  const hintW = modeTag ? displayWidth(HINT) : 0;
1307
1308
  const hintPart = modeTag ? `${ui.dim}${HINT}${ui.reset}` : '';
1308
1309
  const tokChip = formatTurnTokenChip(status.lastTurnUsage);
@@ -1822,7 +1823,7 @@ export function repaint() {
1822
1823
  drawStatusBar();
1823
1824
  }
1824
1825
  /** 进入输入态:画空输入框 + 状态行,光标入输入框。 */
1825
- export function enterInputMode(status = '空闲') {
1826
+ export function enterInputMode(status = t('repl.idle')) {
1826
1827
  mode = 'input';
1827
1828
  statusText = status;
1828
1829
  spinnerFrame = undefined;
package/dist/ui/prompt.js CHANGED
@@ -4,6 +4,7 @@ import { ui } from './theme.js';
4
4
  import { displayWidth, padEndDisplay, truncateDisplay, visColToCharCol, wrapByDisplayWidth } from './render.js';
5
5
  import * as layout from './layout.js';
6
6
  import * as mouse from './mouse.js';
7
+ import { t } from '../i18n/index.js';
7
8
  // ── 粘贴检测(块级 + 时间窗)──
8
9
  // 块级:多字节大块(len>8)或含 CR/LF 的小块 = 粘贴(键盘单键 1 字节,Enter=\r 单字节)。
9
10
  // 时间窗:每块重置 50ms 计时器,静默 50ms 即"粘贴结束"→ onPasteEnd。跨多块的大粘贴(块间 <50ms)累积进
@@ -101,18 +102,20 @@ export async function promptWithSlashMenu(opts) {
101
102
  else if (selected >= menuTop + visibleCount)
102
103
  menuTop = selected - visibleCount + 1;
103
104
  const windowItems = filtered.slice(menuTop, menuTop + visibleCount);
104
- const maxName = Math.max(...windowItems.map((c) => displayWidth(c.name)));
105
+ const maxName = Math.max(...windowItems.map((item) => displayWidth(item.node.name + (item.node.children?.length ? ' ›' : ''))));
105
106
  const hasMoreAbove = menuTop > 0;
106
107
  const hasMoreBelow = menuTop + visibleCount < filtered.length;
107
- return windowItems.map((c, i) => {
108
+ return windowItems.map((item, i) => {
109
+ const c = item.node;
108
110
  const globalIdx = menuTop + i;
109
111
  // 选中项:▸ 与文字均 cyan+bold(去 dim),未选中项保持 dim——选中行整体高亮。
110
112
  const isSel = globalIdx === selected;
111
113
  const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
112
114
  const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
113
- const name = padEndDisplay(c.name, maxName);
115
+ const branchSuffix = c.children?.length ? ' ›' : '';
116
+ const name = padEndDisplay(c.name + branchSuffix, maxName);
114
117
  // 首/末附加滚动指示(▲/▼)而非替换整行
115
- let desc = c.desc;
118
+ const desc = c.desc;
116
119
  let scrollHint = '';
117
120
  if (i === 0 && hasMoreAbove)
118
121
  scrollHint = ' ▲';
@@ -135,8 +138,11 @@ export async function promptWithSlashMenu(opts) {
135
138
  return '';
136
139
  const lines = chip.split('\n').length;
137
140
  const chars = chip.length;
138
- const charStr = chars < 1000 ? `${chars} 字符` : `${(chars / 1000).toFixed(1)}K 字符`;
139
- return `[📋 ${lines} 行 · ${charStr}] `;
141
+ const lineText = t('prompt.lines', { count: lines });
142
+ const charText = t('prompt.chars', {
143
+ count: chars < 1000 ? chars : `${(chars / 1000).toFixed(1)}K`,
144
+ });
145
+ return `[📋 ${lineText} · ${charText}] `;
140
146
  }
141
147
  /** chipPre 按行拆分(粘贴发生前光标之前已有的文本,可能多行,原样保留在 chip 之前)。 */
142
148
  function chipPreLines() {
@@ -225,9 +231,24 @@ export async function promptWithSlashMenu(opts) {
225
231
  return;
226
232
  applyPastedText(text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
227
233
  }
234
+ /** 根据输入文本找到当前层。prefix 始终包含已进入分支及末尾空格。 */
235
+ function menuContext(input) {
236
+ let nodes = opts.commands;
237
+ let prefix = '';
238
+ while (true) {
239
+ const branch = nodes.find((node) => node.children?.length && input.startsWith(`${prefix}${node.name} `));
240
+ if (!branch?.children)
241
+ return { nodes, prefix };
242
+ prefix = `${prefix}${branch.name} `;
243
+ nodes = branch.children;
244
+ }
245
+ }
228
246
  function computeFiltered() {
229
247
  if (cl === 0 && lines[0].startsWith('/')) {
230
- filtered = opts.commands.filter((c) => c.name.startsWith(lines[0]));
248
+ const { nodes, prefix } = menuContext(lines[0]);
249
+ filtered = nodes
250
+ .map((node) => ({ node, input: `${prefix}${node.name}` }))
251
+ .filter((item) => item.input.startsWith(lines[0]));
231
252
  menuOpen = filtered.length > 0;
232
253
  if (selected >= filtered.length) {
233
254
  selected = 0;
@@ -371,13 +392,38 @@ export async function promptWithSlashMenu(opts) {
371
392
  cleanup();
372
393
  resolve(value);
373
394
  }
374
- /** 提交:菜单打开时先补全选中项到第 0 行。chipPre + chip + suffix 依次拼回全文(保留粘贴前后原有文字)。 */
375
- function submit() {
376
- if (menuOpen && filtered[selected]) {
377
- lines = [filtered[selected].name];
395
+ /**
396
+ * 应用当前菜单项。分支节点只进入下一层;叶子节点补全 value/input。
397
+ * 返回 true 表示调用方应立即提交,false 表示仍留在输入态。
398
+ */
399
+ function applySelected(submitLeaf) {
400
+ const item = filtered[selected];
401
+ if (!menuOpen || !item)
402
+ return submitLeaf;
403
+ if (item.node.children?.length) {
404
+ lines = [`${item.input} `];
378
405
  cl = 0;
379
406
  cc = lines[0].length;
407
+ selected = 0;
408
+ menuTop = 0;
409
+ computeFiltered();
410
+ redraw();
411
+ return false;
412
+ }
413
+ lines = [item.node.value ?? item.input];
414
+ cl = 0;
415
+ cc = lines[0].length;
416
+ computeFiltered();
417
+ if (!submitLeaf || item.node.submit === false) {
418
+ redraw();
419
+ return false;
380
420
  }
421
+ return true;
422
+ }
423
+ /** 提交:菜单打开时先应用选中项;分支进入子菜单,需要参数的叶子只补全。 */
424
+ function submit() {
425
+ if (menuOpen && !applySelected(true))
426
+ return;
381
427
  const suffix = lines.join('\n');
382
428
  let content = suffix;
383
429
  if (chip) {
@@ -589,21 +635,29 @@ export async function promptWithSlashMenu(opts) {
589
635
  }
590
636
  return;
591
637
  case 'tab':
592
- if (menuOpen && filtered[selected]) {
593
- lines[0] = filtered[selected].name;
638
+ if (menuOpen && filtered[selected])
639
+ applySelected(false);
640
+ return;
641
+ case 'escape': {
642
+ const { prefix } = menuContext(lines[0]);
643
+ if (prefix) {
644
+ // 子层 Esc 回到父节点;例如 /model sw → /model。
645
+ lines = [prefix.trimEnd()];
594
646
  cl = 0;
595
647
  cc = lines[0].length;
648
+ selected = 0;
649
+ menuTop = 0;
596
650
  computeFiltered();
597
- redraw();
598
651
  }
599
- return;
600
- case 'escape':
601
- menuOpen = false;
602
- filtered = [];
603
- selected = 0;
604
- menuTop = 0;
652
+ else {
653
+ menuOpen = false;
654
+ filtered = [];
655
+ selected = 0;
656
+ menuTop = 0;
657
+ }
605
658
  redraw();
606
659
  return;
660
+ }
607
661
  case 'left':
608
662
  if (cc > 0) {
609
663
  cc--;
@@ -680,7 +734,7 @@ export async function promptTurnPicker(items) {
680
734
  if (!layout.isActive() || items.length === 0)
681
735
  return null;
682
736
  const emitter = stdin;
683
- const hint = '↑↓ 选择 · Enter 回滚到该轮 · Esc 取消';
737
+ const hint = t('prompt.chooseRollback');
684
738
  let selected = items.length - 1; // 默认聚焦最新(末项,菜单底、靠近输入框)
685
739
  let resolved = false;
686
740
  let resolve;
@@ -804,12 +858,12 @@ export async function promptSessionPicker(items, recentCap = 10) {
804
858
  }
805
859
  /** 输入框行的操作提示;折叠态显「a 全部(N)」,展开态显「a 仅最近N」。不超 cap 时无 a 项。 */
806
860
  function hint() {
807
- const base = '↑↓ 选择 · Enter 续接 · Esc 取消';
861
+ const base = t('prompt.chooseResume');
808
862
  if (!canToggle)
809
863
  return base;
810
864
  return showAll
811
- ? `↑↓ 选择 · Enter 续接 · a 仅最近${cap} · Esc 取消`
812
- : `↑↓ 选择 · Enter 续接 · a 全部(${items.length}) · Esc 取消`;
865
+ ? t('prompt.recent', { count: cap })
866
+ : t('prompt.all', { count: items.length });
813
867
  }
814
868
  /** 菜单行(带开窗):超屏高时以 selected 为中心取窗,保光标可见。行格式:▸ N title subtitle。 */
815
869
  function menuLines() {
@@ -951,7 +1005,7 @@ export async function promptThemePicker(items) {
951
1005
  let resolved = false;
952
1006
  let resolve;
953
1007
  let reject;
954
- const hint = '↑↓ 选择 · Enter 切换 · Esc 取消';
1008
+ const hint = t('prompt.chooseSwitch');
955
1009
  /** 菜单行(带开窗):超屏高时以 selected 为中心取窗,保光标可见。行格式:▸ N title subtitle。 */
956
1010
  function menuLines() {
957
1011
  const g = layout.getGeo();
@@ -1076,10 +1130,11 @@ export async function promptRevertChoice(fileCount) {
1076
1130
  if (!layout.isActive())
1077
1131
  return null;
1078
1132
  const emitter = stdin;
1079
- const hint = '↑↓ 选择 · Enter 确认 · Esc 保留文件';
1133
+ const hint = t('prompt.keepFiles');
1134
+ const detail = fileCount > 0 ? t('prompt.revertDetail', { count: fileCount }) : '';
1080
1135
  const items = [
1081
- `撤销文件改动${fileCount > 0 ? `(${fileCount} 个文件恢复到回滚前)` : ''}`,
1082
- '只撤销消息,保留文件改动',
1136
+ t('prompt.revertFiles', { detail }),
1137
+ t('prompt.messagesOnly'),
1083
1138
  ];
1084
1139
  let selected = 0; // 默认聚焦首项(撤销文件)
1085
1140
  let resolved = false;
package/dist/ui/render.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { stdout } from 'node:process';
2
2
  import { createRequire } from 'node:module';
3
3
  import { ui } from './theme.js';
4
+ import { t } from '../i18n/index.js';
4
5
  const VERSION = createRequire(import.meta.url)('../../package.json').version;
5
6
  /**
6
7
  * 清空整屏 + 滚动缓冲(向上滚动可见的历史输出),光标归位。
@@ -245,12 +246,12 @@ const LOGO_LINES = [
245
246
  function logoLine(idx) {
246
247
  return `${ui.accent}${padEndDisplay(LOGO_LINES[idx], LOGO_W)}${ui.reset}${' '.repeat(LOGO_GAP)}`;
247
248
  }
248
- function labelContent(label, value) {
249
- return `${ui.dim}${padEndDisplay(label, 6)}${ui.reset}${value}`;
249
+ function labelContent(label, value, width) {
250
+ return `${ui.dim}${padEndDisplay(label, width)}${ui.reset}${value}`;
250
251
  }
251
- /** 把布尔转成显式中文:banner 上避免「是/否」歧义,用「开/关」。 */
252
+ /** 把记忆开关转成本地化标签。 */
252
253
  function memoryLabel(on) {
253
- return on ? '开启' : '关闭';
254
+ return on ? t('banner.enabled') : t('banner.disabled');
254
255
  }
255
256
  /** 记忆状态染色:开启用 ui.accent(亮),关闭用 ui.dim(弱),让远观有差别。
256
257
  * banner 里整体仍走 ui.reset 兜底——染色只贴在值那一列。 */
@@ -261,12 +262,14 @@ function memoryValue(on) {
261
262
  * 布局:大字 logo(4 行,块字符)左对齐,右侧并排放标题/信息(neofetch 风)。 */
262
263
  export function bannerString(info) {
263
264
  const title = `${ui.bold}${ui.accent}◆ MoCode${ui.reset} ${ui.dim}v${VERSION}${ui.reset}`;
264
- // 信息行 = 标题 + 模型 + 目录 + 记忆(共 4 行,与 4 行 logo 对齐)
265
+ const labels = [t('banner.model'), t('banner.directory'), t('banner.memory')];
266
+ // 标签列按当前语言最长文本动态定宽,并至少留两个空格;中文保持原 6 列,英文扩至 11 列。
267
+ const labelWidth = Math.max(...labels.map(displayWidth)) + 2;
265
268
  const rows = [
266
269
  logoLine(0) + title,
267
- logoLine(1) + labelContent('模型', info.model),
268
- logoLine(2) + labelContent('目录', truncateDisplay(info.cwd, 48)),
269
- logoLine(3) + labelContent('记忆', memoryValue(info.memoryEnabled)),
270
+ logoLine(1) + labelContent(labels[0], info.model, labelWidth),
271
+ logoLine(2) + labelContent(labels[1], truncateDisplay(info.cwd, 48), labelWidth),
272
+ logoLine(3) + labelContent(labels[2], memoryValue(info.memoryEnabled), labelWidth),
270
273
  ];
271
274
  return rows.map((r) => MARGIN + r).join('\n') + '\n\n';
272
275
  }
@@ -331,13 +334,13 @@ export function summarizeToolResult(name, output) {
331
334
  const nonEmpty = output.split('\n').filter((l) => l.trim().length > 0);
332
335
  switch (name) {
333
336
  case 'read_file':
334
- return nonEmpty.length ? `${nonEmpty.length} 行` : '(空文件)';
337
+ return nonEmpty.length ? t('toolSummary.lines', { count: nonEmpty.length }) : t('toolSummary.emptyFile');
335
338
  case 'glob':
336
- return nonEmpty.length ? `${nonEmpty.length} 个文件` : '(无匹配)';
339
+ return nonEmpty.length ? t('toolSummary.files', { count: nonEmpty.length }) : t('toolSummary.noMatches');
337
340
  case 'grep':
338
- return nonEmpty.length ? `${nonEmpty.length} 处匹配` : '(无匹配)';
341
+ return nonEmpty.length ? t('toolSummary.matches', { count: nonEmpty.length }) : t('toolSummary.noMatches');
339
342
  case 'run_command':
340
- return truncateDisplay(nonEmpty[0] ?? '', 100) || '(无输出)';
343
+ return truncateDisplay(nonEmpty[0] ?? '', 100) || t('toolSummary.noOutput');
341
344
  default:
342
345
  return truncateDisplay(nonEmpty[0] ?? '', 100);
343
346
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {