mocode-ai 0.7.3 → 1.0.2
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 +86 -34
- 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/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 +59 -46
- 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/write-file.js +52 -17
- package/dist/tools/constants.js +3 -3
- package/dist/tools/registry.js +5 -2
- package/dist/ui/batch.js +56 -24
- package/dist/ui/layout.js +8 -3
- package/package.json +1 -1
|
@@ -1,36 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { commitChangeSet, createChangeSet, normalizeContentHash, summarizeChangeSet, } from '../../changeset/index.js';
|
|
2
|
+
function conflict(path, details) {
|
|
3
|
+
return {
|
|
4
|
+
status: 'error',
|
|
5
|
+
code: 'CHANGE_CONFLICT',
|
|
6
|
+
retryable: false,
|
|
7
|
+
changedFiles: [],
|
|
8
|
+
staleFiles: [path],
|
|
9
|
+
output: `CHANGE_CONFLICT: ${path} was not changed. ${details} Do not retry these arguments. Call read_file on this exact path, then use the returned hash; use expected_hash=null only if read_file reports that the path is missing.`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
5
12
|
export const writeFileTool = {
|
|
6
13
|
name: 'write_file',
|
|
7
|
-
description: 'Create or
|
|
14
|
+
description: 'Create or replace one file transactionally. expected_hash may be omitted (or null) only for create-only writes to a path that must not exist; overwriting requires the hash from a fresh read_file artifact header.',
|
|
8
15
|
risk: 'confirm',
|
|
9
16
|
parameters: {
|
|
10
17
|
type: 'object',
|
|
11
18
|
properties: {
|
|
12
19
|
path: { type: 'string', description: 'File path' },
|
|
13
20
|
content: { type: 'string', description: 'Full file content' },
|
|
21
|
+
expected_hash: {
|
|
22
|
+
type: ['string', 'null'],
|
|
23
|
+
description: 'Optional sha256 hash from read_file. Omit or pass null only when the path must not exist.',
|
|
24
|
+
},
|
|
14
25
|
},
|
|
15
26
|
required: ['path', 'content'],
|
|
16
27
|
},
|
|
17
|
-
async execute(args) {
|
|
18
|
-
const
|
|
28
|
+
async execute(args, ctx) {
|
|
29
|
+
const file = String(args.path);
|
|
19
30
|
const content = String(args.content);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
let expectedHash = null;
|
|
32
|
+
// Missing and explicit null are both safe create-only requests. They never
|
|
33
|
+
// overwrite: ChangeSet compares expectedHash=null against the current path.
|
|
34
|
+
if (args.expected_hash != null) {
|
|
35
|
+
expectedHash = normalizeContentHash(String(args.expected_hash));
|
|
36
|
+
if (!expectedHash)
|
|
37
|
+
return conflict(file, 'expected_hash 必须是 null 或 sha256:<64 hex>。');
|
|
38
|
+
}
|
|
39
|
+
const operation = expectedHash === null ? 'create' : 'update';
|
|
40
|
+
const result = await commitChangeSet(createChangeSet([{
|
|
41
|
+
path: file,
|
|
42
|
+
operation,
|
|
43
|
+
expectedHash,
|
|
44
|
+
replacement: content,
|
|
45
|
+
}]), ctx?.signal);
|
|
46
|
+
if (result.status === 'conflict') {
|
|
47
|
+
const item = result.conflicts[0];
|
|
48
|
+
return conflict(file, `expected=${item?.expectedHash ?? 'missing'}, actual=${item?.actualHash ?? 'missing'}。请重新读取后再写入。`);
|
|
49
|
+
}
|
|
50
|
+
if (result.status === 'failed') {
|
|
25
51
|
return {
|
|
26
52
|
status: 'error',
|
|
27
|
-
code: '
|
|
53
|
+
code: 'EXECUTION_ERROR',
|
|
28
54
|
retryable: false,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.join('\n'),
|
|
55
|
+
changedFiles: [],
|
|
56
|
+
output: `错误:ChangeSet 提交失败并已执行恢复: ${result.error}`,
|
|
32
57
|
};
|
|
33
58
|
}
|
|
34
|
-
|
|
59
|
+
const summary = summarizeChangeSet(result.changeSet);
|
|
60
|
+
return {
|
|
61
|
+
status: 'success',
|
|
62
|
+
code: 'OK',
|
|
63
|
+
retryable: false,
|
|
64
|
+
changedFiles: result.changedFiles,
|
|
65
|
+
changeSet: summary,
|
|
66
|
+
output: result.changedFiles.length === 0
|
|
67
|
+
? `文件 ${file} 内容未变化 (ChangeSet ${summary.id})。`
|
|
68
|
+
: `已事务化写入 ${file} (${content.length} 字符, ChangeSet ${summary.id}, sha256=${summary.changes[0]?.afterHash})。`,
|
|
69
|
+
};
|
|
35
70
|
},
|
|
36
71
|
};
|
package/dist/tools/constants.js
CHANGED
|
@@ -26,7 +26,7 @@ export const IGNORE = ['**/node_modules/**', '**/.git/**'];
|
|
|
26
26
|
// ── plan 模式(只读规划,不执行)──────────────────────────────────────────────
|
|
27
27
|
/**
|
|
28
28
|
* plan 模式下从工具 schema 里剔除的工具(模型根本看不到 → 调不到):
|
|
29
|
-
* 写盘 / 命令 / 记忆写入类 +
|
|
29
|
+
* 写盘 / 命令 / 记忆写入类 + sub-agent(派生子 agent,plan 模式只读不可有副作用)。单一事实源,
|
|
30
30
|
* 被 llm(planChatTools)与 agent(防御 backstop)共用。
|
|
31
31
|
* 只读工具(read_file/glob/grep/codegraph/web_search/web_fetch/use_skill/ask_human/memory_search/memory_list)保留。
|
|
32
32
|
*/
|
|
@@ -37,7 +37,7 @@ export const PLAN_DISABLED_TOOLS = new Set([
|
|
|
37
37
|
'memory_save',
|
|
38
38
|
'memory_update',
|
|
39
39
|
'memory_forget',
|
|
40
|
-
'
|
|
40
|
+
'sub-agent',
|
|
41
41
|
]);
|
|
42
42
|
/**
|
|
43
43
|
* 按当前 isMemoryEnabled() 现算 plan 模式应屏蔽的工具。
|
|
@@ -56,5 +56,5 @@ export function getPlanDisabledTools() {
|
|
|
56
56
|
}
|
|
57
57
|
/** auto/plan 共用的运行时功能开关防线;关闭时即使模型幻觉调用也不得执行。 */
|
|
58
58
|
export function getRuntimeDisabledTools() {
|
|
59
|
-
return isSubAgentEnabled() ? new Set() : new Set(['
|
|
59
|
+
return isSubAgentEnabled() ? new Set() : new Set(['sub-agent']);
|
|
60
60
|
}
|
package/dist/tools/registry.js
CHANGED
|
@@ -145,7 +145,9 @@ async function executeToolAttempt(tool, args, signal, opts, notifyLockAcquired)
|
|
|
145
145
|
opts?.onLockAcquired?.(args);
|
|
146
146
|
const mutationBefore = getCurrentTurnMutationState();
|
|
147
147
|
mutationVersionBefore = mutationBefore.version;
|
|
148
|
-
|
|
148
|
+
// Transactional tools own their full write-set capture inside ChangeSet commit.
|
|
149
|
+
const pathCapture = !capabilities.delegatesResourceLocks &&
|
|
150
|
+
isFileMutationTool(tool.name) && typeof args.path === 'string' && args.path
|
|
149
151
|
? beginPathMutation(args.path)
|
|
150
152
|
: null;
|
|
151
153
|
capturedPath = pathCapture?.path;
|
|
@@ -169,7 +171,8 @@ async function executeToolAttempt(tool, args, signal, opts, notifyLockAcquired)
|
|
|
169
171
|
: mutationAfter.changedFiles.map((item) => item.path)
|
|
170
172
|
: [];
|
|
171
173
|
if (signal?.aborted) {
|
|
172
|
-
|
|
174
|
+
const aborted = terminalOutcome('aborted', 'ABORTED', String(isStructuredOutcome(raw) ? raw.output : raw), startedAt, changedFiles);
|
|
175
|
+
return isStructuredOutcome(raw) ? { ...aborted, usage: raw.usage } : aborted;
|
|
173
176
|
}
|
|
174
177
|
return normalizeOutcome(raw, Date.now() - startedAt, changedFiles);
|
|
175
178
|
});
|
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 failure = e.failed ? `${ui.red}×${ui.reset} ` : '';
|
|
155
|
+
return ` ${ui.dim}${branch}${ui.reset} ${failure}${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 据此在非滚动键时回尾。 */
|