mocode-ai 1.7.9 → 1.7.10

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.
@@ -130,7 +130,7 @@ function writeToolHeader(tc, toolRuntime) {
130
130
  const id = batch.beginBatch();
131
131
  currentBatchId = id;
132
132
  batch.bindCall(tc.id, id);
133
- batch.recordCall(id, tc.name, summarizeToolCall(tc.name, tc.arguments));
133
+ batch.recordCall(id, tc.name, summarizeToolCall(tc.name, tc.arguments), tc.id);
134
134
  batch.showLiveBatch(id, layout);
135
135
  }
136
136
  else {
@@ -143,7 +143,7 @@ function writeToolHeader(tc, toolRuntime) {
143
143
  // 结果按 tool_call id 归位:并行执行时 currentBatchId 会漂移,只按“当前批”回填会漏填,
144
144
  // 摘要行就永远停在 ◇(用户实测:子 agent 跑完主侧菱形没变成实心圆)。
145
145
  batch.bindCall(tc.id, id);
146
- batch.recordCall(id, tc.name, summarizeToolCall(tc.name, tc.arguments));
146
+ batch.recordCall(id, tc.name, summarizeToolCall(tc.name, tc.arguments), tc.id);
147
147
  // 第一条工具开始时立即落摘要;后续调用加入同一 batch,并原地刷新计数。
148
148
  batch.showLiveBatch(id, layout);
149
149
  }
@@ -160,7 +160,7 @@ function writeToolResult(tc, output, parsed, preWriteOld, editStartLine, toolRun
160
160
  // 无需独立批。全部 entry 完成后收口组容器批(落完成态 + 开放点击展开),
161
161
  // 并与后续内容约定一条分隔空行。
162
162
  const preview = summarizeToolResult(tc.name, output);
163
- batch.recordResult(batchId, tc.name, preview, null, output, isToolErrorOutput(output), tc.id);
163
+ batch.recordResult(batchId, tc.name, preview, null, output, isToolErrorOutput(output), tc.id, layout);
164
164
  if (batch.isBatchComplete(batchId)) {
165
165
  batch.endBatch(batchId, layout);
166
166
  subAgentGroupId = null;
@@ -189,7 +189,7 @@ function writeToolResult(tc, output, parsed, preWriteOld, editStartLine, toolRun
189
189
  });
190
190
  }
191
191
  const preview = diff ? '' : summarizeToolResult(tc.name, output);
192
- batch.recordResult(batchId, tc.name, preview, diff, output, isToolErrorOutput(output));
192
+ batch.recordResult(batchId, tc.name, preview, diff, output, isToolErrorOutput(output), tc.id, layout);
193
193
  batch.showLiveBatch(batchId, layout);
194
194
  // mutation 结果(成功 diff 或错误输出)立即可见,并阻止后续普通工具并入这一批。
195
195
  if (isMutationTool(tc.name, toolRuntime))
@@ -404,6 +404,7 @@ runtimeOrContext = defaultRuntime) {
404
404
  writeToolHeader(tc, runtime.context.toolRuntime);
405
405
  },
406
406
  onToolStart: (name) => spinner.start(t('agent.executing', { tool: name })),
407
+ onToolOutput: (tc, chunk) => batch.recordOutput(tc.id, chunk, layout),
407
408
  onToolDone: () => spinner.stop(),
408
409
  onToolResult: (tc, output, parsed, preWriteOld, editStartLine) => writeToolResult(tc, output, parsed, preWriteOld, editStartLine, runtime.context.toolRuntime),
409
410
  onToolBatchEnd: () => {
@@ -326,6 +326,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
326
326
  delegation: delegationForOrchestrator,
327
327
  readDedup,
328
328
  argumentErrorHint: (name) => argumentErrorHint(name, runtimeContextState),
329
+ onToolOutput: (call, chunk) => hooks.onToolOutput?.(call, chunk),
329
330
  ...(opts.toolPolicy
330
331
  ? {
331
332
  expandToolGroups: (groups, reason) => opts.toolPolicy.expand(groups, reason),
@@ -396,6 +397,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
396
397
  for (const index of eligibleReadonly) {
397
398
  startedReadonly[index] = ctx.toolRuntime.executeToolOutcome(calls[index].name, calls[index].arguments, signal, {
398
399
  callId: calls[index].id,
400
+ onOutput: (chunk) => hooks.onToolOutput?.(calls[index], chunk),
399
401
  allowedToolNames: currentAllowedToolNames(),
400
402
  delegation: delegationForOrchestrator(),
401
403
  readDedup,
@@ -566,6 +568,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
566
568
  hooks.onToolStart?.(batch[0].name);
567
569
  const started = batch.map((tc) => ctx.toolRuntime.executeToolOutcome(tc.name, tc.arguments, signal, {
568
570
  callId: tc.id,
571
+ onOutput: (chunk) => hooks.onToolOutput?.(tc, chunk),
569
572
  allowedToolNames: currentAllowedToolNames(),
570
573
  delegation: delegationForOrchestrator(),
571
574
  readDedup,
@@ -663,6 +666,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
663
666
  ? entry.denied
664
667
  : await ctx.toolRuntime.executeToolOutcome(entry.tc.name, entry.tc.arguments, signal, {
665
668
  callId: entry.tc.id,
669
+ onOutput: (chunk) => hooks.onToolOutput?.(entry.tc, chunk),
666
670
  allowedToolNames: currentAllowedToolNames(),
667
671
  delegation: delegationForOrchestrator(),
668
672
  readDedup,
@@ -751,6 +755,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
751
755
  const hint = argumentErrorHint(entry.tc.name, runtimeContextState);
752
756
  return ctx.toolRuntime.executeToolOutcome(entry.tc.name, entry.tc.arguments, signal, {
753
757
  callId: entry.tc.id,
758
+ onOutput: (chunk) => hooks.onToolOutput?.(entry.tc, chunk),
754
759
  allowedToolNames: currentAllowedToolNames(),
755
760
  delegation: delegationForOrchestrator(),
756
761
  readDedup,
@@ -846,6 +851,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
846
851
  const serialHint = argumentErrorHint(tc.name, runtimeContextState);
847
852
  const outcome = await ctx.toolRuntime.executeToolOutcome(tc.name, tc.arguments, signal, {
848
853
  callId: tc.id,
854
+ onOutput: (chunk) => hooks.onToolOutput?.(tc, chunk),
849
855
  allowedToolNames: currentAllowedToolNames(),
850
856
  delegation: delegationForOrchestrator(),
851
857
  readDedup,
@@ -44,6 +44,7 @@ class LegacyCompatibleToolDispatcher {
44
44
  });
45
45
  const execute = (call, hint, onLockAcquired) => toolRuntime.executeToolOutcome(call.name, call.arguments, request.signal, {
46
46
  callId: call.id,
47
+ onOutput: (chunk) => request.onToolOutput?.(call, chunk),
47
48
  allowedToolNames: request.currentAllowedToolNames(),
48
49
  delegation: request.delegation(),
49
50
  readDedup: request.readDedup,
@@ -164,6 +164,8 @@ const zhCN = {
164
164
  'repl.queuedCommandHeld': '排队的是斜杠命令,已填入输入框,请确认后手动发送',
165
165
  'repl.loadFailed': '(加载失败)',
166
166
  'repl.resumeAnchor': '已恢复会话 {id} · {count} 条消息 · 当前模型 {model}(/model switch 可切换)',
167
+ 'repl.compactedNotice': '── 更早的对话已被压缩成摘要(助手回复与工具过程不再保留),以下为摘要与保留的用户原话 ──',
168
+ 'repl.compactedTruncated': '…(摘要共 {total} 字符,仅显示前 {shown} 字符)',
167
169
  'repl.noSessions': '(没有已保存的会话)',
168
170
  'repl.noValue': '(无)',
169
171
  'repl.historyCleared': '(历史已清空,保留系统提示)',
@@ -554,6 +556,8 @@ const en = {
554
556
  'repl.queuedCommandHeld': 'Queued text is a slash command; filled into the input for you to send manually',
555
557
  'repl.loadFailed': '(load failed)',
556
558
  'repl.resumeAnchor': 'Resumed session {id} · {count} messages · model {model} (/model switch to change)',
559
+ 'repl.compactedNotice': '── Earlier conversation was compacted into a summary (assistant replies and tool steps are not kept); below are the summary and the preserved user messages ──',
560
+ 'repl.compactedTruncated': '… (summary is {total} chars, showing the first {shown})',
557
561
  'repl.noSessions': '(no saved sessions)',
558
562
  'repl.noValue': '(none)',
559
563
  'repl.historyCleared': '(history cleared; system prompt kept)',
@@ -3,7 +3,12 @@ import * as layout from '../ui/layout.js';
3
3
  import * as batch from '../ui/batch.js';
4
4
  import { displayWidth, padEndDisplay, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
5
5
  import { renderChip } from '../attachments/image.js';
6
+ import { t } from '../i18n/index.js';
6
7
  import { PROMPT } from './commands.js';
8
+ /** compact 摘要消息头(与 session/compact.ts SUMMARY_MARKER 同值;本地复制避免拉入 compact 的重依赖图,同 retention.ts)。 */
9
+ const SUMMARY_MARKER = '# 会话摘要';
10
+ /** 回放时摘要最多显示的字符数(摘要上限 6000,全量回放会淹没保留的用户原话)。 */
11
+ const SUMMARY_REPLAY_MAX_CHARS = 3000;
7
12
  /** side channel:history 中 user 消息的 index → 附件列表,供 renderHistory 复显文件名。runTurn 在 push 前设。 */
8
13
  export const messageAttachments = new Map();
9
14
  export function formatUserMessage(lines, trailingBlank = true) {
@@ -69,8 +74,25 @@ export function renderHistory(history) {
69
74
  };
70
75
  for (let idx = 0; idx < history.length; idx++) {
71
76
  const m = history[idx];
72
- if (m.role === 'system')
77
+ if (m.role === 'system') {
78
+ // compact 摘要是被压缩掉的助手回复/工具过程的唯一留存:回放时显示出来(history[0] 的系统提示仍跳过)。
79
+ const body = textOf(m.content);
80
+ if (idx > 0 && body.startsWith(SUMMARY_MARKER)) {
81
+ flushBatch();
82
+ layout.normalizeMutationBoundary();
83
+ layout.contentWrite(`${ui.dim}${t('repl.compactedNotice')}${ui.reset}\n\n`);
84
+ const text = body.slice(SUMMARY_MARKER.length).trim();
85
+ if (text.length > SUMMARY_REPLAY_MAX_CHARS) {
86
+ layout.contentWriteMdOnce(text.slice(0, SUMMARY_REPLAY_MAX_CHARS));
87
+ layout.contentWrite(`${ui.dim}${t('repl.compactedTruncated', { total: text.length, shown: SUMMARY_REPLAY_MAX_CHARS })}${ui.reset}\n`);
88
+ }
89
+ else {
90
+ layout.contentWriteMdOnce(text);
91
+ }
92
+ layout.normalizeMutationBoundary();
93
+ }
73
94
  continue;
95
+ }
74
96
  if (m.role === 'user') {
75
97
  flushBatch(); // 上一轮 batch(若有)收尾
76
98
  const lines = textOf(m.content).split('\n');
@@ -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') {
@@ -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.10",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 27 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {