mocode-ai 1.7.9 → 1.7.11

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.
@@ -46,7 +46,7 @@ class BoundedCommandOutput {
46
46
  }
47
47
  }
48
48
  /** Execute a command with the same sandbox, output cap and cancellation semantics as run_command. */
49
- export async function runCommandRaw(command, timeout = DEFAULT_COMMAND_TIMEOUT_MS, signal, cwd, shell) {
49
+ export async function runCommandRaw(command, timeout = DEFAULT_COMMAND_TIMEOUT_MS, signal, cwd, shell, onOutput) {
50
50
  const startedAt = Date.now();
51
51
  // 内部调用方(skill 注入等)也走同一钳制:防止任何路径把前台命令挂成无限期持锁。
52
52
  const effectiveTimeout = clampCommandTimeout(timeout);
@@ -124,7 +124,16 @@ export async function runCommandRaw(command, timeout = DEFAULT_COMMAND_TIMEOUT_M
124
124
  const onAbort = () => {
125
125
  terminate('aborted');
126
126
  };
127
- const onChunk = (chunk) => output.append(chunk.toString('utf8'));
127
+ const onChunk = (chunk) => {
128
+ const text = chunk.toString('utf8');
129
+ output.append(text);
130
+ try {
131
+ onOutput?.(text);
132
+ }
133
+ catch {
134
+ // Output observers must not affect the command process lifecycle.
135
+ }
136
+ };
128
137
  // 先起 timer 再挂事件:finish/onAbort 是闭包,里面要 clearTimeout(timer)。
129
138
  // 把 timer 的初始化提到所有引用它的注册点之前,避免依赖"事件回调必然异步"这一前提。
130
139
  const timer = setTimeout(() => {
@@ -221,7 +230,7 @@ export const runCommandTool = {
221
230
  }
222
231
  shell = parsed;
223
232
  }
224
- const result = await runCommandRaw(command, timeout, ctx?.signal, undefined, shell);
233
+ const result = await runCommandRaw(command, timeout, ctx?.signal, undefined, shell, ctx?.onOutput);
225
234
  const outcome = commandOutcome(result);
226
235
  // 钳制要显式告知:模型传了 30min 却按 10min 判超时,不说清它会以为是环境抽风而盲目重试。
227
236
  if (args.timeout !== undefined && Number(args.timeout) !== timeout && result.status === 'timed_out') {
@@ -29,6 +29,10 @@ export const MAX_OLD_TOOL_STUB = 600;
29
29
  export const MAX_TRACE_BYTES = 512 * 1024;
30
30
  /** 保留的历史 trace 轮转份数(trace.1 .. trace.N),更老的硬删。 */
31
31
  export const MAX_TRACE_ROTATIONS = 3;
32
+ /** compacted/ 压缩归档目录总量上限(字节):旧区原文逐段累积,超了从最老 seg 淘汰。
33
+ * 段大小与窗口正相关(200k 窗口单段约几百 KB),10MiB 约覆盖十几~几十次压缩。
34
+ * 可用 MOCODE_MAX_COMPACTED_BYTES 覆盖(测试/小盘用)。 */
35
+ export const MAX_COMPACTED_ARCHIVE_BYTES = 10 * 1024 * 1024;
32
36
  /** rollback snapshots 压缩前滚动窗口:无论是否 compact,仅保留最近 N 个 user turn 的快照。 */
33
37
  export const MAX_SNAPSHOT_TURNS = 50;
34
38
  // ── 上下文压缩摘要(见 session/compact.ts defaultSummarize)────────────────
@@ -69,6 +69,9 @@ export function runRipgrep(params, signal) {
69
69
  // --no-require-git:rg 默认只在 git 仓库内才读 .gitignore;加上后非 git 项目的 .gitignore
70
70
  // 同样生效(实测不加时 ignored/ 会被扫到)。
71
71
  const args = ['--json', '--hidden', '--no-require-git', '--path-separator', '/'];
72
+ // glob 显式指向 gitignore 内的 .mocode(回捞压缩归档等)时放开 VCS ignore。
73
+ if (params.noIgnoreVcs)
74
+ args.push('--no-ignore-vcs');
72
75
  if (params.context > 0)
73
76
  args.push('-C', String(params.context));
74
77
  if (params.glob && params.glob !== '**/*')
@@ -169,7 +172,14 @@ export function runRipgrep(params, signal) {
169
172
  handleLine(buffer.trim());
170
173
  // exit 2 + stderr = 正则/参数错误;无命中事件时按 regex_error 上报。
171
174
  if (code === 2 && byPath.size === 0 && stderr.trim()) {
172
- finish({ status: 'regex_error', message: stderr.trim() });
175
+ const message = stderr.trim();
176
+ // 过滤器把所有文件排除(典型:glob 指向 .gitignore 内的 .mocode 却没放开 ignore)
177
+ // 不是正则错误——按无匹配处理,避免误导模型去改 pattern。
178
+ if (/No files were searched/i.test(message)) {
179
+ finish({ status: 'no-match', scanned, truncated });
180
+ return;
181
+ }
182
+ finish({ status: 'regex_error', message });
173
183
  return;
174
184
  }
175
185
  if (byPath.size === 0) {
@@ -290,6 +290,7 @@ export class ToolRuntime {
290
290
  raw = await tool.execute(args, {
291
291
  signal,
292
292
  callId: opts?.callId,
293
+ onOutput: opts?.onOutput,
293
294
  allowedToolNames: opts?.allowedToolNames,
294
295
  delegation: opts?.delegation,
295
296
  readDedup: opts?.readDedup,
package/dist/ui/batch.js CHANGED
@@ -38,6 +38,8 @@ const callToBatch = new Map();
38
38
  const groupChildIndexByCall = new Map();
39
39
  /** 展开时完整输出的最大行数;超出截断,避免巨型输出撑爆 viewport。 */
40
40
  const MAX_EXPAND_LINES = 200;
41
+ const MAX_LIVE_OUTPUT_CHARS = 256 * 1024;
42
+ const LIVE_OUTPUT_REFRESH_MS = 80;
41
43
  /** 自洽行允许的最大显示宽(= 终端 cols)。buffer 行超 cols 会被终端 auto-wrap,
42
44
  * 物理行与缓冲行失配 → repaintViewport 的 CUP 寻址全错(屏幕错乱)。 */
43
45
  let maxCols = 200;
@@ -66,6 +68,12 @@ export function isMutationToolName(name) {
66
68
  }
67
69
  /** 通知 buffer 整体清空(clearContent / exitAltScreen / 新一轮 turn)——本模块状态同步归零。 */
68
70
  export function reset() {
71
+ for (const record of batches.values()) {
72
+ for (const entry of record.entries) {
73
+ if (entry.liveRefreshTimer)
74
+ clearTimeout(entry.liveRefreshTimer);
75
+ }
76
+ }
69
77
  batches.clear();
70
78
  absLineToBatchId.clear();
71
79
  absLineToEntry.clear();
@@ -154,7 +162,61 @@ export function isBatchComplete(id) {
154
162
  /** 记一条工具结果(diff 块或单行 preview);agent 在 onToolResult 时调,匹配最后一条未填的 entry。
155
163
  * fullOutput:工具原始完整输出(纯文本),展开时显示;mutation 工具的 diff 块已自含无需传。
156
164
  * callId:组容器批用,直接把结果填到对应 entry(避免多个 sub-agent 同名时反向匹配错位)。 */
157
- export function recordResult(id, name, resultSummary, diffBlock, fullOutput, failed = false, callId) {
165
+ function refreshEntryDetails(b, entryIndex, layout) {
166
+ if (!expandedBatches.has(b.id) || !b.expandedEntries.has(entryIndex))
167
+ return;
168
+ const headerIdx = [...absLineToEntry].find(([, target]) => target.batchId === b.id && target.entryIndex === entryIndex)?.[0];
169
+ if (headerIdx == null)
170
+ return;
171
+ const entry = b.entries[entryIndex];
172
+ const oldCount = entry.renderedDetailCount ?? 0;
173
+ const lines = buildEntryDetailLines(entry, entryDetailIndent(b.indent ?? ''));
174
+ const replacement = lines.slice(0, MAX_EXPAND_LINES);
175
+ if (oldCount > 0)
176
+ layout.contentDeleteFrom(headerIdx + 1, oldCount);
177
+ if (replacement.length > 0)
178
+ layout.contentInsertAfter(headerIdx, replacement, false);
179
+ entry.renderedDetailCount = replacement.length;
180
+ entry.renderedDigest = entryLineDigest(entry);
181
+ layout.contentReplaceLine(headerIdx, buildEntryLine(b, entryIndex, b.indent ?? ''));
182
+ }
183
+ export function recordOutput(callId, chunk, layout) {
184
+ const batchId = callToBatch.get(callId);
185
+ const b = batchId ? batches.get(batchId) : undefined;
186
+ const entryIndex = groupChildIndexByCall.get(callId);
187
+ if (!b || entryIndex == null || b.entries[entryIndex]?.name !== 'run_command')
188
+ return;
189
+ const entry = b.entries[entryIndex];
190
+ entry.liveOutput = `${entry.liveOutput ?? ''}${chunk}`;
191
+ if (entry.liveOutput.length > MAX_LIVE_OUTPUT_CHARS) {
192
+ entry.liveOutput = entry.liveOutput.slice(-MAX_LIVE_OUTPUT_CHARS);
193
+ }
194
+ if (!expandedBatches.has(b.id))
195
+ return;
196
+ if (!b.expandedEntries.has(entryIndex)) {
197
+ const headerIdx = [...absLineToEntry].find(([, target]) => target.batchId === b.id && target.entryIndex === entryIndex)?.[0];
198
+ if (headerIdx != null) {
199
+ entry.renderedDigest = entryLineDigest(entry);
200
+ layout.contentReplaceLine(headerIdx, buildEntryLine(b, entryIndex, b.indent ?? ''));
201
+ }
202
+ return;
203
+ }
204
+ if (entry.liveRefreshTimer)
205
+ return;
206
+ const refresh = () => {
207
+ entry.liveRefreshTimer = undefined;
208
+ entry.liveRefreshAt = Date.now();
209
+ refreshEntryDetails(b, entryIndex, layout);
210
+ };
211
+ const wait = Math.max(0, LIVE_OUTPUT_REFRESH_MS - (Date.now() - (entry.liveRefreshAt ?? 0)));
212
+ if (wait === 0)
213
+ refresh();
214
+ else {
215
+ entry.liveRefreshTimer = setTimeout(refresh, wait);
216
+ entry.liveRefreshTimer.unref();
217
+ }
218
+ }
219
+ export function recordResult(id, name, resultSummary, diffBlock, fullOutput, failed = false, callId, layout) {
158
220
  const b = batches.get(id);
159
221
  if (!b || b.entries.length === 0)
160
222
  return;
@@ -168,8 +230,14 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput, fai
168
230
  e.fullOutput = fullOutput;
169
231
  e.failed = failed;
170
232
  e.done = true;
233
+ if (e.liveRefreshTimer)
234
+ clearTimeout(e.liveRefreshTimer);
235
+ e.liveRefreshTimer = undefined;
236
+ e.liveOutput = undefined;
171
237
  if (b.entries.every(isEntryDone))
172
238
  b.finishedAt = Date.now();
239
+ if (layout)
240
+ refreshEntryDetails(b, idx, layout);
173
241
  return;
174
242
  }
175
243
  }
@@ -181,8 +249,14 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput, fai
181
249
  b.entries[i].fullOutput = fullOutput;
182
250
  b.entries[i].failed = failed;
183
251
  b.entries[i].done = true;
252
+ if (b.entries[i].liveRefreshTimer)
253
+ clearTimeout(b.entries[i].liveRefreshTimer);
254
+ b.entries[i].liveRefreshTimer = undefined;
255
+ b.entries[i].liveOutput = undefined;
184
256
  if (b.entries.every(isEntryDone))
185
257
  b.finishedAt = Date.now();
258
+ if (layout)
259
+ refreshEntryDetails(b, i, layout);
186
260
  return;
187
261
  }
188
262
  }
@@ -194,8 +268,14 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput, fai
194
268
  last.fullOutput = fullOutput;
195
269
  last.failed = failed;
196
270
  last.done = true;
271
+ if (last.liveRefreshTimer)
272
+ clearTimeout(last.liveRefreshTimer);
273
+ last.liveRefreshTimer = undefined;
274
+ last.liveOutput = undefined;
197
275
  if (b.entries.every(isEntryDone))
198
276
  b.finishedAt = Date.now();
277
+ if (layout)
278
+ refreshEntryDetails(b, b.entries.length - 1, layout);
199
279
  }
200
280
  }
201
281
  // ── 摘要行文本生成 ──
@@ -465,11 +545,10 @@ function buildEntryDetailLines(e, indent = DETAIL_INDENT) {
465
545
  lines.push(sanitizeRow(prefixed));
466
546
  }
467
547
  }
468
- else if (e.fullOutput) {
469
- // 完整工具输出(纯文本):按行展开,每行缩进 + dim 样式;长输出截断到 MAX_EXPAND_LINES 行。
470
- // 每行经 sanitizeRow 钳宽:fullOutput 可能含 grep 扫二进制(db/压缩文件)得到的
471
- // 超长行 + 控制字符,不钳会让终端 auto-wrap 打乱屏位。
472
- const rawLines = e.fullOutput.split('\n');
548
+ else if (e.liveOutput || e.fullOutput) {
549
+ // 实时输出优先展示;结束后由 recordResult 替换成最终有界工具结果。
550
+ // 每行 sanitizeRow 会清除控制字符并钳宽,避免 ANSI/终端控制序列污染 TUI。
551
+ const rawLines = (e.liveOutput || e.fullOutput || '').split(/\r\n|\n|\r/);
473
552
  const truncated = rawLines.length > MAX_EXPAND_LINES;
474
553
  const displayLines = truncated ? rawLines.slice(0, MAX_EXPAND_LINES) : rawLines;
475
554
  for (const line of displayLines) {
@@ -493,7 +572,7 @@ function buildEntryDetailLines(e, indent = DETAIL_INDENT) {
493
572
  * 只取有无不取内容:完整输出可能上万字符,拼进指纹是浪费,且其内容不影响 entry 行渲染。
494
573
  */
495
574
  function entryLineDigest(e) {
496
- return `${e.resultSummary}\u0001${e.diffBlock ? '1' : '0'}\u0001${e.fullOutput ? '1' : '0'}\u0001${e.failed ? '1' : '0'}`;
575
+ return `${e.resultSummary}\u0001${e.diffBlock ? '1' : '0'}\u0001${e.fullOutput || e.liveOutput ? '1' : '0'}\u0001${e.failed ? '1' : '0'}`;
497
576
  }
498
577
  /**
499
578
  * 第二层展开三角:折叠 ▸ / 展开 ▾。替代原先的 ├─ / └─。
@@ -537,7 +616,7 @@ const DETAIL_RESULT_MARK = '· ';
537
616
  /** entry 是否有可展开的详情行。须与 buildEntryDetailLines 的分支一致,
538
617
  * 否则会画出点了没反应的三角(toggleEntry 对空 details 直接 return)。 */
539
618
  function hasEntryDetail(e) {
540
- return Boolean(e.diffBlock || e.fullOutput || e.resultSummary);
619
+ return Boolean(e.diffBlock || e.fullOutput || e.liveOutput || e.resultSummary);
541
620
  }
542
621
  /** entry 行的展开三角(含尾随空格与配色)。折叠 dim、展开 accent,一眼区分当前态。 */
543
622
  function entryCaret(e, expanded) {
@@ -938,9 +1017,11 @@ export function toggleEntry(batchId, entryIndex, layout) {
938
1017
  layout.contentReplaceLine(headerIdx, buildEntryLine(b, entryIndex, b.indent ?? ''));
939
1018
  if (wasExpanded) {
940
1019
  layout.contentDeleteFrom(headerIdx + 1, details.length);
1020
+ b.entries[entryIndex].renderedDetailCount = 0;
941
1021
  }
942
1022
  else {
943
1023
  layout.contentInsertAfter(headerIdx, details);
1024
+ b.entries[entryIndex].renderedDetailCount = Math.min(details.length, MAX_EXPAND_LINES);
944
1025
  }
945
1026
  }
946
1027
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.7.9",
3
+ "version": "1.7.11",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 27 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {