@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.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 (58) hide show
  1. package/README.md +6 -5
  2. package/package.json +11 -7
  3. package/relay/relay.mjs +45 -2
  4. package/skills/workflow-script-format/SKILL.md +2 -56
  5. package/src/host/__tests__/inflight-reporter.test.ts +26 -1
  6. package/src/host/__tests__/pi-host.test.ts +418 -4
  7. package/src/host/inflight-reporter.ts +13 -10
  8. package/src/host/pi-host.ts +205 -17
  9. package/src/index.ts +21 -72
  10. package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
  11. package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
  12. package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
  13. package/src/injectors/resource-list-injector.ts +103 -32
  14. package/src/injectors/subagent-list-injector.ts +4 -28
  15. package/src/injectors/workflow-list-injector.ts +8 -4
  16. package/src/interface/__tests__/capture-tool.ts +56 -0
  17. package/src/interface/__tests__/commands-resume.test.ts +103 -0
  18. package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
  19. package/src/interface/__tests__/tool-render.test.ts +105 -2
  20. package/src/interface/__tests__/tool-subagents.test.ts +24 -26
  21. package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
  22. package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
  23. package/src/interface/__tests__/tool-workflow.test.ts +884 -0
  24. package/src/interface/bg-notify-render.ts +18 -9
  25. package/src/interface/command-actions.ts +22 -17
  26. package/src/interface/commands.ts +58 -12
  27. package/src/interface/format.ts +50 -16
  28. package/src/interface/id-preview.ts +4 -4
  29. package/src/interface/subagent-actions.ts +18 -50
  30. package/src/interface/subagent-tool.ts +26 -18
  31. package/src/interface/subagents.ts +5 -11
  32. package/src/interface/tool-render.ts +14 -30
  33. package/src/interface/tool-shared.ts +55 -12
  34. package/src/interface/tool-subagents.ts +21 -11
  35. package/src/interface/tool-workflow-script.ts +52 -72
  36. package/src/interface/tool-workflow.ts +345 -139
  37. package/src/interface/views/WorkflowsView.ts +10 -7
  38. package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
  39. package/src/interface/views/detail-content.ts +3 -5
  40. package/src/jsonl-run-store.ts +656 -544
  41. package/src/model-events.ts +35 -0
  42. package/src/session-lifecycle.ts +376 -85
  43. package/src/subagents-events.ts +56 -0
  44. package/src/workflow-events.ts +285 -200
  45. package/src/workflow-notify.ts +456 -0
  46. package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
  47. package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
  48. package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
  49. package/src/interface/__tests__/detectors.test.ts +0 -161
  50. package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
  51. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
  52. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
  53. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
  54. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
  55. package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
  56. package/src/interface/helpers.ts +0 -272
  57. package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
  58. package/src/interface/views/__tests__/record-progress.test.ts +0 -235
@@ -25,15 +25,14 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
25
25
  import { displayAgentName } from "@zhushanwen/subagent-core";
26
26
  import { deriveOutcome } from "@zhushanwen/subagent-core";
27
27
  import { CLOSED_REASONS } from "@zhushanwen/subagent-core";
28
- import type { ClosedReason, ExecutionOutcome } from "@zhushanwen/subagent-core";
28
+ import type { ClosedReason } from "@zhushanwen/subagent-core";
29
29
  import {
30
30
  firstLineSanitized,
31
- padToVisible,
32
31
  shortId,
33
32
  statusGlyph,
34
33
  type ThemeLike,
35
- truncLine,
36
34
  } from "./format.ts";
35
+ import { padToVisible, truncLine } from "./tui-kit.ts";
37
36
 
38
37
  /** agent 名最大显示宽度。 */
39
38
  const AGENT_MAX_WIDTH = 40;
@@ -50,6 +49,16 @@ const BORDER_CHARS = 2;
50
49
  /** 能画出完整边框的最小宽度(│ │ 各 1 + 至少 1 内容列)。 */
51
50
  const MIN_BORDER_WIDTH = BORDER_CHARS + INNER_PAD_TOTAL + 1;
52
51
 
52
+ /**
53
+ * record 域轮终 outcome 子集([W2 D5] 单源 RunOutcome 收窄别名——interrupted 只由
54
+ * run 收编/回收路径写入,record 轮终构造性不可达)。本文件原经 core 的
55
+ * ExecutionOutcome 消费同值联合;[D2](workflow-run-resume-revision)RunOutcome
56
+ * 词表重构后 Exclude 派生失效(interrupted 已移出),本域改独立实体字面量、与
57
+ * core execution/assembly/types.ts 的 ExecutionOutcome 三值同构(record 域终局
58
+ * 形态不含 run 级 time_limited)。
59
+ */
60
+ type RecordOutcome = "completed" | "failed" | "cancelled";
61
+
53
62
  /**
54
63
  * background 完成通知的 record 形态(从 message.details 提取)。
55
64
  * notifier.ts 构造,本文件防御性解析。
@@ -58,10 +67,10 @@ interface BgNotifyRecord {
58
67
  id: string;
59
68
  /** v4 B-1: closed(终态,含 cancelled)或 running(对话模式轮次完成,旧 idle)。 */
60
69
  status: "running" | "closed";
61
- /** L2 关闭原因子枚举(内部诊断 + outcome 兑底派生输入;经 toClosedReason 防御性收窄)。 */
70
+ /** L2 关闭原因子枚举(内部诊断 + outcome 兜底派生输入;经 toClosedReason 防御性收窄)。 */
62
71
  closedReason?: ClosedReason;
63
- /** 终态三态对外语义(U3 C-outcome)。缺失(升级前旧消息重放)时按 deriveOutcome 兑底。 */
64
- outcome?: ExecutionOutcome;
72
+ /** 终态三态对外语义(U3 C-outcome)。缺失(升级前旧消息重放)时按 deriveOutcome 兜底。 */
73
+ outcome?: RecordOutcome;
65
74
  agent: string;
66
75
  model?: string;
67
76
  result?: string;
@@ -213,7 +222,7 @@ function renderRecordLines(record: BgNotifyRecord, t: ThemeLike): string[] {
213
222
  ? ` ${t.fg("dim", "·")} ${t.fg("accent", truncLine(record.model, MODEL_MAX_WIDTH))}`
214
223
  : "";
215
224
  // U3 C-outcome:verb 与正文分流只读 outcome——单一权威派生(升级前旧消息重放等
216
- // details 缺 outcome 的存量形态经 deriveOutcome(closedReason, error) 兑底,非同构
225
+ // details 缺 outcome 的存量形态经 deriveOutcome(closedReason, error) 兜底,非同构
217
226
  // 重写)。判定先于 patchFile:failed 分支不展示 patch/result(失败轮也会写
218
227
  // patchFile,历史 bug 存档见 deriveOutcome 注释)。
219
228
  const outcome = record.outcome ?? deriveOutcome(record.closedReason, record.error);
@@ -257,14 +266,14 @@ function renderRecordLines(record: BgNotifyRecord, t: ThemeLike): string[] {
257
266
 
258
267
  /**
259
268
  * details.closedReason 防御性收窄:任意字符串 → ClosedReason | undefined。
260
- * 旧数据/外部构造的非法值按缺失处理,交由 deriveOutcome 兑底(消费方不崩溃)。
269
+ * 旧数据/外部构造的非法值按缺失处理,交由 deriveOutcome 兜底(消费方不崩溃)。
261
270
  */
262
271
  function toClosedReason(value: unknown): ClosedReason | undefined {
263
272
  return CLOSED_REASONS.find((reason) => reason === value);
264
273
  }
265
274
 
266
275
  /** details.outcome 防御性收窄:仅接受三态枚举值,其余按缺失处理。 */
267
- function toOutcome(value: unknown): ExecutionOutcome | undefined {
276
+ function toOutcome(value: unknown): RecordOutcome | undefined {
268
277
  return value === "completed" || value === "failed" || value === "cancelled" ? value : undefined;
269
278
  }
270
279
 
@@ -22,22 +22,22 @@ export type SubagentRpcAction =
22
22
  /** /workflows RPC action 判别联合。 */
23
23
  export type WorkflowRpcAction =
24
24
  | { action: "abort"; runId: string }
25
- | { action: "lifecycle-missing-id"; verb: "abort" }
26
- | { action: "lifecycle-removed"; verb: "pause" | "resume" }
25
+ | { action: "resume"; runId: string }
26
+ | { action: "lifecycle-missing-id"; verb: "abort" | "resume" }
27
+ | { action: "lifecycle-removed"; verb: "pause" }
27
28
  | { action: "noop" };
28
29
 
29
30
  /**
30
- * 已移除的 lifecycle verb 集合(pause/resume——run 一次性生命周期化后删除,
31
- * 解析为 lifecycle-removed 提示,而非 unknown noop)。
31
+ * 已移除的 lifecycle verb 集合(pause——run 一次性生命周期化后删除,解析为
32
+ * lifecycle-removed 提示,而非 unknown noop)。resume 不在此列:断点续跑能力
33
+ * 落地后 resume 是一等 action(workflow-run-resume-revision U3「action 词表
34
+ * 适配」覆盖 RPC 通道——taiji GUI 用户经 client.prompt 发 /workflows resume)。
32
35
  */
33
- const REMOVED_LIFECYCLE_VERBS: ReadonlySet<"pause" | "resume"> = new Set([
34
- "pause",
35
- "resume",
36
- ]);
36
+ const REMOVED_LIFECYCLE_VERBS: ReadonlySet<"pause"> = new Set(["pause"]);
37
37
 
38
- /** verb 是否为已移除的 lifecycle action(类型守卫,收窄到 "pause" | "resume")。 */
39
- function isRemovedLifecycleVerb(verb: string): verb is "pause" | "resume" {
40
- return REMOVED_LIFECYCLE_VERBS.has(verb as "pause" | "resume");
38
+ /** verb 是否为已移除的 lifecycle action(类型守卫,收窄到 "pause")。 */
39
+ function isRemovedLifecycleVerb(verb: string): verb is "pause" {
40
+ return REMOVED_LIFECYCLE_VERBS.has(verb as "pause");
41
41
  }
42
42
 
43
43
  /**
@@ -128,8 +128,11 @@ export function parseSubagentRpcCommand(argsStr: string): SubagentRpcAction {
128
128
  * 支持格式:
129
129
  * - `abort <runId>` → { action: "abort", runId }
130
130
  * - `abort`(无 runId)→ { action: "lifecycle-missing-id", verb: "abort" }
131
- * - `pause|resume ...`(已移除的 lifecycle verb,带或不带 runId 均同)→
132
- * { action: "lifecycle-removed", verb }——removed verb 优先于 missing-id 判定
131
+ * - `resume <runId>` → { action: "resume", runId }(断点续跑——与 TUI verb、
132
+ * workflow tool action:"resume" 三通道同语义)
133
+ * - `resume`(无 runId)→ { action: "lifecycle-missing-id", verb: "resume" }
134
+ * - `pause ...`(已移除的 lifecycle verb,带或不带 runId 均同)→
135
+ * { action: "lifecycle-removed", verb: "pause" }——removed verb 优先于 missing-id 判定
133
136
  * (提示语义优先:用户应得知能力已删除并获 abort 指引,而非被引导补 runId)
134
137
  * - 其他(空 / 未知 action / 无参)→ { action: "noop" }
135
138
  */
@@ -141,10 +144,12 @@ export function parseWorkflowRpcCommand(argsStr: string): WorkflowRpcAction {
141
144
  if (isRemovedLifecycleVerb(verb)) {
142
145
  return { action: "lifecycle-removed", verb };
143
146
  }
144
- // abort 是 lifecycle verb 单成员(pause/resume 已移除,见上),直判即可,无需集合机件
145
- if (verb === "abort") {
146
- if (!runId) return { action: "lifecycle-missing-id", verb: "abort" };
147
- return { action: "abort", runId };
147
+ // abort/resume 是 lifecycle verb 现行成员(pause 已移除,见上),直判即可,无需集合机件
148
+ if (verb === "abort" || verb === "resume") {
149
+ if (!runId) return { action: "lifecycle-missing-id", verb };
150
+ return verb === "abort"
151
+ ? { action: "abort", runId }
152
+ : { action: "resume", runId };
148
153
  }
149
154
  return { action: "noop" };
150
155
  }
@@ -8,6 +8,7 @@
8
8
  * - /workflow run <name> → 用 workflow tool { action: "run" }
9
9
  * - /workflow list → 用 workflow tool { action: "status" }
10
10
  * - /workflow abort <run-id> → 用 workflow tool { action: "abort" }
11
+ * - /workflow resume <run-id> → 用 workflow tool { action: "resume" }(interrupted 态断点续跑)
11
12
  * - /workflow save <name> → 用 workflow-script tool { action: "save" }
12
13
  * - /workflow delete <name> → 用 workflow-script tool { action: "delete" }
13
14
  *
@@ -22,17 +23,22 @@
22
23
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
23
24
 
24
25
  import type { LauncherDeps } from "@zhushanwen/subagent-core";
25
- import { abortRun, getSubagentService } from "@zhushanwen/subagent-core";
26
+ import { abortRun, getSubagentService, resumeRun } from "@zhushanwen/subagent-core";
26
27
  import type { WorkflowRun } from "@zhushanwen/subagent-core";
27
28
  import { parseWorkflowRpcCommand, type WorkflowRpcAction } from "./command-actions.ts";
28
29
  import { createWorkflowsView, type ViewActions } from "./views/WorkflowsView.ts";
29
30
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
30
31
  import { LIST_LIMIT } from "./list-shared.ts";
31
32
  import { ID_PREVIEW_LENGTH } from "./id-preview.ts";
33
+ import { displayStatusOf } from "./tool-workflow.ts";
32
34
 
33
- /** status 显示顺序:running 优先(活跃态在前),再 startedAt 倒序。 */
35
+ /** status 显示顺序:running 优先(活跃态在前),interrupted 次之([D2] 暂停态
36
+ * ——无活体但可续跑,排活体后、终局前),再 startedAt 倒序。
37
+ * [W2/V1 D1 第 6 行] 权重表消费键 = displayStatusOf 投影三态(排序键不读
38
+ * outcome 细分——outcome 由 runSummary.reason 并列承载)。 */
34
39
  const STATUS_ORDER: Record<string, number> = {
35
40
  running: 0,
41
+ interrupted: 1,
36
42
  done: 2,
37
43
  };
38
44
  /** 未知 status 的默认排序权重(排在已知 status 之后)。 */
@@ -45,6 +51,7 @@ const UNKNOWN_STATUS_WEIGHT = 9;
45
51
  *
46
52
  * 行为:
47
53
  * - 无 UI(RPC/print/json 模式)→ notify 提示(降级,不打开 TUI)
54
+ * - `/workflows resume <runId>`(TUI)→ 执行断点续跑后 notify,不打开面板
48
55
  * - `/workflows <runId>` 或前缀匹配唯一 run → 直接打开该 run 的 view
49
56
  * - `/workflows`(无参):
50
57
  * · 0 runs → notify "No workflows"
@@ -56,7 +63,7 @@ const UNKNOWN_STATUS_WEIGHT = 9;
56
63
  *
57
64
  * @param api ExtensionAPI
58
65
  * @param getRuns 获取当前 session 的 runs(Map<runId, WorkflowRun>)
59
- * @param deps LauncherDeps(lifecycle abort 用)
66
+ * @param deps LauncherDeps(lifecycle abort / resumeRun 用)
60
67
  */
61
68
  export function registerWorkflowsCommand(
62
69
  api: ExtensionAPI,
@@ -64,7 +71,7 @@ export function registerWorkflowsCommand(
64
71
  deps: LauncherDeps,
65
72
  ): void {
66
73
  api.registerCommand("workflows", {
67
- description: "Open workflow panel. /workflows [runId] | /workflows abort <runId>",
74
+ description: "Open workflow panel. /workflows [runId] | /workflows abort <runId> | /workflows resume <runId>",
68
75
  getArgumentCompletions(prefix: string) {
69
76
  const trimmed = prefix.trimStart();
70
77
  const parts = trimmed.split(/\s+/).filter(Boolean);
@@ -73,18 +80,19 @@ export function registerWorkflowsCommand(
73
80
  if (parts.length <= 1) {
74
81
  return [
75
82
  { label: "abort", value: "abort ", description: "Abort a workflow run" },
83
+ { label: "resume", value: "resume ", description: "Resume an interrupted workflow run" },
76
84
  ].filter((opt) => opt.label.startsWith(trimmed.toLowerCase()));
77
85
  }
78
86
 
79
87
  // 第二级:lifecycle 动词后补全当前 session 的 runId
80
- if (parts[0] === "abort") {
88
+ if (parts[0] === "abort" || parts[0] === "resume") {
81
89
  try {
82
90
  const runs = sortedRuns(getRuns());
83
91
  if (runs.length === 0) return null;
84
92
  return runs.map((r) => ({
85
93
  label: r.runId,
86
94
  value: r.runId,
87
- description: `${r.spec.scriptName} [${r.state.status}]`,
95
+ description: `${r.spec.scriptName} [${displayStatusOf(r)}]`,
88
96
  }));
89
97
  } catch {
90
98
  // 拿不到运行时数据(getRuns 抛错)→ 静默降级,补全失败不影响 command
@@ -107,8 +115,30 @@ export function registerWorkflowsCommand(
107
115
  return;
108
116
  }
109
117
 
118
+ // TUI 动词命令:/workflows resume <runId>——执行断点续跑后 notify,不打开面板
119
+ // (abort 动词在 TUI 侧无既有通道,不在此顺手扩——resume 是本批接入面)
120
+ const trimmedArgs = args.trim();
121
+ const [verb, ...rest] = trimmedArgs.split(/\s+/).filter(Boolean);
122
+ if (verb === "resume") {
123
+ const runId = rest[0];
124
+ if (!runId) {
125
+ ctx.ui.notify("Usage: /workflows resume <runId>", "warning");
126
+ return;
127
+ }
128
+ try {
129
+ await resumeRun(runId, deps);
130
+ ctx.ui.notify(
131
+ `Workflow ${runId}: resuming — completed calls replay at zero token cost, unfinished calls re-dispatched`,
132
+ "info",
133
+ );
134
+ } catch (err) {
135
+ ctx.ui.notify(`Failed to resume workflow ${runId}: ${toErrorMessage(err)}`, "warning");
136
+ }
137
+ return;
138
+ }
139
+
110
140
  // 直接按 runId / 前缀匹配打开
111
- const directRunId = args.trim();
141
+ const directRunId = trimmedArgs;
112
142
  if (directRunId) {
113
143
  await openByRunId(directRunId, getRuns, ctx, deps);
114
144
  return;
@@ -127,7 +157,10 @@ export function registerWorkflowsCommand(
127
157
  *
128
158
  * 各 action 语义:
129
159
  * - abort → 调 lifecycle abortRun,成功/失败均 notify(不向上抛)
130
- * - lifecycle-removed → 已移除的 verb(pause/resume),给定制指引而非 Usage
160
+ * - resume → 调 core resumeRun 断点续跑(与 TUI verb、workflow tool
161
+ * action:"resume" 三通道同语义——D14 args 校验面在 tool action 入口,命令
162
+ * 通道不带 args,沿用历史 args),成功/失败均 notify(不向上抛)
163
+ * - lifecycle-removed → 已移除的 verb(pause),给定制指引而非 Usage
131
164
  * (F3 定稿——提示语义优先于 missing-id:run 一次性生命周期后不可挂起)
132
165
  * - lifecycle-missing-id → Usage 提示
133
166
  * - noop → 无 action 或未知 action:GUI 端已屏蔽此 command 入口,此处兜底
@@ -149,6 +182,19 @@ async function handleRpcMode(
149
182
  }
150
183
  return;
151
184
  }
185
+ case "resume": {
186
+ try {
187
+ await resumeRun(parsed.runId, deps);
188
+ ctx.ui.notify(
189
+ `Workflow ${parsed.runId}: resuming — completed calls replay at zero token cost, unfinished calls re-dispatched`,
190
+ "info",
191
+ );
192
+ } catch (err) {
193
+ const msg = toErrorMessage(err);
194
+ ctx.ui.notify(`Failed to resume workflow ${parsed.runId}: ${msg}`, "warning");
195
+ }
196
+ return;
197
+ }
152
198
  case "lifecycle-removed":
153
199
  ctx.ui.notify(
154
200
  `Workflow ${parsed.verb} has been removed — runs are one-shot. To stop a run early: /workflows abort <runId>`,
@@ -217,7 +263,7 @@ async function openFromList(
217
263
 
218
264
  // 多 run——select 选择
219
265
  const entries = all.map(
220
- (r) => `${r.spec.scriptName} [${r.state.status}] (${r.runId.slice(0, ID_PREVIEW_LENGTH)})`,
266
+ (r) => `${r.spec.scriptName} [${displayStatusOf(r)}] (${r.runId.slice(0, ID_PREVIEW_LENGTH)})`,
221
267
  );
222
268
  const selected = await ctx.ui.select("Select workflow:", entries);
223
269
  if (!selected) return;
@@ -233,8 +279,8 @@ async function openFromList(
233
279
  function sortedRuns(runs: Map<string, WorkflowRun>): WorkflowRun[] {
234
280
  const arr = Array.from(runs.values());
235
281
  return arr.sort((a, b) => {
236
- const sa = STATUS_ORDER[a.state.status] ?? UNKNOWN_STATUS_WEIGHT;
237
- const sb = STATUS_ORDER[b.state.status] ?? UNKNOWN_STATUS_WEIGHT;
282
+ const sa = STATUS_ORDER[displayStatusOf(a)] ?? UNKNOWN_STATUS_WEIGHT;
283
+ const sb = STATUS_ORDER[displayStatusOf(b)] ?? UNKNOWN_STATUS_WEIGHT;
238
284
  if (sa !== sb) return sa - sb;
239
285
  const ta = a.meta.startedAt ? new Date(a.meta.startedAt).getTime() : 0;
240
286
  const tb = b.meta.startedAt ? new Date(b.meta.startedAt).getTime() : 0;
@@ -246,7 +292,7 @@ function sortedRuns(runs: Map<string, WorkflowRun>): WorkflowRun[] {
246
292
  * 打开 WorkflowsView(三级导航 TUI),注入 lifecycle ViewActions。
247
293
  *
248
294
  * ViewActions 通过 deps 调 lifecycle(abort),与 view 解耦——
249
- * view 单测可注入 mock actions(见 views/__tests__/WorkflowsView-signature.test.ts)。
295
+ * view 单测可注入 mock actions(见 views/__tests__/WorkflowsView.test.ts)。
250
296
  *
251
297
  * [H2 W3] live 进度数据源(设计 D2 进度源切换):view 经 store 查询
252
298
  * collectRecordsByParentRunId(run.runId)(内存 ∪ 磁盘重建 ∪ manifest,LIST_LIMIT
@@ -6,8 +6,8 @@
6
6
  // `·` 同级并列字段/thinking 图标;`()` 元数据分组;`›` 工具;`>` 输出;`·` thinking.
7
7
  // 禁用 `│` 做 stats 分隔、`├─`/`└─` 做 eventLog 前缀.
8
8
  //
9
- // 截断/填充/换行(truncLine / padToVisible / segFillColored / wrapText)已迁
10
- // ./tui-kit.ts(post-convergence C4 零依赖叶),本文件 re-export 维持既有导入面.
9
+ // 截断/填充/换行(truncLine / padToVisible / segFillColored / wrapText)单定义
10
+ // ./tui-kit.ts(零依赖叶),消费方直接从 tui-kit import。
11
11
 
12
12
  import os from "node:os";
13
13
 
@@ -20,17 +20,12 @@ import { DEFAULT_AGENT_NAME } from "@zhushanwen/subagent-core";
20
20
  import type {
21
21
  DoneReason,
22
22
  ExecutionTraceNode,
23
+ RunEventFoldCheckpoint,
23
24
  RunStatus,
24
25
  ToolCallEntry,
25
26
  } from "@zhushanwen/subagent-core";
26
27
  import { displayAgentName } from "@zhushanwen/subagent-core";
27
28
 
28
- import { padToVisible, segFillColored, truncLine, wrapText } from "./tui-kit.ts";
29
-
30
- // ANSI 可见宽度布局家族单定义在 ./tui-kit.ts(零依赖叶);此处 re-export 保持
31
- // 既有消费方(tool-render / bg-notify-render / 测试等)导入面零改动。
32
- export { padToVisible, segFillColored, truncLine, wrapText };
33
-
34
29
  /**
35
30
  * ThemeLike:TUI 语义 token 着色接口(duck-typed,兼容 Pi Theme).
36
31
  *
@@ -419,7 +414,7 @@ export function formatDisplayItem(item: DisplayItem, theme: ThemeLike): string {
419
414
  // 自 views/format.ts 并入(D7-① 双轨合并):workflow 视图特有的 badge/phase/
420
415
  // trace 行格式化在此作差异段保留。并入时收敛的同构构件——ThemeLike、
421
416
  // formatElapsedSeconds(本文件版含小时分支,>1h 显示 "1h15m" 而非 "75m30s")、
422
- // segFillColored、padToVisible(后者已随 C4 迁 ./tui-kit.ts,经 re-export 复用)。
417
+ // segFillColored、padToVisible(后者已随 C4 迁 ./tui-kit.ts,消费方直接 import)。
423
418
  // ============================================================
424
419
 
425
420
  // ── Workflow view 布局常量 ────────────────────────────────────
@@ -437,14 +432,16 @@ const MS_PER_SEC = 1000;
437
432
  /**
438
433
  * 可显示的状态文本集合。
439
434
  *
440
- * 包含 RunStatus("running"|"done" 不直接显示,转 reason)+ DoneReason
441
- * (completed/failed/aborted/budget_limited/time_limited)+ ExecutionTraceNode.status
442
- * (含 "pending"——trace 节点的初始态)。
435
+ * 包含 RunStatus("running"|"done" 不直接显示,转 reason)+ 中断投影态
436
+ * "interrupted"([D2] 重水合中断 run 经 meta.interruptedAt 投影——displayStatusOf
437
+ * 三态输出的第三值)+ DoneReason(completed/failed/aborted/budget_limited/
438
+ * time_limited)+ ExecutionTraceNode.status(含 "pending"——trace 节点的初始态)。
443
439
  *
444
440
  * 收窄自 string → 显式联合,编译器会在新增 status 时强制 switch 补齐分支。
445
441
  */
446
442
  export type StatusText =
447
443
  | RunStatus
444
+ | "interrupted"
448
445
  | DoneReason
449
446
  | "pending";
450
447
 
@@ -476,6 +473,7 @@ export function formatStatusBadge(
476
473
  ): string {
477
474
  switch (status) {
478
475
  case "running": return theme.fg("warning", "\u25CF running");
476
+ case "interrupted": return theme.fg("muted", "\u25CB interrupted");
479
477
  case "completed": return theme.fg("success", "\u2713 completed");
480
478
  case "failed": return theme.fg("error", "\u2717 failed");
481
479
  case "aborted": return theme.fg("error", "\u2717 aborted");
@@ -589,10 +587,28 @@ export function formatActivityLine(entry: ToolCallEntry, maxWidth: number): stri
589
587
 
590
588
  // ── Workflow phase group(filters empty phases)──────────────
591
589
 
590
+ /**
591
+ * phase 折叠投影的展示消费面([D3] phase 状态机与展示分组同源)。
592
+ *
593
+ * 数据源 = core fold 的 phases 半边(RunEventFoldCheckpoint["phases"]:
594
+ * phase 名 → { startedAt, settledAt? }——settledAt 有无即 running/settled 态,
595
+ * 由 phase-settled 转移事件驱动)。展示分组的 phase 完成判定读它而非「组内节点
596
+ * 全部 completed」的推导——「续聊时算不算完成」的唯一答案:phase 看 call 落定
597
+ * 事件(fold 权威),agent 会话续聊是另一层。
598
+ */
599
+ export type PhaseFoldProjection = RunEventFoldCheckpoint["phases"];
600
+
592
601
  export interface PhaseGroup {
593
602
  name: string;
594
603
  nodes: ExecutionTraceNode[];
595
604
  doneCount: number;
605
+ /**
606
+ * [D3] phase 状态机态的展示位:true = phase-settled 转移已落账(fold 投影
607
+ * settledAt 有值)。注入折叠投影时读投影(权威);未注入(活体聚合 trace 的
608
+ * 既有调用形态)回落「组内全部节点 completed」推导——两者的同源性由测试
609
+ * 断言钉住(同一事件流喂两侧恒等)。
610
+ */
611
+ settled: boolean;
596
612
  }
597
613
 
598
614
  /** Group trace nodes by phase. Nodes without phase go to "(no phase)". */
@@ -617,16 +633,34 @@ function groupByPhase(nodes: ExecutionTraceNode[]): Map<string, ExecutionTraceNo
617
633
  /** The fallback phase name when node has no explicit phase. */
618
634
  const NO_PHASE = "(default)";
619
635
 
620
- /** Build phase groups. Nodes without a phase are placed in an unnamed group. */
621
- export function buildPhaseGroups(nodes: ExecutionTraceNode[]): PhaseGroup[] {
636
+ /**
637
+ * Build phase groups. Nodes without a phase are placed in an unnamed group.
638
+ *
639
+ * @param nodes trace 节点(活体聚合或 fold 重建的 calls 投影)
640
+ * @param phaseFold 可选的 phase 折叠投影([D3] 状态机权威)——在场时组完成判定
641
+ * 读投影(phase 名 → settledAt 有无);(default) 组(无 phase 归属节点)在投影
642
+ * 中无对应行(fold 只登记显式 phase),恒走节点计数推导兜底。
643
+ */
644
+ export function buildPhaseGroups(
645
+ nodes: ExecutionTraceNode[],
646
+ phaseFold?: PhaseFoldProjection,
647
+ ): PhaseGroup[] {
622
648
  const map = groupByPhase(nodes);
623
649
  const result: PhaseGroup[] = [];
624
650
  for (const [name, phaseNodes] of map) {
625
651
  if (phaseNodes.length > 0) {
652
+ const doneCount = phaseNodes.filter((n) => n.status === "completed").length;
653
+ // 投影在场且该 phase 有行 → 读投影(行存在即权威——running 行 settledAt
654
+ // undefined 判 false,不回落推导);未注入投影 / 无该行((default) 组)→ 推导兜底
655
+ const foldRow = phaseFold?.get(name);
656
+ const settled = foldRow !== undefined
657
+ ? foldRow.settledAt !== undefined
658
+ : doneCount === phaseNodes.length;
626
659
  result.push({
627
660
  name: name === NO_PHASE ? "" : name,
628
661
  nodes: phaseNodes,
629
- doneCount: phaseNodes.filter((n) => n.status === "completed").length,
662
+ doneCount,
663
+ settled,
630
664
  });
631
665
  }
632
666
  }
@@ -643,7 +677,7 @@ export function formatPhaseLine(
643
677
  maxWidth: number,
644
678
  ): string {
645
679
  const pointer = isSelected ? "❯ " : " ";
646
- const dot = statusDotStr(pg.doneCount === pg.nodes.length ? "completed" : "running", theme);
680
+ const dot = statusDotStr(pg.settled ? "completed" : "running", theme);
647
681
  const name = pg.name || "(unnamed)";
648
682
  const label = `${idx + 1} ${name} ${pg.doneCount}/${pg.nodes.length}`;
649
683
  // pointer(2) + dot(1) + space(1)
@@ -3,10 +3,10 @@
3
3
  // run/subagent 标识符的截断展示口径(单点常量)。
4
4
  //
5
5
  // runId(wf-<ts>-<rand>)与 subagentId(UUID)分属两族标识符,但所有展示面
6
- // (workflow tool 渲染 / /workflows 命令行 / GUI label / subagent GUI header)
7
- // 统一取前 8 字符做预览——同一口径保证各界面引用同一 run/subagent 时展示一致
8
- // (helpers 的 GUI label 与 buildWorkflowGui 对齐先例 I#3)。此前 4 处本地常量
9
- // 3 种命名(RUNID_SHORT ×2 / RUN_ID_DISPLAY_LENGTH / SUBAGENT_ID_PREVIEW)收敛到此。
6
+ // (workflow tool 渲染 / /workflows 命令行 / subagent GUI header)统一取前 8 字符
7
+ // 做预览——同一口径保证各界面引用同一 run/subagent 时展示一致(对齐先例 I#3)。
8
+ // 此前 4 处本地常量 3 种命名(RUNID_SHORT ×2 / RUN_ID_DISPLAY_LENGTH /
9
+ // SUBAGENT_ID_PREVIEW)收敛到此。
10
10
 
11
11
  /** 标识符截断展示长度(前 N 字符)。 */
12
12
  export const ID_PREVIEW_LENGTH = 8;
@@ -1,25 +1,19 @@
1
1
  // src/interface/subagent-actions.ts
2
2
  //
3
- // subagent tool 六 action 的 pi adapter 壳(sink 设计 D6② 消费收缩)。
3
+ // subagent tool 六 action 的 TUI/GUI 渲染壳(sink 设计 D6② 消费收缩)。
4
4
  //
5
5
  // 领域内核(入参校验 / message 拒绝文案分流 / fork-from 拒绝链 / 终态映射 /
6
- // list 投影)已下沉 core @zhushanwen/subagent-core/execution/assembly/subagent-actions-core——
7
- // ⛔4 行为快照等值锚定见 core __tests__/subagent-actions-core.test.ts(期望值硬编码自
8
- // 本文件迁移前实测输出,含错误文案逐字锚)。本壳按设计仅保留三类职责:
9
- // 1. core 调用面 re-export:六 handler + 领域类型(签名不变,消费方零改动)
10
- // 2. 参数提取:位于 subagent-tool.ts executeSubagent(action 路由 / listParam /
11
- // cancelParam 解包 / skillPath-cwd 路径检查),本次收缩零改动
12
- // 3. TUI/GUI 渲染:adapter / buildGuiComponent(pi TUI 渲染族按设计留壳)
6
+ // list 投影)在 core @zhushanwen/subagent-core/execution/assembly/subagent-actions-core,
7
+ // 六 handler + 领域类型消费方经 core barrel import;行为快照等值锚定见 core
8
+ // __tests__/subagent-actions-core.test.ts(期望值硬编码自迁移前实测输出,含错误
9
+ // 文案逐字锚)。参数提取位于 subagent-tool.ts executeSubagent(action 路由 /
10
+ // listParam / cancelParam 解包 / skillPath-cwd 路径检查)。本壳只做渲染:
11
+ // adapter(领域对象 → {content, details})+ buildGuiComponent(GUI 协议组件)。
13
12
  //
14
13
  // content(JSON 字符串)给 LLM,details(SubagentToolResult)给 renderResult,同源同处生成。
15
14
 
16
15
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
17
- import {
18
- guiComponent,
19
- type GuiContext,
20
- guiResult,
21
- isGuiCapable,
22
- } from "@zhushanwen/extension-protocol";
16
+ import { guiComponent, type GuiContext } from "@zhushanwen/extension-protocol";
23
17
 
24
18
  import type {
25
19
  CancelHandlerResult,
@@ -32,37 +26,7 @@ import type {
32
26
  } from "@zhushanwen/subagent-core";
33
27
  import { mapRunIcon, mapRunStatus } from "./gui-mappers.ts";
34
28
  import { ID_PREVIEW_LENGTH } from "./id-preview.ts";
35
-
36
- // ============================================================
37
- // core 领域内核 re-export(pi 消费面符号与收缩前一致,经 core barrel 统一消费)
38
- // ============================================================
39
-
40
- export {
41
- cancelHandler,
42
- closeHandler,
43
- endedMessageGuard,
44
- forkFromHandler,
45
- listHandler,
46
- mapExternalState,
47
- messageHandler,
48
- recordToListItem,
49
- startHandler,
50
- } from "@zhushanwen/subagent-core";
51
-
52
- export type {
53
- CancelHandlerInput,
54
- CancelHandlerResult,
55
- CloseHandlerInput,
56
- CloseHandlerResult,
57
- ForkFromHandlerInput,
58
- ForkFromHandlerResult,
59
- ListHandlerInput,
60
- ListHandlerResult,
61
- MessageHandlerInput,
62
- MessageHandlerResult,
63
- StartHandlerInput,
64
- StartHandlerResult,
65
- } from "@zhushanwen/subagent-core";
29
+ import { withGuiAttach } from "./tool-shared.ts";
66
30
 
67
31
  // ============================================================
68
32
  // 渲染层常量 / 类型(pi TUI 渲染族,按设计留壳)
@@ -76,8 +40,9 @@ function assertNever(value: never): string {
76
40
  /**
77
41
  * action ↔ domain 配对的承重类型(替代三处松散 `as`)。
78
42
  * 调用方必须传匹配的 {action, domain}——TS 在调用点校验,错配编译报错。
43
+ * export:gui.test.ts 经 type-only import 消费(RPC/TUI 模式分发契约)。
79
44
  */
80
- type AdapterInput =
45
+ export type AdapterInput =
81
46
  | { action: "start"; domain: StartHandlerResult }
82
47
  | { action: "list"; domain: ListHandlerResult }
83
48
  | { action: "cancel"; domain: CancelHandlerResult }
@@ -128,10 +93,13 @@ export function adapter(
128
93
  detailsBase = { action: "start", subagentId: d.subagentId, sessionFile: d.sessionFile ?? null, slug: d.slug, model: d.model, bgResponse: d.response };
129
94
  }
130
95
 
131
- // GUI 协议:RPC 模式下附加结构化渲染数据(union 各成员已声明 __gui__?,无需强转)
132
- const details: SubagentToolResult = ctx && isGuiCapable(ctx)
133
- ? { ...detailsBase, __gui__: guiResult(buildGuiComponent(input, result)) }
134
- : detailsBase;
96
+ // GUI 协议:RPC 模式下附加结构化渲染数据(attach 单点在 tool-shared;组件构造
97
+ // 用 input/result 而非 detailsBase——与 adapter 领域对象同源)
98
+ const details: SubagentToolResult = withGuiAttach(
99
+ detailsBase,
100
+ ctx,
101
+ () => buildGuiComponent(input, result),
102
+ );
135
103
 
136
104
  // [W3 修复] list action 追加 reminder text block:LLM 调 list 时提醒不要轮询。
137
105
  // reminder 作为第二个 text block(独立追加,不污染 details/JSON schema)。