@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
@@ -21,11 +21,12 @@
21
21
 
22
22
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
23
23
 
24
- import type { LauncherDeps } from "@zhushanwen/subagent-core/orchestration/launcher.ts";
25
- import { abortRun } from "@zhushanwen/subagent-core/orchestration/lifecycle.ts";
26
- import type { WorkflowRun } from "@zhushanwen/subagent-core/orchestration/models/workflow-run.ts";
27
- import { parseWorkflowRpcCommand } from "./command-actions.ts";
24
+ import type { LauncherDeps } from "@zhushanwen/subagent-core";
25
+ import { abortRun } from "@zhushanwen/subagent-core";
26
+ import type { WorkflowRun } from "@zhushanwen/subagent-core";
27
+ import { parseWorkflowRpcCommand, type WorkflowRpcAction } from "./command-actions.ts";
28
28
  import { createWorkflowsView, type ViewActions } from "./views/WorkflowsView.ts";
29
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
29
30
 
30
31
  /** runId 截断长度(显示用)。 */
31
32
  const RUNID_SHORT = 8;
@@ -97,40 +98,8 @@ export function registerWorkflowsCommand(
97
98
  // ── RPC 模式(xyz-agent GUI):解析 lifecycle action 直接执行,不打开 TUI ──
98
99
  // hasUI 在 TUI 和 RPC 都为 true,不能用于区分;用 ctx.mode === "rpc" 判定 GUI 通道。
99
100
  if (ctx.mode === "rpc") {
100
- const parsed = parseWorkflowRpcCommand(args);
101
- switch (parsed.action) {
102
- case "abort": {
103
- try {
104
- await abortRun(parsed.runId, deps);
105
- // abort 单态后 pastTense 固定(原三态拼接随 pause/resume 删除)
106
- ctx.ui.notify(`Workflow ${parsed.runId}: aborted`, "info");
107
- } catch (err) {
108
- const msg = err instanceof Error ? err.message : String(err);
109
- ctx.ui.notify(`Failed to abort workflow ${parsed.runId}: ${msg}`, "warning");
110
- }
111
- return;
112
- }
113
- case "lifecycle-removed":
114
- // 已移除的 lifecycle verb(pause/resume):run 一次性生命周期后不可挂起,
115
- // 给定制指引而非 Usage(F3 定稿——提示语义优先于 missing-id)
116
- ctx.ui.notify(
117
- `Workflow ${parsed.verb} has been removed — runs are one-shot. To stop a run early: /workflows abort <runId>`,
118
- "warning",
119
- );
120
- return;
121
- case "lifecycle-missing-id":
122
- ctx.ui.notify(`Usage: /workflows ${parsed.verb} <runId>`, "warning");
123
- return;
124
- case "noop":
125
- // 无 action 或未知 action:GUI 端已屏蔽此 command 入口,此处兜底
126
- ctx.ui.notify("View workflows in the sidebar Flows tab", "info");
127
- return;
128
- default: {
129
- // exhaustiveness 断言:未来新增 action verb 忘加 case 时 tsc 报错
130
- const _exhaustive: never = parsed;
131
- throw new Error(`Unhandled workflow RPC action: ${String(_exhaustive)}`);
132
- }
133
- }
101
+ await handleRpcMode(parseWorkflowRpcCommand(args), ctx, deps);
102
+ return;
134
103
  }
135
104
 
136
105
  // ── print/json 模式(headless):不可交互 ──
@@ -139,54 +108,125 @@ export function registerWorkflowsCommand(
139
108
  return;
140
109
  }
141
110
 
142
- // 直接按 runId / 前缀匹配打开
111
+ // 直接按 runId / 前缀匹配打开
143
112
  const directRunId = args.trim();
144
113
  if (directRunId) {
145
- const all = sortedRuns(getRuns());
146
- // 精确匹配优先
147
- const exact = all.find((r) => r.runId === directRunId);
148
- if (exact) {
149
- await openView(exact, ctx.ui.theme, ctx, deps);
150
- return;
151
- }
152
- // 前缀匹配
153
- const matched = all.filter((r) => r.runId.startsWith(directRunId));
154
- if (matched.length === 1) {
155
- await openView(matched[0], ctx.ui.theme, ctx, deps);
156
- return;
157
- }
158
- ctx.ui.notify(`Workflow '${directRunId}' not found`, "error");
159
- return;
160
- }
161
-
162
- // 无参——列表选择
163
- const all = sortedRuns(getRuns());
164
- if (all.length === 0) {
165
- ctx.ui.notify("No workflows in current session.", "info");
166
- return;
167
- }
168
-
169
- // 单 run 直开
170
- if (all.length === 1) {
171
- await openView(all[0], ctx.ui.theme, ctx, deps);
114
+ await openByRunId(directRunId, getRuns, ctx, deps);
172
115
  return;
173
116
  }
174
117
 
175
- // 多 run——select 选择
176
- const entries = all.map(
177
- (r) => `${r.spec.scriptName} [${r.state.status}] (${r.runId.slice(0, RUNID_SHORT)})`,
178
- );
179
- const selected = await ctx.ui.select("Select workflow:", entries);
180
- if (!selected) return;
181
- const idx = entries.indexOf(selected);
182
- if (idx === -1) return;
183
- await openView(all[idx], ctx.ui.theme, ctx, deps);
118
+ // 无参——列表选择
119
+ await openFromList(getRuns, ctx, deps);
184
120
  },
185
121
  });
186
122
  }
187
123
 
188
124
  // ── Helpers ──────────────────────────────────────────────────
189
125
 
126
+ /**
127
+ * RPC 模式分支(xyz-agent GUI):解析后的 lifecycle action 直接执行,不打开 TUI。
128
+ *
129
+ * 各 action 语义:
130
+ * - abort → 调 lifecycle abortRun,成功/失败均 notify(不向上抛)
131
+ * - lifecycle-removed → 已移除的 verb(pause/resume),给定制指引而非 Usage
132
+ * (F3 定稿——提示语义优先于 missing-id:run 一次性生命周期后不可挂起)
133
+ * - lifecycle-missing-id → Usage 提示
134
+ * - noop → 无 action 或未知 action:GUI 端已屏蔽此 command 入口,此处兜底
135
+ */
136
+ async function handleRpcMode(
137
+ parsed: WorkflowRpcAction,
138
+ ctx: ExtensionCommandContext,
139
+ deps: LauncherDeps,
140
+ ): Promise<void> {
141
+ switch (parsed.action) {
142
+ case "abort": {
143
+ try {
144
+ await abortRun(parsed.runId, deps);
145
+ // abort 单态后 pastTense 固定(原三态拼接随 pause/resume 删除)
146
+ ctx.ui.notify(`Workflow ${parsed.runId}: aborted`, "info");
147
+ } catch (err) {
148
+ const msg = toErrorMessage(err);
149
+ ctx.ui.notify(`Failed to abort workflow ${parsed.runId}: ${msg}`, "warning");
150
+ }
151
+ return;
152
+ }
153
+ case "lifecycle-removed":
154
+ ctx.ui.notify(
155
+ `Workflow ${parsed.verb} has been removed — runs are one-shot. To stop a run early: /workflows abort <runId>`,
156
+ "warning",
157
+ );
158
+ return;
159
+ case "lifecycle-missing-id":
160
+ ctx.ui.notify(`Usage: /workflows ${parsed.verb} <runId>`, "warning");
161
+ return;
162
+ case "noop":
163
+ ctx.ui.notify("View workflows in the sidebar Flows tab", "info");
164
+ return;
165
+ default: {
166
+ // exhaustiveness 断言:未来新增 action verb 忘加 case 时 tsc 报错
167
+ const _exhaustive: never = parsed;
168
+ throw new Error(`Unhandled workflow RPC action: ${String(_exhaustive)}`);
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * TUI 模式:按 runId 直接打开对应 run 的 view(精确匹配优先,前缀唯一匹配次之)。
175
+ * 均未命中 → error notify。
176
+ */
177
+ async function openByRunId(
178
+ directRunId: string,
179
+ getRuns: () => Map<string, WorkflowRun>,
180
+ ctx: ExtensionCommandContext,
181
+ deps: LauncherDeps,
182
+ ): Promise<void> {
183
+ const all = sortedRuns(getRuns());
184
+ // 精确匹配优先
185
+ const exact = all.find((r) => r.runId === directRunId);
186
+ if (exact) {
187
+ await openView(exact, ctx.ui.theme, ctx, deps);
188
+ return;
189
+ }
190
+ // 前缀匹配
191
+ const matched = all.filter((r) => r.runId.startsWith(directRunId));
192
+ if (matched.length === 1) {
193
+ await openView(matched[0], ctx.ui.theme, ctx, deps);
194
+ return;
195
+ }
196
+ ctx.ui.notify(`Workflow '${directRunId}' not found`, "error");
197
+ }
198
+
199
+ /**
200
+ * TUI 模式无参分支:0 runs → notify;单 run 直开;多 run → select 选择后打开。
201
+ */
202
+ async function openFromList(
203
+ getRuns: () => Map<string, WorkflowRun>,
204
+ ctx: ExtensionCommandContext,
205
+ deps: LauncherDeps,
206
+ ): Promise<void> {
207
+ const all = sortedRuns(getRuns());
208
+ if (all.length === 0) {
209
+ ctx.ui.notify("No workflows in current session.", "info");
210
+ return;
211
+ }
212
+
213
+ // 单 run 直开
214
+ if (all.length === 1) {
215
+ await openView(all[0], ctx.ui.theme, ctx, deps);
216
+ return;
217
+ }
218
+
219
+ // 多 run——select 选择
220
+ const entries = all.map(
221
+ (r) => `${r.spec.scriptName} [${r.state.status}] (${r.runId.slice(0, RUNID_SHORT)})`,
222
+ );
223
+ const selected = await ctx.ui.select("Select workflow:", entries);
224
+ if (!selected) return;
225
+ const idx = entries.indexOf(selected);
226
+ if (idx === -1) return;
227
+ await openView(all[idx], ctx.ui.theme, ctx, deps);
228
+ }
229
+
190
230
  /**
191
231
  * 取 runs 按 status(running 优先)+ startedAt 倒序排序。
192
232
  * 复用 main 的 UX 顺序(活跃态在前,新的在前)。