mocode-ai 0.4.3 → 0.4.5
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 +10 -0
- package/README.zh-CN.md +10 -0
- package/dist/agent/index.js +30 -13
- package/dist/config/index.js +27 -20
- package/dist/llm/capabilities.js +4 -0
- package/dist/repl/index.js +113 -32
- package/dist/session/persist.js +15 -6
- package/dist/tools/builtins/ask-human.js +7 -6
- package/dist/tools/builtins/codegraph.js +1 -1
- package/dist/ui/batch.js +218 -0
- package/dist/ui/content.js +51 -0
- package/dist/ui/intervention.js +0 -1
- package/dist/ui/layout.js +266 -81
- package/dist/ui/prompt.js +99 -5
- package/dist/ui/render.js +22 -0
- package/package.json +1 -1
|
@@ -79,11 +79,12 @@ export function coerceOptions(raw) {
|
|
|
79
79
|
export const askHumanTool = {
|
|
80
80
|
name: 'ask_human',
|
|
81
81
|
description: [
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
|
|
82
|
+
'Present the user with a menu of choices to pick from — not a generic "ask" tool.',
|
|
83
|
+
' DEFAULT: pass 2–6 concrete options via `options`; the user picks one and the pick comes back.',
|
|
84
|
+
' FREE-TEXT (omit `options`): only when the answer truly cannot be reduced to a few choices',
|
|
85
|
+
' (e.g. "paste the error message", "enter the exact URL") — this blocks with a text input.',
|
|
86
|
+
' DO NOT call when the task is clear and you can pick a sensible default — decide and proceed.',
|
|
87
|
+
].join(' '),
|
|
87
88
|
parameters: {
|
|
88
89
|
type: 'object',
|
|
89
90
|
properties: {
|
|
@@ -94,7 +95,7 @@ export const askHumanTool = {
|
|
|
94
95
|
options: {
|
|
95
96
|
type: 'array',
|
|
96
97
|
items: { type: 'string' },
|
|
97
|
-
description: '
|
|
98
|
+
description: '2–6 concrete choices the user can pick with one click. Required in most cases; omit only when free-form text is genuinely needed.',
|
|
98
99
|
},
|
|
99
100
|
context: {
|
|
100
101
|
type: 'string',
|
|
@@ -53,7 +53,7 @@ export const codegraphTool = {
|
|
|
53
53
|
description: 'FIRST CHOICE for understanding/locating code, tracing call chains, or assessing the impact of changes — use this before read_file/grep when a .codegraph/ index exists.' +
|
|
54
54
|
' Returns symbol source + call paths in one shot — more accurate and economical than piecing together read_file/grep.' +
|
|
55
55
|
' When to use: starting any code exploration; locating a symbol; understanding how a feature works; seeing what calls a function or what a change affects.' +
|
|
56
|
-
' When NOT to use: no .codegraph/ index (build it first with `codegraph init`); reading a file you just edited; editing a single known small file — in those cases go straight to read_file/edit_file.' +
|
|
56
|
+
' When NOT to use: no .codegraph/ index (build it first with `codegraph init`); reading a file you just edited; editing a single known small file — in those cases go straight to read_file/edit_file; or the symbol/area was already retrieved earlier in this session — reuse that result instead of calling again.' +
|
|
57
57
|
' Fallback: if codegraph misses or the result is incomplete, then use read_file/grep/glob to fill the gaps.',
|
|
58
58
|
parameters: {
|
|
59
59
|
type: 'object',
|
package/dist/ui/batch.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具调用批量折叠渲染(仿 Claude Code):
|
|
3
|
+
* agent 一轮返回 N 个 tool_calls 时,不逐个打印 `● name ↳ result`,
|
|
4
|
+
* 改输出一行摘要 `● Ran N tools · read 3, grep 1, glob 1`;
|
|
5
|
+
* 鼠标点击该摘要行 → 展开完整明细(● 头 + ↳ preview / diff 块),再点折回。
|
|
6
|
+
*
|
|
7
|
+
* 设计要点:
|
|
8
|
+
* - 实时执行 + renderHistory 回放共用本渲染器;
|
|
9
|
+
* - 仅 buffer 写一行,详情行展开时才插入(mid-buffer insert via layout.contentInsertAfter);
|
|
10
|
+
* - 默认折叠;展开/折叠不影响历史回放的 history 结构(history 仍存完整 tool_calls+tool 消息)。
|
|
11
|
+
*
|
|
12
|
+
* 状态全模块级(单实例)——一次 REPL 内所有 batch 共享,清内容区 / 退 alt 屏时统一重置。
|
|
13
|
+
*/
|
|
14
|
+
import { ui } from './theme.js';
|
|
15
|
+
const batches = new Map();
|
|
16
|
+
/** 绝对行索引 → 所属 batch id(仅记录 summary 行;用于鼠标点击反查)。
|
|
17
|
+
* buffer 行数变化时本表可能漂移——但只在 insertAfter/deleteFrom 后由本模块同步更新,
|
|
18
|
+
* 并保持 buffer 当前状态对应。 */
|
|
19
|
+
const absLineToBatchId = new Map();
|
|
20
|
+
/** 已展开的 batch id;默认空(全折叠);layout.mouse release 点击摘要行时切。
|
|
21
|
+
* 含 mutation 的 batch 不在此 set——它们走 forceExpanded 永远展开,与 toggle 隔离。 */
|
|
22
|
+
const expandedBatches = new Set();
|
|
23
|
+
/** mutation 工具名集合(写盘操作);与 src/agent/core.ts 的 isMutationTool 同步,本模块独立持有
|
|
24
|
+
* 避免 ui → agent 反向依赖。 */
|
|
25
|
+
const MUTATION_TOOLS = new Set(['write_file', 'edit_file']);
|
|
26
|
+
function isMutationTool(name) {
|
|
27
|
+
return MUTATION_TOOLS.has(name);
|
|
28
|
+
}
|
|
29
|
+
/** 通知 buffer 整体清空(clearContent / exitAltScreen / 新一轮 turn)——本模块状态同步归零。 */
|
|
30
|
+
export function reset() {
|
|
31
|
+
batches.clear();
|
|
32
|
+
absLineToBatchId.clear();
|
|
33
|
+
expandedBatches.clear();
|
|
34
|
+
}
|
|
35
|
+
/** 新建一个 batch(在 agent 拿到第一条 onToolHeader 时调)。返回 id。 */
|
|
36
|
+
export function beginBatch() {
|
|
37
|
+
const id = `b${++_idCounter}`;
|
|
38
|
+
batches.set(id, { id, summaryAbsIdx: -1, entries: [], forceExpanded: false });
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
/** 记一条工具调用(在 onToolHeader 时调,与 setEntryResult 配对;entries 顺序 = agent 调用顺序)。 */
|
|
42
|
+
export function recordCall(id, name, callSummary) {
|
|
43
|
+
const b = batches.get(id);
|
|
44
|
+
if (!b)
|
|
45
|
+
return;
|
|
46
|
+
b.entries.push({ name, callSummary, resultSummary: '', diffBlock: null });
|
|
47
|
+
}
|
|
48
|
+
/** 记一条工具结果(diff 块或单行 preview);agent 在 onToolResult 时调,匹配最后一条未填的 entry。 */
|
|
49
|
+
export function recordResult(id, name, resultSummary, diffBlock) {
|
|
50
|
+
const b = batches.get(id);
|
|
51
|
+
if (!b || b.entries.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
// 反向找最后一条同名的 entry 填结果;同名工具一批多次调用时正向遍历更安全——用 lastIndexOf 同名回退
|
|
54
|
+
for (let i = b.entries.length - 1; i >= 0; i--) {
|
|
55
|
+
if (b.entries[i].name === name && !b.entries[i].resultSummary) {
|
|
56
|
+
b.entries[i].resultSummary = resultSummary;
|
|
57
|
+
b.entries[i].diffBlock = diffBlock;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 兜底:无匹配则填最后一条
|
|
62
|
+
const last = b.entries[b.entries.length - 1];
|
|
63
|
+
if (!last.resultSummary) {
|
|
64
|
+
last.resultSummary = resultSummary;
|
|
65
|
+
last.diffBlock = diffBlock;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── 摘要行文本生成 ──
|
|
69
|
+
/** 把 entry 列表压缩成一行摘要(Claude Code 风格)。 */
|
|
70
|
+
function buildSummaryLine(entries) {
|
|
71
|
+
if (entries.length === 0) {
|
|
72
|
+
return ` ${ui.brightMagenta}●${ui.reset} ${ui.dim}No tools${ui.reset}`;
|
|
73
|
+
}
|
|
74
|
+
if (entries.length === 1) {
|
|
75
|
+
const e = entries[0];
|
|
76
|
+
return ` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}`;
|
|
77
|
+
}
|
|
78
|
+
// N>1:同类合并 "read_file 3, glob 1, grep 1"
|
|
79
|
+
const counts = new Map();
|
|
80
|
+
for (const e of entries)
|
|
81
|
+
counts.set(e.name, (counts.get(e.name) ?? 0) + 1);
|
|
82
|
+
const parts = [];
|
|
83
|
+
for (const [n, c] of counts)
|
|
84
|
+
parts.push(`${n} ${c}`);
|
|
85
|
+
return ` ${ui.brightMagenta}●${ui.reset} ${ui.dim}Ran ${entries.length} tools · ${parts.join(', ')}${ui.reset}`;
|
|
86
|
+
}
|
|
87
|
+
// ── 展开/折叠 ──
|
|
88
|
+
/** 把 batch 的详情行展开成自洽行数组(供 layout.contentInsertAfter 走 mid-buffer 插入)。
|
|
89
|
+
* 每行末尾必须以 \x1B[0m 收尾(SGR 自洽模型),行内允许含 SGR(行末 reset 不影响行内样式),
|
|
90
|
+
* 但**绝不**带 \n——rows[] 是行数组,不是流输出。 */
|
|
91
|
+
function buildExpandedLines(entries) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
for (const e of entries) {
|
|
94
|
+
lines.push(` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}\x1B[0m`);
|
|
95
|
+
if (e.diffBlock) {
|
|
96
|
+
// diff 块多行文本(由 renderFileChange 渲染);按 \n 拆成物理行,
|
|
97
|
+
// 每行单独入 rows[]。行末 reset 由本函数统一追加(若原行已带 reset,终端合并即可)。
|
|
98
|
+
const block = e.diffBlock.endsWith('\n') ? e.diffBlock : e.diffBlock + '\n';
|
|
99
|
+
for (const line of block.split('\n')) {
|
|
100
|
+
if (line === '' && lines.length > 0 && lines[lines.length - 1] === '')
|
|
101
|
+
continue; // 折叠连续空行
|
|
102
|
+
if (line === '' && lines.length > 0)
|
|
103
|
+
continue; // 跳过首尾空行(diff 头/尾换行)
|
|
104
|
+
lines.push(line.endsWith('\x1B[0m') ? line : line + '\x1B[0m');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (e.resultSummary) {
|
|
108
|
+
lines.push(` ${ui.gray}↳ ${e.resultSummary}${ui.reset}\x1B[0m`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
/** 在 batch 收尾时(onToolBatchEnd):写摘要行 + 登记 summaryAbsIdx;若已展开(回放场景)立即插详情。 */
|
|
114
|
+
export function endBatch(id, layout) {
|
|
115
|
+
const b = batches.get(id);
|
|
116
|
+
if (!b || b.summaryAbsIdx >= 0)
|
|
117
|
+
return; // 幂等
|
|
118
|
+
// 含 mutation(write_file/edit_file)时强制展开——写盘操作必须让用户看到 diff
|
|
119
|
+
b.forceExpanded = b.entries.some((e) => isMutationTool(e.name));
|
|
120
|
+
const summary = buildSummaryLine(b.entries);
|
|
121
|
+
// 写摘要行(以 \n 收尾;contentWrite 会 breakRow 让其成为完整物理行)
|
|
122
|
+
layout.contentWrite(summary + '\n');
|
|
123
|
+
// 摘要行绝对索引 = totalRows - 2(hasCurrent 那行是新空行)
|
|
124
|
+
b.summaryAbsIdx = Math.max(0, layout.totalRows() - 2);
|
|
125
|
+
absLineToBatchId.set(b.summaryAbsIdx, b.id);
|
|
126
|
+
// forceExpanded 时立刻展开(让 diff 在收尾后立即可见,无需点击)
|
|
127
|
+
if (b.forceExpanded) {
|
|
128
|
+
expand(b, layout);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** 绝对行索引 → 命中 batch id(用于鼠标 release 反查);非摘要行返 null。 */
|
|
132
|
+
export function findBatchByAbsLine(absLine) {
|
|
133
|
+
return absLineToBatchId.get(absLine) ?? null;
|
|
134
|
+
}
|
|
135
|
+
/** 当前 batch 是否已展开。 */
|
|
136
|
+
export function isExpanded(id) {
|
|
137
|
+
return expandedBatches.has(id);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 切换 batch 展开/折叠;无变化时 no-op。
|
|
141
|
+
* 折叠:从 buffer 删详情行(mid-buffer delete);
|
|
142
|
+
* 展开:把详情行插入摘要行下方(mid-buffer insert)。
|
|
143
|
+
* 同步更新 absLineToBatchId 中所有受影响的索引:
|
|
144
|
+
* - 删除/插入点之后的 batch 摘要行索引相应平移。
|
|
145
|
+
* 含 mutation(write_file/edit_file)的 batch 强制展开——不允许折叠(写盘操作必须始终可见)。
|
|
146
|
+
*/
|
|
147
|
+
export function toggleBatch(id, layout) {
|
|
148
|
+
const b = batches.get(id);
|
|
149
|
+
if (!b)
|
|
150
|
+
return;
|
|
151
|
+
if (b.forceExpanded) {
|
|
152
|
+
// 写盘操作的 batch 强制展开,toggle 拒绝折叠(用户能看到完整 diff 即用)
|
|
153
|
+
if (!expandedBatches.has(id))
|
|
154
|
+
expand(b, layout);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (expandedBatches.has(id)) {
|
|
158
|
+
collapse(b, layout);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
expand(b, layout);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function expand(b, layout) {
|
|
165
|
+
const lines = buildExpandedLines(b.entries);
|
|
166
|
+
layout.contentInsertAfter(b.summaryAbsIdx, lines);
|
|
167
|
+
expandedBatches.add(b.id);
|
|
168
|
+
}
|
|
169
|
+
function collapse(b, layout) {
|
|
170
|
+
const lines = buildExpandedLines(b.entries);
|
|
171
|
+
layout.contentDeleteFrom(b.summaryAbsIdx + 1, lines.length);
|
|
172
|
+
expandedBatches.delete(b.id);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 当 buffer 中段插/删 N 行后,所有受影响 batch 的 summaryAbsIdx 需平移。
|
|
176
|
+
* 由 layout.contentInsertAfter / contentDeleteFrom 在每次变动后调一次,
|
|
177
|
+
* 参数 afterIdx 是被插入/删除点的绝对索引(插入点之前索引不变;之后索引 += delta)。
|
|
178
|
+
*/
|
|
179
|
+
export function shiftBatchesAfter(absIdx, delta) {
|
|
180
|
+
if (delta === 0)
|
|
181
|
+
return;
|
|
182
|
+
// 重建 absLineToBatchId:删除所有 <= absIdx 的项,把 > absIdx 的项按 delta 平移
|
|
183
|
+
const next = new Map();
|
|
184
|
+
for (const [idx, id] of absLineToBatchId) {
|
|
185
|
+
if (idx <= absIdx) {
|
|
186
|
+
next.set(idx, id);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const newIdx = idx + delta;
|
|
190
|
+
if (newIdx >= 0)
|
|
191
|
+
next.set(newIdx, id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
absLineToBatchId.clear();
|
|
195
|
+
for (const [k, v] of next)
|
|
196
|
+
absLineToBatchId.set(k, v);
|
|
197
|
+
// 同步每个 batch 的 summaryAbsIdx
|
|
198
|
+
for (const b of batches.values()) {
|
|
199
|
+
if (b.summaryAbsIdx > absIdx)
|
|
200
|
+
b.summaryAbsIdx = Math.max(0, b.summaryAbsIdx + delta);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ── history 回放支持 ──
|
|
204
|
+
/** 把已构造好的 BatchEntry[] 直接落成摘要行(用于 renderHistory 回放;不记录 id 也不需可切换)。
|
|
205
|
+
* 含 mutation(write_file/edit_file)时整批展开——与实时 endBatch 行为一致。 */
|
|
206
|
+
export function writeSummaryOnly(entries, layout) {
|
|
207
|
+
layout.contentWrite(buildSummaryLine(entries) + '\n');
|
|
208
|
+
const hasMutation = entries.some((e) => isMutationTool(e.name));
|
|
209
|
+
if (hasMutation) {
|
|
210
|
+
// 回放时同步展开:重新调 expand 路径需要 BatchRecord,此处直接拼 line 插入
|
|
211
|
+
const summaryIdx = Math.max(0, layout.totalRows() - 2);
|
|
212
|
+
const lines = buildExpandedLines(entries);
|
|
213
|
+
layout.contentInsertAfter(summaryIdx, lines);
|
|
214
|
+
// 不入 batches/absLineToBatchId/expandedBatches——回放行不支持点击 toggle(设计取舍:
|
|
215
|
+
// 简化模型;若需要支持回放也可展开/折叠,可在 alt-screen 启动时建一张临时映射)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let _idCounter = 0;
|
package/dist/ui/content.js
CHANGED
|
@@ -136,3 +136,54 @@ export function lineAt(abs) {
|
|
|
136
136
|
const all = snapshot();
|
|
137
137
|
return abs >= 0 && abs < all.length ? all[abs] : null;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* 在绝对行索引 after(0-based,已 commit)之后插入 N 条自洽行。
|
|
141
|
+
* 用于「折叠摘要行下展开明细」——把详情行在已写入摘要行后面塞入缓冲,
|
|
142
|
+
* 不重写尾部已有内容;且不影响更早的 buffer 行(hasCurrent 先 commit 再 splice)。
|
|
143
|
+
*
|
|
144
|
+
* 行须自洽(每行带行末 \x1B[0m),不经 feedChar/feedSgr,直接入 rows。
|
|
145
|
+
* after < 0 视为在所有已 commit 行之前插入;after ≥ 已 commit 行数则追加到末尾。
|
|
146
|
+
* 不动 segMark——此函数用于非 md 路径(BatchRenderer 展开/折叠);
|
|
147
|
+
* md 段切回 layout.contentWriteMd 时 setLines 仍按 segMark 截断。
|
|
148
|
+
*
|
|
149
|
+
* 后置条件:插入后 hasCurrent=false(新空行不由本函数建立);调用方须自行决定续写位
|
|
150
|
+
* (BatchRenderer 在插入后调 layout.contentWrite 续写,新 \n 自然在详情块后建新行)。
|
|
151
|
+
*/
|
|
152
|
+
export function insertAfter(after, lines) {
|
|
153
|
+
if (lines.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
if (hasCurrent) {
|
|
156
|
+
rows.push(rowStartSgr + curRaw + '\x1B[0m');
|
|
157
|
+
curRaw = '';
|
|
158
|
+
rowStartSgr = curSgr;
|
|
159
|
+
hasCurrent = false;
|
|
160
|
+
}
|
|
161
|
+
const committed = rows.length;
|
|
162
|
+
// after 是绝对行索引;若超过 committed(例如快照时 hasCurrent=true),钳到末尾
|
|
163
|
+
const target = after < 0 ? 0 : Math.min(after + 1, committed);
|
|
164
|
+
rows.splice(target, 0, ...lines);
|
|
165
|
+
if (rows.length > MAX_ROWS + 512)
|
|
166
|
+
rows.splice(0, rows.length - MAX_ROWS);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 从绝对行索引 startIdx(0-based,已 commit)起删 n 行。
|
|
170
|
+
* 用于「已展开明细折回摘要」——把详情行从中段裁掉,保留摘要行和后续内容。
|
|
171
|
+
* startIdx 越界或 n <= 0 直接 no-op。hasCurrent 时先 commit(同 insertAfter)。
|
|
172
|
+
*
|
|
173
|
+
* 后置条件:删除后 hasCurrent=false。后续 layout.contentWrite 自然续写。
|
|
174
|
+
*/
|
|
175
|
+
export function deleteFrom(startIdx, n) {
|
|
176
|
+
if (n <= 0)
|
|
177
|
+
return;
|
|
178
|
+
if (hasCurrent) {
|
|
179
|
+
rows.push(rowStartSgr + curRaw + '\x1B[0m');
|
|
180
|
+
curRaw = '';
|
|
181
|
+
rowStartSgr = curSgr;
|
|
182
|
+
hasCurrent = false;
|
|
183
|
+
}
|
|
184
|
+
const committed = rows.length;
|
|
185
|
+
if (startIdx >= committed)
|
|
186
|
+
return;
|
|
187
|
+
const end = Math.min(startIdx + n, committed);
|
|
188
|
+
rows.splice(startIdx, end - startIdx);
|
|
189
|
+
}
|
package/dist/ui/intervention.js
CHANGED