@zhushanwen/pi-subagent-workflow 8.7.0 → 8.8.1
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 +5 -11
- package/package.json +8 -11
- package/skills/workflow-script-format/SKILL.md +32 -13
- package/src/host/__tests__/pi-host.test.ts +23 -2
- package/src/host/pi-host.ts +61 -6
- package/src/index.ts +155 -528
- package/src/injectors/__tests__/engine-awareness.test.ts +5 -3
- package/src/injectors/__tests__/engine-section-stability.test.ts +48 -33
- package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
- package/src/injectors/__tests__/subagent-list-injector.test.ts +54 -14
- package/src/injectors/__tests__/workflow-list-injector.test.ts +26 -12
- package/src/injectors/engine-awareness.ts +80 -8
- package/src/injectors/model-list-injector.ts +15 -58
- package/src/injectors/resource-list-injector.ts +209 -0
- package/src/injectors/subagent-list-injector.ts +53 -224
- package/src/injectors/workflow-list-injector.ts +36 -196
- package/src/interface/__tests__/detectors.test.ts +46 -35
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +14 -3
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +38 -2
- package/src/interface/bg-notify-render.ts +7 -17
- package/src/interface/command-actions.ts +5 -15
- package/src/interface/commands.ts +116 -76
- package/src/interface/format.ts +354 -245
- package/src/interface/gui-mappers.ts +18 -22
- package/src/interface/helpers.ts +9 -130
- package/src/interface/list-component.ts +164 -117
- package/src/interface/list-shared.ts +3 -3
- package/src/interface/list-view.ts +130 -73
- package/src/interface/subagent-actions.ts +50 -675
- package/src/interface/subagent-tool-schema.ts +7 -9
- package/src/interface/subagent-tool.ts +5 -4
- package/src/interface/subagents.ts +9 -8
- package/src/interface/tool-render.ts +5 -15
- package/src/interface/tool-workflow-script.ts +34 -75
- package/src/interface/tool-workflow.ts +62 -96
- package/src/interface/tui-kit.ts +305 -0
- package/src/interface/views/WorkflowsView.ts +270 -207
- 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 +144 -228
- package/agents/analyst.md +0 -61
- package/agents/coder.md +0 -70
- package/agents/debugger.md +0 -67
- package/agents/doc-reviewer.md +0 -50
- package/agents/explorer.md +0 -64
- package/agents/general-purpose.md +0 -32
- package/agents/orchestrator.md +0 -63
- package/agents/planner.md +0 -54
- package/agents/researcher.md +0 -65
- package/agents/reviewer.md +0 -74
- package/src/interface/views/format.ts +0 -320
|
@@ -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,24 +47,25 @@ 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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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";
|
|
66
|
+
|
|
67
|
+
// L2 详情内容构建 + 滚动按键(纯函数)抽到 detail-content.ts(view 与其测试直接 import)。
|
|
68
|
+
import { buildDetailContent, detailContentLength, type DetailScrollContext, processDetailKey } from "./detail-content.ts";
|
|
70
69
|
|
|
71
70
|
// ── TUI layout constants ──────────────────────────────────────
|
|
72
71
|
|
|
@@ -85,30 +84,10 @@ const TICK_MS = 200;
|
|
|
85
84
|
/** 可打印 ASCII 字符下限(用于 save overlay 输入过滤)。 */
|
|
86
85
|
const PRINTABLE_CHAR_MIN = 32;
|
|
87
86
|
|
|
88
|
-
// ── 边框着色 helper
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
/** 着色单个框线字符(borderMuted)。 */
|
|
93
|
-
function b(theme: ThemeLike, s: string): string {
|
|
94
|
-
return theme.fg("borderMuted", s);
|
|
95
|
-
}
|
|
96
|
-
/** 着色单字符填充用的 ─(供 segFillColored 的 fillStyled)。 */
|
|
97
|
-
function dash(theme: ThemeLike): string {
|
|
98
|
-
return theme.fg("borderMuted", "─");
|
|
99
|
-
}
|
|
100
|
-
/** 满宽 ─ 填充串(borderMuted)。n 次单字符着色,ANSI 自然延续。 */
|
|
101
|
-
function dashes(theme: ThemeLike, n: number): string {
|
|
102
|
-
return dash(theme).repeat(Math.max(0, n));
|
|
103
|
-
}
|
|
104
|
-
/** 纯线顶/底框(无标题):左角 + ─×W + 右角。 */
|
|
105
|
-
function plainBorder(theme: ThemeLike, left: string, right: string, contentWidth: number): string {
|
|
106
|
-
return b(theme, left) + dashes(theme, contentWidth) + b(theme, right);
|
|
107
|
-
}
|
|
108
|
-
/** 内容行墙:│ + 内容(pad 到 contentWidth) + │,墙字符 borderMuted。 */
|
|
109
|
-
function walled(theme: ThemeLike, content: string, contentWidth: number): string {
|
|
110
|
-
return `${b(theme, "│")}${padVisible(content, contentWidth)}${b(theme, "│")}`;
|
|
111
|
-
}
|
|
87
|
+
// ── 边框着色 helper 家族(b/dash/dashes/titleBorder/plainBorder/walled)──
|
|
88
|
+
// 单定义 ../tui-kit.ts(post-convergence C4):本文件原私有自由函数副本与
|
|
89
|
+
// subagents list-component 的方法形态副本收敛为 kit 一份(本文件形态胜出),
|
|
90
|
+
// 统一 borderMuted token,所有 ╭╮╰╯├┤┬┴─│ 颜色一致。
|
|
112
91
|
|
|
113
92
|
// ── Minimal TUI duck-types(避免直接 import TUI/KeybindingsManager 类型 ──
|
|
114
93
|
// 共享类型 fallback 不导出 TUI 类,
|
|
@@ -150,17 +129,17 @@ export interface ViewActions {
|
|
|
150
129
|
*
|
|
151
130
|
* | 签名字段 | 消费点 |
|
|
152
131
|
* |---|---|
|
|
153
|
-
* | run.state.status | WorkflowsView.ts renderHeader :
|
|
154
|
-
* | 秒桶 Math.floor(now/1000) | renderHeader :
|
|
155
|
-
* | completed/total(节点 status 推导)| renderHeader :
|
|
156
|
-
* | 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 |
|
|
157
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 条)|
|
|
158
|
-
* | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile :
|
|
137
|
+
* | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile :987(trace 导出 `### [#stepIndex]` 标题)|
|
|
159
138
|
* | 节点 sessionFile | detail-content.ts :314-317(renderSessionSection)|
|
|
160
|
-
* | 节点 status | 节点行 :
|
|
161
|
-
* | live.totalTokens | 节点行 :
|
|
162
|
-
* | 工具计数(签名路径用 countAllToolCalls 免克隆计数,与 getAllToolCalls(node.live).length 恒等——projectLiveProgress 投影无 toolCallCount 字段,不得引用)| 节点行 :
|
|
163
|
-
* | 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 |
|
|
164
143
|
* | live.turns | detail :224 / :276 |
|
|
165
144
|
* | live.eventLog.length(append-only,长度变化即尾部窗口右移出新事件)| detail :222(filter turn_end)+ :231-235 |
|
|
166
145
|
* | live.currentActivity(type+label 均入签名)| detail :227-228 |
|
|
@@ -299,14 +278,9 @@ export function createWorkflowsView(
|
|
|
299
278
|
};
|
|
300
279
|
|
|
301
280
|
// ── L2 详情滚动辅助 ──
|
|
302
|
-
/** 安全读 terminal.rows(duck-type 失败兜底,对齐 subagents termRows)。 */
|
|
303
|
-
function termRows(): number {
|
|
304
|
-
const rows = tui.terminal?.rows;
|
|
305
|
-
return typeof rows === "number" && rows > 0 ? rows : TERM_ROWS_FALLBACK;
|
|
306
|
-
}
|
|
307
281
|
/** L2 右侧 detail viewport 高度(与 renderLayout 的 viewH 同源)。 */
|
|
308
282
|
function detailViewportHeight(): number {
|
|
309
|
-
return Math.max(MIN_BODY_LINES, bodyHeight(termRows()));
|
|
283
|
+
return Math.max(MIN_BODY_LINES, bodyHeight(termRows(tui)));
|
|
310
284
|
}
|
|
311
285
|
/** 重置 detail 滚动到底部对齐(切 agent / 进 L2 时调用)。 */
|
|
312
286
|
function resetDetailScroll(): void {
|
|
@@ -317,6 +291,8 @@ export function createWorkflowsView(
|
|
|
317
291
|
// ── Key handling(SDK Component.handleInput 模式,对齐 main) ──
|
|
318
292
|
// matchesKey(data, KeyId) 处理终端转义序列差异(xterm/iTerm/kitty),
|
|
319
293
|
// 优于手写 \x1b[A 等原始序列。escape/ctrl+c 在 keybindings 里同映射到 exit。
|
|
294
|
+
// 阶段化编排(形态 1):主函数只留「saveMode 拦截 → 各键阶段短路链」,
|
|
295
|
+
// 各 handle*Key 返回是否消费该键;阶段顺序与原单函数 if 链逐一对齐。
|
|
320
296
|
function handleInput(data: string): void {
|
|
321
297
|
if (state.disposed) return;
|
|
322
298
|
|
|
@@ -326,108 +302,123 @@ export function createWorkflowsView(
|
|
|
326
302
|
return;
|
|
327
303
|
}
|
|
328
304
|
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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;
|
|
340
319
|
}
|
|
320
|
+
state.level = (state.level - 1) as NavLevel;
|
|
321
|
+
state.promptExpanded = false;
|
|
322
|
+
cache.key = undefined;
|
|
323
|
+
requestRender();
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
341
326
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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 进详情即钉底)
|
|
365
366
|
}
|
|
367
|
+
cache.key = undefined;
|
|
368
|
+
requestRender();
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
366
371
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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++;
|
|
371
379
|
state.agentIdx = 0;
|
|
372
380
|
state.phaseScrollOffset = 0; // 切 phase → 重置滚动
|
|
373
381
|
state.agentScrollOffset = 0; // 切 phase → 重置滚动
|
|
374
|
-
} else if (state.level === 1 && state.agentIdx > 0) {
|
|
375
|
-
state.agentIdx--;
|
|
376
|
-
// agentScrollOffset 由 renderLevel1 自动调整
|
|
377
|
-
} else if (state.level === NAV_LEVEL_DETAIL && state.agentIdx > 0) {
|
|
378
|
-
state.agentIdx--;
|
|
379
|
-
state.promptExpanded = false;
|
|
380
|
-
resetDetailScroll(); // 切 agent → 底部对齐(对齐 subagents 进详情即钉底)
|
|
381
382
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
state.phaseScrollOffset = 0; // 切 phase → 重置滚动
|
|
393
|
-
state.agentScrollOffset = 0; // 切 phase → 重置滚动
|
|
394
|
-
}
|
|
395
|
-
} else if (state.level === 1) {
|
|
396
|
-
const agents = currentPhaseAgents();
|
|
397
|
-
if (state.agentIdx < agents.length - 1) state.agentIdx++;
|
|
398
|
-
// agentScrollOffset 由 renderLevel1 自动调整
|
|
399
|
-
} else if (state.level === NAV_LEVEL_DETAIL) {
|
|
400
|
-
const agents = currentPhaseAgents();
|
|
401
|
-
if (state.agentIdx < agents.length - 1) {
|
|
402
|
-
state.agentIdx++;
|
|
403
|
-
state.promptExpanded = false;
|
|
404
|
-
resetDetailScroll(); // 切 agent → 底部对齐
|
|
405
|
-
}
|
|
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 → 底部对齐
|
|
406
393
|
}
|
|
407
|
-
cache.key = undefined;
|
|
408
|
-
requestRender();
|
|
409
|
-
return;
|
|
410
394
|
}
|
|
395
|
+
cache.key = undefined;
|
|
396
|
+
requestRender();
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
411
399
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
cache.key = undefined;
|
|
427
|
-
requestRender();
|
|
428
|
-
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 收敛;保持当前锚点语义
|
|
429
414
|
}
|
|
415
|
+
cache.key = undefined;
|
|
416
|
+
requestRender();
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
430
419
|
|
|
420
|
+
/** lifecycle 快捷键(链尾,命中即消费且不再回退):a abort(仅 running)/ s 进 save mode / S 导出 trace。 */
|
|
421
|
+
function handleShortcutKeys(data: string): void {
|
|
431
422
|
// ── Lifecycle shortcuts (no restart per D-9; no pause/resume — one-shot) ──
|
|
432
423
|
if (data === "a") {
|
|
433
424
|
if (run.state.status === "running") {
|
|
@@ -452,7 +443,6 @@ export function createWorkflowsView(
|
|
|
452
443
|
// ── Trace export(对齐 main 的 S 键):导出完整 trace 到 Markdown 文件 ──
|
|
453
444
|
if (data === "S") {
|
|
454
445
|
saveTraceToFile(run, ctx);
|
|
455
|
-
return;
|
|
456
446
|
}
|
|
457
447
|
}
|
|
458
448
|
|
|
@@ -649,12 +639,12 @@ function renderHeader(
|
|
|
649
639
|
lines.push(walled(theme, nameLine, contentWidth));
|
|
650
640
|
|
|
651
641
|
if (run.spec.description) {
|
|
652
|
-
const maxDesc = contentWidth -
|
|
642
|
+
const maxDesc = contentWidth - visibleWidth(rightPart) - 1;
|
|
653
643
|
const descText = run.spec.description.length > maxDesc
|
|
654
644
|
? run.spec.description.slice(0, maxDesc - 1) + ELLIPSIS
|
|
655
645
|
: run.spec.description;
|
|
656
646
|
const descPart = theme.fg("dim", descText);
|
|
657
|
-
const padLen = Math.max(0, contentWidth -
|
|
647
|
+
const padLen = Math.max(0, contentWidth - visibleWidth(descPart) - visibleWidth(rightPart));
|
|
658
648
|
lines.push(`${b(theme, "│")}${descPart}${" ".repeat(padLen)}${rightPart}${b(theme, "│")}`);
|
|
659
649
|
} else {
|
|
660
650
|
lines.push(walled(theme, rightPart, contentWidth));
|
|
@@ -699,7 +689,7 @@ function mergeBody(
|
|
|
699
689
|
): void {
|
|
700
690
|
const bodyHeightVal = Math.max(leftLines.length, rightLines.length);
|
|
701
691
|
for (let i = 0; i < bodyHeightVal; i++) {
|
|
702
|
-
const left =
|
|
692
|
+
const left = padToVisible(leftLines[i] ?? "", SIDEBAR_WIDTH);
|
|
703
693
|
lines.push(left + b(theme, "│") + (rightLines[i] ?? ""));
|
|
704
694
|
}
|
|
705
695
|
}
|
|
@@ -774,19 +764,16 @@ function renderLevel0(
|
|
|
774
764
|
|
|
775
765
|
// ── Level 1: Agent selection ──────────────────────────────────
|
|
776
766
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
767
|
+
/** L1 左列:sidebar title + phase list(固定高度 viewport + 滚动,只构建可见行)。
|
|
768
|
+
* 与 renderLevel0 左列同构——本 helper 只供 renderLevel1(renderLevel0 不在本单元目标内,不并轨,
|
|
769
|
+
* 避免扩大行为面;后续如消重可让 L0 复用)。 */
|
|
770
|
+
function buildLevelSidebar(
|
|
780
771
|
phases: ReturnType<typeof buildPhaseGroups>,
|
|
781
772
|
state: ViewState,
|
|
782
773
|
theme: ThemeLike,
|
|
783
|
-
mainWidth: number,
|
|
784
|
-
now: number,
|
|
785
774
|
bodyH: number,
|
|
786
|
-
):
|
|
775
|
+
): string[] {
|
|
787
776
|
const leftLines: string[] = [];
|
|
788
|
-
const rightLines: string[] = [];
|
|
789
|
-
|
|
790
777
|
// Left: sidebar title + phase list(固定高度 viewport + 滚动,只构建可见行)
|
|
791
778
|
leftLines.push(theme.fg("muted", "Phases"));
|
|
792
779
|
leftLines.push(dashes(theme, SIDEBAR_WIDTH));
|
|
@@ -797,10 +784,18 @@ function renderLevel1(
|
|
|
797
784
|
state.phaseScrollOffset = phaseStart;
|
|
798
785
|
while (leftLines.length < bodyH) leftLines.push("");
|
|
799
786
|
leftLines.length = bodyH;
|
|
787
|
+
return leftLines;
|
|
788
|
+
}
|
|
800
789
|
|
|
801
|
-
|
|
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 {
|
|
802
798
|
const currentPhase = phases[state.phaseIdx];
|
|
803
|
-
const agents = currentPhase?.nodes ?? [];
|
|
804
799
|
if (currentPhase) {
|
|
805
800
|
rightLines.push(theme.fg("muted", currentPhase.name
|
|
806
801
|
? `${currentPhase.name} · ${currentPhase.nodes.length} agents`
|
|
@@ -810,28 +805,67 @@ function renderLevel1(
|
|
|
810
805
|
rightLines.push(theme.fg("muted", "(no phase)"));
|
|
811
806
|
rightLines.push(dashes(theme, mainWidth));
|
|
812
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 ?? [];
|
|
813
866
|
const { startIdx: agentStart, viewportH: agentViewportH } = computeViewport(agents.length, state.agentIdx, bodyH);
|
|
814
867
|
for (let i = agentStart; i < agentStart + agentViewportH && i < agents.length; i++) {
|
|
815
|
-
|
|
816
|
-
const pointer = i === state.agentIdx ? "❯ " : " ";
|
|
817
|
-
const dot = statusDotStr(node.status, theme);
|
|
818
|
-
// Live 路径优先:运行中从 node.live 读实时 token/tool 计数 + elapsed。
|
|
819
|
-
if (node.live) {
|
|
820
|
-
const live = projectLiveProgress(node.live);
|
|
821
|
-
const tokStr = live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "";
|
|
822
|
-
const tcCount = getAllToolCalls(node.live).length;
|
|
823
|
-
const elapsed = formatElapsedSeconds(live.elapsedSeconds);
|
|
824
|
-
rightLines.push(`${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
|
|
825
|
-
} else {
|
|
826
|
-
const elapsed = formatElapsed(
|
|
827
|
-
node.startedAt,
|
|
828
|
-
node.completedAt ? new Date(node.completedAt).getTime() : now,
|
|
829
|
-
);
|
|
830
|
-
const tok = node.result?.usage;
|
|
831
|
-
const tokStr = tok ? `${Math.round((tok.input + tok.output) / BUDGET_TOKENS_DIVISOR)}k tok` : "";
|
|
832
|
-
const tcCount = node.result?.toolCalls?.length ?? 0;
|
|
833
|
-
rightLines.push(`${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
|
|
834
|
-
}
|
|
868
|
+
rightLines.push(formatLevel1AgentLine(agents[i], theme, now, i === state.agentIdx));
|
|
835
869
|
}
|
|
836
870
|
state.agentScrollOffset = agentStart;
|
|
837
871
|
while (rightLines.length < bodyH) rightLines.push("");
|
|
@@ -865,7 +899,7 @@ function renderLevel2(
|
|
|
865
899
|
const pointer = i === state.agentIdx ? "❯ " : " ";
|
|
866
900
|
const maxNameWidth = SIDEBAR_WIDTH - AGENT_NAME_BUDGET;
|
|
867
901
|
const agentRef = displayAgentName(a.agent);
|
|
868
|
-
const agentName =
|
|
902
|
+
const agentName = visibleWidth(agentRef) > maxNameWidth
|
|
869
903
|
? agentRef.slice(0, maxNameWidth - 1) + ELLIPSIS
|
|
870
904
|
: agentRef;
|
|
871
905
|
leftLines.push(`${pointer}${agentName}`);
|
|
@@ -917,34 +951,18 @@ const TRACE_ACTIVITY_WIDTH = 80;
|
|
|
917
951
|
* 导出完整 workflow trace 到 Markdown 文件。
|
|
918
952
|
* 路径:<agentDir>/workflow-traces/{runId}.md(agentDir = getAgentDir(),实例隔离)
|
|
919
953
|
* 对齐 main 的 saveTraceToFile(WorkflowsView.ts:365-396)。
|
|
954
|
+
* 阶段化编排:header 行 + 逐 phase 逐 node 全段同步拼完后,才走 mkdir→write→notify 异步链
|
|
955
|
+
* (与原单函数时序一致:文件内容构建全部先于首次 fs 调用)。
|
|
920
956
|
*/
|
|
921
957
|
function saveTraceToFile(run: WorkflowRun, ctx: ExtensionContext): void {
|
|
922
958
|
const dir = pathJoin(getAgentDir(), "workflow-traces");
|
|
923
959
|
const filePath = pathJoin(dir, `${run.runId}.md`);
|
|
924
|
-
const lines
|
|
925
|
-
lines.push(`# Workflow Trace: ${run.spec.scriptName} (${run.runId})`, "");
|
|
926
|
-
lines.push(`Status: ${run.state.status} | Started: ${run.meta.startedAt ?? "-"} | Duration: ${formatElapsed(run.meta.startedAt)}`);
|
|
927
|
-
const budget = run.state.budget;
|
|
928
|
-
lines.push(`Budget: ${budget.usedTokens}/${budget.maxTokens ?? "unlimited"} tokens, $${budget.usedCost.toFixed(BUDGET_COST_DECIMALS)}`, "");
|
|
960
|
+
const lines = traceHeaderLines(run);
|
|
929
961
|
const phases = buildPhaseGroups([...run.state.trace.toArray()]);
|
|
930
962
|
for (const pg of phases) {
|
|
931
963
|
lines.push(`## Phase: ${pg.name || "(unnamed)"}`, "");
|
|
932
964
|
for (const node of pg.nodes) {
|
|
933
|
-
lines.push(
|
|
934
|
-
lines.push(`- Model: ${node.model}`);
|
|
935
|
-
lines.push(`- Duration: ${formatElapsed(node.startedAt, node.completedAt ? new Date(node.completedAt).getTime() : Date.now())}`, "");
|
|
936
|
-
lines.push("**Prompt:**", node.task, "");
|
|
937
|
-
const toolCalls = node.result?.toolCalls ?? [];
|
|
938
|
-
if (toolCalls.length > 0) {
|
|
939
|
-
lines.push("**Activity:**");
|
|
940
|
-
for (const tc of toolCalls) lines.push(`- ${formatActivityLine(tc, TRACE_ACTIVITY_WIDTH)}`);
|
|
941
|
-
lines.push("");
|
|
942
|
-
}
|
|
943
|
-
lines.push("**Outcome:**");
|
|
944
|
-
if (node.status === "running") lines.push("Still running...");
|
|
945
|
-
else if (node.result?.error) lines.push(node.result.error);
|
|
946
|
-
else if (node.result?.content) lines.push(node.result.content.slice(0, TRACE_OUTCOME_SLICE));
|
|
947
|
-
lines.push("");
|
|
965
|
+
lines.push(...traceNodeLines(node));
|
|
948
966
|
}
|
|
949
967
|
}
|
|
950
968
|
fsPromises.mkdir(dir, { recursive: true })
|
|
@@ -953,6 +971,51 @@ function saveTraceToFile(run: WorkflowRun, ctx: ExtensionContext): void {
|
|
|
953
971
|
.catch((err: Error) => ctx.ui.notify(`Save failed: ${err.message}`, "error"));
|
|
954
972
|
}
|
|
955
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
|
+
|
|
956
1019
|
// ── Save overlay(缺陷 #3 恢复,从 main 移植简化版)────────────
|
|
957
1020
|
|
|
958
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
|
|