mocode-ai 0.7.2 → 1.0.1
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 +58 -4
- package/README.zh-CN.md +58 -4
- package/dist/agent/core.js +119 -39
- package/dist/agent/index.js +57 -6
- package/dist/agent/spawn.js +74 -30
- package/dist/agents/coordinator.js +60 -0
- package/dist/changeset/index.js +289 -0
- package/dist/changeset/types.js +1 -0
- package/dist/config/index.js +25 -13
- package/dist/context/artifacts.js +254 -0
- package/dist/context/classifier.js +1 -1
- package/dist/context/index.js +1 -0
- package/dist/i18n/index.js +16 -4
- package/dist/llm/index.js +2 -2
- package/dist/permissions/index.js +7 -1
- package/dist/repl/index.js +31 -5
- package/dist/rollback/index.js +43 -8
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +2 -2
- package/dist/sandbox/root.js +9 -3
- package/dist/session/compact.js +9 -3
- package/dist/session/scheduler.js +4 -0
- package/dist/session/state.js +3 -19
- package/dist/tools/builtins/apply-patch.js +174 -0
- package/dist/tools/builtins/edit-file.js +60 -37
- package/dist/tools/builtins/index.js +10 -8
- package/dist/tools/builtins/read-file.js +4 -2
- package/dist/tools/builtins/task.js +47 -16
- package/dist/tools/builtins/web-fetch.js +21 -5
- package/dist/tools/builtins/web-search.js +28 -4
- package/dist/tools/builtins/write-file.js +53 -18
- package/dist/tools/constants.js +3 -3
- package/dist/tools/registry.js +108 -49
- package/dist/tools/retry.js +105 -0
- package/dist/tools/validation.js +80 -0
- package/dist/ui/batch.js +56 -24
- package/dist/ui/layout.js +8 -3
- package/package.json +2 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const TOOL_RETRY_MAX_ATTEMPTS = 3;
|
|
2
|
+
export const TOOL_RETRY_BASE_MS = 250;
|
|
3
|
+
export const TOOL_RETRY_MAX_MS = 1_000;
|
|
4
|
+
export const TOOL_RETRY_TOTAL_BUDGET_MS = 15_000;
|
|
5
|
+
export const TOOL_RETRY_SAME_ARGS_WINDOW_MS = 60_000;
|
|
6
|
+
export const TOOL_RETRY_SAME_ARGS_BUDGET = 2;
|
|
7
|
+
const NEVER_RETRY_CODES = new Set([
|
|
8
|
+
'INVALID_JSON',
|
|
9
|
+
'INVALID_ARGUMENTS',
|
|
10
|
+
'INVALID_TOOL_SCHEMA',
|
|
11
|
+
'UNKNOWN_TOOL',
|
|
12
|
+
'SANDBOX_DENIED',
|
|
13
|
+
'PERMISSION_DENIED',
|
|
14
|
+
'TOOL_DISABLED',
|
|
15
|
+
'MODE_DENIED',
|
|
16
|
+
'ABORTED',
|
|
17
|
+
'EDIT_CONFLICT',
|
|
18
|
+
'POSTCONDITION_FAILED',
|
|
19
|
+
'PROCESS_FAILED',
|
|
20
|
+
'MCP_ERROR',
|
|
21
|
+
]);
|
|
22
|
+
const fingerprintBudgets = new Map();
|
|
23
|
+
const MAX_TRACKED_FINGERPRINTS = 512;
|
|
24
|
+
function reserveFingerprintRetry(fingerprint, now) {
|
|
25
|
+
let budget = fingerprintBudgets.get(fingerprint);
|
|
26
|
+
if (!budget || now - budget.startedAt >= TOOL_RETRY_SAME_ARGS_WINDOW_MS) {
|
|
27
|
+
budget = { startedAt: now, retries: 0 };
|
|
28
|
+
fingerprintBudgets.set(fingerprint, budget);
|
|
29
|
+
}
|
|
30
|
+
if (budget.retries >= TOOL_RETRY_SAME_ARGS_BUDGET)
|
|
31
|
+
return false;
|
|
32
|
+
budget.retries++;
|
|
33
|
+
if (fingerprintBudgets.size > MAX_TRACKED_FINGERPRINTS) {
|
|
34
|
+
const oldest = fingerprintBudgets.keys().next().value;
|
|
35
|
+
if (oldest)
|
|
36
|
+
fingerprintBudgets.delete(oldest);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function shouldRetry(outcome, capabilities) {
|
|
41
|
+
return capabilities.retry !== 'never' &&
|
|
42
|
+
outcome.status === 'error' &&
|
|
43
|
+
outcome.retryable === true &&
|
|
44
|
+
!NEVER_RETRY_CODES.has(outcome.code);
|
|
45
|
+
}
|
|
46
|
+
function backoff(attempt) {
|
|
47
|
+
return Math.min(TOOL_RETRY_MAX_MS, TOOL_RETRY_BASE_MS * 2 ** (attempt - 1));
|
|
48
|
+
}
|
|
49
|
+
function abortError() {
|
|
50
|
+
const error = new Error('Tool retry aborted');
|
|
51
|
+
error.name = 'AbortError';
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
function sleep(ms, signal) {
|
|
55
|
+
if (signal?.aborted)
|
|
56
|
+
return Promise.reject(abortError());
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
signal?.removeEventListener('abort', onAbort);
|
|
60
|
+
resolve();
|
|
61
|
+
}, ms);
|
|
62
|
+
const onAbort = () => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
signal?.removeEventListener('abort', onAbort);
|
|
65
|
+
reject(abortError());
|
|
66
|
+
};
|
|
67
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** Retry safe/idempotent transient outcomes; each execute call owns one complete lock attempt. */
|
|
71
|
+
export async function executeWithToolRetry(capabilities, fingerprint, signal, execute, onRetry) {
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
let retryDelayMs = 0;
|
|
74
|
+
for (let attempt = 1; attempt <= TOOL_RETRY_MAX_ATTEMPTS; attempt++) {
|
|
75
|
+
const outcome = await execute(attempt);
|
|
76
|
+
const elapsed = Date.now() - startedAt;
|
|
77
|
+
const waitMs = backoff(attempt);
|
|
78
|
+
const canRetry = attempt < TOOL_RETRY_MAX_ATTEMPTS &&
|
|
79
|
+
shouldRetry(outcome, capabilities) &&
|
|
80
|
+
elapsed + waitMs <= TOOL_RETRY_TOTAL_BUDGET_MS &&
|
|
81
|
+
!signal?.aborted &&
|
|
82
|
+
reserveFingerprintRetry(fingerprint, Date.now());
|
|
83
|
+
if (!canRetry) {
|
|
84
|
+
return {
|
|
85
|
+
...outcome,
|
|
86
|
+
durationMs: elapsed,
|
|
87
|
+
attempts: attempt,
|
|
88
|
+
retryDelayMs,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
onRetry?.({ attempt, nextAttempt: attempt + 1, waitMs, code: outcome.code });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Retry telemetry is best-effort and must never change tool execution.
|
|
96
|
+
}
|
|
97
|
+
await sleep(waitMs, signal);
|
|
98
|
+
retryDelayMs += waitMs;
|
|
99
|
+
}
|
|
100
|
+
throw new Error('unreachable tool retry state');
|
|
101
|
+
}
|
|
102
|
+
/** Test/session reset seam; production never needs to clear the bounded TTL map. */
|
|
103
|
+
export function resetToolRetryBudgets() {
|
|
104
|
+
fingerprintBudgets.clear();
|
|
105
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
3
|
+
const options = {
|
|
4
|
+
allErrors: true,
|
|
5
|
+
strict: false,
|
|
6
|
+
coerceTypes: false,
|
|
7
|
+
useDefaults: false,
|
|
8
|
+
removeAdditional: false,
|
|
9
|
+
validateFormats: false,
|
|
10
|
+
allowUnionTypes: true,
|
|
11
|
+
};
|
|
12
|
+
const draft7 = new Ajv(options);
|
|
13
|
+
const draft2020 = new Ajv2020(options);
|
|
14
|
+
const cache = new WeakMap();
|
|
15
|
+
function compile(schema) {
|
|
16
|
+
const cached = cache.get(schema);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
const preferred = typeof schema.$schema === 'string' && schema.$schema.includes('2020-12')
|
|
20
|
+
? [draft2020, draft7]
|
|
21
|
+
: [draft7, draft2020];
|
|
22
|
+
let lastError;
|
|
23
|
+
for (const ajv of preferred) {
|
|
24
|
+
try {
|
|
25
|
+
const result = { valid: true, validate: ajv.compile(schema) };
|
|
26
|
+
cache.set(schema, result);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
lastError = error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const result = {
|
|
34
|
+
valid: false,
|
|
35
|
+
message: lastError instanceof Error ? lastError.message : String(lastError),
|
|
36
|
+
};
|
|
37
|
+
cache.set(schema, result);
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function formatErrors(errors) {
|
|
41
|
+
if (!errors?.length)
|
|
42
|
+
return '参数不符合 JSON Schema';
|
|
43
|
+
return errors.slice(0, 5).map((error) => {
|
|
44
|
+
const location = error.instancePath || '/';
|
|
45
|
+
if (error.keyword === 'required') {
|
|
46
|
+
const property = String(error.params.missingProperty ?? '?');
|
|
47
|
+
return `${location} 缺少必填字段 ${JSON.stringify(property)}`;
|
|
48
|
+
}
|
|
49
|
+
if (error.keyword === 'additionalProperties') {
|
|
50
|
+
const property = String(error.params.additionalProperty ?? '?');
|
|
51
|
+
return `${location} 含未知字段 ${JSON.stringify(property)}`;
|
|
52
|
+
}
|
|
53
|
+
return `${location} ${error.message ?? error.keyword}`;
|
|
54
|
+
}).join('; ');
|
|
55
|
+
}
|
|
56
|
+
/** Validate without coercing, defaulting, removing, or otherwise mutating model arguments. */
|
|
57
|
+
export function validateToolArguments(tool, args) {
|
|
58
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
code: 'INVALID_ARGUMENTS',
|
|
62
|
+
message: '参数根节点必须是 JSON object',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const compiled = compile(tool.parameters);
|
|
66
|
+
if (!compiled.valid) {
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
code: 'INVALID_TOOL_SCHEMA',
|
|
70
|
+
message: `工具 schema 无法编译: ${compiled.message}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (compiled.validate(args))
|
|
74
|
+
return { valid: true };
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
code: 'INVALID_ARGUMENTS',
|
|
78
|
+
message: formatErrors(compiled.validate.errors),
|
|
79
|
+
};
|
|
80
|
+
}
|
package/dist/ui/batch.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* 状态全模块级(单实例)——一次 REPL 内所有 batch 共享,清内容区 / 退 alt 屏时统一重置。
|
|
13
13
|
*/
|
|
14
14
|
import { ui } from './theme.js';
|
|
15
|
+
import { t } from '../i18n/index.js';
|
|
15
16
|
const batches = new Map();
|
|
16
17
|
/** 绝对行索引 → 所属 batch id(仅记录 summary 行;用于鼠标点击反查)。
|
|
17
18
|
* buffer 行数变化时本表可能漂移——但只在 insertAfter/deleteFrom 后由本模块同步更新,
|
|
@@ -36,7 +37,7 @@ export function reset() {
|
|
|
36
37
|
/** 新建一个 batch(在 agent 拿到第一条 onToolHeader 时调)。返回 id。 */
|
|
37
38
|
export function beginBatch() {
|
|
38
39
|
const id = `b${++_idCounter}`;
|
|
39
|
-
batches.set(id, { id, summaryAbsIdx: -1, entries: [], expandedEntries: new Set() });
|
|
40
|
+
batches.set(id, { id, summaryAbsIdx: -1, entries: [], expandedEntries: new Set(), startedAt: Date.now() });
|
|
40
41
|
return id;
|
|
41
42
|
}
|
|
42
43
|
/** 记一条工具调用(在 onToolHeader 时调,与 setEntryResult 配对;entries 顺序 = agent 调用顺序)。 */
|
|
@@ -44,11 +45,13 @@ export function recordCall(id, name, callSummary) {
|
|
|
44
45
|
const b = batches.get(id);
|
|
45
46
|
if (!b)
|
|
46
47
|
return;
|
|
48
|
+
// 已完成的累计探索后又追加工具:恢复进行中,待新结果返回再完成。
|
|
49
|
+
b.finishedAt = undefined;
|
|
47
50
|
b.entries.push({ name, callSummary, resultSummary: '', diffBlock: null });
|
|
48
51
|
}
|
|
49
52
|
/** 记一条工具结果(diff 块或单行 preview);agent 在 onToolResult 时调,匹配最后一条未填的 entry。
|
|
50
53
|
* fullOutput:工具原始完整输出(纯文本),展开时显示;mutation 工具的 diff 块已自含无需传。 */
|
|
51
|
-
export function recordResult(id, name, resultSummary, diffBlock, fullOutput) {
|
|
54
|
+
export function recordResult(id, name, resultSummary, diffBlock, fullOutput, failed = false) {
|
|
52
55
|
const b = batches.get(id);
|
|
53
56
|
if (!b || b.entries.length === 0)
|
|
54
57
|
return;
|
|
@@ -58,6 +61,9 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput) {
|
|
|
58
61
|
b.entries[i].resultSummary = resultSummary;
|
|
59
62
|
b.entries[i].diffBlock = diffBlock;
|
|
60
63
|
b.entries[i].fullOutput = fullOutput;
|
|
64
|
+
b.entries[i].failed = failed;
|
|
65
|
+
if (b.entries.every((e) => e.resultSummary || e.diffBlock || e.failed))
|
|
66
|
+
b.finishedAt = Date.now();
|
|
61
67
|
return;
|
|
62
68
|
}
|
|
63
69
|
}
|
|
@@ -67,19 +73,17 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput) {
|
|
|
67
73
|
last.resultSummary = resultSummary;
|
|
68
74
|
last.diffBlock = diffBlock;
|
|
69
75
|
last.fullOutput = fullOutput;
|
|
76
|
+
last.failed = failed;
|
|
77
|
+
if (b.entries.every((e) => e.resultSummary || e.diffBlock || e.failed))
|
|
78
|
+
b.finishedAt = Date.now();
|
|
70
79
|
}
|
|
71
80
|
}
|
|
72
81
|
// ── 摘要行文本生成 ──
|
|
73
82
|
/** 把 entry 列表压缩成一行摘要。 */
|
|
74
|
-
function buildSummaryLine(
|
|
83
|
+
function buildSummaryLine(record, live = false) {
|
|
84
|
+
const entries = record.entries;
|
|
75
85
|
if (entries.length === 0) {
|
|
76
|
-
return ` ${ui.bold}${ui.accent}
|
|
77
|
-
}
|
|
78
|
-
if (entries.length === 1) {
|
|
79
|
-
const e = entries[0];
|
|
80
|
-
// 实时摘要必须稳定保持单行;完整参数放在第一层工具概要中,避免长 JSON 自动折行后
|
|
81
|
-
// 原地刷新只能覆盖最后一条物理行、残留旧摘要前半段。
|
|
82
|
-
return ` ${ui.bold}${ui.accent}●${ui.reset} ${ui.dim}Ran 1 tool · ${e.name} 1${ui.reset}`;
|
|
86
|
+
return ` ${ui.dim}│${ui.reset} ${ui.bold}${ui.accent}◇${ui.reset} ${ui.dim}No tools${ui.reset}`;
|
|
83
87
|
}
|
|
84
88
|
// N>1:同类合并 "read_file 3, glob 1, grep 1"
|
|
85
89
|
const counts = new Map();
|
|
@@ -88,7 +92,23 @@ function buildSummaryLine(entries) {
|
|
|
88
92
|
const parts = [];
|
|
89
93
|
for (const [n, c] of counts)
|
|
90
94
|
parts.push(`${n} ${c}`);
|
|
91
|
-
|
|
95
|
+
const completed = entries.filter((e) => e.resultSummary || e.diffBlock || e.failed).length;
|
|
96
|
+
const failed = entries.some((e) => e.failed);
|
|
97
|
+
// 工具本身完成就立即显示完成态,不等待整轮正文流完/onDone。
|
|
98
|
+
const finished = completed >= entries.length;
|
|
99
|
+
const symbol = failed ? '×' : finished ? '◆' : '◇';
|
|
100
|
+
const color = failed ? ui.red : finished ? ui.green : ui.accent;
|
|
101
|
+
const label = failed
|
|
102
|
+
? t('agent.toolsFailed')
|
|
103
|
+
: finished
|
|
104
|
+
? t('agent.toolsComplete')
|
|
105
|
+
: t('agent.toolsRunning');
|
|
106
|
+
const progress = live && !finished ? ` ${completed}/${entries.length}` : ` ${entries.length}`;
|
|
107
|
+
const elapsedMs = record.finishedAt ? record.finishedAt - record.startedAt : 0;
|
|
108
|
+
const elapsed = record.finishedAt
|
|
109
|
+
? ` ${elapsedMs < 100 ? '<0.1s' : `${(elapsedMs / 1000).toFixed(1)}s`}`
|
|
110
|
+
: '';
|
|
111
|
+
return ` ${ui.bold}${color}${symbol}${ui.reset} ${label}${progress}${elapsed} ${ui.dim}${parts.join(' ')}${ui.reset}`;
|
|
92
112
|
}
|
|
93
113
|
// ── 展开/折叠 ──
|
|
94
114
|
/** 把 batch 的详情行展开成自洽行数组(供 layout.contentInsertAfter 走 mid-buffer 插入)。
|
|
@@ -105,7 +125,8 @@ function buildEntryDetailLines(e, indent = ' ') {
|
|
|
105
125
|
continue; // 折叠连续空行
|
|
106
126
|
if (line === '' && lines.length > 0)
|
|
107
127
|
continue; // 跳过首尾空行(diff 头/尾换行)
|
|
108
|
-
|
|
128
|
+
const prefixed = `${ui.dim}${indent}${ui.reset}${line}`;
|
|
129
|
+
lines.push(prefixed.endsWith('\x1B[0m') ? prefixed : prefixed + '\x1B[0m');
|
|
109
130
|
}
|
|
110
131
|
}
|
|
111
132
|
else if (e.fullOutput) {
|
|
@@ -126,24 +147,30 @@ function buildEntryDetailLines(e, indent = ' ') {
|
|
|
126
147
|
return lines;
|
|
127
148
|
}
|
|
128
149
|
/** 第一层只展示有哪些调用及其简短结果,不展开完整输出。 */
|
|
129
|
-
function buildExpandedLines(entries
|
|
130
|
-
return entries.map((e) => {
|
|
150
|
+
function buildExpandedLines(entries) {
|
|
151
|
+
return entries.map((e, index) => {
|
|
131
152
|
const result = e.resultSummary ? ` ${ui.gray}↳ ${e.resultSummary}${ui.reset}` : '';
|
|
132
|
-
|
|
153
|
+
const branch = index === entries.length - 1 ? '└─' : '├─';
|
|
154
|
+
const symbol = e.failed ? `${ui.red}×${ui.reset}` : `${ui.green}•${ui.reset}`;
|
|
155
|
+
return ` ${ui.dim}${branch}${ui.reset} ${symbol} ${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}\x1B[0m`;
|
|
133
156
|
});
|
|
134
157
|
}
|
|
158
|
+
function entryDetailIndent(entries, index) {
|
|
159
|
+
return index < entries.length - 1 ? ' │ ' : ' ';
|
|
160
|
+
}
|
|
135
161
|
/** 在 batch 收尾时(onToolBatchEnd):写摘要行 + 登记 summaryAbsIdx;若已展开(回放场景)立即插详情。 */
|
|
136
162
|
export function endBatch(id, layout) {
|
|
137
163
|
const b = batches.get(id);
|
|
138
164
|
if (!b)
|
|
139
165
|
return;
|
|
166
|
+
b.finishedAt ??= Date.now();
|
|
140
167
|
if (b.summaryAbsIdx >= 0) {
|
|
141
|
-
layout.contentReplaceLine?.(b.summaryAbsIdx, buildSummaryLine(b
|
|
168
|
+
layout.contentReplaceLine?.(b.summaryAbsIdx, buildSummaryLine(b));
|
|
142
169
|
// 执行阶段只展示实时摘要;到 endBatch 才开放点击,避免未完成 batch 的第一层列表失步。
|
|
143
170
|
absLineToBatchId.set(b.summaryAbsIdx, b.id);
|
|
144
171
|
return;
|
|
145
172
|
}
|
|
146
|
-
const summary = buildSummaryLine(b
|
|
173
|
+
const summary = buildSummaryLine(b);
|
|
147
174
|
// 写摘要行(以 \n 收尾;contentWrite 会 breakRow 让其成为完整物理行)
|
|
148
175
|
layout.contentWrite(summary + '\n');
|
|
149
176
|
// 摘要行绝对索引 = totalRows - 2(hasCurrent 那行是新空行)
|
|
@@ -157,7 +184,7 @@ export function showLiveBatch(id, layout) {
|
|
|
157
184
|
const b = batches.get(id);
|
|
158
185
|
if (!b)
|
|
159
186
|
return;
|
|
160
|
-
const summary = buildSummaryLine(b
|
|
187
|
+
const summary = buildSummaryLine(b, true);
|
|
161
188
|
if (b.summaryAbsIdx < 0) {
|
|
162
189
|
layout.contentWrite(summary + '\n');
|
|
163
190
|
b.summaryAbsIdx = Math.max(0, layout.totalRows() - 2);
|
|
@@ -211,7 +238,7 @@ export function expandSingleEntryFully(id, layout) {
|
|
|
211
238
|
return;
|
|
212
239
|
const lines = [
|
|
213
240
|
...buildExpandedLines(b.entries),
|
|
214
|
-
...buildEntryDetailLines(b.entries[0]),
|
|
241
|
+
...buildEntryDetailLines(b.entries[0], entryDetailIndent(b.entries, 0)),
|
|
215
242
|
];
|
|
216
243
|
layout.contentInsertAfter(b.summaryAbsIdx, lines);
|
|
217
244
|
expandedBatches.add(id);
|
|
@@ -220,8 +247,9 @@ export function expandSingleEntryFully(id, layout) {
|
|
|
220
247
|
}
|
|
221
248
|
function collapse(b, layout) {
|
|
222
249
|
let lineCount = b.entries.length;
|
|
223
|
-
for (const i of b.expandedEntries)
|
|
224
|
-
lineCount += buildEntryDetailLines(b.entries[i]).length;
|
|
250
|
+
for (const i of b.expandedEntries) {
|
|
251
|
+
lineCount += buildEntryDetailLines(b.entries[i], entryDetailIndent(b.entries, i)).length;
|
|
252
|
+
}
|
|
225
253
|
layout.contentDeleteFrom(b.summaryAbsIdx + 1, lineCount);
|
|
226
254
|
expandedBatches.delete(b.id);
|
|
227
255
|
b.expandedEntries.clear();
|
|
@@ -246,7 +274,7 @@ export function toggleEntry(batchId, entryIndex, layout) {
|
|
|
246
274
|
}
|
|
247
275
|
if (headerIdx < 0)
|
|
248
276
|
return;
|
|
249
|
-
const details = buildEntryDetailLines(b.entries[entryIndex]);
|
|
277
|
+
const details = buildEntryDetailLines(b.entries[entryIndex], entryDetailIndent(b.entries, entryIndex));
|
|
250
278
|
if (details.length === 0)
|
|
251
279
|
return;
|
|
252
280
|
if (b.expandedEntries.has(entryIndex)) {
|
|
@@ -303,8 +331,8 @@ export function shiftBatchesAfter(absIdx, delta) {
|
|
|
303
331
|
}
|
|
304
332
|
}
|
|
305
333
|
// ── history 回放支持 ──
|
|
306
|
-
/** 把已构造好的 BatchEntry[]
|
|
307
|
-
* 含 mutation(write_file/edit_file)
|
|
334
|
+
/** 把已构造好的 BatchEntry[] 落成可切换摘要行(用于 renderHistory 回放)。
|
|
335
|
+
* 含 mutation(write_file/edit_file)时整批展开;普通批次保留与实时 flushToolBatch 相同的空行边界。 */
|
|
308
336
|
export function writeSummaryOnly(entries, layout) {
|
|
309
337
|
const id = beginBatch();
|
|
310
338
|
const b = batches.get(id);
|
|
@@ -315,5 +343,9 @@ export function writeSummaryOnly(entries, layout) {
|
|
|
315
343
|
if (entries.length === 1 && isMutationToolName(entries[0].name)) {
|
|
316
344
|
expandSingleEntryFully(id, layout);
|
|
317
345
|
}
|
|
346
|
+
else {
|
|
347
|
+
// endBatch 已用一个换行结束摘要;再提交当前空行,避免下一段 assistant 正文紧贴工具结果。
|
|
348
|
+
layout.contentWrite('\n');
|
|
349
|
+
}
|
|
318
350
|
}
|
|
319
351
|
let _idCounter = 0;
|
package/dist/ui/layout.js
CHANGED
|
@@ -3,7 +3,7 @@ import { charWidth, displayWidth, truncateDisplay, truncateDisplayHead, truncate
|
|
|
3
3
|
import { ui, applyTerminalBackground, resetTerminalBackground } from './theme.js';
|
|
4
4
|
import * as content from './content.js';
|
|
5
5
|
import * as mouse from './mouse.js';
|
|
6
|
-
import { shiftBatchesAfter } from './batch.js';
|
|
6
|
+
import { reset as resetBatches, shiftBatchesAfter } from './batch.js';
|
|
7
7
|
import { copyToClipboard, readClipboard } from './clipboard.js';
|
|
8
8
|
import { renderMarkdown } from './markdown.js';
|
|
9
9
|
import { t } from '../i18n/index.js';
|
|
@@ -604,6 +604,10 @@ export function normalizeMutationBoundary() {
|
|
|
604
604
|
if (scrollOffset === 0)
|
|
605
605
|
repaintViewport();
|
|
606
606
|
}
|
|
607
|
+
/** 命令/Agent 输出→下一条输入气泡前,统一保留恰好一条视觉空行。 */
|
|
608
|
+
export function normalizeInputBoundary() {
|
|
609
|
+
normalizeMutationBoundary();
|
|
610
|
+
}
|
|
607
611
|
/** 原地刷新一条内容行(行数不变),用于运行中的工具 batch 更新计数。 */
|
|
608
612
|
export function contentReplaceLine(absIdx, line) {
|
|
609
613
|
if (!active)
|
|
@@ -616,8 +620,9 @@ export function contentReplaceLine(absIdx, line) {
|
|
|
616
620
|
}
|
|
617
621
|
/** 清空内容区时通知 batch 渲染器重置(摘要行映射与展开态)。 */
|
|
618
622
|
export function notifyContentReset() {
|
|
619
|
-
//
|
|
620
|
-
|
|
623
|
+
// 必须同步清理:clearContent() 后调用方会立即 renderHistory() 重建 batch。
|
|
624
|
+
// 若异步 reset,旧清理会在回放完成后反过来抹掉新摘要的点击映射。
|
|
625
|
+
resetBatches();
|
|
621
626
|
}
|
|
622
627
|
// ── viewport 滚动回看(Phase 2)──
|
|
623
628
|
/** 是否处于滚动回看态(offset>0,内容区显历史)。prompt 据此在非滚动键时回尾。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocode-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"prepare": "npm run build"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"ajv": "8.20.0",
|
|
30
31
|
"cli-highlight": "^2.1.11",
|
|
31
32
|
"dotenv": "^16.0.0",
|
|
32
33
|
"fast-glob": "^3.0.0",
|