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.
- package/README.md +9 -4
- package/README.zh-CN.md +5 -0
- package/dist/agent/core.js +5 -5
- package/dist/agent/index.js +10 -8
- package/dist/agent/spawn.js +1 -5
- package/dist/commands/config.js +9 -7
- package/dist/config/index.js +49 -41
- package/dist/context/budget.js +2 -3
- package/dist/i18n/index.js +450 -0
- package/dist/index.js +8 -4
- package/dist/llm/index.js +19 -16
- package/dist/mcp/client.js +389 -0
- package/dist/mcp/config.js +128 -0
- package/dist/mcp/index.js +105 -0
- package/dist/mcp/registry.js +5 -0
- package/dist/mcp/types.js +1 -0
- package/dist/permissions/index.js +16 -12
- package/dist/pet/state.js +2 -1
- package/dist/repl/index.js +210 -102
- package/dist/rollback/index.js +311 -119
- package/dist/session/persist.js +11 -5
- package/dist/tools/builtins/ask-human.js +4 -3
- package/dist/tools/builtins/run-command.js +6 -5
- package/dist/tools/builtins/task.js +5 -4
- package/dist/tools/registry.js +58 -22
- package/dist/tools/result.js +7 -0
- package/dist/ui/intervention.js +12 -7
- package/dist/ui/layout.js +4 -3
- package/dist/ui/prompt.js +83 -28
- package/dist/ui/render.js +15 -12
- package/package.json +1 -1
package/dist/tools/registry.js
CHANGED
|
@@ -1,51 +1,87 @@
|
|
|
1
1
|
import { builtinTools } from './builtins/index.js';
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
6
|
+
* 可扩展工具注册表。数组实例始终稳定,使已经持有 tools 引用的 agent/LLM 能看到运行时新增工具。
|
|
7
|
+
* 扩展按 source 替换,MCP 重连或配置刷新不会累积旧工具。
|
|
7
8
|
*/
|
|
8
|
-
|
|
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
|
|
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
|
|
51
|
+
return t('toolError.invalidJson', { name, arguments: argsRaw });
|
|
26
52
|
}
|
|
27
53
|
try {
|
|
28
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/ui/intervention.js
CHANGED
|
@@ -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
|
|
10
|
-
const
|
|
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
|
-
|
|
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:
|
|
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}
|
|
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 = '
|
|
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 =
|
|
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 = '
|
|
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((
|
|
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((
|
|
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
|
|
115
|
+
const branchSuffix = c.children?.length ? ' ›' : '';
|
|
116
|
+
const name = padEndDisplay(c.name + branchSuffix, maxName);
|
|
114
117
|
// 首/末附加滚动指示(▲/▼)而非替换整行
|
|
115
|
-
|
|
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
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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 = '
|
|
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 = '
|
|
861
|
+
const base = t('prompt.chooseResume');
|
|
808
862
|
if (!canToggle)
|
|
809
863
|
return base;
|
|
810
864
|
return showAll
|
|
811
|
-
?
|
|
812
|
-
:
|
|
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 = '
|
|
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 = '
|
|
1133
|
+
const hint = t('prompt.keepFiles');
|
|
1134
|
+
const detail = fileCount > 0 ? t('prompt.revertDetail', { count: fileCount }) : '';
|
|
1080
1135
|
const items = [
|
|
1081
|
-
|
|
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,
|
|
249
|
+
function labelContent(label, value, width) {
|
|
250
|
+
return `${ui.dim}${padEndDisplay(label, width)}${ui.reset}${value}`;
|
|
250
251
|
}
|
|
251
|
-
/**
|
|
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
|
-
|
|
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(
|
|
268
|
-
logoLine(2) + labelContent(
|
|
269
|
-
logoLine(3) + labelContent(
|
|
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 ?
|
|
337
|
+
return nonEmpty.length ? t('toolSummary.lines', { count: nonEmpty.length }) : t('toolSummary.emptyFile');
|
|
335
338
|
case 'glob':
|
|
336
|
-
return nonEmpty.length ?
|
|
339
|
+
return nonEmpty.length ? t('toolSummary.files', { count: nonEmpty.length }) : t('toolSummary.noMatches');
|
|
337
340
|
case 'grep':
|
|
338
|
-
return 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
|
}
|