@zhushanwen/pi-subagent-workflow 8.8.0 → 8.9.0
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/package.json +9 -8
- package/skills/subagent-ext-config/SKILL.md +21 -1
- package/src/host/pi-host.ts +5 -4
- package/src/index.ts +145 -464
- package/src/injectors/__tests__/engine-awareness.test.ts +4 -2
- package/src/injectors/__tests__/engine-section-stability.test.ts +42 -29
- package/src/injectors/engine-awareness.ts +80 -4
- package/src/injectors/resource-list-injector.ts +209 -0
- package/src/injectors/subagent-list-injector.ts +30 -143
- package/src/injectors/workflow-list-injector.ts +23 -150
- package/src/interface/__tests__/detectors.test.ts +1 -1
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +15 -6
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +1 -1
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
- package/src/interface/bg-notify-render.ts +4 -4
- package/src/interface/commands.ts +115 -75
- package/src/interface/format.ts +339 -241
- package/src/interface/helpers.ts +2 -2
- package/src/interface/list-component.ts +163 -116
- package/src/interface/list-shared.ts +2 -2
- package/src/interface/list-view.ts +129 -72
- package/src/interface/subagent-actions.ts +5 -5
- package/src/interface/subagent-tool-schema.ts +21 -6
- package/src/interface/subagent-tool.ts +31 -29
- package/src/interface/subagents.ts +8 -7
- package/src/interface/tool-render.ts +2 -2
- package/src/interface/tool-workflow-script.ts +15 -14
- package/src/interface/tool-workflow.ts +14 -11
- package/src/interface/tui-kit.ts +305 -0
- package/src/interface/views/WorkflowsView.ts +267 -196
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
- package/src/interface/views/detail-content.ts +16 -14
- package/src/interface/views/view-constants.ts +19 -0
- package/src/jsonl-run-store.ts +11 -7
- package/src/session-lifecycle.ts +561 -0
- package/src/interface/views/format.ts +0 -281
|
@@ -27,20 +27,18 @@ import { join as pathJoin } from "node:path";
|
|
|
27
27
|
|
|
28
28
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
29
29
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
30
|
-
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
30
|
+
import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
33
|
countAllToolCalls,
|
|
34
34
|
getAllToolCalls,
|
|
35
35
|
projectLiveProgress,
|
|
36
|
-
} from "@zhushanwen/subagent-core
|
|
37
|
-
import type { ExecutionTraceNode } from "@zhushanwen/subagent-core
|
|
38
|
-
import type { WorkflowRun } from "@zhushanwen/subagent-core
|
|
39
|
-
import { saveWorkflow } from "@zhushanwen/subagent-core
|
|
40
|
-
import { displayAgentName } from "@zhushanwen/subagent-core
|
|
36
|
+
} from "@zhushanwen/subagent-core";
|
|
37
|
+
import type { ExecutionTraceNode } from "@zhushanwen/subagent-core";
|
|
38
|
+
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
39
|
+
import { saveWorkflow } from "@zhushanwen/subagent-core";
|
|
40
|
+
import { displayAgentName } from "@zhushanwen/subagent-core";
|
|
41
41
|
import {
|
|
42
|
-
BOX_BORDER_CHARS,
|
|
43
|
-
BUDGET_TOKENS_DIVISOR,
|
|
44
42
|
buildPhaseGroups,
|
|
45
43
|
ELLIPSIS,
|
|
46
44
|
formatActivityLine,
|
|
@@ -49,13 +47,22 @@ import {
|
|
|
49
47
|
formatElapsedSeconds,
|
|
50
48
|
formatPhaseLine,
|
|
51
49
|
formatStatusBadge,
|
|
52
|
-
padVisible,
|
|
53
|
-
SIDEBAR_WIDTH,
|
|
54
50
|
statusDotStr,
|
|
55
|
-
TERM_ROWS_FALLBACK,
|
|
56
51
|
type ThemeLike,
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
} from "../format.ts";
|
|
53
|
+
import {
|
|
54
|
+
b,
|
|
55
|
+
dashes,
|
|
56
|
+
padToVisible,
|
|
57
|
+
plainBorder,
|
|
58
|
+
termRows,
|
|
59
|
+
walled,
|
|
60
|
+
} from "../tui-kit.ts";
|
|
61
|
+
import {
|
|
62
|
+
BOX_BORDER_CHARS,
|
|
63
|
+
BUDGET_TOKENS_DIVISOR,
|
|
64
|
+
SIDEBAR_WIDTH,
|
|
65
|
+
} from "./view-constants.ts";
|
|
59
66
|
|
|
60
67
|
// L2 详情内容构建 + 滚动按键(纯函数)抽到 detail-content.ts(view 与其测试直接 import)。
|
|
61
68
|
import { buildDetailContent, detailContentLength, type DetailScrollContext, processDetailKey } from "./detail-content.ts";
|
|
@@ -77,30 +84,10 @@ const TICK_MS = 200;
|
|
|
77
84
|
/** 可打印 ASCII 字符下限(用于 save overlay 输入过滤)。 */
|
|
78
85
|
const PRINTABLE_CHAR_MIN = 32;
|
|
79
86
|
|
|
80
|
-
// ── 边框着色 helper
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
/** 着色单个框线字符(borderMuted)。 */
|
|
85
|
-
function b(theme: ThemeLike, s: string): string {
|
|
86
|
-
return theme.fg("borderMuted", s);
|
|
87
|
-
}
|
|
88
|
-
/** 着色单字符填充用的 ─(本文件 dashes() 满宽填充复用)。 */
|
|
89
|
-
function dash(theme: ThemeLike): string {
|
|
90
|
-
return theme.fg("borderMuted", "─");
|
|
91
|
-
}
|
|
92
|
-
/** 满宽 ─ 填充串(borderMuted)。n 次单字符着色,ANSI 自然延续。 */
|
|
93
|
-
function dashes(theme: ThemeLike, n: number): string {
|
|
94
|
-
return dash(theme).repeat(Math.max(0, n));
|
|
95
|
-
}
|
|
96
|
-
/** 纯线顶/底框(无标题):左角 + ─×W + 右角。 */
|
|
97
|
-
function plainBorder(theme: ThemeLike, left: string, right: string, contentWidth: number): string {
|
|
98
|
-
return b(theme, left) + dashes(theme, contentWidth) + b(theme, right);
|
|
99
|
-
}
|
|
100
|
-
/** 内容行墙:│ + 内容(pad 到 contentWidth) + │,墙字符 borderMuted。 */
|
|
101
|
-
function walled(theme: ThemeLike, content: string, contentWidth: number): string {
|
|
102
|
-
return `${b(theme, "│")}${padVisible(content, contentWidth)}${b(theme, "│")}`;
|
|
103
|
-
}
|
|
87
|
+
// ── 边框着色 helper 家族(b/dash/dashes/titleBorder/plainBorder/walled)──
|
|
88
|
+
// 单定义 ../tui-kit.ts(post-convergence C4):本文件原私有自由函数副本与
|
|
89
|
+
// subagents list-component 的方法形态副本收敛为 kit 一份(本文件形态胜出),
|
|
90
|
+
// 统一 borderMuted token,所有 ╭╮╰╯├┤┬┴─│ 颜色一致。
|
|
104
91
|
|
|
105
92
|
// ── Minimal TUI duck-types(避免直接 import TUI/KeybindingsManager 类型 ──
|
|
106
93
|
// 共享类型 fallback 不导出 TUI 类,
|
|
@@ -142,17 +129,17 @@ export interface ViewActions {
|
|
|
142
129
|
*
|
|
143
130
|
* | 签名字段 | 消费点 |
|
|
144
131
|
* |---|---|
|
|
145
|
-
* | run.state.status | WorkflowsView.ts renderHeader :
|
|
146
|
-
* | 秒桶 Math.floor(now/1000) | renderHeader :
|
|
147
|
-
* | completed/total(节点 status 推导)| renderHeader :
|
|
148
|
-
* | budget 量化值(tokens round-k + cost toFixed(4)=BUDGET_COST_DECIMALS,与 :
|
|
132
|
+
* | run.state.status | WorkflowsView.ts renderHeader :629 / detail-content.ts :75(statusDotStr+statusLabel)/ renderFooter :666-678 |
|
|
133
|
+
* | 秒桶 Math.floor(now/1000) | renderHeader :628 formatElapsed(现算 elapsed)+ detail-content.ts :69-72(now 参与未完成节点 elapsed)|
|
|
134
|
+
* | completed/total(节点 status 推导)| renderHeader :626-629 |
|
|
135
|
+
* | budget 量化值(tokens round-k + cost toFixed(4)=BUDGET_COST_DECIMALS,与 :631 同精度——第 3-4 位小数变化是可见变化)| renderHeader :631 / saveTraceToFile :980 |
|
|
149
136
|
* | run.state.errorLogs 指纹 = length+末条 level:message(errorLogs 仅经 push + slice(-MAX_ERROR_LOGS) 变异——append-only + 前向淘汰、条目不可变;封顶后 length 不变内容移,单取 length 会漏失效,而任何可见变化必伴随 length 变或末条变,故 length+末条是完备且最小的指纹)| detail-content.ts :174-191(renderWorkerLogSection:total 标签 + 末 20 条)|
|
|
150
|
-
* | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile :
|
|
137
|
+
* | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile :987(trace 导出 `### [#stepIndex]` 标题)|
|
|
151
138
|
* | 节点 sessionFile | detail-content.ts :314-317(renderSessionSection)|
|
|
152
|
-
* | 节点 status | 节点行 :
|
|
153
|
-
* | live.totalTokens | 节点行 :
|
|
154
|
-
* | 工具计数(签名路径用 countAllToolCalls 免克隆计数,与 getAllToolCalls(node.live).length 恒等——projectLiveProgress 投影无 toolCallCount 字段,不得引用)| 节点行 :
|
|
155
|
-
* | live.elapsedSeconds | 节点行 :
|
|
139
|
+
* | 节点 status | 节点行 :844 statusDotStr / detail :75 |
|
|
140
|
+
* | live.totalTokens | 节点行 :819 / detail :79 / :275 |
|
|
141
|
+
* | 工具计数(签名路径用 countAllToolCalls 免克隆计数,与 getAllToolCalls(node.live).length 恒等——projectLiveProgress 投影无 toolCallCount 字段,不得引用)| 节点行 :820 / detail :80 / :223 |
|
|
142
|
+
* | live.elapsedSeconds | 节点行 :821 / detail :81 / :276 |
|
|
156
143
|
* | live.turns | detail :224 / :276 |
|
|
157
144
|
* | live.eventLog.length(append-only,长度变化即尾部窗口右移出新事件)| detail :222(filter turn_end)+ :231-235 |
|
|
158
145
|
* | live.currentActivity(type+label 均入签名)| detail :227-228 |
|
|
@@ -291,14 +278,9 @@ export function createWorkflowsView(
|
|
|
291
278
|
};
|
|
292
279
|
|
|
293
280
|
// ── L2 详情滚动辅助 ──
|
|
294
|
-
/** 安全读 terminal.rows(duck-type 失败兜底,对齐 subagents termRows)。 */
|
|
295
|
-
function termRows(): number {
|
|
296
|
-
const rows = tui.terminal?.rows;
|
|
297
|
-
return typeof rows === "number" && rows > 0 ? rows : TERM_ROWS_FALLBACK;
|
|
298
|
-
}
|
|
299
281
|
/** L2 右侧 detail viewport 高度(与 renderLayout 的 viewH 同源)。 */
|
|
300
282
|
function detailViewportHeight(): number {
|
|
301
|
-
return Math.max(MIN_BODY_LINES, bodyHeight(termRows()));
|
|
283
|
+
return Math.max(MIN_BODY_LINES, bodyHeight(termRows(tui)));
|
|
302
284
|
}
|
|
303
285
|
/** 重置 detail 滚动到底部对齐(切 agent / 进 L2 时调用)。 */
|
|
304
286
|
function resetDetailScroll(): void {
|
|
@@ -309,6 +291,8 @@ export function createWorkflowsView(
|
|
|
309
291
|
// ── Key handling(SDK Component.handleInput 模式,对齐 main) ──
|
|
310
292
|
// matchesKey(data, KeyId) 处理终端转义序列差异(xterm/iTerm/kitty),
|
|
311
293
|
// 优于手写 \x1b[A 等原始序列。escape/ctrl+c 在 keybindings 里同映射到 exit。
|
|
294
|
+
// 阶段化编排(形态 1):主函数只留「saveMode 拦截 → 各键阶段短路链」,
|
|
295
|
+
// 各 handle*Key 返回是否消费该键;阶段顺序与原单函数 if 链逐一对齐。
|
|
312
296
|
function handleInput(data: string): void {
|
|
313
297
|
if (state.disposed) return;
|
|
314
298
|
|
|
@@ -318,108 +302,123 @@ export function createWorkflowsView(
|
|
|
318
302
|
return;
|
|
319
303
|
}
|
|
320
304
|
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
305
|
+
if (handleEscapeKey(data)) return;
|
|
306
|
+
if (handleDetailScrollKey(data)) return;
|
|
307
|
+
if (handleNavUpKey(data)) return;
|
|
308
|
+
if (handleNavDownKey(data)) return;
|
|
309
|
+
if (handleEnterKey(data)) return;
|
|
310
|
+
handleShortcutKeys(data);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** escape/ctrl+c:L0 关闭 overlay(wrappedDone),否则降一级并重绘。 */
|
|
314
|
+
function handleEscapeKey(data: string): boolean {
|
|
315
|
+
if (!matchesKey(data, Key.escape)) return false;
|
|
316
|
+
if (state.level === 0) {
|
|
317
|
+
wrappedDone();
|
|
318
|
+
return true;
|
|
332
319
|
}
|
|
320
|
+
state.level = (state.level - 1) as NavLevel;
|
|
321
|
+
state.promptExpanded = false;
|
|
322
|
+
cache.key = undefined;
|
|
323
|
+
requestRender();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
333
326
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
327
|
+
/** L2 详情滚动键(委托 processDetailKey)。非 L2 / 无节点 / 未命中 → 返回 false 落到导航键
|
|
328
|
+
* (与原嵌套 if 跳过语义一致:L2 无节点时 up/down 仍可切 agent)。 */
|
|
329
|
+
function handleDetailScrollKey(data: string): boolean {
|
|
330
|
+
if (state.level !== NAV_LEVEL_DETAIL) return false;
|
|
331
|
+
const node = currentPhaseAgents()[state.agentIdx];
|
|
332
|
+
if (!node) return false;
|
|
333
|
+
const detailCtx: DetailScrollContext = {
|
|
334
|
+
viewportHeight: detailViewportHeight(),
|
|
335
|
+
contentLines: detailContentLength(node, state, run, theme),
|
|
336
|
+
isRunning: node.status === "running",
|
|
337
|
+
};
|
|
338
|
+
const r = processDetailKey(
|
|
339
|
+
data,
|
|
340
|
+
{ scrollOffset: state.detailScrollOffset, followTail: state.followTail },
|
|
341
|
+
detailCtx,
|
|
342
|
+
);
|
|
343
|
+
if (!r.handled) return false;
|
|
344
|
+
state.detailScrollOffset = r.scrollOffset;
|
|
345
|
+
state.followTail = r.followTail;
|
|
346
|
+
cache.key = undefined;
|
|
347
|
+
requestRender();
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** up:L0 上移 phase / L1、L2 上移 agent(L2 同时重置详情滚动)。命中 up 即消费(无条件重绘收尾)。 */
|
|
352
|
+
function handleNavUpKey(data: string): boolean {
|
|
353
|
+
if (!matchesKey(data, Key.up)) return false;
|
|
354
|
+
if (state.level === 0 && state.phaseIdx > 0) {
|
|
355
|
+
state.phaseIdx--;
|
|
356
|
+
state.agentIdx = 0;
|
|
357
|
+
state.phaseScrollOffset = 0; // 切 phase → 重置滚动
|
|
358
|
+
state.agentScrollOffset = 0; // 切 phase → 重置滚动
|
|
359
|
+
} else if (state.level === 1 && state.agentIdx > 0) {
|
|
360
|
+
state.agentIdx--;
|
|
361
|
+
// agentScrollOffset 由 renderLevel1 自动调整
|
|
362
|
+
} else if (state.level === NAV_LEVEL_DETAIL && state.agentIdx > 0) {
|
|
363
|
+
state.agentIdx--;
|
|
364
|
+
state.promptExpanded = false;
|
|
365
|
+
resetDetailScroll(); // 切 agent → 底部对齐(对齐 subagents 进详情即钉底)
|
|
357
366
|
}
|
|
367
|
+
cache.key = undefined;
|
|
368
|
+
requestRender();
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
358
371
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
372
|
+
/** down:L0/L1/L2 逐级下移(边界截停)。命中 down 即消费(无条件重绘收尾)。 */
|
|
373
|
+
function handleNavDownKey(data: string): boolean {
|
|
374
|
+
if (!matchesKey(data, Key.down)) return false;
|
|
375
|
+
if (state.level === 0) {
|
|
376
|
+
const live = buildPhaseGroups([...run.state.trace.toArray()]);
|
|
377
|
+
if (state.phaseIdx < live.length - 1) {
|
|
378
|
+
state.phaseIdx++;
|
|
363
379
|
state.agentIdx = 0;
|
|
364
380
|
state.phaseScrollOffset = 0; // 切 phase → 重置滚动
|
|
365
381
|
state.agentScrollOffset = 0; // 切 phase → 重置滚动
|
|
366
|
-
} else if (state.level === 1 && state.agentIdx > 0) {
|
|
367
|
-
state.agentIdx--;
|
|
368
|
-
// agentScrollOffset 由 renderLevel1 自动调整
|
|
369
|
-
} else if (state.level === NAV_LEVEL_DETAIL && state.agentIdx > 0) {
|
|
370
|
-
state.agentIdx--;
|
|
371
|
-
state.promptExpanded = false;
|
|
372
|
-
resetDetailScroll(); // 切 agent → 底部对齐(对齐 subagents 进详情即钉底)
|
|
373
382
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
state.phaseScrollOffset = 0; // 切 phase → 重置滚动
|
|
385
|
-
state.agentScrollOffset = 0; // 切 phase → 重置滚动
|
|
386
|
-
}
|
|
387
|
-
} else if (state.level === 1) {
|
|
388
|
-
const agents = currentPhaseAgents();
|
|
389
|
-
if (state.agentIdx < agents.length - 1) state.agentIdx++;
|
|
390
|
-
// agentScrollOffset 由 renderLevel1 自动调整
|
|
391
|
-
} else if (state.level === NAV_LEVEL_DETAIL) {
|
|
392
|
-
const agents = currentPhaseAgents();
|
|
393
|
-
if (state.agentIdx < agents.length - 1) {
|
|
394
|
-
state.agentIdx++;
|
|
395
|
-
state.promptExpanded = false;
|
|
396
|
-
resetDetailScroll(); // 切 agent → 底部对齐
|
|
397
|
-
}
|
|
383
|
+
} else if (state.level === 1) {
|
|
384
|
+
const agents = currentPhaseAgents();
|
|
385
|
+
if (state.agentIdx < agents.length - 1) state.agentIdx++;
|
|
386
|
+
// agentScrollOffset 由 renderLevel1 自动调整
|
|
387
|
+
} else if (state.level === NAV_LEVEL_DETAIL) {
|
|
388
|
+
const agents = currentPhaseAgents();
|
|
389
|
+
if (state.agentIdx < agents.length - 1) {
|
|
390
|
+
state.agentIdx++;
|
|
391
|
+
state.promptExpanded = false;
|
|
392
|
+
resetDetailScroll(); // 切 agent → 底部对齐
|
|
398
393
|
}
|
|
399
|
-
cache.key = undefined;
|
|
400
|
-
requestRender();
|
|
401
|
-
return;
|
|
402
394
|
}
|
|
395
|
+
cache.key = undefined;
|
|
396
|
+
requestRender();
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
403
399
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
cache.key = undefined;
|
|
419
|
-
requestRender();
|
|
420
|
-
return;
|
|
400
|
+
/** enter:L0→L1→L2 逐级下钻 / L2 切换 prompt 展开。命中 enter 即消费(无条件重绘收尾)。 */
|
|
401
|
+
function handleEnterKey(data: string): boolean {
|
|
402
|
+
if (!matchesKey(data, Key.enter)) return false;
|
|
403
|
+
if (state.level === 0 && currentPhaseAgents().length > 0) {
|
|
404
|
+
state.level = 1;
|
|
405
|
+
state.agentIdx = 0;
|
|
406
|
+
state.agentScrollOffset = 0; // 进 L1 → 重置滚动
|
|
407
|
+
} else if (state.level === 1) {
|
|
408
|
+
state.level = NAV_LEVEL_DETAIL;
|
|
409
|
+
state.promptExpanded = false;
|
|
410
|
+
resetDetailScroll(); // 进 L2 → 底部对齐
|
|
411
|
+
} else if (state.level === NAV_LEVEL_DETAIL) {
|
|
412
|
+
state.promptExpanded = !state.promptExpanded;
|
|
413
|
+
// 展开/折叠改变内容长度,render 路径 clamp 收敛;保持当前锚点语义
|
|
421
414
|
}
|
|
415
|
+
cache.key = undefined;
|
|
416
|
+
requestRender();
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
422
419
|
|
|
420
|
+
/** lifecycle 快捷键(链尾,命中即消费且不再回退):a abort(仅 running)/ s 进 save mode / S 导出 trace。 */
|
|
421
|
+
function handleShortcutKeys(data: string): void {
|
|
423
422
|
// ── Lifecycle shortcuts (no restart per D-9; no pause/resume — one-shot) ──
|
|
424
423
|
if (data === "a") {
|
|
425
424
|
if (run.state.status === "running") {
|
|
@@ -444,7 +443,6 @@ export function createWorkflowsView(
|
|
|
444
443
|
// ── Trace export(对齐 main 的 S 键):导出完整 trace 到 Markdown 文件 ──
|
|
445
444
|
if (data === "S") {
|
|
446
445
|
saveTraceToFile(run, ctx);
|
|
447
|
-
return;
|
|
448
446
|
}
|
|
449
447
|
}
|
|
450
448
|
|
|
@@ -641,12 +639,12 @@ function renderHeader(
|
|
|
641
639
|
lines.push(walled(theme, nameLine, contentWidth));
|
|
642
640
|
|
|
643
641
|
if (run.spec.description) {
|
|
644
|
-
const maxDesc = contentWidth -
|
|
642
|
+
const maxDesc = contentWidth - visibleWidth(rightPart) - 1;
|
|
645
643
|
const descText = run.spec.description.length > maxDesc
|
|
646
644
|
? run.spec.description.slice(0, maxDesc - 1) + ELLIPSIS
|
|
647
645
|
: run.spec.description;
|
|
648
646
|
const descPart = theme.fg("dim", descText);
|
|
649
|
-
const padLen = Math.max(0, contentWidth -
|
|
647
|
+
const padLen = Math.max(0, contentWidth - visibleWidth(descPart) - visibleWidth(rightPart));
|
|
650
648
|
lines.push(`${b(theme, "│")}${descPart}${" ".repeat(padLen)}${rightPart}${b(theme, "│")}`);
|
|
651
649
|
} else {
|
|
652
650
|
lines.push(walled(theme, rightPart, contentWidth));
|
|
@@ -691,7 +689,7 @@ function mergeBody(
|
|
|
691
689
|
): void {
|
|
692
690
|
const bodyHeightVal = Math.max(leftLines.length, rightLines.length);
|
|
693
691
|
for (let i = 0; i < bodyHeightVal; i++) {
|
|
694
|
-
const left =
|
|
692
|
+
const left = padToVisible(leftLines[i] ?? "", SIDEBAR_WIDTH);
|
|
695
693
|
lines.push(left + b(theme, "│") + (rightLines[i] ?? ""));
|
|
696
694
|
}
|
|
697
695
|
}
|
|
@@ -766,19 +764,16 @@ function renderLevel0(
|
|
|
766
764
|
|
|
767
765
|
// ── Level 1: Agent selection ──────────────────────────────────
|
|
768
766
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
767
|
+
/** L1 左列:sidebar title + phase list(固定高度 viewport + 滚动,只构建可见行)。
|
|
768
|
+
* 与 renderLevel0 左列同构——本 helper 只供 renderLevel1(renderLevel0 不在本单元目标内,不并轨,
|
|
769
|
+
* 避免扩大行为面;后续如消重可让 L0 复用)。 */
|
|
770
|
+
function buildLevelSidebar(
|
|
772
771
|
phases: ReturnType<typeof buildPhaseGroups>,
|
|
773
772
|
state: ViewState,
|
|
774
773
|
theme: ThemeLike,
|
|
775
|
-
mainWidth: number,
|
|
776
|
-
now: number,
|
|
777
774
|
bodyH: number,
|
|
778
|
-
):
|
|
775
|
+
): string[] {
|
|
779
776
|
const leftLines: string[] = [];
|
|
780
|
-
const rightLines: string[] = [];
|
|
781
|
-
|
|
782
777
|
// Left: sidebar title + phase list(固定高度 viewport + 滚动,只构建可见行)
|
|
783
778
|
leftLines.push(theme.fg("muted", "Phases"));
|
|
784
779
|
leftLines.push(dashes(theme, SIDEBAR_WIDTH));
|
|
@@ -789,10 +784,18 @@ function renderLevel1(
|
|
|
789
784
|
state.phaseScrollOffset = phaseStart;
|
|
790
785
|
while (leftLines.length < bodyH) leftLines.push("");
|
|
791
786
|
leftLines.length = bodyH;
|
|
787
|
+
return leftLines;
|
|
788
|
+
}
|
|
792
789
|
|
|
793
|
-
|
|
790
|
+
/** L1 右列 header:phase 名 · agent 计数(无 phase 时 "(no phase)" 占位,两分支都补分隔线)。 */
|
|
791
|
+
function pushLevel1Header(
|
|
792
|
+
rightLines: string[],
|
|
793
|
+
phases: ReturnType<typeof buildPhaseGroups>,
|
|
794
|
+
state: ViewState,
|
|
795
|
+
theme: ThemeLike,
|
|
796
|
+
mainWidth: number,
|
|
797
|
+
): void {
|
|
794
798
|
const currentPhase = phases[state.phaseIdx];
|
|
795
|
-
const agents = currentPhase?.nodes ?? [];
|
|
796
799
|
if (currentPhase) {
|
|
797
800
|
rightLines.push(theme.fg("muted", currentPhase.name
|
|
798
801
|
? `${currentPhase.name} · ${currentPhase.nodes.length} agents`
|
|
@@ -802,28 +805,67 @@ function renderLevel1(
|
|
|
802
805
|
rightLines.push(theme.fg("muted", "(no phase)"));
|
|
803
806
|
rightLines.push(dashes(theme, mainWidth));
|
|
804
807
|
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** L1 agent 行统计三元组:live 路径优先(运行中从 node.live 读实时 token/tool + elapsed),
|
|
811
|
+
* 终态回退 result 统计。 */
|
|
812
|
+
function level1AgentCells(
|
|
813
|
+
node: ExecutionTraceNode,
|
|
814
|
+
now: number,
|
|
815
|
+
): { tokStr: string; tcCount: number; elapsed: string } {
|
|
816
|
+
if (node.live) {
|
|
817
|
+
const live = projectLiveProgress(node.live);
|
|
818
|
+
return {
|
|
819
|
+
tokStr: live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "",
|
|
820
|
+
tcCount: getAllToolCalls(node.live).length,
|
|
821
|
+
elapsed: formatElapsedSeconds(live.elapsedSeconds),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const elapsed = formatElapsed(
|
|
825
|
+
node.startedAt,
|
|
826
|
+
node.completedAt ? new Date(node.completedAt).getTime() : now,
|
|
827
|
+
);
|
|
828
|
+
const tok = node.result?.usage;
|
|
829
|
+
return {
|
|
830
|
+
tokStr: tok ? `${Math.round((tok.input + tok.output) / BUDGET_TOKENS_DIVISOR)}k tok` : "",
|
|
831
|
+
tcCount: node.result?.toolCalls?.length ?? 0,
|
|
832
|
+
elapsed,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** L1 单个 agent 展示行:pointer + 状态点 + agent/model + tok/tools/elapsed(live 与终态同模板)。 */
|
|
837
|
+
function formatLevel1AgentLine(
|
|
838
|
+
node: ExecutionTraceNode,
|
|
839
|
+
theme: ThemeLike,
|
|
840
|
+
now: number,
|
|
841
|
+
selected: boolean,
|
|
842
|
+
): string {
|
|
843
|
+
const pointer = selected ? "❯ " : " ";
|
|
844
|
+
const dot = statusDotStr(node.status, theme);
|
|
845
|
+
const { tokStr, tcCount, elapsed } = level1AgentCells(node, now);
|
|
846
|
+
return `${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function renderLevel1(
|
|
850
|
+
lines: string[],
|
|
851
|
+
_run: WorkflowRun,
|
|
852
|
+
phases: ReturnType<typeof buildPhaseGroups>,
|
|
853
|
+
state: ViewState,
|
|
854
|
+
theme: ThemeLike,
|
|
855
|
+
mainWidth: number,
|
|
856
|
+
now: number,
|
|
857
|
+
bodyH: number,
|
|
858
|
+
): void {
|
|
859
|
+
const leftLines = buildLevelSidebar(phases, state, theme, bodyH);
|
|
860
|
+
|
|
861
|
+
// Right: agent list(固定高度 viewport + 滚动)
|
|
862
|
+
const rightLines: string[] = [];
|
|
863
|
+
pushLevel1Header(rightLines, phases, state, theme, mainWidth);
|
|
864
|
+
const currentPhase = phases[state.phaseIdx];
|
|
865
|
+
const agents = currentPhase?.nodes ?? [];
|
|
805
866
|
const { startIdx: agentStart, viewportH: agentViewportH } = computeViewport(agents.length, state.agentIdx, bodyH);
|
|
806
867
|
for (let i = agentStart; i < agentStart + agentViewportH && i < agents.length; i++) {
|
|
807
|
-
|
|
808
|
-
const pointer = i === state.agentIdx ? "❯ " : " ";
|
|
809
|
-
const dot = statusDotStr(node.status, theme);
|
|
810
|
-
// Live 路径优先:运行中从 node.live 读实时 token/tool 计数 + elapsed。
|
|
811
|
-
if (node.live) {
|
|
812
|
-
const live = projectLiveProgress(node.live);
|
|
813
|
-
const tokStr = live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "";
|
|
814
|
-
const tcCount = getAllToolCalls(node.live).length;
|
|
815
|
-
const elapsed = formatElapsedSeconds(live.elapsedSeconds);
|
|
816
|
-
rightLines.push(`${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
|
|
817
|
-
} else {
|
|
818
|
-
const elapsed = formatElapsed(
|
|
819
|
-
node.startedAt,
|
|
820
|
-
node.completedAt ? new Date(node.completedAt).getTime() : now,
|
|
821
|
-
);
|
|
822
|
-
const tok = node.result?.usage;
|
|
823
|
-
const tokStr = tok ? `${Math.round((tok.input + tok.output) / BUDGET_TOKENS_DIVISOR)}k tok` : "";
|
|
824
|
-
const tcCount = node.result?.toolCalls?.length ?? 0;
|
|
825
|
-
rightLines.push(`${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
|
|
826
|
-
}
|
|
868
|
+
rightLines.push(formatLevel1AgentLine(agents[i], theme, now, i === state.agentIdx));
|
|
827
869
|
}
|
|
828
870
|
state.agentScrollOffset = agentStart;
|
|
829
871
|
while (rightLines.length < bodyH) rightLines.push("");
|
|
@@ -857,7 +899,7 @@ function renderLevel2(
|
|
|
857
899
|
const pointer = i === state.agentIdx ? "❯ " : " ";
|
|
858
900
|
const maxNameWidth = SIDEBAR_WIDTH - AGENT_NAME_BUDGET;
|
|
859
901
|
const agentRef = displayAgentName(a.agent);
|
|
860
|
-
const agentName =
|
|
902
|
+
const agentName = visibleWidth(agentRef) > maxNameWidth
|
|
861
903
|
? agentRef.slice(0, maxNameWidth - 1) + ELLIPSIS
|
|
862
904
|
: agentRef;
|
|
863
905
|
leftLines.push(`${pointer}${agentName}`);
|
|
@@ -909,34 +951,18 @@ const TRACE_ACTIVITY_WIDTH = 80;
|
|
|
909
951
|
* 导出完整 workflow trace 到 Markdown 文件。
|
|
910
952
|
* 路径:<agentDir>/workflow-traces/{runId}.md(agentDir = getAgentDir(),实例隔离)
|
|
911
953
|
* 对齐 main 的 saveTraceToFile(WorkflowsView.ts:365-396)。
|
|
954
|
+
* 阶段化编排:header 行 + 逐 phase 逐 node 全段同步拼完后,才走 mkdir→write→notify 异步链
|
|
955
|
+
* (与原单函数时序一致:文件内容构建全部先于首次 fs 调用)。
|
|
912
956
|
*/
|
|
913
957
|
function saveTraceToFile(run: WorkflowRun, ctx: ExtensionContext): void {
|
|
914
958
|
const dir = pathJoin(getAgentDir(), "workflow-traces");
|
|
915
959
|
const filePath = pathJoin(dir, `${run.runId}.md`);
|
|
916
|
-
const lines
|
|
917
|
-
lines.push(`# Workflow Trace: ${run.spec.scriptName} (${run.runId})`, "");
|
|
918
|
-
lines.push(`Status: ${run.state.status} | Started: ${run.meta.startedAt ?? "-"} | Duration: ${formatElapsed(run.meta.startedAt)}`);
|
|
919
|
-
const budget = run.state.budget;
|
|
920
|
-
lines.push(`Budget: ${budget.usedTokens}/${budget.maxTokens ?? "unlimited"} tokens, $${budget.usedCost.toFixed(BUDGET_COST_DECIMALS)}`, "");
|
|
960
|
+
const lines = traceHeaderLines(run);
|
|
921
961
|
const phases = buildPhaseGroups([...run.state.trace.toArray()]);
|
|
922
962
|
for (const pg of phases) {
|
|
923
963
|
lines.push(`## Phase: ${pg.name || "(unnamed)"}`, "");
|
|
924
964
|
for (const node of pg.nodes) {
|
|
925
|
-
lines.push(
|
|
926
|
-
lines.push(`- Model: ${node.model}`);
|
|
927
|
-
lines.push(`- Duration: ${formatElapsed(node.startedAt, node.completedAt ? new Date(node.completedAt).getTime() : Date.now())}`, "");
|
|
928
|
-
lines.push("**Prompt:**", node.task, "");
|
|
929
|
-
const toolCalls = node.result?.toolCalls ?? [];
|
|
930
|
-
if (toolCalls.length > 0) {
|
|
931
|
-
lines.push("**Activity:**");
|
|
932
|
-
for (const tc of toolCalls) lines.push(`- ${formatActivityLine(tc, TRACE_ACTIVITY_WIDTH)}`);
|
|
933
|
-
lines.push("");
|
|
934
|
-
}
|
|
935
|
-
lines.push("**Outcome:**");
|
|
936
|
-
if (node.status === "running") lines.push("Still running...");
|
|
937
|
-
else if (node.result?.error) lines.push(node.result.error);
|
|
938
|
-
else if (node.result?.content) lines.push(node.result.content.slice(0, TRACE_OUTCOME_SLICE));
|
|
939
|
-
lines.push("");
|
|
965
|
+
lines.push(...traceNodeLines(node));
|
|
940
966
|
}
|
|
941
967
|
}
|
|
942
968
|
fsPromises.mkdir(dir, { recursive: true })
|
|
@@ -945,6 +971,51 @@ function saveTraceToFile(run: WorkflowRun, ctx: ExtensionContext): void {
|
|
|
945
971
|
.catch((err: Error) => ctx.ui.notify(`Save failed: ${err.message}`, "error"));
|
|
946
972
|
}
|
|
947
973
|
|
|
974
|
+
/** trace 导出头部三行:标题 + 空行、Status/Started/Duration、Budget + 空行。 */
|
|
975
|
+
function traceHeaderLines(run: WorkflowRun): string[] {
|
|
976
|
+
const lines: string[] = [];
|
|
977
|
+
lines.push(`# Workflow Trace: ${run.spec.scriptName} (${run.runId})`, "");
|
|
978
|
+
lines.push(`Status: ${run.state.status} | Started: ${run.meta.startedAt ?? "-"} | Duration: ${formatElapsed(run.meta.startedAt)}`);
|
|
979
|
+
const budget = run.state.budget;
|
|
980
|
+
lines.push(`Budget: ${budget.usedTokens}/${budget.maxTokens ?? "unlimited"} tokens, $${budget.usedCost.toFixed(BUDGET_COST_DECIMALS)}`, "");
|
|
981
|
+
return lines;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** 单节点全段:### 标题 + Model/Duration + Prompt + Activity + Outcome(行序同原单函数版)。 */
|
|
985
|
+
function traceNodeLines(node: ExecutionTraceNode): string[] {
|
|
986
|
+
const lines: string[] = [];
|
|
987
|
+
lines.push(`### [#${node.stepIndex}] ${node.agent} — ${node.status}`);
|
|
988
|
+
lines.push(`- Model: ${node.model}`);
|
|
989
|
+
lines.push(`- Duration: ${formatElapsed(node.startedAt, node.completedAt ? new Date(node.completedAt).getTime() : Date.now())}`, "");
|
|
990
|
+
lines.push("**Prompt:**", node.task, "");
|
|
991
|
+
lines.push(...traceActivityLines(node));
|
|
992
|
+
lines.push(...traceOutcomeLines(node));
|
|
993
|
+
return lines;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/** 单节点 Activity 段:**Activity:** + 每条 toolCall 一行 + 空行;无 toolCall 时整段省略。 */
|
|
997
|
+
function traceActivityLines(node: ExecutionTraceNode): string[] {
|
|
998
|
+
const lines: string[] = [];
|
|
999
|
+
const toolCalls = node.result?.toolCalls ?? [];
|
|
1000
|
+
if (toolCalls.length > 0) {
|
|
1001
|
+
lines.push("**Activity:**");
|
|
1002
|
+
for (const tc of toolCalls) lines.push(`- ${formatActivityLine(tc, TRACE_ACTIVITY_WIDTH)}`);
|
|
1003
|
+
lines.push("");
|
|
1004
|
+
}
|
|
1005
|
+
return lines;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/** 单节点 Outcome 段:**Outcome:** + running/error/content(截断)三态互斥 + 空行。 */
|
|
1009
|
+
function traceOutcomeLines(node: ExecutionTraceNode): string[] {
|
|
1010
|
+
const lines: string[] = [];
|
|
1011
|
+
lines.push("**Outcome:**");
|
|
1012
|
+
if (node.status === "running") lines.push("Still running...");
|
|
1013
|
+
else if (node.result?.error) lines.push(node.result.error);
|
|
1014
|
+
else if (node.result?.content) lines.push(node.result.content.slice(0, TRACE_OUTCOME_SLICE));
|
|
1015
|
+
lines.push("");
|
|
1016
|
+
return lines;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
948
1019
|
// ── Save overlay(缺陷 #3 恢复,从 main 移植简化版)────────────
|
|
949
1020
|
|
|
950
1021
|
/**
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
14
14
|
|
|
15
15
|
import { computeRenderSignature } from "../WorkflowsView.ts";
|
|
16
|
-
import type { ExecutionRecord } from "@zhushanwen/subagent-core
|
|
17
|
-
import type { ExecutionTraceNode, WorkerLogEntry } from "@zhushanwen/subagent-core
|
|
18
|
-
import type { WorkflowRun } from "@zhushanwen/subagent-core
|
|
16
|
+
import type { ExecutionRecord } from "@zhushanwen/subagent-core";
|
|
17
|
+
import type { ExecutionTraceNode, WorkerLogEntry } from "@zhushanwen/subagent-core";
|
|
18
|
+
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
19
19
|
|
|
20
20
|
// ── Fixtures(duck typing,对齐 detail-content-session-file.test.ts 先例)──
|
|
21
21
|
|
|
@@ -8,14 +8,17 @@
|
|
|
8
8
|
import { describe, expect, it } from "vitest";
|
|
9
9
|
|
|
10
10
|
import { buildDetailContent } from "../detail-content.ts";
|
|
11
|
-
import type { ThemeLike } from "
|
|
12
|
-
import type { ExecutionTraceNode } from "@zhushanwen/subagent-core
|
|
13
|
-
import type { WorkflowRun } from "@zhushanwen/subagent-core
|
|
11
|
+
import type { ThemeLike } from "../../format.ts";
|
|
12
|
+
import type { ExecutionTraceNode } from "@zhushanwen/subagent-core";
|
|
13
|
+
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
14
14
|
|
|
15
|
-
/** plain theme:
|
|
15
|
+
/** plain theme: 着色方法直接返回原文,无 ANSI 色码——测试断言纯文本。
|
|
16
|
+
* bg/underline 为宽 ThemeLike 接口的 no-op 实现(workflow 渲染面只用 fg/bold)。 */
|
|
16
17
|
const plainTheme: ThemeLike = {
|
|
18
|
+
bg: (_color: string, text: string) => text,
|
|
17
19
|
fg: (_tag: string, text: string) => text,
|
|
18
20
|
bold: (text: string) => text,
|
|
21
|
+
underline: (text: string) => text,
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
function makeNode(overrides: Partial<ExecutionTraceNode> = {}): ExecutionTraceNode {
|