@zhushanwen/pi-subagent-workflow 8.8.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/package.json +8 -8
- 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 +10 -2
- 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 +6 -5
- package/src/interface/subagent-tool.ts +4 -3
- 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/interface/views/format.ts +0 -281
|
@@ -12,29 +12,31 @@
|
|
|
12
12
|
|
|
13
13
|
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
|
-
import { getAllToolCalls, projectLiveProgress } from "@zhushanwen/subagent-core
|
|
16
|
-
import type { AgentEventLogEntry } from "@zhushanwen/subagent-core
|
|
17
|
-
import type { ExecutionTraceNode } from "@zhushanwen/subagent-core
|
|
18
|
-
import type { WorkflowRun } from "@zhushanwen/subagent-core
|
|
15
|
+
import { getAllToolCalls, projectLiveProgress } from "@zhushanwen/subagent-core";
|
|
16
|
+
import type { AgentEventLogEntry } from "@zhushanwen/subagent-core";
|
|
17
|
+
import type { ExecutionTraceNode } from "@zhushanwen/subagent-core";
|
|
18
|
+
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
19
19
|
import {
|
|
20
|
-
BOX_BORDER_CHARS,
|
|
21
|
-
BUDGET_TOKENS_DIVISOR,
|
|
22
20
|
ELLIPSIS,
|
|
23
21
|
formatActivityLine,
|
|
24
22
|
formatElapsed,
|
|
25
23
|
formatElapsedSeconds,
|
|
26
|
-
formatEventLine,
|
|
27
24
|
formatTokenStat,
|
|
25
|
+
formatTraceEventLine,
|
|
26
|
+
statusDotStr,
|
|
27
|
+
type ThemeLike,
|
|
28
|
+
} from "../format.ts";
|
|
29
|
+
import { PAGE_SCROLL_DEFAULT } from "../tui-kit.ts";
|
|
30
|
+
import {
|
|
31
|
+
BOX_BORDER_CHARS,
|
|
32
|
+
BUDGET_TOKENS_DIVISOR,
|
|
28
33
|
MAX_TOOL_CALLS_DISPLAY,
|
|
29
34
|
OUTPUT_TRUNCATE_BYTES,
|
|
30
|
-
PAGE_SCROLL_DEFAULT,
|
|
31
35
|
PROMPT_FOLD_LINES,
|
|
32
|
-
|
|
33
|
-
type ThemeLike,
|
|
34
|
-
} from "./format.ts";
|
|
36
|
+
} from "./view-constants.ts";
|
|
35
37
|
|
|
36
|
-
// ── 共享常量(BOX_BORDER_CHARS / BUDGET_TOKENS_DIVISOR / MAX_TOOL_CALLS_DISPLAY
|
|
37
|
-
//
|
|
38
|
+
// ── 共享常量(BOX_BORDER_CHARS / BUDGET_TOKENS_DIVISOR / MAX_TOOL_CALLS_DISPLAY 等)
|
|
39
|
+
// 落位 ./view-constants.ts(post-convergence C4:views 专属常量自 format.ts 沉回 views/)──
|
|
38
40
|
|
|
39
41
|
/** 探测宽度:足够大避免截断折行影响行数统计(对齐 subagents DETAIL_LEN_PROBE_WIDTH)。 */
|
|
40
42
|
const DETAIL_LEN_PROBE_WIDTH = 9999;
|
|
@@ -232,7 +234,7 @@ function renderActivitySection(
|
|
|
232
234
|
const start = eventLog.length - showCount;
|
|
233
235
|
for (let i = start; i < eventLog.length; i++) {
|
|
234
236
|
const entry = eventLog[i] as AgentEventLogEntry;
|
|
235
|
-
rightLines.push(theme.fg("dim", ` ${
|
|
237
|
+
rightLines.push(theme.fg("dim", ` ${formatTraceEventLine(entry, theme)}`.slice(0, mainWidth - BOX_BORDER_CHARS)));
|
|
236
238
|
}
|
|
237
239
|
if (totalCount === 0 && !live.currentActivity) {
|
|
238
240
|
rightLines.push(theme.fg("dim", " (starting...)"));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/interface/views/view-constants.ts
|
|
2
|
+
//
|
|
3
|
+
// workflow 全屏视图族专属布局常量(WorkflowsView + detail-content 消费)。
|
|
4
|
+
// 自 interface/format.ts 沉回 views/(post-convergence C4:format.ts 只保留
|
|
5
|
+
// 跨视图族共享的格式化函数,views 专属常量就近落位消费方目录)。
|
|
6
|
+
// ELLIPSIS 留在 format.ts(formatActivityLine/formatPhaseLine 亦消费,非 views 专属)。
|
|
7
|
+
|
|
8
|
+
/** 左侧 sidebar 宽度(WorkflowsView 布局)。 */
|
|
9
|
+
export const SIDEBAR_WIDTH = 24;
|
|
10
|
+
/** prompt 区折叠时显示的行数(detail-content L2)。 */
|
|
11
|
+
export const PROMPT_FOLD_LINES = 3;
|
|
12
|
+
/** result 正文渲染的截断预算(字节,detail-content L2)。 */
|
|
13
|
+
export const OUTPUT_TRUNCATE_BYTES = 100_000;
|
|
14
|
+
/** box 左右边框字符宽度(│ x 2),用于内容行截断预算。 */
|
|
15
|
+
export const BOX_BORDER_CHARS = 2;
|
|
16
|
+
/** token 数 → k 单位的除数。 */
|
|
17
|
+
export const BUDGET_TOKENS_DIVISOR = 1000;
|
|
18
|
+
/** Activity 区最多显示的 tool call 条数。 */
|
|
19
|
+
export const MAX_TOOL_CALLS_DISPLAY = 3;
|
package/src/jsonl-run-store.ts
CHANGED
|
@@ -73,19 +73,23 @@ import * as fs from "node:fs";
|
|
|
73
73
|
import * as path from "node:path";
|
|
74
74
|
|
|
75
75
|
import type { CustomEntry, ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
76
|
+
// DEFAULT_SAVE_MIN_INTERVAL_MS / DEFAULT_STATE_MAX_RUNS 保留深路径:core barrel
|
|
77
|
+
// 刻意不导出(exports 面即 semver 契约,见 subagent-core src/index.ts 头注),壳侧
|
|
78
|
+
// barrel 消费(DEFAULT_* 已进 core barrel——u-2c 删 ./* 通配后深路径 tsc 不可解析)
|
|
76
79
|
import {
|
|
77
80
|
DEFAULT_SAVE_MIN_INTERVAL_MS,
|
|
78
81
|
DEFAULT_STATE_MAX_RUNS,
|
|
79
|
-
} from "@zhushanwen/subagent-core
|
|
80
|
-
import { getLogger } from "@zhushanwen/subagent-core
|
|
82
|
+
} from "@zhushanwen/subagent-core";
|
|
83
|
+
import { getLogger } from "@zhushanwen/subagent-core";
|
|
81
84
|
|
|
82
|
-
import { WorkflowRun } from "@zhushanwen/subagent-core
|
|
85
|
+
import { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
83
86
|
import {
|
|
84
87
|
SNAPSHOT_VERSION,
|
|
85
88
|
fromRunSnapshot,
|
|
86
89
|
toRunSnapshot,
|
|
87
90
|
type RunSnapshot,
|
|
88
|
-
} from "@zhushanwen/subagent-core
|
|
91
|
+
} from "@zhushanwen/subagent-core";
|
|
92
|
+
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
89
93
|
|
|
90
94
|
// ── Workflow-record self-describing entry (W17, D4) ─────────
|
|
91
95
|
|
|
@@ -158,7 +162,7 @@ function collectRecordRun(entry: CustomEntry, entryIndex: number, recordRuns: Ma
|
|
|
158
162
|
}
|
|
159
163
|
// D-5: 版本不匹配 = old snapshot format / future version — skip silently
|
|
160
164
|
} catch (err) {
|
|
161
|
-
const reason =
|
|
165
|
+
const reason = toErrorMessage(err);
|
|
162
166
|
logger.warn(
|
|
163
167
|
`[subagent-workflow] workflow-record entry #${entryIndex} corrupted, skipped run rebuild: ${reason}`,
|
|
164
168
|
);
|
|
@@ -232,7 +236,7 @@ async function pruneStateFilesBeyondCap(stateDir: string, maxRuns: number): Prom
|
|
|
232
236
|
names = await fs.promises.readdir(stateDir);
|
|
233
237
|
} catch (err) {
|
|
234
238
|
if (!isEnoentError(err)) {
|
|
235
|
-
const reason =
|
|
239
|
+
const reason = toErrorMessage(err);
|
|
236
240
|
logger.warn(`[subagent-workflow] state retention: readdir ${stateDir} failed: ${reason}`);
|
|
237
241
|
}
|
|
238
242
|
return;
|
|
@@ -258,7 +262,7 @@ async function pruneStateFilesBeyondCap(stateDir: string, maxRuns: number): Prom
|
|
|
258
262
|
logger.debug(`[subagent-workflow] state retention: pruned ${victim.full}`);
|
|
259
263
|
} catch (err) {
|
|
260
264
|
if (isEnoentError(err)) continue; // 并发删除已达成目标
|
|
261
|
-
const reason =
|
|
265
|
+
const reason = toErrorMessage(err);
|
|
262
266
|
logger.warn(`[subagent-workflow] state retention: failed to delete ${victim.full}: ${reason}`);
|
|
263
267
|
}
|
|
264
268
|
}
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow View — Pure formatting functions (FR-4)
|
|
3
|
-
*
|
|
4
|
-
* Stateless functions extracted from WorkflowsView for testability.
|
|
5
|
-
* All functions are pure: no Pi runtime, no side effects.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
|
|
10
|
-
import type { AgentEventLogEntry } from "@zhushanwen/subagent-core/execution/types.ts";
|
|
11
|
-
import type { ExecutionTraceNode, ToolCallEntry } from "@zhushanwen/subagent-core/orchestration/models/types.ts";
|
|
12
|
-
import type { DoneReason, RunStatus } from "@zhushanwen/subagent-core/orchestration/models/types.ts";
|
|
13
|
-
import { displayAgentName } from "@zhushanwen/subagent-core/shared/agent-ref.ts";
|
|
14
|
-
|
|
15
|
-
import type { ThemeLike as BaseThemeLike } from "../format.ts";
|
|
16
|
-
|
|
17
|
-
// ── Constants ─────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
export const SIDEBAR_WIDTH = 24;
|
|
20
|
-
export const PROMPT_FOLD_LINES = 3;
|
|
21
|
-
export const OUTPUT_TRUNCATE_BYTES = 100_000;
|
|
22
|
-
export const ELLIPSIS = "\u2026"; // U+2026
|
|
23
|
-
|
|
24
|
-
// L2 详情滚动常量(对齐 subagents list-view.ts)。
|
|
25
|
-
/** terminal.rows 读不到时的翻页兜底步长(防 NaN)。 */
|
|
26
|
-
export const PAGE_SCROLL_DEFAULT = 10;
|
|
27
|
-
/** tui.terminal.rows 兜底行数(duck-type 失败时,对齐 subagents TERM_ROWS_FALLBACK)。 */
|
|
28
|
-
export const TERM_ROWS_FALLBACK = 24;
|
|
29
|
-
|
|
30
|
-
// 跨 view 共享的布局常量(WorkflowsView + detail-content 都用)。
|
|
31
|
-
/** box 左右边框字符宽度(│ x 2),用于内容行截断预算。 */
|
|
32
|
-
export const BOX_BORDER_CHARS = 2;
|
|
33
|
-
/** token 数 → k 单位的除数。 */
|
|
34
|
-
export const BUDGET_TOKENS_DIVISOR = 1000;
|
|
35
|
-
/** Activity 区最多显示的 tool call 条数。 */
|
|
36
|
-
export const MAX_TOOL_CALLS_DISPLAY = 3;
|
|
37
|
-
|
|
38
|
-
// 时间换算(模块私有常量)。
|
|
39
|
-
const MS_PER_SEC = 1000;
|
|
40
|
-
const SECS_PER_MIN = 60;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 可显示的状态文本集合。
|
|
44
|
-
*
|
|
45
|
-
* 包含 RunStatus("running"|"done" 不直接显示,转 reason)+ DoneReason
|
|
46
|
-
* (completed/failed/aborted/budget_limited/time_limited)+ ExecutionTraceNode.status
|
|
47
|
-
* (含 "pending"——trace 节点的初始态)。
|
|
48
|
-
*
|
|
49
|
-
* 收窄自 string → 显式联合,编译器会在新增 status 时强制 switch 补齐分支。
|
|
50
|
-
*/
|
|
51
|
-
type StatusText =
|
|
52
|
-
| RunStatus
|
|
53
|
-
| DoneReason
|
|
54
|
-
| "pending";
|
|
55
|
-
|
|
56
|
-
// ── Theme interface (avoids importing Pi runtime) ─────────────
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* views 侧主题形状:仅 fg/bold——本文件所有函数只消费这两个方法。
|
|
60
|
-
* 单源派生自 src/interface/format.ts 的 ThemeLike(Pick 收窄),不再独立声明;
|
|
61
|
-
* 保留窄形状是为了让只有 fg/bold 的 duck-typed theme(含测试 stub)可直接传入。
|
|
62
|
-
*/
|
|
63
|
-
export type ThemeLike = Pick<BaseThemeLike, "fg" | "bold">;
|
|
64
|
-
|
|
65
|
-
// ── Status helpers ────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
/** status → 语义颜色 token(用于给任意文本染色,不含符号)。 */
|
|
68
|
-
function statusColorToken(
|
|
69
|
-
status: StatusText,
|
|
70
|
-
): "success" | "warning" | "error" | "muted" {
|
|
71
|
-
switch (status) {
|
|
72
|
-
case "completed": return "success";
|
|
73
|
-
case "running": return "warning";
|
|
74
|
-
case "failed": case "aborted": return "error";
|
|
75
|
-
default: return "muted";
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function statusDotStr(
|
|
80
|
-
status: StatusText,
|
|
81
|
-
theme: ThemeLike,
|
|
82
|
-
): string {
|
|
83
|
-
return theme.fg(statusColorToken(status), "●");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Format a status badge with color for the header area. */
|
|
87
|
-
export function formatStatusBadge(
|
|
88
|
-
status: StatusText,
|
|
89
|
-
theme: ThemeLike,
|
|
90
|
-
): string {
|
|
91
|
-
switch (status) {
|
|
92
|
-
case "running": return theme.fg("warning", "\u25CF running");
|
|
93
|
-
case "completed": return theme.fg("success", "\u2713 completed");
|
|
94
|
-
case "failed": return theme.fg("error", "\u2717 failed");
|
|
95
|
-
case "aborted": return theme.fg("error", "\u2717 aborted");
|
|
96
|
-
case "budget_limited": return theme.fg("error", "\u26A0 budget");
|
|
97
|
-
case "time_limited": return theme.fg("error", "\u26A0 timeout");
|
|
98
|
-
default: return theme.fg("muted", status);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── Pure formatting functions ─────────────────────────────────
|
|
103
|
-
|
|
104
|
-
/** Group trace nodes by phase. Nodes without phase go to "(no phase)". */
|
|
105
|
-
function groupByPhase(nodes: ExecutionTraceNode[]): Map<string, ExecutionTraceNode[]> {
|
|
106
|
-
const map = new Map<string, ExecutionTraceNode[]>();
|
|
107
|
-
for (const node of nodes) {
|
|
108
|
-
const phase = node.phase || "(default)";
|
|
109
|
-
let arr = map.get(phase);
|
|
110
|
-
if (!arr) {
|
|
111
|
-
arr = [];
|
|
112
|
-
map.set(phase, arr);
|
|
113
|
-
}
|
|
114
|
-
arr.push(node);
|
|
115
|
-
}
|
|
116
|
-
// Sort within each phase by stepIndex ascending (FR-3.2)
|
|
117
|
-
for (const arr of map.values()) {
|
|
118
|
-
arr.sort((a, b) => a.stepIndex - b.stepIndex);
|
|
119
|
-
}
|
|
120
|
-
return map;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Format elapsed time string from startedAt. */
|
|
124
|
-
export function formatElapsed(startedAt?: string, now: number = Date.now()): string {
|
|
125
|
-
if (!startedAt) return "-";
|
|
126
|
-
const ms = now - new Date(startedAt).getTime();
|
|
127
|
-
if (ms < MS_PER_SEC) return "0s";
|
|
128
|
-
const secs = Math.floor(ms / MS_PER_SEC);
|
|
129
|
-
if (secs < SECS_PER_MIN) return `${secs}s`;
|
|
130
|
-
const mins = Math.floor(secs / SECS_PER_MIN);
|
|
131
|
-
const remSecs = secs % SECS_PER_MIN;
|
|
132
|
-
return `${mins}m${remSecs}s`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// formatElapsedSeconds 单源自 src/interface/format.ts(见文末「单源折叠垫片」)。
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Format a live eventLog entry(live 路径 Activity 区用)。
|
|
139
|
-
*
|
|
140
|
-
* tool_start → "→ {label}"
|
|
141
|
-
* tool_end → "← {label}"(done)/ "✗ {label}"(failed)
|
|
142
|
-
* turn_end → "∘ {label}"(turn 摘要)
|
|
143
|
-
* error → "✗ {label}"
|
|
144
|
-
*
|
|
145
|
-
* 对齐 subagents formatEventLine 的视觉风格,但用 workflow 的 ThemeLike(无 spinner)。
|
|
146
|
-
*/
|
|
147
|
-
export function formatEventLine(entry: AgentEventLogEntry, theme: ThemeLike): string {
|
|
148
|
-
switch (entry.type) {
|
|
149
|
-
case "tool_start":
|
|
150
|
-
return `→ ${entry.label}`;
|
|
151
|
-
case "tool_end":
|
|
152
|
-
return entry.status === "failed"
|
|
153
|
-
? theme.fg("error", `✗ ${entry.label}`)
|
|
154
|
-
: `✓ ${entry.label}`;
|
|
155
|
-
case "turn_end":
|
|
156
|
-
return theme.fg("dim", `∘ ${entry.label}`);
|
|
157
|
-
case "error":
|
|
158
|
-
return theme.fg("error", `✗ ${entry.label}`);
|
|
159
|
-
default:
|
|
160
|
-
return entry.label;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Format token + tool call statistics. */
|
|
165
|
-
export function formatTokenStat(
|
|
166
|
-
usage?: { input: number; output: number },
|
|
167
|
-
toolCalls?: ToolCallEntry[],
|
|
168
|
-
elapsed?: string,
|
|
169
|
-
): string {
|
|
170
|
-
const tokens = usage ? usage.input + usage.output : 0;
|
|
171
|
-
const tools = toolCalls?.length ?? 0;
|
|
172
|
-
const base = `${tokens} tok · ${tools} tool calls`;
|
|
173
|
-
return elapsed ? `${base} · ${elapsed}` : base;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* renderResult 的文本兜底:从 result.content[0] 提取纯文本。
|
|
178
|
-
* 多处 tool 的 renderResult 曾各自内联此逻辑,提取后统一调用。
|
|
179
|
-
*/
|
|
180
|
-
export function renderTextFallback(
|
|
181
|
-
result: { content?: Array<{ type: string; text?: string }> },
|
|
182
|
-
): string {
|
|
183
|
-
const first = result.content?.[0];
|
|
184
|
-
return first?.type === "text" ? (first.text ?? "") : "";
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Format a single activity line: ToolName(argsPreview). */
|
|
188
|
-
export function formatActivityLine(entry: ToolCallEntry, maxWidth: number): string {
|
|
189
|
-
// 语义阈值与开销:低于此宽度只显名称;括号占 2 字符 (name)。
|
|
190
|
-
const MIN_ACTIVITY_WIDTH = 10;
|
|
191
|
-
const PARENS_OVERHEAD = 2;
|
|
192
|
-
if (maxWidth < MIN_ACTIVITY_WIDTH) return entry.name;
|
|
193
|
-
const argsBudget = maxWidth - entry.name.length - PARENS_OVERHEAD;
|
|
194
|
-
if (argsBudget <= 0) return truncateToWidth(entry.name, maxWidth);
|
|
195
|
-
const truncated = entry.input.length > argsBudget
|
|
196
|
-
? entry.input.slice(0, argsBudget - 1) + ELLIPSIS
|
|
197
|
-
: entry.input;
|
|
198
|
-
return `${entry.name}(${truncated})`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ── 单源折叠垫片(设计 U11 宿主内合并)─────────────────────────
|
|
202
|
-
// 通用文本 helper 已折叠单源到 src/interface/format.ts——truncLine 为自研
|
|
203
|
-
// ANSI 安全实现(pi-tui truncateToWidth 会在省略号处插 \x1b[0m 断裂背景色,
|
|
204
|
-
// 见 interface/format.ts truncLine 注释),统一取 interface 侧实现。
|
|
205
|
-
// 此处按原名 re-export:WorkflowsView / detail-content(本 wave 只读)的
|
|
206
|
-
// import 面不变即完成单源切换。segFillColored 无 views 侧消费方,直接删除。
|
|
207
|
-
export { formatElapsedSeconds } from "../format.ts";
|
|
208
|
-
export { padToVisible as padVisible } from "../format.ts";
|
|
209
|
-
export { visibleWidth as visibleLen } from "@earendil-works/pi-tui";
|
|
210
|
-
|
|
211
|
-
// ── Phase group (filters empty phases) ────────────────────────
|
|
212
|
-
|
|
213
|
-
export interface PhaseGroup {
|
|
214
|
-
name: string;
|
|
215
|
-
nodes: ExecutionTraceNode[];
|
|
216
|
-
doneCount: number;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** The fallback phase name when node has no explicit phase. */
|
|
220
|
-
const NO_PHASE = "(default)";
|
|
221
|
-
|
|
222
|
-
/** Build phase groups. Nodes without a phase are placed in an unnamed group. */
|
|
223
|
-
export function buildPhaseGroups(nodes: ExecutionTraceNode[]): PhaseGroup[] {
|
|
224
|
-
const map = groupByPhase(nodes);
|
|
225
|
-
const result: PhaseGroup[] = [];
|
|
226
|
-
for (const [name, phaseNodes] of map) {
|
|
227
|
-
if (phaseNodes.length > 0) {
|
|
228
|
-
result.push({
|
|
229
|
-
name: name === NO_PHASE ? "" : name,
|
|
230
|
-
nodes: phaseNodes,
|
|
231
|
-
doneCount: phaseNodes.filter((n) => n.status === "completed").length,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return result;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ── Sidebar phase line formatter ─────────────────────────────
|
|
239
|
-
|
|
240
|
-
export function formatPhaseLine(
|
|
241
|
-
pg: PhaseGroup,
|
|
242
|
-
idx: number,
|
|
243
|
-
isSelected: boolean,
|
|
244
|
-
theme: ThemeLike,
|
|
245
|
-
maxWidth: number,
|
|
246
|
-
): string {
|
|
247
|
-
const pointer = isSelected ? "❯ " : " ";
|
|
248
|
-
const dot = statusDotStr(pg.doneCount === pg.nodes.length ? "completed" : "running", theme);
|
|
249
|
-
const name = pg.name || "(unnamed)";
|
|
250
|
-
const label = `${idx + 1} ${name} ${pg.doneCount}/${pg.nodes.length}`;
|
|
251
|
-
// pointer(2) + dot(1) + space(1)
|
|
252
|
-
const PHASE_PREFIX_WIDTH = 4;
|
|
253
|
-
const budget = maxWidth - PHASE_PREFIX_WIDTH;
|
|
254
|
-
const truncated = visibleWidth(label) > budget
|
|
255
|
-
? truncateToWidth(label, budget - 1) + ELLIPSIS
|
|
256
|
-
: label;
|
|
257
|
-
return `${pointer}${dot} ${truncated}`;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ── Agent one-liner for overview right panel ──────────────────
|
|
261
|
-
|
|
262
|
-
const TOKEN_K = 1000;
|
|
263
|
-
|
|
264
|
-
export function formatAgentOneLiner(node: ExecutionTraceNode, theme: ThemeLike): string {
|
|
265
|
-
const dot = statusDotStr(node.status, theme);
|
|
266
|
-
const elapsed = formatElapsed(
|
|
267
|
-
node.startedAt,
|
|
268
|
-
node.completedAt ? new Date(node.completedAt).getTime() : Date.now(),
|
|
269
|
-
);
|
|
270
|
-
const tok = node.result?.usage;
|
|
271
|
-
const tokStr = tok
|
|
272
|
-
? `${Math.round((tok.input + tok.output) / TOKEN_K)}k tok`
|
|
273
|
-
: "";
|
|
274
|
-
const tcCount = node.result?.toolCalls?.length ?? 0;
|
|
275
|
-
const parts = [dot, displayAgentName(node.agent), node.model];
|
|
276
|
-
if (tokStr) parts.push(`${tokStr} · ${tcCount} tools`);
|
|
277
|
-
parts.push(elapsed);
|
|
278
|
-
return parts.join(" ");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|