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
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { promptIntervention } from '../ui/intervention.js';
|
|
5
5
|
import { config } from '../config/index.js';
|
|
6
|
+
import { t } from '../i18n/index.js';
|
|
6
7
|
/**
|
|
7
8
|
* 工具权限系统:基于 risk 字段在执行前拦截确认。
|
|
8
9
|
*
|
|
@@ -60,14 +61,14 @@ export function getToolRisk(tool) {
|
|
|
60
61
|
function summarizeArgs(tool, args) {
|
|
61
62
|
const lines = [];
|
|
62
63
|
if (typeof args.path === 'string')
|
|
63
|
-
lines.push(
|
|
64
|
+
lines.push(t('permission.path', { value: args.path }));
|
|
64
65
|
if (typeof args.command === 'string')
|
|
65
|
-
lines.push(
|
|
66
|
+
lines.push(t('permission.command', { value: args.command }));
|
|
66
67
|
if (typeof args.prompt === 'string') {
|
|
67
68
|
const preview = String(args.prompt).slice(0, 100);
|
|
68
|
-
lines.push(
|
|
69
|
+
lines.push(t('permission.task', { value: `${preview}${String(args.prompt).length > 100 ? '…' : ''}` }));
|
|
69
70
|
}
|
|
70
|
-
return lines.length > 0 ? lines.join('\n') : '
|
|
71
|
+
return lines.length > 0 ? lines.join('\n') : t('permission.noArgs');
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* 检查权限:safe 直接放行;confirm/dangerous 弹面板让用户确认。
|
|
@@ -90,14 +91,17 @@ export async function checkPermission(tool, args, signal) {
|
|
|
90
91
|
return 'allow';
|
|
91
92
|
// 构建确认面板
|
|
92
93
|
const isDangerous = risk === 'dangerous';
|
|
94
|
+
const denyOption = t('permission.deny');
|
|
95
|
+
const foreverOption = t('permission.allowForever');
|
|
96
|
+
const sessionOption = t('permission.allowSession');
|
|
93
97
|
const title = isDangerous
|
|
94
|
-
?
|
|
95
|
-
:
|
|
96
|
-
const detail = summarizeArgs(tool, args) + (isDangerous ?
|
|
98
|
+
? t('permission.dangerTitle', { tool: tool.name })
|
|
99
|
+
: t('permission.confirmTitle', { tool: tool.name });
|
|
100
|
+
const detail = summarizeArgs(tool, args) + (isDangerous ? `\n\n${t('permission.dangerWarning')}` : '');
|
|
97
101
|
// 选项统一结构:dangerous 也提供"以后不再询问"(用户明确授权即尊重,即使 run_command)
|
|
98
102
|
const options = isDangerous
|
|
99
|
-
? ['
|
|
100
|
-
: ['
|
|
103
|
+
? [t('permission.confirmExecute'), foreverOption, denyOption]
|
|
104
|
+
: [t('permission.allow'), sessionOption, foreverOption, denyOption];
|
|
101
105
|
// 弹面板(阻塞直到用户选择;signal 中断时 promptIntervention 内部处理)
|
|
102
106
|
const result = await promptIntervention({
|
|
103
107
|
type: 'choice',
|
|
@@ -111,16 +115,16 @@ export async function checkPermission(tool, args, signal) {
|
|
|
111
115
|
return 'deny';
|
|
112
116
|
// 解析选择
|
|
113
117
|
const value = result.value ?? '';
|
|
114
|
-
if (value ===
|
|
118
|
+
if (value === denyOption)
|
|
115
119
|
return 'deny';
|
|
116
120
|
// 永久允许:写入磁盘 + 加入内存集合(跨会话生效)
|
|
117
|
-
if (value ===
|
|
121
|
+
if (value === foreverOption) {
|
|
118
122
|
permanentAllow.add(tool.name);
|
|
119
123
|
savePermanent();
|
|
120
124
|
return 'allow';
|
|
121
125
|
}
|
|
122
126
|
// 会话允许(confirm 级):加入内存缓存,本次进程内不再弹
|
|
123
|
-
if (!isDangerous && value ===
|
|
127
|
+
if (!isDangerous && value === sessionOption) {
|
|
124
128
|
approvedTools.add(tool.name);
|
|
125
129
|
}
|
|
126
130
|
return 'allow';
|
package/dist/pet/state.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 与既有 TUI hooks 并列注入——两组 hooks 各自独立触发,互不干扰,不修改 core.ts 的任何行为。
|
|
4
4
|
// 子 agent(src/agent/spawn.ts 的 spawnAgent)不引用本模块,故子 agent 永不广播桌宠状态。
|
|
5
5
|
import * as bridge from './bridge.js';
|
|
6
|
+
import { isToolErrorOutput } from '../tools/result.js';
|
|
6
7
|
/**
|
|
7
8
|
* 纯函数:给定当前 hook 事件与其参数,推导下一个 PetState。
|
|
8
9
|
* 前置条件:event 是 AgentHooks 定义的方法名之一。
|
|
@@ -19,7 +20,7 @@ export function deriveState(event, args) {
|
|
|
19
20
|
case 'onToolStart':
|
|
20
21
|
return 'tool_call';
|
|
21
22
|
case 'onToolResult':
|
|
22
|
-
if (args?.toolOutput && args.toolOutput
|
|
23
|
+
if (args?.toolOutput && isToolErrorOutput(args.toolOutput))
|
|
23
24
|
return 'error';
|
|
24
25
|
return 'tool_call';
|
|
25
26
|
case 'onDone':
|
package/dist/repl/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
5
|
+
import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
|
|
5
6
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
6
7
|
import { deletePreset, getPreset, isValidPresetName, listPresets, migrateCurrentToPreset, savePreset, } from '../config/presets.js';
|
|
7
8
|
import { runAgent } from '../agent/index.js';
|
|
@@ -15,8 +16,9 @@ import * as mouse from '../ui/mouse.js';
|
|
|
15
16
|
import * as batch from '../ui/batch.js';
|
|
16
17
|
import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, promptThemePicker, promptRevertChoice, } from '../ui/prompt.js';
|
|
17
18
|
import { promptIntervention } from '../ui/intervention.js';
|
|
18
|
-
import { tools } from '../tools/registry.js';
|
|
19
|
-
import {
|
|
19
|
+
import { tools, registerToolsExtension } from '../tools/registry.js';
|
|
20
|
+
import { initializeAllMcp, getMcpTools, closeAllMcp } from '../mcp/index.js';
|
|
21
|
+
import { estimateMessagesTokens, reconfigureClient, refreshChatTools, } from '../llm/index.js';
|
|
20
22
|
import { loadImageAttachment, renderChip, MAX_INLINE_BYTES_DEFAULT, } from '../attachments/image.js';
|
|
21
23
|
import { modelSupportsVision } from '../llm/capabilities.js';
|
|
22
24
|
import { computePruneStats } from '../context/relevance.js';
|
|
@@ -34,50 +36,113 @@ import { setCurrentSessionId, getCurrentSessionId } from '../session/state.js';
|
|
|
34
36
|
* 颜色码会让光标错位、编辑时漂移。颜色只用在直接 stdout.write 的横幅 / 工具行 / 回复。
|
|
35
37
|
*/
|
|
36
38
|
const PROMPT = '❯ ';
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
39
|
+
/**
|
|
40
|
+
* 斜杠命令树(仅用于输入菜单;分发仍走下方 if 链)。
|
|
41
|
+
* 分支节点只负责导航,叶子的 value 保持现有命令文本,因此不破坏命令兼容性。
|
|
42
|
+
*/
|
|
43
|
+
function buildSlashCommands() {
|
|
44
|
+
const d = (key) => t(key);
|
|
45
|
+
return [
|
|
46
|
+
{ name: '/help', desc: d('commands.help') },
|
|
47
|
+
{ name: '/exit', desc: d('commands.exit') },
|
|
48
|
+
{ name: '/clear', desc: d('commands.clear') },
|
|
49
|
+
{ name: '/context', desc: d('commands.context') },
|
|
50
|
+
{ name: '/skills', desc: d('commands.skills') },
|
|
51
|
+
{ name: '/compact', desc: d('commands.compact') },
|
|
52
|
+
{ name: '/resume', desc: d('commands.sessionResume') },
|
|
53
|
+
{ name: '/sessions', desc: d('commands.sessionBrowse') },
|
|
54
|
+
{ name: '/rollback', desc: d('commands.sessionRollback') },
|
|
55
|
+
{
|
|
56
|
+
name: '/memory', desc: d('commands.memory'), children: [
|
|
57
|
+
{ name: 'overview', value: '/memory', desc: d('commands.memoryOverview') },
|
|
58
|
+
{ name: 'toggle', value: '/memory_switch', desc: d('commands.memoryToggle') },
|
|
59
|
+
{ name: 'on', value: '/memory_switch on', desc: d('commands.memoryOn') },
|
|
60
|
+
{ name: 'off', value: '/memory_switch off', desc: d('commands.memoryOff') },
|
|
61
|
+
{ name: 'status', value: '/memory_status', desc: d('commands.memoryStatus') },
|
|
62
|
+
{ name: 'reflect', value: '/reflect', desc: d('commands.memoryReflect') },
|
|
63
|
+
{ name: 'init', value: '/init', desc: d('commands.memoryInit') },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: '/project_skill', desc: d('commands.skill'), children: [
|
|
68
|
+
{ name: 'toggle', value: '/project_skill', desc: d('commands.toggle') },
|
|
69
|
+
{ name: 'on', value: '/project_skill on', desc: d('commands.skillOn') },
|
|
70
|
+
{ name: 'off', value: '/project_skill off', desc: d('commands.skillOff') },
|
|
71
|
+
{ name: 'view', value: '/project_skill view', desc: d('commands.skillView') },
|
|
72
|
+
{ name: 'init', value: '/project_skill init', desc: d('commands.skillInit') },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: '/snapshot', desc: d('commands.snapshot'), children: [
|
|
77
|
+
{ name: 'toggle', value: '/snapshot', desc: d('commands.snapshotToggle') },
|
|
78
|
+
{ name: 'on', value: '/snapshot on', desc: d('commands.snapshotOn') },
|
|
79
|
+
{ name: 'off', value: '/snapshot off', desc: d('commands.snapshotOff') },
|
|
80
|
+
{ name: 'status', value: '/snapshot status', desc: d('commands.snapshotStatus') },
|
|
81
|
+
{ name: 'refresh', value: '/snapshot_refresh', desc: d('commands.snapshotRefresh') },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{ name: '/theme', desc: d('commands.theme') },
|
|
85
|
+
{
|
|
86
|
+
name: '/model', desc: d('commands.model'), children: [
|
|
87
|
+
{ name: 'configure', value: '/model', desc: d('commands.modelConfigure') },
|
|
88
|
+
{ name: 'switch', value: '/model switch', desc: d('commands.modelSwitch') },
|
|
89
|
+
{ name: 'list', value: '/model list', desc: d('commands.modelList') },
|
|
90
|
+
{ name: 'show', value: '/model show', desc: d('commands.modelShow') },
|
|
91
|
+
{ name: 'use <name>', value: '/model use ', submit: false, desc: d('commands.modelUse') },
|
|
92
|
+
{ name: 'delete <name>', value: '/model delete ', submit: false, desc: d('commands.modelDelete') },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: '/mode', desc: d('commands.mode'), children: [
|
|
97
|
+
{ name: 'plan', value: '/plan', desc: d('commands.modePlan') },
|
|
98
|
+
{ name: 'auto', value: '/auto', desc: d('commands.modeAuto') },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: '/pet', desc: d('commands.pet'), children: [
|
|
103
|
+
{ name: 'toggle', value: '/pet', desc: d('commands.petToggle') },
|
|
104
|
+
{ name: 'skin', value: '/pet skin', desc: d('commands.petSkin') },
|
|
105
|
+
{ name: 'quit', value: '/pet quit', desc: d('commands.petQuit') },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: '/image', desc: d('commands.image'), children: [
|
|
110
|
+
{ name: 'attach <path>', value: '/image ', submit: false, desc: d('commands.imageAttach') },
|
|
111
|
+
{ name: 'list', value: '/image list', desc: d('commands.imageList') },
|
|
112
|
+
{ name: 'clear', value: '/image clear', desc: d('commands.imageClear') },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: '/language', desc: d('commands.language'), children: [
|
|
117
|
+
{ name: 'zh-CN', value: '/language zh-CN', desc: d('commands.languageZh') },
|
|
118
|
+
{ name: 'en', value: '/language en', desc: d('commands.languageEn') },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
/** 从菜单树递归生成 /help 内容;叶子 value 与菜单路径不同则同时展示真实命令。 */
|
|
124
|
+
function slashHelpLines(nodes = buildSlashCommands(), parentPath = '', depth = 0) {
|
|
125
|
+
const lines = [];
|
|
126
|
+
for (const node of nodes) {
|
|
127
|
+
const menuPath = parentPath ? `${parentPath} ${node.name}` : node.name;
|
|
128
|
+
const isBranch = Boolean(node.children?.length);
|
|
129
|
+
const actual = node.value?.trimEnd();
|
|
130
|
+
const mapping = actual && actual !== menuPath ? ` ${ui.dim}→ ${actual}${ui.reset}` : '';
|
|
131
|
+
const marker = isBranch ? ` ${ui.dim}›${ui.reset}` : '';
|
|
132
|
+
lines.push(`${' '.repeat(depth)}${ui.accent}${menuPath}${ui.reset}${marker}${mapping} ${ui.dim}${node.desc}${ui.reset}`);
|
|
133
|
+
if (node.children?.length)
|
|
134
|
+
lines.push(...slashHelpLines(node.children, menuPath, depth + 1));
|
|
135
|
+
}
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
/** 主题名 → 本地化描述。 */
|
|
139
|
+
function themeDescription(name) {
|
|
140
|
+
const key = `theme.${name}`;
|
|
141
|
+
return name in {
|
|
142
|
+
default: 1, light: 1, solarized: 1, gruvbox: 1, nord: 1, orange: 1,
|
|
143
|
+
rose: 1, emerald: 1, amber: 1, lavender: 1, sunset: 1,
|
|
144
|
+
} ? t(key) : '';
|
|
145
|
+
}
|
|
81
146
|
/** /model 预设后端:选一个预填 baseURL,仍可逐项改。base_url 取自 README 常见表。 */
|
|
82
147
|
const MODEL_PRESETS = [
|
|
83
148
|
{ label: 'GLM(智谱)', baseURL: 'https://open.bigmodel.cn/api/v3', model: 'glm-4.6', window: 128000 },
|
|
@@ -146,7 +211,7 @@ function renderContextBar(history) {
|
|
|
146
211
|
const W = 10;
|
|
147
212
|
const filled = Math.round(pct * W);
|
|
148
213
|
const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
149
|
-
const src = contextState.lastUsage ? '
|
|
214
|
+
const src = contextState.lastUsage ? t('status.measured') : t('status.estimated');
|
|
150
215
|
const k = (n) => `${Math.round(n / 1000)}k`;
|
|
151
216
|
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.accent;
|
|
152
217
|
const lifecycle = contextState.lifecycleStats;
|
|
@@ -155,7 +220,7 @@ function renderContextBar(history) {
|
|
|
155
220
|
? `\n lifecycle · live ${lifecycle.live} · referenced ${lifecycle.referenced} · digested ${lifecycle.digested} · stubbed ${lifecycle.stubbed}`
|
|
156
221
|
: '\n lifecycle · no active snapshot (run a tool-enabled turn first)';
|
|
157
222
|
const archiveLine = `\n archived tool results · ${archived.stubbed}`;
|
|
158
|
-
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${Math.round(pct * 100)}% ${k(est)}/${k(win)} tokens · ${history.length}
|
|
223
|
+
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${Math.round(pct * 100)}% ${k(est)}/${k(win)} tokens · ${t('status.messages', { count: history.length })} (${src})${ui.reset}${lifecycleLine}${archiveLine}`;
|
|
159
224
|
}
|
|
160
225
|
/** 状态行用量条(精简版,进底栏):[bar] pct% k/k。
|
|
161
226
|
* 只计算对话内容(不含 system prompt),让用户感知"我发了多少、agent 回复了多少"占用 context。 */
|
|
@@ -209,39 +274,38 @@ function refreshStatusBase(history, lastTurnUsage) {
|
|
|
209
274
|
function runningStateFor(cmd) {
|
|
210
275
|
switch (cmd) {
|
|
211
276
|
case '/compact':
|
|
212
|
-
return { status: '
|
|
277
|
+
return { status: t('running.compact'), placeholder: t('running.compacting') };
|
|
213
278
|
case '/resume':
|
|
214
|
-
return { status: '
|
|
279
|
+
return { status: t('running.resume'), placeholder: t('running.chooseSession') };
|
|
215
280
|
case '/rollback':
|
|
216
|
-
return { status: '
|
|
281
|
+
return { status: t('running.rollback'), placeholder: t('running.chooseTurn') };
|
|
217
282
|
case '/init':
|
|
218
|
-
return { status: '
|
|
283
|
+
return { status: t('running.init'), placeholder: t('running.generateMemory') };
|
|
219
284
|
case '/snapshot_refresh':
|
|
220
|
-
return { status: '
|
|
285
|
+
return { status: t('running.snapshot'), placeholder: t('running.scanning') };
|
|
221
286
|
case '/plan':
|
|
222
|
-
return { status: '
|
|
287
|
+
return { status: t('running.plan'), placeholder: '…' };
|
|
223
288
|
case '/auto':
|
|
224
|
-
return { status: '
|
|
289
|
+
return { status: t('running.auto'), placeholder: '…' };
|
|
225
290
|
case '/clear':
|
|
226
|
-
return { status: '
|
|
291
|
+
return { status: t('running.clear'), placeholder: '…' };
|
|
227
292
|
case '/theme':
|
|
228
|
-
return { status: '
|
|
293
|
+
return { status: t('running.theme'), placeholder: t('running.chooseTheme') };
|
|
229
294
|
case '/model':
|
|
230
|
-
return { status: '
|
|
295
|
+
return { status: t('running.model'), placeholder: t('running.configuring') };
|
|
231
296
|
case '/pet':
|
|
232
|
-
return { status: '
|
|
297
|
+
return { status: t('running.pet'), placeholder: t('running.processing') };
|
|
233
298
|
case '/memory_switch':
|
|
234
|
-
return { status: '
|
|
299
|
+
return { status: t('running.memory'), placeholder: t('running.switching') };
|
|
235
300
|
case '/memory_status':
|
|
236
|
-
return { status: '
|
|
301
|
+
return { status: t('running.memoryStatus'), placeholder: '…' };
|
|
237
302
|
case '/project_skill':
|
|
238
|
-
return { status: '
|
|
303
|
+
return { status: t('running.skill'), placeholder: t('running.processing') };
|
|
239
304
|
case '/snapshot':
|
|
240
|
-
return { status: '
|
|
305
|
+
return { status: t('running.snapshot'), placeholder: t('running.switching') };
|
|
306
|
+
case '/language':
|
|
307
|
+
return { status: t('running.language'), placeholder: t('running.chooseLanguage') };
|
|
241
308
|
default:
|
|
242
|
-
// 输入框留空(运行中可 typeahead 打字,dim 回显);运行状态由内联 spinner 承载(思考中/执行…),
|
|
243
|
-
// 状态行只显走时——故常态 status 留空,不塞「处理」这种与内联重复的泛标签。
|
|
244
|
-
// 不把「思考中… Ctrl+C 中断」塞进输入框占位——避免看起来像已有输入、妨碍正常输入。
|
|
245
309
|
return { status: '', placeholder: '' };
|
|
246
310
|
}
|
|
247
311
|
}
|
|
@@ -365,7 +429,7 @@ function stopRunningListener() {
|
|
|
365
429
|
* 满宽 pad 使终端背景色覆盖整行(含行尾空单元格),上滑滚动时用户消息呈连续色块、与 assistant 正文区分。
|
|
366
430
|
* 末尾多留一空行(\n\n 收尾):用户消息与后续(agent 流式输出 / 下条消息)之间空一行。
|
|
367
431
|
*/
|
|
368
|
-
function formatUserMessage(lines) {
|
|
432
|
+
function formatUserMessage(lines, trailingBlank = true) {
|
|
369
433
|
const cols = layout.getGeo().cols;
|
|
370
434
|
const promptW = displayWidth(PROMPT);
|
|
371
435
|
const indent = ' '.repeat(promptW);
|
|
@@ -377,13 +441,13 @@ function formatUserMessage(lines) {
|
|
|
377
441
|
const padded = padEndDisplay(full, cols);
|
|
378
442
|
return `${userBg}${padded}${reset}`;
|
|
379
443
|
})
|
|
380
|
-
.join('\n') + '\n\n');
|
|
444
|
+
.join('\n') + (trailingBlank ? '\n\n' : '\n'));
|
|
381
445
|
}
|
|
382
446
|
/** 把多行提交输入回显进内容区(❯ 首行,续行按 prompt 宽度缩进)。仅 TUI 态回显(非 TTY 由 readline 自带回显)。 */
|
|
383
|
-
function echoInput(lines) {
|
|
447
|
+
function echoInput(lines, trailingBlank = true) {
|
|
384
448
|
if (!layout.isActive())
|
|
385
449
|
return;
|
|
386
|
-
layout.contentWrite(formatUserMessage(lines));
|
|
450
|
+
layout.contentWrite(formatUserMessage(lines, trailingBlank));
|
|
387
451
|
// 多模态附件:每张一行 chip 跟在 user bubble 后(原 /rollback /resume 复显一致)
|
|
388
452
|
for (const a of pendingAttachments) {
|
|
389
453
|
layout.contentWrite(` ${ui.dim}${renderChip(a)}${ui.reset}\n`);
|
|
@@ -406,7 +470,7 @@ function echoInput(lines) {
|
|
|
406
470
|
*/
|
|
407
471
|
function awaitPendingRecall(input, attachmentsCount, placeholder) {
|
|
408
472
|
pendingRecall = { lines: input, attachmentsCount, placeholder };
|
|
409
|
-
layout.setStatus('
|
|
473
|
+
layout.setStatus(t('agent.sending'), '●');
|
|
410
474
|
// 非 TTY:setRawMode 抛错 → window=0 直返 true(向后退化,不走 raw + 不挂监听)。
|
|
411
475
|
let ttyReady = false;
|
|
412
476
|
try {
|
|
@@ -591,6 +655,10 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
591
655
|
// 沙箱根:文件操作边界。优先级 --sandbox-root > SANDBOX_ROOT env > process.cwd()。
|
|
592
656
|
// 纯边界记录(不 chdir),jail.ts 内部 resolve。子 agent 同进程继承全局 root。
|
|
593
657
|
setSandboxRoot(sandboxRootOverride ?? config.sandboxRoot ?? process.cwd());
|
|
658
|
+
// MCP 在工具表和 LLM schema 创建前连接;失败的单个 server 只给提示,不阻断 REPL。
|
|
659
|
+
const mcpReport = await initializeAllMcp();
|
|
660
|
+
registerToolsExtension('mcp', getMcpTools());
|
|
661
|
+
refreshChatTools();
|
|
594
662
|
// 项目快照:sandboxRoot 设定后异步构建(完全由 LLM 生成)。
|
|
595
663
|
// 构建失败不阻断 REPL:snapshot 内部 catch 所有异常。
|
|
596
664
|
if (config.projectSnapshotEnabled) {
|
|
@@ -653,6 +721,12 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
653
721
|
else {
|
|
654
722
|
layout.writeBanner(bannerLines(banner()));
|
|
655
723
|
}
|
|
724
|
+
if (mcpReport.connected.length > 0) {
|
|
725
|
+
layout.contentWrite(`${ui.dim} ↳ 已连接 MCP: ${mcpReport.connected.join(', ')} (${getMcpTools().length} 个工具;外部工具每次均需授权)${ui.reset}\n`);
|
|
726
|
+
}
|
|
727
|
+
for (const warning of mcpReport.warnings) {
|
|
728
|
+
layout.contentWrite(`${ui.yellow} ⚠ ${warning}${ui.reset}\n`);
|
|
729
|
+
}
|
|
656
730
|
if (!isModelConfigured()) {
|
|
657
731
|
// 未配置 baseURL/apiKey:醒目提示引导 /model(不退出,REPL 仍可用;发消息会失败但不崩)。
|
|
658
732
|
layout.contentWrite(`${ui.yellow} ⚠ 未配置大模型。输入 ${ui.cyan}/model${ui.yellow} 配置 baseURL / apiKey / model(即时生效),或退出后运行 ${ui.cyan}mocode config${ui.yellow} 走向导。${ui.reset}\n`);
|
|
@@ -854,11 +928,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
854
928
|
/不支持(视觉|图片|图像|多模态)/.test(msg) ||
|
|
855
929
|
(/图片|图像|视觉|多模态/.test(msg) && /不支持|invalid|reject|fail/i.test(lower));
|
|
856
930
|
if (looksLikeImageError) {
|
|
857
|
-
layout.contentWrite(`${ui.red}
|
|
858
|
-
layout.contentWrite(`${ui.dim}
|
|
931
|
+
layout.contentWrite(`${ui.red}${t('repl.errorLabel')}${ui.reset} ${t('repl.visionUnsupported', { model: `${ui.accent}${config.model}${ui.reset}` })}${ui.dim}${t('repl.originalError', { message: msg })}${ui.reset}\n`);
|
|
932
|
+
layout.contentWrite(`${ui.dim}${t('repl.visionHint')}${ui.reset}\n`);
|
|
859
933
|
}
|
|
860
934
|
else {
|
|
861
|
-
layout.contentWrite(`${ui.red}
|
|
935
|
+
layout.contentWrite(`${ui.red}${t('repl.errorLabel')}${ui.reset} ${msg}\n`);
|
|
862
936
|
}
|
|
863
937
|
}
|
|
864
938
|
finally {
|
|
@@ -873,7 +947,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
873
947
|
return; // Esc / Ctrl+D 取消
|
|
874
948
|
const loaded = loadSession(pick.id);
|
|
875
949
|
if (!loaded || !loaded.history.length) {
|
|
876
|
-
layout.contentWrite(`${ui.yellow}(
|
|
950
|
+
layout.contentWrite(`${ui.yellow}${t('repl.loadFailed')}${ui.reset}\n`);
|
|
877
951
|
return;
|
|
878
952
|
}
|
|
879
953
|
if (loaded.history[0]?.role === 'system') {
|
|
@@ -893,7 +967,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
893
967
|
layout.clearContent();
|
|
894
968
|
renderHistory(history);
|
|
895
969
|
// 末尾 \n\n:与后续用户消息(❯ bubble)之间空一行。
|
|
896
|
-
layout.contentWrite(`${ui.dim}(
|
|
970
|
+
layout.contentWrite(`${ui.dim}${t('repl.resumed', { id: loaded.id })}${ui.reset}\n\n`);
|
|
897
971
|
}
|
|
898
972
|
while (true) {
|
|
899
973
|
// INPUT 态:画底栏输入框 + 状态行,光标入输入框
|
|
@@ -905,12 +979,12 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
905
979
|
layout.contentWrite(` ${ui.gray}↳ ${formatReflectResult(reflectRes)}${ui.reset}\n`);
|
|
906
980
|
clearLastReflectResult();
|
|
907
981
|
}
|
|
908
|
-
layout.enterInputMode('
|
|
982
|
+
layout.enterInputMode(t('repl.idle'));
|
|
909
983
|
let input = null;
|
|
910
984
|
try {
|
|
911
985
|
input = await promptWithSlashMenu({
|
|
912
986
|
prompt: PROMPT,
|
|
913
|
-
commands:
|
|
987
|
+
commands: buildSlashCommands(),
|
|
914
988
|
onCycleMode: cycleMode,
|
|
915
989
|
// /rollback 预填优先;否则上一轮运行中 typeahead 打的字 → 预填进输入框,用户可改可发
|
|
916
990
|
...(pendingPrefill
|
|
@@ -934,34 +1008,66 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
934
1008
|
if (line === '/exit' || line === '/quit')
|
|
935
1009
|
break;
|
|
936
1010
|
// RUNNING 态:回显输入 → 底栏改 dim 占位、光标回内容续写位
|
|
937
|
-
echoInput(input);
|
|
938
1011
|
const cmd = line.split(/\s+/)[0];
|
|
1012
|
+
// 语言命令把视觉分隔放在确认文案之后;避免回显后先空一行、下一条命令却紧贴确认。
|
|
1013
|
+
echoInput(input, cmd !== '/language');
|
|
939
1014
|
const { status, placeholder } = runningStateFor(cmd);
|
|
940
1015
|
refreshStatusBase(history);
|
|
941
1016
|
layout.enterRunningMode(status, placeholder);
|
|
1017
|
+
if (line === '/help') {
|
|
1018
|
+
layout.contentWrite(`${ui.bold}${t('help.title')}${ui.reset}\n`);
|
|
1019
|
+
layout.contentWrite(`${ui.dim}${t('help.hint')}${ui.reset}\n`);
|
|
1020
|
+
layout.contentWrite(`${slashHelpLines().join('\n')}\n`);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (line === '/language' || line.startsWith('/language ')) {
|
|
1024
|
+
const arg = line === '/language' ? '' : line.slice('/language '.length).trim();
|
|
1025
|
+
if (!arg) {
|
|
1026
|
+
const currentName = getLanguage() === 'zh-CN' ? t('language.zh') : t('language.en');
|
|
1027
|
+
layout.contentWrite(`${ui.dim}${t('language.current', { language: currentName })}${ui.reset}\n`);
|
|
1028
|
+
layout.contentWrite(`${ui.dim}${t('language.usage')}${ui.reset}\n\n`);
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const next = normalizeLanguage(arg);
|
|
1032
|
+
if (!next) {
|
|
1033
|
+
layout.contentWrite(`${ui.yellow}${t('language.invalid', { value: arg })}${ui.reset}\n\n`);
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
updateLanguageConfig(next);
|
|
1037
|
+
updateConfigKey('MOCODE_LANGUAGE', next);
|
|
1038
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1039
|
+
refreshStatusBase(history);
|
|
1040
|
+
// 横幅是内容缓冲顶部的固定区域;语言切换后等长原地替换,不移动后续对话。
|
|
1041
|
+
layout.rewriteBanner(bannerLines(banner()));
|
|
1042
|
+
layout.contentWrite(`${ui.cyan}${t('language.changed')}${ui.reset}\n`);
|
|
1043
|
+
if (languageFromShell) {
|
|
1044
|
+
layout.contentWrite(`${ui.dim}${t('language.shellOverride')}${ui.reset}\n`);
|
|
1045
|
+
}
|
|
1046
|
+
layout.contentWrite('\n');
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
942
1049
|
if (line === '/init') {
|
|
943
1050
|
// /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 MOCODE.md),fall through 走 runAgent
|
|
944
1051
|
joined = INIT_PROMPT;
|
|
945
1052
|
}
|
|
946
1053
|
if (line === '/plan') {
|
|
947
|
-
// /plan:切到 plan 模式(只读探查 + 产出计划)。
|
|
948
|
-
// 中文 IME(微软拼音用 Shift 切中英)与部分终端会吞掉 Shift+Tab 的 \x1b[Z,命令不受影响。
|
|
1054
|
+
// /plan:切到 plan 模式(只读探查 + 产出计划)。
|
|
949
1055
|
if (getAgentMode() === 'plan') {
|
|
950
|
-
layout.contentWrite(`${ui.dim}(
|
|
1056
|
+
layout.contentWrite(`${ui.dim}${t('repl.planAlready')}${ui.reset}\n`);
|
|
951
1057
|
}
|
|
952
1058
|
else {
|
|
953
|
-
setAgentMode('plan');
|
|
954
|
-
layout.contentWrite(`${ui.dim}(
|
|
1059
|
+
setAgentMode('plan');
|
|
1060
|
+
layout.contentWrite(`${ui.dim}${t('repl.planChanged')}${ui.reset}\n`);
|
|
955
1061
|
}
|
|
956
1062
|
continue;
|
|
957
1063
|
}
|
|
958
1064
|
if (line === '/auto') {
|
|
959
1065
|
if (getAgentMode() === 'auto') {
|
|
960
|
-
layout.contentWrite(`${ui.dim}(
|
|
1066
|
+
layout.contentWrite(`${ui.dim}${t('repl.autoAlready')}${ui.reset}\n`);
|
|
961
1067
|
}
|
|
962
1068
|
else {
|
|
963
|
-
setAgentMode('auto');
|
|
964
|
-
layout.contentWrite(`${ui.dim}(
|
|
1069
|
+
setAgentMode('auto');
|
|
1070
|
+
layout.contentWrite(`${ui.dim}${t('repl.autoChanged')}${ui.reset}\n`);
|
|
965
1071
|
}
|
|
966
1072
|
continue;
|
|
967
1073
|
}
|
|
@@ -1028,7 +1134,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1028
1134
|
pendingAttachments = []; // 一并清空待发图片
|
|
1029
1135
|
layout.clearContent();
|
|
1030
1136
|
layout.writeBanner(bannerLines(banner()));
|
|
1031
|
-
layout.contentWrite(`${ui.dim}(
|
|
1137
|
+
layout.contentWrite(`${ui.dim}${t('repl.historyCleared')}${ui.reset}\n`);
|
|
1032
1138
|
continue;
|
|
1033
1139
|
}
|
|
1034
1140
|
// /image:附加本地图片到下一条 user 消息(支持 /image <path> · /image list · /image clear)。
|
|
@@ -1290,7 +1396,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1290
1396
|
const items = listThemes().map((t) => ({
|
|
1291
1397
|
id: t,
|
|
1292
1398
|
title: t,
|
|
1293
|
-
subtitle:
|
|
1399
|
+
subtitle: themeDescription(t),
|
|
1294
1400
|
}));
|
|
1295
1401
|
let pick;
|
|
1296
1402
|
try {
|
|
@@ -1302,11 +1408,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1302
1408
|
name = pick?.id ?? null;
|
|
1303
1409
|
}
|
|
1304
1410
|
else if (arg === 'list' || !themeExists(arg)) {
|
|
1305
|
-
layout.contentWrite(`${ui.dim}
|
|
1306
|
-
for (const
|
|
1307
|
-
layout.contentWrite(` ${ui.accent}${
|
|
1411
|
+
layout.contentWrite(`${ui.dim}${t('repl.themeList')}${ui.reset}\n`);
|
|
1412
|
+
for (const theme of listThemes()) {
|
|
1413
|
+
layout.contentWrite(` ${ui.accent}${theme}${ui.reset} ${ui.dim}${themeDescription(theme)}${ui.reset}\n`);
|
|
1308
1414
|
}
|
|
1309
|
-
layout.contentWrite(`${ui.dim}(
|
|
1415
|
+
layout.contentWrite(`${ui.dim}${t('repl.themeCurrent', { theme: getTheme() })}${ui.reset}\n`);
|
|
1310
1416
|
continue;
|
|
1311
1417
|
}
|
|
1312
1418
|
else {
|
|
@@ -1326,7 +1432,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1326
1432
|
else {
|
|
1327
1433
|
layout.writeBanner(bannerLines(banner()));
|
|
1328
1434
|
}
|
|
1329
|
-
layout.contentWrite(`${ui.dim}(
|
|
1435
|
+
layout.contentWrite(`${ui.dim}${t('repl.themeChanged', { theme: name })}${ui.reset}\n`);
|
|
1330
1436
|
updateConfigKey('MOCODE_THEME', name);
|
|
1331
1437
|
if (config.themeFromShell) {
|
|
1332
1438
|
layout.contentWrite(`${ui.dim}(shell 环境变量 MOCODE_THEME 已设,文件写入下次启动被其覆盖;取消该 shell 设置后生效)${ui.reset}\n`);
|
|
@@ -1988,7 +2094,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1988
2094
|
// (runTurn 入口的 pendingAttachments.flush 仍按现有逻辑把附件塞进 userInput)。
|
|
1989
2095
|
layout.rewindContent(bubbleRows);
|
|
1990
2096
|
pendingPrefill = input;
|
|
1991
|
-
layout.enterInputMode('
|
|
2097
|
+
layout.enterInputMode(t('repl.idle'));
|
|
1992
2098
|
continue;
|
|
1993
2099
|
}
|
|
1994
2100
|
const initialPlan = getAgentMode() === 'plan'; // 轮首模式(在 runTurn 之前读)
|
|
@@ -2000,21 +2106,23 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
2000
2106
|
// 桌宠:计划审批面板弹出期间广播 waiting_human(红灯闪烁);面板不在 runAgent/hooks 体系内,
|
|
2001
2107
|
// 需在此单独广播——用户响应后由下一次 /pet 状态事件(或 idle 兜底)覆盖。
|
|
2002
2108
|
sendState('waiting_human');
|
|
2109
|
+
const executePlanOption = t('plan.execute');
|
|
2003
2110
|
const res = await promptIntervention({
|
|
2004
2111
|
type: 'choice',
|
|
2005
|
-
title: '
|
|
2006
|
-
detail: '
|
|
2007
|
-
options: [
|
|
2112
|
+
title: t('plan.ready'),
|
|
2113
|
+
detail: t('plan.approvalDetail'),
|
|
2114
|
+
options: [executePlanOption, t('plan.refine')],
|
|
2008
2115
|
});
|
|
2009
2116
|
sendState('idle');
|
|
2010
|
-
if (res.action === 'selected' && res.value ===
|
|
2117
|
+
if (res.action === 'selected' && res.value === executePlanOption) {
|
|
2011
2118
|
// setAgentMode('auto') 由 runTurn 入口做(listener 重写 history[0] 回 auto);这里只切运行态 + 合成执行轮。
|
|
2012
|
-
layout.enterRunningMode('
|
|
2013
|
-
await runTurn('
|
|
2119
|
+
layout.enterRunningMode(t('plan.running'), t('plan.executing'));
|
|
2120
|
+
await runTurn(t('plan.executePrompt'), false, t('plan.executing'));
|
|
2014
2121
|
}
|
|
2015
2122
|
}
|
|
2016
2123
|
}
|
|
2017
2124
|
// 退出前等在飞反思收尾(Ctrl+C 走 SIGINT 直退不等;fire-and-forget 不承诺中断时完成)。
|
|
2018
2125
|
await drainMemoryBackground();
|
|
2126
|
+
await closeAllMcp();
|
|
2019
2127
|
layout.exitAltScreen();
|
|
2020
2128
|
}
|