@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
|
@@ -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,不重复生成)。 */
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// 按键处理 processKey 仍留 list-view,由 list-view factory 注入给组件(KeyHandler 类型在此),
|
|
7
7
|
// 故 list-component 无需 import list-view,循环消除。
|
|
8
8
|
|
|
9
|
-
import type { SubagentService } from "@zhushanwen/subagent-core
|
|
10
|
-
import type { SubagentRecord } from "@zhushanwen/subagent-core
|
|
9
|
+
import type { SubagentService } from "@zhushanwen/subagent-core";
|
|
10
|
+
import type { SubagentRecord } from "@zhushanwen/subagent-core";
|
|
11
11
|
|
|
12
12
|
// ============================================================
|
|
13
13
|
// 常量
|