mocode-ai 1.6.4 → 1.6.6
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 +382 -365
- package/README.zh-CN.md +19 -2
- package/dist/agent/model-turn.js +14 -1
- package/dist/agent/run-coordinator.js +7 -1
- package/dist/agent/spawn.js +38 -5
- package/dist/agent/stages/model-runner.js +1 -1
- package/dist/agent/stages/tool-dispatcher.js +1 -0
- package/dist/agent/tool-turn.js +2 -0
- package/dist/agent/work-discipline.js +3 -1
- package/dist/bots/bus.js +117 -0
- package/dist/bots/cli.js +102 -0
- package/dist/bots/store.js +100 -0
- package/dist/config/index.js +53 -2
- package/dist/config/presets.js +40 -1
- package/dist/config/profiles.js +9 -1
- package/dist/context/clearing.js +53 -0
- package/dist/context/relevance.js +6 -0
- package/dist/context/text-search.js +113 -0
- package/dist/headless.js +302 -0
- package/dist/index.js +171 -3
- package/dist/jobs/approval.js +153 -0
- package/dist/jobs/approve-cli.js +30 -0
- package/dist/jobs/attach.js +61 -0
- package/dist/jobs/checkpoint.js +33 -0
- package/dist/jobs/launch.js +107 -0
- package/dist/jobs/runner.js +59 -0
- package/dist/jobs/store.js +101 -0
- package/dist/jobs/worktree.js +63 -0
- package/dist/llm/index.js +35 -8
- package/dist/llm/providers/anthropic.js +14 -4
- package/dist/llm/reasoning.js +141 -0
- package/dist/memory/reflect.js +1 -1
- package/dist/memory/store.js +31 -28
- package/dist/models/budgets.js +11 -0
- package/dist/models/catalog.js +130 -0
- package/dist/models/map-preset.js +50 -0
- package/dist/models/model-panel.js +227 -0
- package/dist/models/protocol.js +44 -0
- package/dist/models/reasoning-cap.js +88 -0
- package/dist/models/search.js +58 -0
- package/dist/models/types.js +8 -0
- package/dist/notify/index.js +144 -0
- package/dist/repl/commands/bots.js +32 -0
- package/dist/repl/commands/effort.js +81 -0
- package/dist/repl/commands/jobs.js +109 -0
- package/dist/repl/commands/model-actions.js +138 -0
- package/dist/repl/commands/model-catalog.js +67 -0
- package/dist/repl/commands/model-panel-ui.js +107 -0
- package/dist/repl/commands/model.js +22 -20
- package/dist/repl/commands/registry.js +12 -0
- package/dist/repl/commands/schedule.js +37 -0
- package/dist/repl/commands/session.js +54 -0
- package/dist/repl/commands/stats.js +28 -0
- package/dist/repl/commands.js +6 -58
- package/dist/repl/runtime.js +5 -1
- package/dist/repl/status-bar.js +4 -2
- package/dist/rollback/store.js +17 -0
- package/dist/runtime/runtime.js +31 -1
- package/dist/schedule/cli.js +114 -0
- package/dist/schedule/cron.js +79 -0
- package/dist/schedule/daemon.js +166 -0
- package/dist/schedule/store.js +98 -0
- package/dist/session/compact.js +84 -3
- package/dist/session/index.js +2 -0
- package/dist/session/retention.js +297 -0
- package/dist/session/scheduler.js +30 -3
- package/dist/session/store.js +108 -6
- package/dist/session/usage-stats.js +90 -0
- package/dist/skills/activation.js +2 -0
- package/dist/skills/builtin-skills.js +153 -0
- package/dist/skills/discover.js +3 -1
- package/dist/skills/runner.js +4 -1
- package/dist/tools/builtins/arena.js +156 -0
- package/dist/tools/builtins/index.js +8 -0
- package/dist/tools/builtins/message-bus.js +98 -0
- package/dist/tools/builtins/read-file.js +19 -1
- package/dist/tools/builtins/run-command.js +2 -1
- package/dist/tools/builtins/session-search.js +33 -0
- package/dist/tools/builtins/task.js +2 -0
- package/dist/tools/constants.js +8 -0
- package/dist/tools/read-dedup.js +62 -0
- package/dist/tools/tool-runtime.js +1 -0
- package/dist/ui/batch.js +140 -104
- package/dist/ui/composer.js +4 -18
- package/dist/ui/fuzzy-picker.js +227 -0
- package/dist/ui/hierarchical-picker.js +302 -0
- package/dist/ui/intervention.js +17 -1
- package/dist/ui/layout-internal/statusbar.js +18 -7
- package/dist/ui/prompt-internal/editor.js +10 -27
- package/dist/ui/render.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { config } from '../../config/index.js';
|
|
2
|
+
import { searchArchiveIndex } from '../../session/retention.js';
|
|
3
|
+
// ---------- session_search ----------
|
|
4
|
+
// episodic 层入口:跨会话搜索已归档会话(首条用户消息 + 摘要,BM25)。
|
|
5
|
+
// 已 purge(正文已删)的会话同样可命中——meta+摘要永久保留。需要整段恢复时,
|
|
6
|
+
// 未 purge 的会话仍可用 --resume <id> 从 archive 取回(见 retention loadArchivedSession)。
|
|
7
|
+
export const sessionSearchTool = {
|
|
8
|
+
name: 'session_search',
|
|
9
|
+
description: 'Search archived past sessions in this workspace by keyword (BM25 over the first user message and the session summary). Returns session id, date, and summary. Use to recall what was decided or done in earlier conversations.',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
query: { type: 'string', description: 'Keywords describing the past session' },
|
|
14
|
+
limit: { type: 'integer', description: 'Number of sessions to return, default 10' },
|
|
15
|
+
},
|
|
16
|
+
required: ['query'],
|
|
17
|
+
},
|
|
18
|
+
async execute(args) {
|
|
19
|
+
const query = String(args.query ?? '').trim();
|
|
20
|
+
if (!query)
|
|
21
|
+
return '错误:缺少 query。';
|
|
22
|
+
const limit = typeof args.limit === 'number' ? args.limit : 10;
|
|
23
|
+
const hits = searchArchiveIndex(config.sessionDir, query, limit);
|
|
24
|
+
if (hits.length === 0)
|
|
25
|
+
return `(无匹配的归档会话:query="${query}")`;
|
|
26
|
+
return hits
|
|
27
|
+
.map((h) => {
|
|
28
|
+
const status = h.purgedAt ? ' [正文已清除,仅摘要]' : '';
|
|
29
|
+
return `# ${h.id} · ${h.createdAt}${status}\n${h.summary || h.firstUser}`;
|
|
30
|
+
})
|
|
31
|
+
.join('\n\n---\n\n');
|
|
32
|
+
},
|
|
33
|
+
};
|
package/dist/tools/constants.js
CHANGED
|
@@ -18,6 +18,14 @@ export const MAX_HISTORY_RESULT = 8000;
|
|
|
18
18
|
export const MAX_SKILL_RESULT = 64000;
|
|
19
19
|
/** 微压缩时旧工具结果截到的存根长度(字符)。 */
|
|
20
20
|
export const MAX_OLD_TOOL_STUB = 600;
|
|
21
|
+
// ── 磁盘侧治理(见 session/store.ts)──────────────────────────────────────
|
|
22
|
+
/** trace.jsonl 单文件大小上限(字节):超过即把整份 gzip 轮转为 trace.N.jsonl.gz 并清空。
|
|
23
|
+
* trace 是纯诊断数据,可激进轮转;512KB 约含千余条事件。 */
|
|
24
|
+
export const MAX_TRACE_BYTES = 512 * 1024;
|
|
25
|
+
/** 保留的历史 trace 轮转份数(trace.1 .. trace.N),更老的硬删。 */
|
|
26
|
+
export const MAX_TRACE_ROTATIONS = 3;
|
|
27
|
+
/** rollback snapshots 压缩前滚动窗口:无论是否 compact,仅保留最近 N 个 user turn 的快照。 */
|
|
28
|
+
export const MAX_SNAPSHOT_TURNS = 50;
|
|
21
29
|
// ── 上下文压缩摘要(见 session/compact.ts defaultSummarize)────────────────
|
|
22
30
|
/** 摘要输入单条消息封顶:逐条封顶保证每轮对话都有代表,替代整段中截(会切掉中间整轮)。
|
|
23
31
|
* 转录总量超 SUMMARY_TRANSCRIPT_WINDOW_RATIO 时,这些封顶会被等比缩小重拼。 */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const MAX_RANGES_PER_PATH = 5;
|
|
2
|
+
const MAX_PATHS = 200;
|
|
3
|
+
export class ReadDedup {
|
|
4
|
+
/** Map 保序:超容量时删最老(第一个)key。 */
|
|
5
|
+
entries = new Map();
|
|
6
|
+
currentStep = 0;
|
|
7
|
+
/** model-turn 每步调用:钉住 step(指针消息里展示 "since step K")。 */
|
|
8
|
+
beginStep(step) {
|
|
9
|
+
this.currentStep = step;
|
|
10
|
+
}
|
|
11
|
+
/** trim 变更 history 内容后调用:所有存活条目失活(其内容可能已不在 context)。 */
|
|
12
|
+
markContextChanged() {
|
|
13
|
+
for (const list of this.entries.values())
|
|
14
|
+
for (const e of list)
|
|
15
|
+
e.hot = false;
|
|
16
|
+
}
|
|
17
|
+
touch(path) {
|
|
18
|
+
let list = this.entries.get(path);
|
|
19
|
+
if (list) {
|
|
20
|
+
// LRU:命中路径挪到末尾。
|
|
21
|
+
this.entries.delete(path);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
list = [];
|
|
25
|
+
if (this.entries.size >= MAX_PATHS) {
|
|
26
|
+
const oldest = this.entries.keys().next().value;
|
|
27
|
+
if (oldest !== undefined)
|
|
28
|
+
this.entries.delete(oldest);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
this.entries.set(path, list);
|
|
32
|
+
return list;
|
|
33
|
+
}
|
|
34
|
+
/** 记录一次实际返回的读取(全量返回之后调用)。 */
|
|
35
|
+
record(path, startLine, endLine, hash) {
|
|
36
|
+
const list = this.touch(path);
|
|
37
|
+
list.push({ startLine, endLine, hash, step: this.currentStep, hot: true });
|
|
38
|
+
while (list.length > MAX_RANGES_PER_PATH)
|
|
39
|
+
list.shift();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 查询重复读命中:存在 hot 条目,其行区间覆盖请求区间、hash 相同。
|
|
43
|
+
* 无命中返 null(调用方照常全量返回)。
|
|
44
|
+
*/
|
|
45
|
+
findUnchanged(path, startLine, endLine, hash) {
|
|
46
|
+
const list = this.entries.get(path);
|
|
47
|
+
if (!list)
|
|
48
|
+
return null;
|
|
49
|
+
// 从新到旧找。
|
|
50
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
51
|
+
const e = list[i];
|
|
52
|
+
if (e.hot && e.hash === hash && e.startLine <= startLine && e.endLine >= endLine) {
|
|
53
|
+
return { step: e.step, hash };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** 每用户 turn 一个 scope。 */
|
|
60
|
+
export function createReadDedup() {
|
|
61
|
+
return new ReadDedup();
|
|
62
|
+
}
|
package/dist/ui/batch.js
CHANGED
|
@@ -72,6 +72,8 @@ export function reset() {
|
|
|
72
72
|
expandedBatches.clear();
|
|
73
73
|
callToBatch.clear();
|
|
74
74
|
groupChildIndexByCall.clear();
|
|
75
|
+
// 复位外部暂停态(兜底:面板异常退出未走 cleanup 时,避免暂停泄漏到后续 turn)。
|
|
76
|
+
_sweepSuspended = false;
|
|
75
77
|
// 扫光心跳随运行态批自停:batch 全清后 hasRunningBatch 必为 false。
|
|
76
78
|
syncSweepTimer();
|
|
77
79
|
}
|
|
@@ -82,6 +84,12 @@ export function reset() {
|
|
|
82
84
|
* opts.groupChildIndex:本批作为组子批时在父 entries 中的序号(插入锚点用)。 */
|
|
83
85
|
export function beginBatch(label, opts) {
|
|
84
86
|
const id = `b${++_idCounter}`;
|
|
87
|
+
const parentId = opts?.parentId && batches.has(opts.parentId) ? opts.parentId : undefined;
|
|
88
|
+
const parent = parentId ? batches.get(parentId) : undefined;
|
|
89
|
+
// 缩进未显式给定时按父层级自动推导:组容器批的子批 = 两层 entry 缩进;
|
|
90
|
+
// 普通子 agent 批下再嵌套 = 父批缩进 + 两层(树状逐层加深)。
|
|
91
|
+
const indent = opts?.indent ??
|
|
92
|
+
(parent ? (parent.groupParent ? SUB_BATCH_INDENT : (parent.indent ?? '') + SUB_BATCH_INDENT) : undefined);
|
|
85
93
|
batches.set(id, {
|
|
86
94
|
id,
|
|
87
95
|
summaryAbsIdx: -1,
|
|
@@ -90,8 +98,8 @@ export function beginBatch(label, opts) {
|
|
|
90
98
|
renderedCount: 0,
|
|
91
99
|
startedAt: Date.now(),
|
|
92
100
|
label,
|
|
93
|
-
indent
|
|
94
|
-
parentId
|
|
101
|
+
indent,
|
|
102
|
+
parentId,
|
|
95
103
|
groupParent: opts?.groupParent ?? false,
|
|
96
104
|
groupChildIndex: opts?.groupChildIndex,
|
|
97
105
|
running: opts?.running ?? false,
|
|
@@ -123,7 +131,8 @@ export function recordCall(id, name, callSummary, callId) {
|
|
|
123
131
|
// 已完成的累计探索后又追加工具:恢复进行中,待新结果返回再完成。
|
|
124
132
|
b.finishedAt = undefined;
|
|
125
133
|
b.entries.push({ name, callSummary, resultSummary: '', diffBlock: null });
|
|
126
|
-
|
|
134
|
+
// 对所有批登记:普通批内的 sub-agent 调用(子 agent 嵌套派生)同样要靠它定位锚点。
|
|
135
|
+
if (callId)
|
|
127
136
|
groupChildIndexByCall.set(callId, b.entries.length - 1);
|
|
128
137
|
// 新增 entry = 本批重新「在飞」→ 扫光心跳起(此前收口时可能已停)。
|
|
129
138
|
syncSweepTimer();
|
|
@@ -149,8 +158,8 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput, fai
|
|
|
149
158
|
const b = batches.get(id);
|
|
150
159
|
if (!b || b.entries.length === 0)
|
|
151
160
|
return;
|
|
152
|
-
//
|
|
153
|
-
if (callId
|
|
161
|
+
// 优先按 callId 定位 entry,防止多个同名工具(并行调用 / 嵌套 sub-agent)结果互相填错。
|
|
162
|
+
if (callId) {
|
|
154
163
|
const idx = groupChildIndexByCall.get(callId);
|
|
155
164
|
if (idx != null && idx < b.entries.length && !isEntryDone(b.entries[idx])) {
|
|
156
165
|
const e = b.entries[idx];
|
|
@@ -227,6 +236,12 @@ let _sweepTimer = null;
|
|
|
227
236
|
* 存 layout 而不自己 import 判断 TUI 是否激活,是沿用「agent 侧注入」的既有形态。
|
|
228
237
|
*/
|
|
229
238
|
let _sweepLayout = null;
|
|
239
|
+
/**
|
|
240
|
+
* 扫光被外部暂停(介入面板 ask_human 期间):摘要行已在内容区底部,而菜单同样从
|
|
241
|
+
* contentBottom 向上展开,心跳若继续逐帧重画该行会写进菜单区、把「◇ 正在探索」
|
|
242
|
+
* 泄漏到选项之间(用户截图实证)。暂停只冻结心跳,不清状态;resumeSweep 后立即恢复。
|
|
243
|
+
*/
|
|
244
|
+
let _sweepSuspended = false;
|
|
230
245
|
/**
|
|
231
246
|
* 批是否「在飞」(需要扫光与实时耗时)。判据是**已收口即落定**:
|
|
232
247
|
* `endBatch` 必设 finishedAt,故先看它 —— 否则任何「endBatch 了但 running 标志残留」
|
|
@@ -250,7 +265,7 @@ function hasLiveBatch() {
|
|
|
250
265
|
}
|
|
251
266
|
/** 扫光可用性:TUI(非 TTY 输出纯文本,不能塞动画转义)+ 有在飞批 + 有可写回的 layout。 */
|
|
252
267
|
function sweepEnabled() {
|
|
253
|
-
return ui.isTTY && _sweepLayout != null && hasLiveBatch();
|
|
268
|
+
return !_sweepSuspended && ui.isTTY && _sweepLayout != null && hasLiveBatch();
|
|
254
269
|
}
|
|
255
270
|
/**
|
|
256
271
|
* 同步扫光心跳开关(建批/收批时调)。心跳随最后一个运行态批结束自停,不常驻空转。
|
|
@@ -295,6 +310,28 @@ export function syncSweepTimer() {
|
|
|
295
310
|
return timer;
|
|
296
311
|
})();
|
|
297
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* 暂停扫光心跳(介入面板进入时调):syncSweepTimer 经 sweepEnabled 判定立即清 timer;
|
|
315
|
+
* 暂停期间 showLiveBatch/endBatch 再调 syncSweepTimer 也不会重启。幂等:重复挂起不抖状态。
|
|
316
|
+
*/
|
|
317
|
+
export function suspendSweep() {
|
|
318
|
+
if (_sweepSuspended)
|
|
319
|
+
return;
|
|
320
|
+
_sweepSuspended = true;
|
|
321
|
+
syncSweepTimer();
|
|
322
|
+
}
|
|
323
|
+
/** 恢复扫光(介入面板退出时调):若仍有在飞批则心跳立即重建;无在飞批则保持停止。幂等。 */
|
|
324
|
+
export function resumeSweep() {
|
|
325
|
+
if (!_sweepSuspended)
|
|
326
|
+
return;
|
|
327
|
+
_sweepSuspended = false;
|
|
328
|
+
syncSweepTimer();
|
|
329
|
+
}
|
|
330
|
+
/** 测试钩子:扫光当前是否处于外部暂停态(介入面板泄漏回归用)。 */
|
|
331
|
+
export const __sweepTest = {
|
|
332
|
+
isSuspended: () => _sweepSuspended,
|
|
333
|
+
timerActive: () => _sweepTimer !== null,
|
|
334
|
+
};
|
|
298
335
|
/**
|
|
299
336
|
* 计算本帧高亮带覆盖的字符区间 `[start, end)`(纯函数,导出供帧推进/带宽测试)。
|
|
300
337
|
*
|
|
@@ -544,16 +581,16 @@ export function endBatch(id, layout) {
|
|
|
544
581
|
absLineToBatchId.set(b.summaryAbsIdx, b.id);
|
|
545
582
|
return;
|
|
546
583
|
}
|
|
547
|
-
// 子批尚未落盘(
|
|
548
|
-
//
|
|
584
|
+
// 子批尚未落盘(父批折叠期间被隐藏 / 折叠后才新建)。绝不能 contentWrite 到 buffer 末尾:
|
|
585
|
+
// 那会在正文区留下一条游离的子批摘要行,父批再展开时就变成「多出来的第三条」。
|
|
549
586
|
if (b.parentId) {
|
|
550
587
|
const parent = batches.get(b.parentId);
|
|
551
|
-
if (parent
|
|
588
|
+
if (parent) {
|
|
552
589
|
// 父批折叠中:不渲染,等 expand() 统一恢复(那时会用最新状态重建摘要行)。
|
|
553
590
|
if (!expandedBatches.has(parent.id) || parent.summaryAbsIdx < 0)
|
|
554
591
|
return;
|
|
555
|
-
//
|
|
556
|
-
const anchor =
|
|
592
|
+
// 父批已展开:插到对应的内部 sub-agent entry 行下方。
|
|
593
|
+
const anchor = childBatchAnchor(parent, b.groupChildIndex);
|
|
557
594
|
layout.contentInsertAfter(anchor, [sanitizeRow(buildSummaryLine(b))]);
|
|
558
595
|
b.summaryAbsIdx = anchor + 1;
|
|
559
596
|
absLineToBatchId.set(b.summaryAbsIdx, b.id);
|
|
@@ -584,29 +621,15 @@ export function showLiveBatch(id, layout) {
|
|
|
584
621
|
// 让「子 agent 的工具明细」始终跟在自己的父调用行下——并行派发时才不串行。
|
|
585
622
|
const parent = b.parentId ? batches.get(b.parentId) : undefined;
|
|
586
623
|
if (parent && parent.summaryAbsIdx >= 0 && layout.contentInsertAfter) {
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
if (
|
|
624
|
+
// 父批(组容器批或普通子 agent 批)折叠时,子批摘要行先不渲染;
|
|
625
|
+
// 等父批展开时由 expand 统一恢复,避免摘要残留/重复行。
|
|
626
|
+
if (!expandedBatches.has(parent.id)) {
|
|
590
627
|
b.summaryAbsIdx = -1;
|
|
591
628
|
return;
|
|
592
629
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
// 不能用固定偏移 summaryAbsIdx+1+childIndex——前面兄弟子批的内容会把它后面的
|
|
597
|
-
// entry 行整体下移,固定偏移会错位;用 absLineToEntry 登记的真实绝对索引。
|
|
598
|
-
let entryAbs = parent.summaryAbsIdx + 1 + b.groupChildIndex;
|
|
599
|
-
for (const [idx, target] of absLineToEntry) {
|
|
600
|
-
if (target.batchId === parent.id && target.entryIndex === b.groupChildIndex) {
|
|
601
|
-
entryAbs = idx;
|
|
602
|
-
break;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
anchor = entryAbs;
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
anchor = parent.summaryAbsIdx + (expandedBatches.has(parent.id) ? parent.renderedCount : 0);
|
|
609
|
-
}
|
|
630
|
+
// 父批已展开:插到本批对应的内部 sub-agent entry 行下方(按真实登记位置,
|
|
631
|
+
// 不用固定偏移——兄弟内容会把后续行整体下移);找不到 entry 时退化到父批块末。
|
|
632
|
+
const anchor = childBatchAnchor(parent, b.groupChildIndex);
|
|
610
633
|
layout.contentInsertAfter(anchor, [sanitizeRow(summary)], false);
|
|
611
634
|
b.summaryAbsIdx = anchor + 1;
|
|
612
635
|
absLineToBatchId.set(b.summaryAbsIdx, b.id); // 运行态同样开放点击(endBatch 幂等重登)
|
|
@@ -687,43 +710,17 @@ export function refreshBatchExpanded(id, layout) {
|
|
|
687
710
|
const newEntries = b.entries.slice(b.renderedCount);
|
|
688
711
|
const lines = buildExpandedLines(b, b.indent ?? '', b.renderedCount);
|
|
689
712
|
// 实时追加:不锚定视口,让新明细行自然出现在屏底。
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
//
|
|
713
|
+
// 锚点必须是本批已渲染块的真实最末行——不能简单用 summaryAbsIdx+renderedCount:
|
|
714
|
+
// ①toggleEntry 展开的二层明细行会把后续行下移;
|
|
715
|
+
// ②挂在内部 sub-agent entry 下的嵌套子批(摘要 + 其展开内容)也在本批块内。
|
|
716
|
+
// 组容器批与普通子 agent 批走同一计算。
|
|
694
717
|
let anchor = b.summaryAbsIdx + b.renderedCount;
|
|
695
718
|
for (const j of b.expandedEntries) {
|
|
696
719
|
if (j < b.renderedCount) {
|
|
697
720
|
anchor += buildEntryDetailLines(b.entries[j], entryDetailIndent(b.indent ?? '')).length;
|
|
698
721
|
}
|
|
699
722
|
}
|
|
700
|
-
|
|
701
|
-
let maxIdx = anchor;
|
|
702
|
-
for (const [idx, target] of absLineToEntry) {
|
|
703
|
-
if (target.batchId !== b.id)
|
|
704
|
-
continue;
|
|
705
|
-
// entry 行自身(含其二层明细)的块末位置
|
|
706
|
-
const end = b.expandedEntries.has(target.entryIndex)
|
|
707
|
-
? idx + buildEntryDetailLines(b.entries[target.entryIndex], entryDetailIndent(b.indent ?? '')).length
|
|
708
|
-
: idx;
|
|
709
|
-
if (end > maxIdx)
|
|
710
|
-
maxIdx = end;
|
|
711
|
-
}
|
|
712
|
-
for (const child of batches.values()) {
|
|
713
|
-
if (child.parentId === b.id && child.summaryAbsIdx >= 0) {
|
|
714
|
-
let childEnd = child.summaryAbsIdx;
|
|
715
|
-
if (expandedBatches.has(child.id)) {
|
|
716
|
-
childEnd += child.renderedCount;
|
|
717
|
-
for (const j of child.expandedEntries) {
|
|
718
|
-
childEnd += buildEntryDetailLines(child.entries[j], entryDetailIndent(child.indent ?? '')).length;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
if (childEnd > maxIdx)
|
|
722
|
-
maxIdx = childEnd;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
anchor = maxIdx;
|
|
726
|
-
}
|
|
723
|
+
anchor = blockEndAbsIdx(b, anchor);
|
|
727
724
|
layout.contentInsertAfter(anchor, lines, false);
|
|
728
725
|
// 登记新增明细行的点击命中(按实际插入位置)
|
|
729
726
|
for (let i = 0; i < newEntries.length; i++) {
|
|
@@ -761,15 +758,15 @@ function expand(b, layout, live = false) {
|
|
|
761
758
|
absLineToEntry.set(b.summaryAbsIdx + 1 + i, { batchId: b.id, entryIndex: i });
|
|
762
759
|
b.entries[i].renderedDigest = entryLineDigest(b.entries[i]); // 记指纹,供运行态结果回填原位刷新
|
|
763
760
|
}
|
|
764
|
-
//
|
|
761
|
+
// 展开时:把之前被折叠隐藏的子批摘要行重新插回对应的内部 sub-agent entry 下方,
|
|
765
762
|
// 否则子批摘要行会留在父批摘要行之后、造成明细重复/错位。
|
|
766
|
-
|
|
763
|
+
// 组容器批与普通子 agent 批同一路径(嵌套孙批保持折叠,逐层展开)。
|
|
764
|
+
{
|
|
767
765
|
const children = [...batches.values()]
|
|
768
766
|
.filter((x) => x.parentId === b.id)
|
|
769
|
-
.sort((
|
|
767
|
+
.sort((x, y) => (x.groupChildIndex ?? 0) - (y.groupChildIndex ?? 0));
|
|
770
768
|
for (const child of children) {
|
|
771
|
-
const
|
|
772
|
-
const anchor = entryAbs ?? b.summaryAbsIdx + b.renderedCount;
|
|
769
|
+
const anchor = childBatchAnchor(b, child.groupChildIndex);
|
|
773
770
|
const summary = buildSummaryLine(child, true);
|
|
774
771
|
layout.contentInsertAfter(anchor, [sanitizeRow(summary)], !live);
|
|
775
772
|
child.summaryAbsIdx = anchor + 1;
|
|
@@ -784,6 +781,75 @@ function findParentEntryAbsLine(parentId, entryIndex) {
|
|
|
784
781
|
}
|
|
785
782
|
return null;
|
|
786
783
|
}
|
|
784
|
+
/** 子批挂到父批(组容器批或普通子 agent 批)时的插入锚点:
|
|
785
|
+
* 优先取该子批对应的内部 sub-agent entry 真实行;未登记则退化到父批已渲染块末。 */
|
|
786
|
+
function childBatchAnchor(parent, childIndex) {
|
|
787
|
+
if (childIndex != null) {
|
|
788
|
+
const entryAbs = findParentEntryAbsLine(parent.id, childIndex);
|
|
789
|
+
if (entryAbs != null)
|
|
790
|
+
return entryAbs;
|
|
791
|
+
}
|
|
792
|
+
let end = parent.summaryAbsIdx + parent.renderedCount;
|
|
793
|
+
for (const j of parent.expandedEntries) {
|
|
794
|
+
end += buildEntryDetailLines(parent.entries[j], entryDetailIndent(parent.indent ?? '')).length;
|
|
795
|
+
}
|
|
796
|
+
return blockEndAbsIdx(parent, end);
|
|
797
|
+
}
|
|
798
|
+
/** 批已渲染块的真实最末绝对行:把直接挂在本批下的子批(摘要行;若子批展开,
|
|
799
|
+
* 还含其明细行与再嵌套子批)全部计入,递归得到整棵树的块末。 */
|
|
800
|
+
function blockEndAbsIdx(b, initial) {
|
|
801
|
+
let maxIdx = initial;
|
|
802
|
+
for (const child of batches.values()) {
|
|
803
|
+
if (child.parentId !== b.id || child.summaryAbsIdx < 0)
|
|
804
|
+
continue;
|
|
805
|
+
let childEnd = child.summaryAbsIdx;
|
|
806
|
+
if (expandedBatches.has(child.id)) {
|
|
807
|
+
childEnd += child.renderedCount;
|
|
808
|
+
for (const j of child.expandedEntries) {
|
|
809
|
+
childEnd += buildEntryDetailLines(child.entries[j], entryDetailIndent(child.indent ?? '')).length;
|
|
810
|
+
}
|
|
811
|
+
childEnd = blockEndAbsIdx(child, childEnd);
|
|
812
|
+
}
|
|
813
|
+
if (childEnd > maxIdx)
|
|
814
|
+
maxIdx = childEnd;
|
|
815
|
+
}
|
|
816
|
+
return maxIdx;
|
|
817
|
+
return maxIdx;
|
|
818
|
+
}
|
|
819
|
+
/** 折叠父批的所有直接子批(递归):返回这些子批在 buffer 中占据的总行数。
|
|
820
|
+
* 未落盘子批(父批折叠期间新建)在 buffer 无对应行,只清状态不计数,避免多删相邻正文。 */
|
|
821
|
+
function foldChildren(parentId) {
|
|
822
|
+
let count = 0;
|
|
823
|
+
for (const child of batches.values()) {
|
|
824
|
+
if (child.parentId !== parentId)
|
|
825
|
+
continue;
|
|
826
|
+
if (child.summaryAbsIdx < 0) {
|
|
827
|
+
child.renderedCount = 0;
|
|
828
|
+
child.expandedEntries.clear();
|
|
829
|
+
expandedBatches.delete(child.id);
|
|
830
|
+
foldChildren(child.id);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
count += 1; // 子批摘要行本身
|
|
834
|
+
if (expandedBatches.has(child.id)) {
|
|
835
|
+
count += child.renderedCount;
|
|
836
|
+
for (const j of child.expandedEntries) {
|
|
837
|
+
count += buildEntryDetailLines(child.entries[j], entryDetailIndent(child.indent ?? '')).length;
|
|
838
|
+
}
|
|
839
|
+
count += foldChildren(child.id);
|
|
840
|
+
child.expandedEntries.clear();
|
|
841
|
+
expandedBatches.delete(child.id);
|
|
842
|
+
}
|
|
843
|
+
child.renderedCount = 0;
|
|
844
|
+
absLineToBatchId.delete(child.summaryAbsIdx);
|
|
845
|
+
child.summaryAbsIdx = -1;
|
|
846
|
+
for (const [idx, target] of absLineToEntry) {
|
|
847
|
+
if (target.batchId === child.id)
|
|
848
|
+
absLineToEntry.delete(idx);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return count;
|
|
852
|
+
}
|
|
787
853
|
/** mutation 独占 batch 收尾后立即展示其调用概要和 diff。 */
|
|
788
854
|
export function expandSingleEntryFully(id, layout) {
|
|
789
855
|
const b = batches.get(id);
|
|
@@ -815,38 +881,9 @@ function collapse(b, layout) {
|
|
|
815
881
|
for (const i of b.expandedEntries) {
|
|
816
882
|
lineCount += buildEntryDetailLines(b.entries[i], entryDetailIndent(b.indent ?? '')).length;
|
|
817
883
|
}
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
const children = [...batches.values()].filter((x) => x.parentId === b.id);
|
|
822
|
-
for (const child of children) {
|
|
823
|
-
// 未落盘的子批(父批折叠期间新建)在 buffer 里没有对应行,计进 lineCount 会多删相邻正文。
|
|
824
|
-
if (child.summaryAbsIdx < 0) {
|
|
825
|
-
child.renderedCount = 0;
|
|
826
|
-
child.expandedEntries.clear();
|
|
827
|
-
expandedBatches.delete(child.id);
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
lineCount += 1; // 子批摘要行本身
|
|
831
|
-
if (expandedBatches.has(child.id)) {
|
|
832
|
-
// 子批自身展开时,它的第一层明细行(renderedCount)也在父批块内,必须一并计入,
|
|
833
|
-
// 否则删少了会留下孤儿明细行。
|
|
834
|
-
lineCount += child.renderedCount;
|
|
835
|
-
for (const j of child.expandedEntries) {
|
|
836
|
-
lineCount += buildEntryDetailLines(child.entries[j], entryDetailIndent(child.indent ?? '')).length;
|
|
837
|
-
}
|
|
838
|
-
child.expandedEntries.clear();
|
|
839
|
-
expandedBatches.delete(child.id);
|
|
840
|
-
}
|
|
841
|
-
child.renderedCount = 0;
|
|
842
|
-
absLineToBatchId.delete(child.summaryAbsIdx);
|
|
843
|
-
child.summaryAbsIdx = -1;
|
|
844
|
-
for (const [idx, target] of absLineToEntry) {
|
|
845
|
-
if (target.batchId === child.id)
|
|
846
|
-
absLineToEntry.delete(idx);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
884
|
+
// 折叠时:递归移除挂在本批下的整棵子批树(子批摘要行 + 已展开明细),
|
|
885
|
+
// 否则父批再次展开后子批摘要/明细仍残留,出现重复行。组容器批与普通子 agent 批同一路径。
|
|
886
|
+
lineCount += foldChildren(b.id);
|
|
850
887
|
layout.contentDeleteFrom(b.summaryAbsIdx + 1, lineCount);
|
|
851
888
|
expandedBatches.delete(b.id);
|
|
852
889
|
b.renderedCount = 0;
|
|
@@ -867,11 +904,10 @@ export function toggleEntry(batchId, entryIndex, layout) {
|
|
|
867
904
|
const b = batches.get(batchId);
|
|
868
905
|
if (!b || !expandedBatches.has(batchId))
|
|
869
906
|
return;
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
if (b.
|
|
874
|
-
// 如果该 sub-agent entry 没有可展开的详情,fallback 到 toggle 子批工具列表。
|
|
907
|
+
// sub-agent entry(组容器批的顶层调用,或子 agent 内部嵌套派生)对应一个子批:
|
|
908
|
+
// 点击 entry 行优先展开/折叠该 entry 自身详情(子 agent 返回的完整文本输出);
|
|
909
|
+
// 只有没有自身详情时,才 fallback 到 toggle 对应子批的工具列表。
|
|
910
|
+
if (b.entries[entryIndex]?.name === 'sub-agent') {
|
|
875
911
|
const details = buildEntryDetailLines(b.entries[entryIndex], entryDetailIndent(b.indent ?? ''));
|
|
876
912
|
if (details.length === 0) {
|
|
877
913
|
const child = [...batches.values()].find((x) => x.parentId === batchId && x.groupChildIndex === entryIndex);
|
package/dist/ui/composer.js
CHANGED
|
@@ -344,22 +344,6 @@ export async function promptComposer(opts = {}) {
|
|
|
344
344
|
}
|
|
345
345
|
return rows.length - 1;
|
|
346
346
|
}
|
|
347
|
-
/** 上下移动一行(按展示行)。 */
|
|
348
|
-
function moveVertically(delta, extend) {
|
|
349
|
-
const rows = dispRows();
|
|
350
|
-
const idx = cursorDispIdx(rows);
|
|
351
|
-
const target = Math.max(0, Math.min(rows.length - 1, idx + delta));
|
|
352
|
-
const r = rows[target];
|
|
353
|
-
if (!r)
|
|
354
|
-
return;
|
|
355
|
-
// 保持视觉列:当前光标视觉列 → 目标展示行同列
|
|
356
|
-
const curRow = rows[idx];
|
|
357
|
-
const curText = curRow ? curRow.text : '';
|
|
358
|
-
const curOff = cur.col - (curRow ? curRow.start : 0);
|
|
359
|
-
const vis = displayWidth([...curText].slice(0, Math.max(0, curOff)).join(''));
|
|
360
|
-
const charCol = visColToCharCol(r.text, vis);
|
|
361
|
-
moveTo({ line: r.li, col: r.start + charCol }, extend);
|
|
362
|
-
}
|
|
363
347
|
function moveByPage(delta, extend) {
|
|
364
348
|
const rows = dispRows();
|
|
365
349
|
const visible = Math.max(1, textRowCount());
|
|
@@ -716,10 +700,12 @@ export async function promptComposer(opts = {}) {
|
|
|
716
700
|
}
|
|
717
701
|
return;
|
|
718
702
|
case 'up':
|
|
719
|
-
|
|
703
|
+
// ↑ 始终跳到全文最前(Shift 扩展选区)
|
|
704
|
+
moveTo({ line: 0, col: 0 }, extend);
|
|
720
705
|
return;
|
|
721
706
|
case 'down':
|
|
722
|
-
|
|
707
|
+
// ↓ 始终跳到全文最后(Shift 扩展选区)
|
|
708
|
+
moveTo({ line: lines.length - 1, col: lineLen(lines.length - 1) }, extend);
|
|
723
709
|
return;
|
|
724
710
|
case 'home':
|
|
725
711
|
if (key.ctrl) {
|