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.
- package/dist/agent/index.js +5 -4
- package/dist/agent/run-coordinator.js +6 -0
- package/dist/agent/stages/tool-dispatcher.js +1 -0
- package/dist/i18n/index.js +4 -0
- package/dist/repl/message-format.js +23 -1
- package/dist/tools/builtins/run-command.js +12 -3
- package/dist/tools/tool-runtime.js +1 -0
- package/dist/ui/batch.js +89 -8
- package/package.json +1 -1
package/dist/agent/index.js
CHANGED
|
@@ -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,
|
package/dist/i18n/index.js
CHANGED
|
@@ -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) =>
|
|
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') {
|
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
|
-
|
|
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
|
-
//
|
|
470
|
-
//
|
|
471
|
-
|
|
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
|
/**
|