@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.
Files changed (38) hide show
  1. package/package.json +9 -8
  2. package/skills/subagent-ext-config/SKILL.md +21 -1
  3. package/src/host/pi-host.ts +5 -4
  4. package/src/index.ts +145 -464
  5. package/src/injectors/__tests__/engine-awareness.test.ts +4 -2
  6. package/src/injectors/__tests__/engine-section-stability.test.ts +42 -29
  7. package/src/injectors/engine-awareness.ts +80 -4
  8. package/src/injectors/resource-list-injector.ts +209 -0
  9. package/src/injectors/subagent-list-injector.ts +30 -143
  10. package/src/injectors/workflow-list-injector.ts +23 -150
  11. package/src/interface/__tests__/detectors.test.ts +1 -1
  12. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
  13. package/src/interface/__tests__/subagent-tool-prompt.test.ts +15 -6
  14. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +1 -1
  15. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
  16. package/src/interface/bg-notify-render.ts +4 -4
  17. package/src/interface/commands.ts +115 -75
  18. package/src/interface/format.ts +339 -241
  19. package/src/interface/helpers.ts +2 -2
  20. package/src/interface/list-component.ts +163 -116
  21. package/src/interface/list-shared.ts +2 -2
  22. package/src/interface/list-view.ts +129 -72
  23. package/src/interface/subagent-actions.ts +5 -5
  24. package/src/interface/subagent-tool-schema.ts +21 -6
  25. package/src/interface/subagent-tool.ts +31 -29
  26. package/src/interface/subagents.ts +8 -7
  27. package/src/interface/tool-render.ts +2 -2
  28. package/src/interface/tool-workflow-script.ts +15 -14
  29. package/src/interface/tool-workflow.ts +14 -11
  30. package/src/interface/tui-kit.ts +305 -0
  31. package/src/interface/views/WorkflowsView.ts +267 -196
  32. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  33. package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
  34. package/src/interface/views/detail-content.ts +16 -14
  35. package/src/interface/views/view-constants.ts +19 -0
  36. package/src/jsonl-run-store.ts +11 -7
  37. package/src/session-lifecycle.ts +561 -0
  38. 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/execution/execution-record.ts";
15
- import type { SubagentService } from "@zhushanwen/subagent-core/execution/subagent-service.ts";
16
- import type { SubagentRecord } from "@zhushanwen/subagent-core/execution/types.ts";
17
- import { displayAgentName } from "@zhushanwen/subagent-core/shared/agent-ref.ts";
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 "./format.ts";
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.termRows();
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.termRows() - PAD_ROWS);
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(统一 borderMuted,避 ANSI 嵌套失色)──
271
-
272
- /** 着色框线字符(borderMuted)。所有 ╭╮╰╯├┤┬┴─│ 统一走这里。 */
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
- this.plainBorder("╭", "╮", contentWidth),
305
- this.walled(padToVisible(msg, contentWidth), contentWidth),
306
- this.plainBorder("╰", "╯", contentWidth),
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
- this.titleBorder("╭", title, "╮", contentWidth),
318
- this.walled("", contentWidth),
319
- this.walled(truncLine(t.fg("dim", "(no subagent records)"), contentWidth), contentWidth),
320
- this.walled("", contentWidth),
321
- this.walled(truncLine(t.fg("dim", "Esc to exit"), contentWidth), contentWidth),
322
- this.plainBorder("╰", "╯", contentWidth),
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 = this.b("│");
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(this.titleBorder("╭", t.fg("accent", t.bold(` ${TITLE_SPLIT} `)), "╮", contentWidth));
398
+ lines.push(titleBorder(t, "╭", t.fg("accent", t.bold(` ${TITLE_SPLIT} `)), "╮", contentWidth));
353
399
 
354
400
  // filter 行(阶段 2 时隐藏 filter 提示,显示锚定提示)
355
- const filterLine = inDetail
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
- this.b("├") + segFillColored(leftTitleStyled, this.dash(), leftWidth)
369
- + this.b("┬") + segFillColored(rightTitleStyled, this.dash(), rightWidth) + this.b("┤"),
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
- let leftLines: string[];
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(this.walled(padToVisible(row, contentWidth), contentWidth));
418
+ lines.push(walled(t, padToVisible(row, contentWidth), contentWidth));
398
419
  }
399
420
 
400
421
  // 底分区线
401
- lines.push(this.b("├") + this.dashes(leftWidth) + this.b("┴") + this.dashes(rightWidth) + this.b("┤"));
422
+ lines.push(b(t, "├") + dashes(t, leftWidth) + b(t, "┴") + dashes(t, rightWidth) + b(t, "┤"));
402
423
 
403
424
  // footer(双文案)
404
- const footer = inDetail
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(this.plainBorder("╰", "╯", contentWidth));
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
- // 任务提示词(最重要信息,置顶)。streaming 时 result 未产出,这是「它在干嘛」的唯一线索。
559
- // detail 模式完整换行展示(word-wrap),不截断——task 是判断 subagent 行为的核心依据,
560
- // 截断成省略号会丢信息。首行带 `task: ` 前缀,续行缩进对齐(缩进宽度 = 前缀可见宽度)。
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
- // 当前活动(仅内存 running 源;磁盘重建为 undefined)。streaming 可观测性。
634
+ /** 当前活动行(仅内存 running 源;磁盘重建为 undefined)。streaming 可观测性。 */
635
+ private pushActivitySection(content: string[], record: SubagentRecord, width: number): void {
602
636
  if (record.currentActivity) {
603
- content.push(truncLine(t.fg("accent", `▸ ${record.currentActivity.label}`), width));
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
- if (item.type === "text") {
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/execution/subagent-service.ts";
10
- import type { SubagentRecord } from "@zhushanwen/subagent-core/execution/types.ts";
9
+ import type { SubagentService } from "@zhushanwen/subagent-core";
10
+ import type { SubagentRecord } from "@zhushanwen/subagent-core";
11
11
 
12
12
  // ============================================================
13
13
  // 常量