@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
|
@@ -28,6 +28,22 @@ export function toGuiCtx(ctx: { mode: GuiContext["mode"]; hasUI: boolean } | und
|
|
|
28
28
|
/** TreeItem.status 枚举(协议三态)。 */
|
|
29
29
|
type TreeStatus = NonNullable<TreeItem["status"]>;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* 状态字符串是否为失败态(mapRunStatus/mapRunIcon 共享谓词——两映射仅返回值形态不同,
|
|
33
|
+
* 关键词表必须同步演化,抽单点防双写漂移)。入参须已 toLowerCase。
|
|
34
|
+
*/
|
|
35
|
+
function isFailedStatus(s: string): boolean {
|
|
36
|
+
return (
|
|
37
|
+
s.includes("failed") ||
|
|
38
|
+
s.includes("abort") ||
|
|
39
|
+
s.includes("cancel") ||
|
|
40
|
+
s.includes("crash") ||
|
|
41
|
+
s.includes("error") ||
|
|
42
|
+
s.includes("budget") ||
|
|
43
|
+
s.includes("time_limited")
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
/**
|
|
32
48
|
* 把 workflow/subagent 状态字符串映射到 list-tree 的三态 status。
|
|
33
49
|
*
|
|
@@ -42,17 +58,7 @@ type TreeStatus = NonNullable<TreeItem["status"]>;
|
|
|
42
58
|
export function mapRunStatus(status: string): TreeStatus {
|
|
43
59
|
const s = status.toLowerCase();
|
|
44
60
|
if (s.includes("running")) return "running";
|
|
45
|
-
if (
|
|
46
|
-
s.includes("failed") ||
|
|
47
|
-
s.includes("abort") ||
|
|
48
|
-
s.includes("cancel") ||
|
|
49
|
-
s.includes("crash") ||
|
|
50
|
-
s.includes("error") ||
|
|
51
|
-
s.includes("budget") ||
|
|
52
|
-
s.includes("time_limited")
|
|
53
|
-
) {
|
|
54
|
-
return "failed";
|
|
55
|
-
}
|
|
61
|
+
if (isFailedStatus(s)) return "failed";
|
|
56
62
|
return "done";
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -66,16 +72,6 @@ export function mapRunStatus(status: string): TreeStatus {
|
|
|
66
72
|
export function mapRunIcon(status: string): TreeItemIcon {
|
|
67
73
|
const s = status.toLowerCase();
|
|
68
74
|
if (s.includes("running")) return "circle";
|
|
69
|
-
if (
|
|
70
|
-
s.includes("failed") ||
|
|
71
|
-
s.includes("abort") ||
|
|
72
|
-
s.includes("cancel") ||
|
|
73
|
-
s.includes("crash") ||
|
|
74
|
-
s.includes("error") ||
|
|
75
|
-
s.includes("budget") ||
|
|
76
|
-
s.includes("time_limited")
|
|
77
|
-
) {
|
|
78
|
-
return "cross";
|
|
79
|
-
}
|
|
75
|
+
if (isFailedStatus(s)) return "cross";
|
|
80
76
|
return "check";
|
|
81
77
|
}
|
package/src/interface/helpers.ts
CHANGED
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// bounded JSON pretty 序列化(IF13/#19,TC5/ES5)已下沉 core shared
|
|
14
|
+
// (u-core-atomic 逐字平移,输出与原本地实现字节一致;本地实现已删)。
|
|
15
|
+
import { boundedPrettySerialize } from "@zhushanwen/subagent-core";
|
|
16
|
+
|
|
17
|
+
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
14
18
|
import {
|
|
15
19
|
guiComponent,
|
|
16
20
|
type GuiContext,
|
|
@@ -22,134 +26,8 @@ import { mapRunIcon, mapRunStatus } from "./gui-mappers.ts";
|
|
|
22
26
|
|
|
23
27
|
// ── 常量 ─────────────────────────────────────────────────────
|
|
24
28
|
|
|
25
|
-
const JSON_INDENT = 2;
|
|
26
29
|
const MAX_RESULT_LENGTH = 8000;
|
|
27
30
|
|
|
28
|
-
// ── bounded pretty 序列化(IF13/#19,TC5/ES5,helpers 私有)─────────
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* bounded JSON pretty 序列化:只生成会被保留的前缀(≤8000 输出与
|
|
32
|
-
* JSON.stringify(value, null, JSON_INDENT) 逐字节一致;>8000 输出与
|
|
33
|
-
* 全量序列化后 .slice(0, budget) + "\n... (truncated)" 逐字节一致——
|
|
34
|
-
* 等价测试锚定,见 __tests__/helpers-bounded-serialize.test.ts)。
|
|
35
|
-
*
|
|
36
|
-
* 现状成本:全量 stringify 产生数 MB 中间串再丢弃 99%;本函数在输出字符流
|
|
37
|
-
* 越过 budget 后停止一切生成。保真策略:原语(string/number/boolean/null/bigint)
|
|
38
|
-
* 逐值复用 JSON.stringify(转义/Unicode/数字格式原生一致,零重实现),仅结构
|
|
39
|
-
* 拼装(2 空格缩进/逗号/括号)自实现。
|
|
40
|
-
*
|
|
41
|
-
* 特殊值语义(TC5 a-d,与原生 JSON.stringify 全值域对齐):
|
|
42
|
-
* (a) 含 toJSON 的对象 → 该子树整体走一次 JSON.stringify(subtree)(原生会先调
|
|
43
|
-
* toJSON,如 Date 输出带引号序列化串;bounded 拼装展开会与原生不同)。注意
|
|
44
|
-
* 边界:toJSON 返回对象时原生 pretty 会按缩进展开,本实现按原语串接其
|
|
45
|
-
* compact 形态——设计 TC5 锚定面为 Date 型(返回字符串)toJSON。
|
|
46
|
-
* (b) 任何 stringify 抛出(BigInt → TypeError)→ 整体回退 String(value)(对齐
|
|
47
|
-
* 旧实现整体 try/catch 的整串回退;禁止逐节点回退——输出形态与整串回退不同)。
|
|
48
|
-
* (c) 对象属性值为 undefined/function/symbol → 拼装时跳过(原生省略)。
|
|
49
|
-
* (d) 数组元素为 undefined/function/symbol → 序列化为 "null"(原生行为)。
|
|
50
|
-
*
|
|
51
|
-
* 截断边界(输出字符流层级,与对完整串 slice 逐字节等价):逐段 append 时本段
|
|
52
|
-
* 会使总长越过 budget → 只 append 本段前 (budget - 已累积) 字符(恰好 budget,
|
|
53
|
-
* 可切在转义序列/括号中间,不补任何结构闭合);越过(exceeded)才追加
|
|
54
|
-
* "\n... (truncated)" 标记——恰好 ===budget 不加(> 判定)。
|
|
55
|
-
*
|
|
56
|
-
* 祖先 Set 循环引用守卫:命中 → 整体回退 String(value)(同 (b) 整串回退语义)。
|
|
57
|
-
*/
|
|
58
|
-
function boundedPrettySerialize(value: unknown, budget: number): string {
|
|
59
|
-
const ancestors = new Set<object>();
|
|
60
|
-
let out = "";
|
|
61
|
-
let exceeded = false;
|
|
62
|
-
|
|
63
|
-
/** 逐段追加:越过 budget 时截到恰好 budget 并置 exceeded(后续生成全部剪枝)。 */
|
|
64
|
-
function append(s: string): void {
|
|
65
|
-
if (exceeded) return;
|
|
66
|
-
if (out.length + s.length <= budget) {
|
|
67
|
-
out += s;
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
out += s.slice(0, budget - out.length);
|
|
71
|
-
exceeded = true;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function serialize(v: unknown, depth: number): void {
|
|
75
|
-
if (exceeded) return;
|
|
76
|
-
const t = typeof v;
|
|
77
|
-
// 原语逐值复用原生序列化;bigint 的 JSON.stringify 抛 TypeError → 顶层 catch
|
|
78
|
-
// 整体回退 (b);NaN/Infinity → "null"(原生行为)
|
|
79
|
-
if (v === null || t === "string" || t === "number" || t === "boolean" || t === "bigint") {
|
|
80
|
-
append(JSON.stringify(v));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (t === "object" && v !== null) {
|
|
84
|
-
const obj = v as object;
|
|
85
|
-
if (ancestors.has(obj)) {
|
|
86
|
-
// 循环引用 → 整体回退 String(value)(旧实现同款整串回退)
|
|
87
|
-
throw new TypeError("circular reference");
|
|
88
|
-
}
|
|
89
|
-
// (a) toJSON 子树整体 stringify(Reflect.get 取属性,避免不安全断言)
|
|
90
|
-
if (typeof Reflect.get(obj, "toJSON") === "function") {
|
|
91
|
-
append(JSON.stringify(obj));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
ancestors.add(obj);
|
|
95
|
-
const childIndent = " ".repeat(JSON_INDENT * (depth + 1));
|
|
96
|
-
const closeIndent = " ".repeat(JSON_INDENT * depth);
|
|
97
|
-
if (Array.isArray(obj)) {
|
|
98
|
-
const arr = obj as unknown[];
|
|
99
|
-
if (arr.length === 0) {
|
|
100
|
-
append("[]");
|
|
101
|
-
} else {
|
|
102
|
-
append("[\n");
|
|
103
|
-
for (let i = 0; i < arr.length && !exceeded; i++) {
|
|
104
|
-
if (i > 0) append(",\n");
|
|
105
|
-
append(childIndent);
|
|
106
|
-
const el = arr[i];
|
|
107
|
-
// (d) 数组元素 undefined/function/symbol → "null"
|
|
108
|
-
if (el === undefined || typeof el === "function" || typeof el === "symbol") {
|
|
109
|
-
append("null");
|
|
110
|
-
} else {
|
|
111
|
-
serialize(el, depth + 1);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
append("\n" + closeIndent + "]");
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
// (c) 对象属性 undefined/function/symbol 跳过(Object.entries 同原生:
|
|
118
|
-
// 只取 own enumerable,symbol 键天然不出现;getter 求值语义与原生一致)
|
|
119
|
-
const entries = Object.entries(obj).filter(
|
|
120
|
-
([, val]) => val !== undefined && typeof val !== "function" && typeof val !== "symbol",
|
|
121
|
-
);
|
|
122
|
-
if (entries.length === 0) {
|
|
123
|
-
append("{}");
|
|
124
|
-
} else {
|
|
125
|
-
append("{\n");
|
|
126
|
-
for (let i = 0; i < entries.length && !exceeded; i++) {
|
|
127
|
-
if (i > 0) append(",\n");
|
|
128
|
-
append(childIndent + JSON.stringify(entries[i][0]) + ": ");
|
|
129
|
-
serialize(entries[i][1], depth + 1);
|
|
130
|
-
}
|
|
131
|
-
append("\n" + closeIndent + "}");
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
ancestors.delete(obj);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// 顶层 undefined/function/symbol(调用方守卫后不可达;防御兜底对齐原生 "null")
|
|
138
|
-
append(JSON.stringify(v) ?? "null");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
serialize(value, 0);
|
|
143
|
-
return exceeded ? out + "\n... (truncated)" : out;
|
|
144
|
-
} catch {
|
|
145
|
-
// (b) 整体回退:String(value)(超 budget 仍截断 + 标记,与旧实现截断路径一致)
|
|
146
|
-
const fallback = String(value);
|
|
147
|
-
return fallback.length > budget
|
|
148
|
-
? fallback.slice(0, budget) + "\n... (truncated)"
|
|
149
|
-
: fallback;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
31
|
/**
|
|
154
32
|
* notifiedRunIds 去重窗口大小。
|
|
155
33
|
*
|
|
@@ -164,10 +42,11 @@ const RUN_ID_DISPLAY_LENGTH = 8;
|
|
|
164
42
|
/**
|
|
165
43
|
* notifyDone 的 details 结构(通过 pi.sendMessage 透传给前端)。
|
|
166
44
|
*
|
|
167
|
-
* 抽取为显式接口替代裸 Record<string, unknown>,明确 __gui__
|
|
168
|
-
*
|
|
45
|
+
* 抽取为显式接口替代裸 Record<string, unknown>,明确 __gui__ 契约。
|
|
46
|
+
* 模块私有(原 export 已删):无跨文件消费者,测试经 notifyDone 公共入口
|
|
47
|
+
* 观察并内联类型。
|
|
169
48
|
*/
|
|
170
|
-
|
|
49
|
+
interface WorkflowNotifyDetails {
|
|
171
50
|
runId: string;
|
|
172
51
|
name: string;
|
|
173
52
|
status: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/interface/list-component.ts
|
|
2
2
|
//
|
|
3
3
|
// /subagents list 全屏带框左右分屏组件实现。
|
|
4
4
|
// 从 list-view.ts 抽出(文件行数控制):list-view.ts 保留 factory / key 处理,
|
|
@@ -11,26 +11,36 @@
|
|
|
11
11
|
import type { Component } from "@earendil-works/pi-tui";
|
|
12
12
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
13
13
|
|
|
14
|
-
import { computeElapsedSeconds } from "@zhushanwen/subagent-core
|
|
15
|
-
import type {
|
|
16
|
-
import type {
|
|
17
|
-
import {
|
|
14
|
+
import { computeElapsedSeconds } from "@zhushanwen/subagent-core";
|
|
15
|
+
import type { DisplayItem } from "@zhushanwen/subagent-core";
|
|
16
|
+
import type { SubagentService } from "@zhushanwen/subagent-core";
|
|
17
|
+
import type { SubagentRecord } from "@zhushanwen/subagent-core";
|
|
18
|
+
import { displayAgentName } from "@zhushanwen/subagent-core";
|
|
18
19
|
import {
|
|
19
20
|
firstLine,
|
|
20
21
|
formatDisplayItem,
|
|
21
22
|
formatElapsedSeconds,
|
|
22
23
|
formatEventLine,
|
|
23
24
|
formatTokens,
|
|
24
|
-
padToVisible,
|
|
25
25
|
sanitizeLabel,
|
|
26
|
-
segFillColored,
|
|
27
26
|
shortId,
|
|
28
27
|
spinnerGlyph,
|
|
29
28
|
statusGlyph,
|
|
30
29
|
type ThemeLike,
|
|
30
|
+
} from "./format.ts";
|
|
31
|
+
import {
|
|
32
|
+
b,
|
|
33
|
+
dash,
|
|
34
|
+
dashes,
|
|
35
|
+
padToVisible,
|
|
36
|
+
plainBorder,
|
|
37
|
+
segFillColored,
|
|
38
|
+
termRows,
|
|
39
|
+
titleBorder,
|
|
31
40
|
truncLine,
|
|
41
|
+
walled,
|
|
32
42
|
wrapText,
|
|
33
|
-
} from "./
|
|
43
|
+
} from "./tui-kit.ts";
|
|
34
44
|
import {
|
|
35
45
|
applyFilter,
|
|
36
46
|
type DetailKeyContext,
|
|
@@ -61,8 +71,6 @@ const BORDER_WIDTH = 2;
|
|
|
61
71
|
const SPLIT_FIXED_LINES = 6;
|
|
62
72
|
/** 终端最小行数(低于此回退紧凑空列表框)。 */
|
|
63
73
|
const MIN_TERM_ROWS = 8;
|
|
64
|
-
/** terminal.rows 读不到时的兜底行数(防 duck-type 失败)。 */
|
|
65
|
-
const TERM_ROWS_FALLBACK = 24;
|
|
66
74
|
/** 自画视觉边距:框外左右各 1 列空白(盖住底下对话流)。 */
|
|
67
75
|
const PAD_COLS = 2;
|
|
68
76
|
/** 自画视觉边距:框外顶底各 1 行空白。 */
|
|
@@ -145,7 +153,7 @@ export class SubagentsListComponent implements Component {
|
|
|
145
153
|
if (this.frameRecords !== undefined && Date.now() - this.frameAt <= FRAME_TTL_MS) {
|
|
146
154
|
return this.frameRecords;
|
|
147
155
|
}
|
|
148
|
-
const records = this.service.collectRecords(LIST_LIMIT);
|
|
156
|
+
const records = this.service.queries.collectRecords(LIST_LIMIT);
|
|
149
157
|
this.frameRecords = records;
|
|
150
158
|
this.frameAt = Date.now();
|
|
151
159
|
return records;
|
|
@@ -153,7 +161,7 @@ export class SubagentsListComponent implements Component {
|
|
|
153
161
|
|
|
154
162
|
/** [perf] 选中项详情数据:优先全量(懒加载 + per-file 缓存),拿不到回退 light。 */
|
|
155
163
|
private fullRecordOf(r: SubagentRecord): SubagentRecord {
|
|
156
|
-
return this.service.getFullRecord(r.id) ?? r;
|
|
164
|
+
return this.service.queries.getFullRecord(r.id) ?? r;
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
invalidate(): void {
|
|
@@ -162,7 +170,7 @@ export class SubagentsListComponent implements Component {
|
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
render(width: number): string[] {
|
|
165
|
-
const rows = this.
|
|
173
|
+
const rows = termRows(this.tui);
|
|
166
174
|
const key = `${width}x${rows}`;
|
|
167
175
|
if (key === this.cachedKey && this.cachedLines) return this.cachedLines;
|
|
168
176
|
const lines = this.buildLines(width, rows);
|
|
@@ -180,7 +188,7 @@ export class SubagentsListComponent implements Component {
|
|
|
180
188
|
// contentLines = 详情内容总行数(含元数据/段头/eventLog/result/error,单一数据源)。
|
|
181
189
|
// 与 renderRightDetail 的 viewH + max 计算保持一致。
|
|
182
190
|
// [perf] 详情长度需重数据:选中项懒加载全量(带缓存)。
|
|
183
|
-
const innerRows = Math.max(MIN_INNER_ROWS, this.
|
|
191
|
+
const innerRows = Math.max(MIN_INNER_ROWS, termRows(this.tui) - PAD_ROWS);
|
|
184
192
|
const bodyH = Math.max(1, innerRows - SPLIT_FIXED_LINES);
|
|
185
193
|
const detailCtx: DetailKeyContext = {
|
|
186
194
|
viewportHeight: bodyH,
|
|
@@ -199,12 +207,6 @@ export class SubagentsListComponent implements Component {
|
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
/** 安全读 terminal.rows(兜底防 duck-type 失败)。 */
|
|
203
|
-
private termRows(): number {
|
|
204
|
-
const rows = this.tui.terminal?.rows;
|
|
205
|
-
return typeof rows === "number" && rows > 0 ? rows : TERM_ROWS_FALLBACK;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
210
|
// ── 内部:渲染 ──────────────────────────────────────────
|
|
209
211
|
|
|
210
212
|
/**
|
|
@@ -267,32 +269,9 @@ export class SubagentsListComponent implements Component {
|
|
|
267
269
|
return result;
|
|
268
270
|
}
|
|
269
271
|
|
|
270
|
-
// ── 边框着色 helper
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
private b(s: string): string {
|
|
274
|
-
return this.theme.fg("borderMuted", s);
|
|
275
|
-
}
|
|
276
|
-
/** 着色单字符填充用的 `─`(供 segFillColored 的 fillStyled)。 */
|
|
277
|
-
private dash(): string {
|
|
278
|
-
return this.theme.fg("borderMuted", "─");
|
|
279
|
-
}
|
|
280
|
-
/** 满宽 `─` 填充串(borderMuted)。n 次单字符着色,ANSI 自然延续。 */
|
|
281
|
-
private dashes(n: number): string {
|
|
282
|
-
return this.dash().repeat(Math.max(0, n));
|
|
283
|
-
}
|
|
284
|
-
/** 顶/底框行:`╭` + 着色标题填充 + `╮`(或 ╰╯)。每段独立着色,无嵌套。 */
|
|
285
|
-
private titleBorder(left: string, titleStyled: string, right: string, contentWidth: number): string {
|
|
286
|
-
return this.b(left) + segFillColored(titleStyled, this.dash(), contentWidth) + this.b(right);
|
|
287
|
-
}
|
|
288
|
-
/** 纯线顶/底框(无标题):`╭` + `─`×W + `╮`。 */
|
|
289
|
-
private plainBorder(left: string, right: string, contentWidth: number): string {
|
|
290
|
-
return this.b(left) + this.dashes(contentWidth) + this.b(right);
|
|
291
|
-
}
|
|
292
|
-
/** 内容行墙:`│` + 内容(pad 到 contentWidth) + `│`,墙字符 borderMuted。 */
|
|
293
|
-
private walled(content: string, contentWidth: number): string {
|
|
294
|
-
return `${this.b("│")}${padToVisible(content, contentWidth)}${this.b("│")}`;
|
|
295
|
-
}
|
|
272
|
+
// ── 边框着色 helper 家族(b/dash/dashes/titleBorder/plainBorder/walled)──
|
|
273
|
+
// 已收敛 ./tui-kit.ts 自由函数形态(post-convergence C4,WorkflowsView 形态胜出);
|
|
274
|
+
// 此处经 kit 导入消费,边框字符统一 borderMuted(避 ANSI 嵌套失色)。
|
|
296
275
|
|
|
297
276
|
// ── 分支 1:终端太小 ──────────────────────────────────
|
|
298
277
|
|
|
@@ -301,9 +280,9 @@ export class SubagentsListComponent implements Component {
|
|
|
301
280
|
const contentWidth = Math.max(1, width - BORDER_WIDTH);
|
|
302
281
|
const msg = t.fg("warning", `Terminal too small (need >=${MIN_TERM_ROWS} rows)`);
|
|
303
282
|
return [
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
283
|
+
plainBorder(t, "╭", "╮", contentWidth),
|
|
284
|
+
walled(t, padToVisible(msg, contentWidth), contentWidth),
|
|
285
|
+
plainBorder(t, "╰", "╯", contentWidth),
|
|
307
286
|
];
|
|
308
287
|
}
|
|
309
288
|
|
|
@@ -314,16 +293,83 @@ export class SubagentsListComponent implements Component {
|
|
|
314
293
|
const contentWidth = Math.max(1, width - BORDER_WIDTH);
|
|
315
294
|
const title = t.fg("accent", t.bold(` ${TITLE_SPLIT} `));
|
|
316
295
|
return [
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
296
|
+
titleBorder(t, "╭", title, "╮", contentWidth),
|
|
297
|
+
walled(t, "", contentWidth),
|
|
298
|
+
walled(t, truncLine(t.fg("dim", "(no subagent records)"), contentWidth), contentWidth),
|
|
299
|
+
walled(t, "", contentWidth),
|
|
300
|
+
walled(t, truncLine(t.fg("dim", "Esc to exit"), contentWidth), contentWidth),
|
|
301
|
+
plainBorder(t, "╰", "╯", contentWidth),
|
|
323
302
|
];
|
|
324
303
|
}
|
|
325
304
|
|
|
326
305
|
// ── 分支 3:分屏满屏框(detailMode 控制右侧预览 vs 完整翻屏)──
|
|
306
|
+
// 主函数只留「顶框 → filter 行 → 分区线 → body → 底分区线 → footer → 底框」编排,
|
|
307
|
+
// 各段文案/内容构建拆至 splitBox* 私有方法。
|
|
308
|
+
|
|
309
|
+
/** filter 行文案:阶段 2 锚定提示 / filter 输入态(光标 _)/ 空态。 */
|
|
310
|
+
private splitBoxFilterLine(selected: SubagentRecord | null, inDetail: boolean): string {
|
|
311
|
+
const t = this.theme;
|
|
312
|
+
return inDetail
|
|
313
|
+
? t.fg("dim", `Pinned: ${selected?.agent ? displayAgentName(selected.agent) : ""} · Esc to return to list`)
|
|
314
|
+
: (this.state.filterText
|
|
315
|
+
? `${t.fg("dim", "filter: ")}${t.bold(this.state.filterText)}${t.fg("accent", "_")}`
|
|
316
|
+
: `${t.fg("dim", "filter: ")}${t.fg("accent", "_")}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 分区线右标题:阶段 2 带滚动位置指示(detailScrollInfo),否则固定 " Detail "。 */
|
|
320
|
+
private splitBoxRightTitle(
|
|
321
|
+
inDetail: boolean,
|
|
322
|
+
selectedFull: SubagentRecord | null,
|
|
323
|
+
bodyH: number,
|
|
324
|
+
detailContent: string[] | null,
|
|
325
|
+
): string {
|
|
326
|
+
const t = this.theme;
|
|
327
|
+
return inDetail
|
|
328
|
+
? t.fg("accent", t.bold(` ${TITLE_RIGHT}${this.detailScrollInfo(selectedFull, bodyH, detailContent?.length)} `))
|
|
329
|
+
: t.fg("accent", t.bold(` ${TITLE_RIGHT} `));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** body 左右列内容:filter 无匹配时双提示占位(保留分屏布局);
|
|
333
|
+
* 否则左列视口窗口(选中行居中,到列表顶/底贴边)+ 右列预览 or 完整翻屏。 */
|
|
334
|
+
private splitBoxBodyLines(
|
|
335
|
+
records: SubagentRecord[],
|
|
336
|
+
selectedFull: SubagentRecord | null,
|
|
337
|
+
inDetail: boolean,
|
|
338
|
+
leftWidth: number,
|
|
339
|
+
rightWidth: number,
|
|
340
|
+
bodyH: number,
|
|
341
|
+
detailContent: string[] | null,
|
|
342
|
+
): { leftLines: string[]; rightLines: string[] } {
|
|
343
|
+
const t = this.theme;
|
|
344
|
+
if (records.length === 0) {
|
|
345
|
+
// filter 无匹配:保留分屏布局,左右都显示提示
|
|
346
|
+
return {
|
|
347
|
+
leftLines: [t.fg("dim", `(no match for "${this.state.filterText}")`)],
|
|
348
|
+
rightLines: [t.fg("dim", "(no record selected)")],
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
// 左列视口窗口:选中行尽量居中,到列表顶/底贴边。
|
|
352
|
+
// 保证 leftLines.length <= bodyH → bodyRows = bodyH 恒定,帧行不溢出终端(无残影)。
|
|
353
|
+
const maxLeftStart = Math.max(0, records.length - bodyH);
|
|
354
|
+
const leftStart = Math.max(0, Math.min(
|
|
355
|
+
Math.floor(this.state.selectedIdx - bodyH / VERT_CENTER_DIVISOR),
|
|
356
|
+
maxLeftStart,
|
|
357
|
+
));
|
|
358
|
+
return {
|
|
359
|
+
leftLines: this.renderLeftColumn(records, leftWidth, leftStart, bodyH),
|
|
360
|
+
rightLines: inDetail
|
|
361
|
+
? this.renderRightDetail(selectedFull, rightWidth, bodyH, detailContent)
|
|
362
|
+
: this.renderRightPreview(selectedFull, rightWidth, bodyH),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** footer 双文案:阶段 2 滚动提示(running 时附取消提示)/ 阶段 1 导航提示。 */
|
|
367
|
+
private splitBoxFooter(inDetail: boolean, selected: SubagentRecord | null): string {
|
|
368
|
+
const t = this.theme;
|
|
369
|
+
return inDetail
|
|
370
|
+
? t.fg("dim", "Esc back to list · Up/Dn/PgUp/PgDn/Home/End scroll detail" + this.cancelHint(selected))
|
|
371
|
+
: t.fg("dim", "Up/Dn navigate · Enter detail · type to filter · Esc exit");
|
|
372
|
+
}
|
|
327
373
|
|
|
328
374
|
private renderSplitBox(records: SubagentRecord[], width: number, rows: number): string[] {
|
|
329
375
|
const t = this.theme;
|
|
@@ -331,7 +377,7 @@ export class SubagentsListComponent implements Component {
|
|
|
331
377
|
// 左右列宽:左按比例,右占余下(减去分隔符 1 列)
|
|
332
378
|
const leftWidth = Math.max(COL_MIN_WIDTH, Math.floor(contentWidth * LEFT_COL_RATIO));
|
|
333
379
|
const rightWidth = Math.max(COL_MIN_WIDTH, contentWidth - leftWidth - 1);
|
|
334
|
-
const sep =
|
|
380
|
+
const sep = b(t, "│");
|
|
335
381
|
|
|
336
382
|
// 满屏可用 body 高 = 内框高 - 固定行(顶框/filter/分区线/底分区线/footer/底框 = 6)
|
|
337
383
|
// rows 参数已是内框高(顶底空白边距已在 buildLines 扣除)。
|
|
@@ -349,65 +395,37 @@ export class SubagentsListComponent implements Component {
|
|
|
349
395
|
const lines: string[] = [];
|
|
350
396
|
|
|
351
397
|
// 顶框(嵌入标题,分段着色)
|
|
352
|
-
lines.push(
|
|
398
|
+
lines.push(titleBorder(t, "╭", t.fg("accent", t.bold(` ${TITLE_SPLIT} `)), "╮", contentWidth));
|
|
353
399
|
|
|
354
400
|
// filter 行(阶段 2 时隐藏 filter 提示,显示锚定提示)
|
|
355
|
-
|
|
356
|
-
? t.fg("dim", `Pinned: ${selected?.agent ? displayAgentName(selected.agent) : ""} · Esc to return to list`)
|
|
357
|
-
: (this.state.filterText
|
|
358
|
-
? `${t.fg("dim", "filter: ")}${t.bold(this.state.filterText)}${t.fg("accent", "_")}`
|
|
359
|
-
: `${t.fg("dim", "filter: ")}${t.fg("accent", "_")}`);
|
|
360
|
-
lines.push(this.walled(padToVisible(truncLine(filterLine, contentWidth), contentWidth), contentWidth));
|
|
401
|
+
lines.push(walled(t, padToVisible(truncLine(this.splitBoxFilterLine(selected, inDetail), contentWidth), contentWidth), contentWidth));
|
|
361
402
|
|
|
362
403
|
// 分区线(嵌入左/右标题,分段着色)
|
|
363
404
|
const leftTitleStyled = t.fg("accent", t.bold(` ${TITLE_LEFT} `));
|
|
364
|
-
const rightTitleStyled = inDetail
|
|
365
|
-
? t.fg("accent", t.bold(` ${TITLE_RIGHT}${this.detailScrollInfo(selectedFull, bodyH, detailContent?.length)} `))
|
|
366
|
-
: t.fg("accent", t.bold(` ${TITLE_RIGHT} `));
|
|
405
|
+
const rightTitleStyled = this.splitBoxRightTitle(inDetail, selectedFull, bodyH, detailContent);
|
|
367
406
|
lines.push(
|
|
368
|
-
|
|
369
|
-
|
|
407
|
+
b(t, "├") + segFillColored(leftTitleStyled, dash(t), leftWidth)
|
|
408
|
+
+ b(t, "┬") + segFillColored(rightTitleStyled, dash(t), rightWidth) + b(t, "┤"),
|
|
370
409
|
);
|
|
371
410
|
|
|
372
411
|
// body:左列 record 列表 + 右列(预览 or 完整翻屏)
|
|
373
|
-
|
|
374
|
-
let rightLines: string[];
|
|
375
|
-
if (records.length === 0) {
|
|
376
|
-
// filter 无匹配:保留分屏布局,左右都显示提示
|
|
377
|
-
leftLines = [t.fg("dim", `(no match for "${this.state.filterText}")`)];
|
|
378
|
-
rightLines = [t.fg("dim", "(no record selected)")];
|
|
379
|
-
} else {
|
|
380
|
-
// 左列视口窗口:选中行尽量居中,到列表顶/底贴边。
|
|
381
|
-
// 保证 leftLines.length <= bodyH → bodyRows = bodyH 恒定,帧行不溢出终端(无残影)。
|
|
382
|
-
const maxLeftStart = Math.max(0, records.length - bodyH);
|
|
383
|
-
const leftStart = Math.max(0, Math.min(
|
|
384
|
-
Math.floor(this.state.selectedIdx - bodyH / VERT_CENTER_DIVISOR),
|
|
385
|
-
maxLeftStart,
|
|
386
|
-
));
|
|
387
|
-
leftLines = this.renderLeftColumn(records, leftWidth, leftStart, bodyH);
|
|
388
|
-
rightLines = inDetail
|
|
389
|
-
? this.renderRightDetail(selectedFull, rightWidth, bodyH, detailContent)
|
|
390
|
-
: this.renderRightPreview(selectedFull, rightWidth, bodyH);
|
|
391
|
-
}
|
|
412
|
+
const { leftLines, rightLines } = this.splitBoxBodyLines(records, selectedFull, inDetail, leftWidth, rightWidth, bodyH, detailContent);
|
|
392
413
|
const bodyRows = Math.max(leftLines.length, rightLines.length, bodyH);
|
|
393
414
|
for (let i = 0; i < bodyRows; i++) {
|
|
394
415
|
const l = leftLines[i] ?? "";
|
|
395
416
|
const r = rightLines[i] ?? "";
|
|
396
417
|
const row = `${padToVisible(truncLine(l, leftWidth), leftWidth)}${sep}${padToVisible(truncLine(r, rightWidth), rightWidth)}`;
|
|
397
|
-
lines.push(
|
|
418
|
+
lines.push(walled(t, padToVisible(row, contentWidth), contentWidth));
|
|
398
419
|
}
|
|
399
420
|
|
|
400
421
|
// 底分区线
|
|
401
|
-
lines.push(
|
|
422
|
+
lines.push(b(t, "├") + dashes(t, leftWidth) + b(t, "┴") + dashes(t, rightWidth) + b(t, "┤"));
|
|
402
423
|
|
|
403
424
|
// footer(双文案)
|
|
404
|
-
|
|
405
|
-
? t.fg("dim", "Esc back to list · Up/Dn/PgUp/PgDn/Home/End scroll detail" + this.cancelHint(selected))
|
|
406
|
-
: t.fg("dim", "Up/Dn navigate · Enter detail · type to filter · Esc exit");
|
|
407
|
-
lines.push(this.walled(padToVisible(truncLine(footer, contentWidth), contentWidth), contentWidth));
|
|
425
|
+
lines.push(walled(t, padToVisible(truncLine(this.splitBoxFooter(inDetail, selected), contentWidth), contentWidth), contentWidth));
|
|
408
426
|
|
|
409
427
|
// 底框
|
|
410
|
-
lines.push(
|
|
428
|
+
lines.push(plainBorder(t, "╰", "╯", contentWidth));
|
|
411
429
|
|
|
412
430
|
return lines;
|
|
413
431
|
}
|
|
@@ -550,14 +568,24 @@ export class SubagentsListComponent implements Component {
|
|
|
550
568
|
return visible;
|
|
551
569
|
}
|
|
552
570
|
|
|
553
|
-
/** 详情内容行(单一数据源:renderRightDetail 渲染 + detailScrollInfo 算长度都走这里)。
|
|
571
|
+
/** 详情内容行(单一数据源:renderRightDetail 渲染 + detailScrollInfo 算长度都走这里)。
|
|
572
|
+
* 只做分段编排;各段构建见 push*Section 家族(顺序 = 原 buildDetailContent 单函数内行序)。 */
|
|
554
573
|
private buildDetailContent(record: SubagentRecord, width: number): string[] {
|
|
555
|
-
const t = this.theme;
|
|
556
574
|
const content: string[] = [];
|
|
575
|
+
this.pushTaskSection(content, record, width);
|
|
576
|
+
this.pushMetaSection(content, record, width);
|
|
577
|
+
this.pushActivitySection(content, record, width);
|
|
578
|
+
this.pushOutputSection(content, record, width);
|
|
579
|
+
this.pushTailSections(content, record, width);
|
|
580
|
+
return content;
|
|
581
|
+
}
|
|
557
582
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
583
|
+
/**
|
|
584
|
+
* task 段:完整 word-wrap 置顶(streaming 时这是「它在干嘛」的唯一线索,不截断)。
|
|
585
|
+
* 首行带 `task: ` 前缀,续行缩进对齐(缩进宽度 = 前缀可见宽度)。
|
|
586
|
+
*/
|
|
587
|
+
private pushTaskSection(content: string[], record: SubagentRecord, width: number): void {
|
|
588
|
+
const t = this.theme;
|
|
561
589
|
const taskPrefix = "task: ";
|
|
562
590
|
const taskWrapWidth = Math.max(1, width - visibleWidth(taskPrefix));
|
|
563
591
|
const taskLines = wrapText(record.task, taskWrapWidth);
|
|
@@ -572,7 +600,11 @@ export class SubagentsListComponent implements Component {
|
|
|
572
600
|
if (taskLines.length === 0) {
|
|
573
601
|
content.push(truncLine(t.fg("accent", `${taskPrefix}(empty)`), width));
|
|
574
602
|
}
|
|
603
|
+
}
|
|
575
604
|
|
|
605
|
+
/** 元数据段:id/mode/status/turns/tokens 行 + model/thinking 行 + parent/children 层级行。 */
|
|
606
|
+
private pushMetaSection(content: string[], record: SubagentRecord, width: number): void {
|
|
607
|
+
const t = this.theme;
|
|
576
608
|
// 元数据:第 1 行 id + 状态 + turns + tokens
|
|
577
609
|
content.push(truncLine(
|
|
578
610
|
t.fg("dim", `${record.id} · ${record.mode} · ${record.status} · ${record.turns} turns · ${formatTokens(record.totalTokens)}`),
|
|
@@ -597,14 +629,21 @@ export class SubagentsListComponent implements Component {
|
|
|
597
629
|
t.fg("dim", `children: ${childIds.length > 0 ? childIds.join(", ") : "(none)"}`),
|
|
598
630
|
width,
|
|
599
631
|
));
|
|
632
|
+
}
|
|
600
633
|
|
|
601
|
-
|
|
634
|
+
/** 当前活动行(仅内存 running 源;磁盘重建为 undefined)。streaming 可观测性。 */
|
|
635
|
+
private pushActivitySection(content: string[], record: SubagentRecord, width: number): void {
|
|
602
636
|
if (record.currentActivity) {
|
|
603
|
-
content.push(truncLine(
|
|
637
|
+
content.push(truncLine(this.theme.fg("accent", `▸ ${record.currentActivity.label}`), width));
|
|
604
638
|
}
|
|
639
|
+
}
|
|
605
640
|
|
|
606
|
-
|
|
607
|
-
|
|
641
|
+
/**
|
|
642
|
+
* Output 段:标题 + displayItems(text 完整 word-wrap / toolCall 单行)。
|
|
643
|
+
* displayItems 为空但 eventLog 有数据时回退 eventLog(旧数据兼容)。
|
|
644
|
+
*/
|
|
645
|
+
private pushOutputSection(content: string[], record: SubagentRecord, width: number): void {
|
|
646
|
+
const t = this.theme;
|
|
608
647
|
content.push("");
|
|
609
648
|
content.push(truncLine(t.fg("accent", t.bold("── Output ──")), width));
|
|
610
649
|
|
|
@@ -621,21 +660,31 @@ export class SubagentsListComponent implements Component {
|
|
|
621
660
|
content.push(truncLine(t.fg("dim", "(no output)"), width));
|
|
622
661
|
} else {
|
|
623
662
|
for (const item of record.displayItems) {
|
|
624
|
-
|
|
625
|
-
// detail 模式:text 完整换行展示(word-wrap),不截断。subagent 的正文输出
|
|
626
|
-
// 可能很长(报告/分析),截断成省略号会丢信息——detail 有翻屏,完整性优先。
|
|
627
|
-
// wrapText 输入纯文本,每行单独着色 toolOutput。
|
|
628
|
-
const textLines = wrapText(item.text ?? "", width);
|
|
629
|
-
for (const tl of textLines) {
|
|
630
|
-
content.push(truncLine(t.fg("toolOutput", tl), width));
|
|
631
|
-
}
|
|
632
|
-
} else {
|
|
633
|
-
// toolCall:单行足够(name + args 摘要 + ✓/✗),truncLine 截断。
|
|
634
|
-
content.push(truncLine(formatDisplayItem(item, t), width));
|
|
635
|
-
}
|
|
663
|
+
this.pushDisplayItemLines(content, item, width);
|
|
636
664
|
}
|
|
637
665
|
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** 单个 displayItem 行:text 完整 word-wrap(每行单独着色),toolCall 单行(name + args 摘要 + ✓/✗)。 */
|
|
669
|
+
private pushDisplayItemLines(content: string[], item: DisplayItem, width: number): void {
|
|
670
|
+
const t = this.theme;
|
|
671
|
+
if (item.type === "text") {
|
|
672
|
+
// detail 模式:text 完整换行展示(word-wrap),不截断。subagent 的正文输出
|
|
673
|
+
// 可能很长(报告/分析),截断成省略号会丢信息——detail 有翻屏,完整性优先。
|
|
674
|
+
// wrapText 输入纯文本,每行单独着色 toolOutput。
|
|
675
|
+
const textLines = wrapText(item.text ?? "", width);
|
|
676
|
+
for (const tl of textLines) {
|
|
677
|
+
content.push(truncLine(t.fg("toolOutput", tl), width));
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
// toolCall:单行足够(name + args 摘要 + ✓/✗),truncLine 截断。
|
|
681
|
+
content.push(truncLine(formatDisplayItem(item, t), width));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
638
684
|
|
|
685
|
+
/** 尾段:result(word-wrap 完整展示)/ error(首行)/ session 文件路径,各自前置空行。 */
|
|
686
|
+
private pushTailSections(content: string[], record: SubagentRecord, width: number): void {
|
|
687
|
+
const t = this.theme;
|
|
639
688
|
if (record.result) {
|
|
640
689
|
content.push("");
|
|
641
690
|
content.push(truncLine(t.fg("accent", "Result:"), width));
|
|
@@ -652,8 +701,6 @@ export class SubagentsListComponent implements Component {
|
|
|
652
701
|
content.push("");
|
|
653
702
|
content.push(truncLine(t.fg("dim", `session: ${record.sessionFile}`), width));
|
|
654
703
|
}
|
|
655
|
-
|
|
656
|
-
return content;
|
|
657
704
|
}
|
|
658
705
|
|
|
659
706
|
/** 详情内容总行数(供 detailScrollInfo 算 max,不重复生成)。 */
|