@zhushanwen/pi-subagent-workflow 0.1.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/agents/context-builder.md +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// src/tui/bg-notify-render.ts
|
|
2
|
+
//
|
|
3
|
+
// background 完成通知的对话流渲染器。
|
|
4
|
+
// pi.registerMessageRenderer("subagent-bg-notify", ...) 注册。
|
|
5
|
+
//
|
|
6
|
+
// notifier.ts 设 display:true + triggerTurn:true:
|
|
7
|
+
// - display:true → Pi 创建 CustomMessageComponent(customMessageBg 背景色块,
|
|
8
|
+
// 与 tool block 的 toolSuccessBg 视觉区分),调本 renderer 渲染内容
|
|
9
|
+
// - triggerTurn:true → 唤醒父 agent 下一 turn,让 LLM 看到「X 完成」
|
|
10
|
+
//
|
|
11
|
+
// 渲染内容(紧凑单行/双行,紫色背景 + 圆角边框):
|
|
12
|
+
// ╭──────────────────────────────────────────────────╮
|
|
13
|
+
// │ ✓ agent · model — background subagent finished │
|
|
14
|
+
// │ {结果首行 / Error 首行} │
|
|
15
|
+
// ╰──────────────────────────────────────────────────╯
|
|
16
|
+
//
|
|
17
|
+
// 注意:renderer 自己施加紫色背景(customMessageBg)。Pi 的
|
|
18
|
+
// CustomMessageComponent 对 customRenderer 返回的组件是「裸 addChild」——
|
|
19
|
+
// 只有 renderer 返回 undefined(走 default 渲染)时才套 customMessageBg box。
|
|
20
|
+
// 故返回裸 Text 会丢失紫色背景,必须显式 Box 包裹。
|
|
21
|
+
|
|
22
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
23
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
firstLine,
|
|
27
|
+
padToVisible,
|
|
28
|
+
shortId,
|
|
29
|
+
statusGlyph,
|
|
30
|
+
type ThemeLike,
|
|
31
|
+
truncLine,
|
|
32
|
+
} from "./format.ts";
|
|
33
|
+
|
|
34
|
+
/** agent 名最大显示宽度。 */
|
|
35
|
+
const AGENT_MAX_WIDTH = 40;
|
|
36
|
+
/** 完成块正文最大显示宽度。 */
|
|
37
|
+
const BODY_MAX_WIDTH = 80;
|
|
38
|
+
/** model 名最大显示宽度(避免 provider/model 路径过长撑爆 head 行)。 */
|
|
39
|
+
const MODEL_MAX_WIDTH = 30;
|
|
40
|
+
/** 边框内左右 padding(│ 后、│ 前各 1 空格)。 */
|
|
41
|
+
const INNER_PAD = 1;
|
|
42
|
+
/** 边框左右 padding 总宽(两侧 INNER_PAD)。 */
|
|
43
|
+
const INNER_PAD_TOTAL = 2;
|
|
44
|
+
/** 边框左右字符占用的列数(│ 和 │ 各 1 列)。 */
|
|
45
|
+
const BORDER_CHARS = 2;
|
|
46
|
+
/** 能画出完整边框的最小宽度(│ │ 各 1 + 至少 1 内容列)。 */
|
|
47
|
+
const MIN_BORDER_WIDTH = BORDER_CHARS + INNER_PAD_TOTAL + 1;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* background 完成通知的 record 形态(从 message.details 提取)。
|
|
51
|
+
* notifier.ts 构造,本文件防御性解析。
|
|
52
|
+
*/
|
|
53
|
+
interface BgNotifyRecord {
|
|
54
|
+
id: string;
|
|
55
|
+
status: "done" | "failed" | "cancelled";
|
|
56
|
+
agent: string;
|
|
57
|
+
model?: string;
|
|
58
|
+
result?: string;
|
|
59
|
+
error?: string;
|
|
60
|
+
/** [MF#1] fork+worktree background 完成通知携带的 patch 文件路径。 */
|
|
61
|
+
patchFile?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 渲染 background 完成通知。
|
|
66
|
+
*
|
|
67
|
+
* 契约(Pi MessageRenderer,core/extensions/types.ts:1060):
|
|
68
|
+
* (message: CustomMessage, options: { expanded }, theme: Theme) => Component | undefined
|
|
69
|
+
*
|
|
70
|
+
* display:true 时 Pi 调本方法,返回 BorderedBgBox(紫色背景 + 圆角边框)。
|
|
71
|
+
* details 异常 → 返回 undefined 走 Pi 默认渲染(兜底)。
|
|
72
|
+
*
|
|
73
|
+
* details 两种形态:
|
|
74
|
+
* - 单条:BgNotifyRecord(status/agent/id/result/error)
|
|
75
|
+
* - 批量:{ batch: true, items: BgNotifyRecord[] }(notifier 合并窗口内多条完成)
|
|
76
|
+
*/
|
|
77
|
+
export function renderBgNotifyMessage(
|
|
78
|
+
message: { details?: unknown },
|
|
79
|
+
_options: { expanded: boolean },
|
|
80
|
+
theme: Theme,
|
|
81
|
+
): Component | undefined {
|
|
82
|
+
const t = theme as ThemeLike;
|
|
83
|
+
|
|
84
|
+
// 批量分支:多条合并,各自渲染一行
|
|
85
|
+
const batch = extractBatch(message.details);
|
|
86
|
+
if (batch) {
|
|
87
|
+
const lines = batch.flatMap((r) => renderRecordLines(r, t));
|
|
88
|
+
return new BorderedBgBox(lines, t);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 单条分支
|
|
92
|
+
const record = extractBgNotifyRecord(message.details);
|
|
93
|
+
if (!record) return undefined;
|
|
94
|
+
return new BorderedBgBox(renderRecordLines(record, t), t);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================
|
|
98
|
+
// 边框 + 背景组件
|
|
99
|
+
// ============================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 紫色背景(customMessageBg)+ 圆角边框(customMessageLabel 色)的自定义组件。
|
|
103
|
+
*
|
|
104
|
+
* 为何不用 Box 组件:Box 只做 padding + 背景,不画 Unicode 边框字符。
|
|
105
|
+
* 本组件在 render(width) 时动态画 `╭─╮ │ ╰─╯` 边框,内部行施加紫色背景。
|
|
106
|
+
*
|
|
107
|
+
* ANSI 安全:内容行可能含 `\x1b[0m`(来自 truncLine 截断或 chalk),
|
|
108
|
+
* 它是全局重置会清除背景色(背景框内省略号后失去背景的根因)。
|
|
109
|
+
* 本组件在施加背景前,把行内全局 reset 替换为只重置前景/粗体/斜体/下划线
|
|
110
|
+
* 的精确 reset(不含背景 `\x1b[49m`),确保背景不断裂。
|
|
111
|
+
*
|
|
112
|
+
* 导出以便其他 message renderer 复用边框样式。
|
|
113
|
+
*/
|
|
114
|
+
export class BorderedBgBox implements Component {
|
|
115
|
+
private lines: string[];
|
|
116
|
+
private t: ThemeLike;
|
|
117
|
+
private cache: { width: number; lines: string[] } | undefined;
|
|
118
|
+
|
|
119
|
+
constructor(lines: string[], t: ThemeLike) {
|
|
120
|
+
this.lines = lines;
|
|
121
|
+
this.t = t;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
invalidate(): void {
|
|
125
|
+
this.cache = undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
render(width: number): string[] {
|
|
129
|
+
if (this.cache && this.cache.width === width) return this.cache.lines;
|
|
130
|
+
|
|
131
|
+
const t = this.t;
|
|
132
|
+
const borderColor = "customMessageLabel";
|
|
133
|
+
|
|
134
|
+
// 窄宽度退化:放不下边框时,去掉边框只保留背景内容(不撑破对齐)
|
|
135
|
+
if (width < MIN_BORDER_WIDTH) {
|
|
136
|
+
const innerWidth = Math.max(1, width);
|
|
137
|
+
const result: string[] = [];
|
|
138
|
+
for (const line of this.lines) {
|
|
139
|
+
const truncated = truncLine(line, innerWidth);
|
|
140
|
+
const bgSafe = sanitizeAnsiForBg(truncated);
|
|
141
|
+
const padded = padToVisible(bgSafe, innerWidth);
|
|
142
|
+
result.push(t.bg("customMessageBg", padded));
|
|
143
|
+
}
|
|
144
|
+
this.cache = { width, lines: result };
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 正常宽度:画完整边框
|
|
149
|
+
// 边框占 BORDER_CHARS 列(左右各 1 个 │),内部 padding 各 INNER_PAD 空格
|
|
150
|
+
const innerWidth = width - BORDER_CHARS - INNER_PAD_TOTAL;
|
|
151
|
+
const horizLen = width - BORDER_CHARS;
|
|
152
|
+
|
|
153
|
+
const result: string[] = [];
|
|
154
|
+
|
|
155
|
+
// 顶边框:╭─...─╮
|
|
156
|
+
result.push(
|
|
157
|
+
t.fg(borderColor, "╭" + "─".repeat(horizLen) + "╮"),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// 内容行:│ {bg(内容)} │
|
|
161
|
+
for (const line of this.lines) {
|
|
162
|
+
const truncated = truncLine(line, innerWidth);
|
|
163
|
+
const bgSafe = sanitizeAnsiForBg(truncated);
|
|
164
|
+
const padded = padToVisible(bgSafe, innerWidth);
|
|
165
|
+
const content = t.bg("customMessageBg", " ".repeat(INNER_PAD) + padded + " ".repeat(INNER_PAD));
|
|
166
|
+
result.push(t.fg(borderColor, "│") + content + t.fg(borderColor, "│"));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 底边框:╰─...─╯
|
|
170
|
+
result.push(
|
|
171
|
+
t.fg(borderColor, "╰" + "─".repeat(horizLen) + "╯"),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
this.cache = { width, lines: result };
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 把行内的全局 ANSI reset 替换为不破坏背景色的精确 reset。
|
|
181
|
+
*
|
|
182
|
+
* `\x1b[0m`(含 `\x1b[0;Nm` 组合形式、`\x1b[m` 省略形式)会清除背景色
|
|
183
|
+
* (`\x1b[49m` 语义),在紫色背景框内导致省略号后失去背景。
|
|
184
|
+
*
|
|
185
|
+
* 替换为 `\x1b[39m\x1b[22m\x1b[23m\x1b[24m`——分别重置前景色、粗体、斜体、
|
|
186
|
+
* 下划线。这覆盖了内容行可能用到的所有前景 SGR 类别,且不含背景 reset。
|
|
187
|
+
*/
|
|
188
|
+
function sanitizeAnsiForBg(text: string): string {
|
|
189
|
+
// 匹配全局 reset:\x1b[0m / \x1b[m / \x1b[0;Nm / \x1b[0;N;Mm 等以 0 开头的 SGR
|
|
190
|
+
return text.replace(/\x1b\[0*(?:;[\d;]*)*m/g, "\x1b[39m\x1b[22m\x1b[23m\x1b[24m");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================
|
|
194
|
+
// 内容行生成
|
|
195
|
+
// ============================================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 渲染单条 record 为多行文本(纯视觉文本,不含边框/背景——由 BorderedBgBox 包裹)。
|
|
199
|
+
*
|
|
200
|
+
* 格式(两行):
|
|
201
|
+
* 第 1 行(标题):✓/✗/■ glyph + agent + model + 状态描述 + shortId
|
|
202
|
+
* - done: `✓ default — background subagent finished - bg-f6f731-10`
|
|
203
|
+
* - failed: `✗ default — background subagent failed - bg-f6f731-10`
|
|
204
|
+
* - cancelled: `■ default — background subagent cancelled - bg-f6f731-10`
|
|
205
|
+
* 第 2 行(正文):结果首行 / Error 首行 / cancelled 无第二行
|
|
206
|
+
*/
|
|
207
|
+
function renderRecordLines(record: BgNotifyRecord, t: ThemeLike): string[] {
|
|
208
|
+
const glyph = statusGlyph(record.status);
|
|
209
|
+
const icon = glyph.icon ?? "•";
|
|
210
|
+
const agent = truncLine(record.agent, AGENT_MAX_WIDTH);
|
|
211
|
+
// model 段:agent 后、状态描述前,accent 色。空则省略(向后兼容旧 record)。
|
|
212
|
+
const modelPart = record.model
|
|
213
|
+
? ` ${t.fg("dim", "·")} ${t.fg("accent", truncLine(record.model, MODEL_MAX_WIDTH))}`
|
|
214
|
+
: "";
|
|
215
|
+
const verb = record.status === "done" ? "finished" : record.status === "failed" ? "failed" : "cancelled";
|
|
216
|
+
const idShort = shortId(record.id);
|
|
217
|
+
const head = `${t.fg(glyph.color, icon)} ${t.bold(agent)}${modelPart}${t.fg("dim", ` — background subagent ${verb} - ${idShort}`)}`;
|
|
218
|
+
|
|
219
|
+
switch (record.status) {
|
|
220
|
+
case "done": {
|
|
221
|
+
if (!record.result && !record.patchFile) return [head];
|
|
222
|
+
const lines: string[] = [];
|
|
223
|
+
if (record.result) {
|
|
224
|
+
lines.push(t.fg("dim", truncLine(firstLineSanitized(record.result), BODY_MAX_WIDTH)));
|
|
225
|
+
}
|
|
226
|
+
// [MF#1] 显示 patch 路径提示(与 LLM content 同源),让用户也能看到改动需 `git apply`。
|
|
227
|
+
if (record.patchFile) {
|
|
228
|
+
lines.push(t.fg("dim", truncLine(`patch: ${record.patchFile} (run: git apply)`, BODY_MAX_WIDTH)));
|
|
229
|
+
}
|
|
230
|
+
return [head, ...lines];
|
|
231
|
+
}
|
|
232
|
+
case "failed":
|
|
233
|
+
return [head, t.fg("dim", truncLine(`Error: ${record.error ? firstLineSanitized(record.error) : "(unknown)"}`, BODY_MAX_WIDTH))];
|
|
234
|
+
case "cancelled":
|
|
235
|
+
default:
|
|
236
|
+
return [head];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 从 message.details 防御性提取批量 record。
|
|
242
|
+
* 形态:{ batch: true, items: BgNotifyRecord[] }。结构不全返回 undefined。
|
|
243
|
+
*/
|
|
244
|
+
function extractBatch(details: unknown): BgNotifyRecord[] | undefined {
|
|
245
|
+
if (typeof details !== "object" || details === null) return undefined;
|
|
246
|
+
const d = details as Record<string, unknown>;
|
|
247
|
+
if (d.batch !== true || !Array.isArray(d.items)) return undefined;
|
|
248
|
+
const records: BgNotifyRecord[] = [];
|
|
249
|
+
for (const item of d.items) {
|
|
250
|
+
const r = extractBgNotifyRecord(item);
|
|
251
|
+
if (r) records.push(r);
|
|
252
|
+
}
|
|
253
|
+
return records.length > 0 ? records : undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 从 message.details 防御性提取 BgNotifyRecord。
|
|
258
|
+
* 结构不全(缺 status / agent)返回 undefined。
|
|
259
|
+
*/
|
|
260
|
+
function extractBgNotifyRecord(details: unknown): BgNotifyRecord | undefined {
|
|
261
|
+
if (typeof details !== "object" || details === null) return undefined;
|
|
262
|
+
const d = details as Record<string, unknown>;
|
|
263
|
+
const status = d.status;
|
|
264
|
+
const agent = d.agent;
|
|
265
|
+
if (
|
|
266
|
+
(status !== "done" && status !== "failed" && status !== "cancelled") ||
|
|
267
|
+
typeof agent !== "string"
|
|
268
|
+
) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
id: typeof d.id === "string" ? d.id : "",
|
|
273
|
+
status,
|
|
274
|
+
agent,
|
|
275
|
+
model: typeof d.model === "string" ? d.model : undefined,
|
|
276
|
+
result: typeof d.result === "string" ? d.result : undefined,
|
|
277
|
+
error: typeof d.error === "string" ? d.error : undefined,
|
|
278
|
+
// [MF#1] 提取 patchFile(fork+worktree background 完成通知携带)。
|
|
279
|
+
patchFile: typeof d.patchFile === "string" ? d.patchFile : undefined,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// firstLine 取首非空行(共享自 ./format.ts);本文件额外压 \r\t 防多行展开。
|
|
284
|
+
function firstLineSanitized(text: string): string {
|
|
285
|
+
return firstLine(text).replace(/[\r\t]+/g, " ");
|
|
286
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — commands
|
|
3
|
+
*
|
|
4
|
+
* 仅注册 /workflows 命令(FR-6)——打开交互式 TUI 面板(WorkflowsView,三级导航
|
|
5
|
+
* phase → agent → detail,UC-3)。
|
|
6
|
+
*
|
|
7
|
+
* 功能由 tool 承担,命令仅保留 /workflows 打开面板:
|
|
8
|
+
* - /workflow run <name> → 用 workflow tool { action: "run" }
|
|
9
|
+
* - /workflow list → 用 workflow tool { action: "status" }
|
|
10
|
+
* - /workflow abort <run-id> → 用 workflow tool { action: "abort" }
|
|
11
|
+
* - /workflow save <name> → 用 workflow-script tool { action: "save" }
|
|
12
|
+
* - /workflow delete <name> → 用 workflow-script tool { action: "delete" }
|
|
13
|
+
*
|
|
14
|
+
* 层归属:Interface。依赖 Pi SDK + Engine lifecycle(pause/resume/abort,注入 ViewActions)
|
|
15
|
+
* + WorkflowsView(读 WorkflowRun 聚合根)。
|
|
16
|
+
*
|
|
17
|
+
* 参考:
|
|
18
|
+
* - domain-models.md §FR-6(command 收口仅 /workflows,打开交互式面板)
|
|
19
|
+
* - spec.md UC-3(用户输入 /workflows,打开三级导航 TUI 面板)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
23
|
+
|
|
24
|
+
import type { LauncherDeps } from "../orchestration/launcher.ts";
|
|
25
|
+
import { abortRun, pauseRun, resumeRun } from "../orchestration/lifecycle.ts";
|
|
26
|
+
import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
|
|
27
|
+
import { createWorkflowsView, type ViewActions } from "./views/WorkflowsView.ts";
|
|
28
|
+
|
|
29
|
+
/** runId 截断长度(显示用)。 */
|
|
30
|
+
const RUNID_SHORT = 8;
|
|
31
|
+
|
|
32
|
+
/** status 显示顺序:running/paused 优先(活跃态在前),再 startedAt 倒序。 */
|
|
33
|
+
const STATUS_ORDER: Record<string, number> = {
|
|
34
|
+
running: 0,
|
|
35
|
+
paused: 1,
|
|
36
|
+
done: 2,
|
|
37
|
+
};
|
|
38
|
+
/** 未知 status 的默认排序权重(排在已知 status 之后)。 */
|
|
39
|
+
const UNKNOWN_STATUS_WEIGHT = 9;
|
|
40
|
+
|
|
41
|
+
// ── /workflows command ───────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 注册 /workflows command——打开 workflow 交互式 TUI 面板(FR-6, UC-3)。
|
|
45
|
+
*
|
|
46
|
+
* 行为:
|
|
47
|
+
* - 无 UI(RPC/print/json 模式)→ notify 提示(降级,不打开 TUI)
|
|
48
|
+
* - `/workflows <runId>` 或前缀匹配唯一 run → 直接打开该 run 的 view
|
|
49
|
+
* - `/workflows`(无参):
|
|
50
|
+
* · 0 runs → notify "No workflows"
|
|
51
|
+
* · 1 run → 直接打开
|
|
52
|
+
* · 多 runs → select 选 → 打开选中 run 的 view
|
|
53
|
+
*
|
|
54
|
+
* ViewActions(pause/resume/abort)由本 command 注入——view 本身不持 lifecycle 依赖,
|
|
55
|
+
* 只通过 actions 回调与 engine 交互(解耦,便于 view 单测)。
|
|
56
|
+
*
|
|
57
|
+
* @param api ExtensionAPI
|
|
58
|
+
* @param getRuns 获取当前 session 的 runs(Map<runId, WorkflowRun>)
|
|
59
|
+
* @param deps LauncherDeps(lifecycle pause/resume/abort 用)
|
|
60
|
+
*/
|
|
61
|
+
export function registerWorkflowsCommand(
|
|
62
|
+
api: ExtensionAPI,
|
|
63
|
+
getRuns: () => Map<string, WorkflowRun>,
|
|
64
|
+
deps: LauncherDeps,
|
|
65
|
+
): void {
|
|
66
|
+
api.registerCommand("workflows", {
|
|
67
|
+
description: "Open workflow interactive panel. /workflows [runId] to open a specific run.",
|
|
68
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
69
|
+
// RPC/print/json 模式无 TUI——降级提示
|
|
70
|
+
if (!ctx.hasUI) {
|
|
71
|
+
ctx.ui.notify("/workflows requires interactive mode", "error");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 直接按 runId / 前缀匹配打开
|
|
76
|
+
const directRunId = args.trim();
|
|
77
|
+
if (directRunId) {
|
|
78
|
+
const all = sortedRuns(getRuns());
|
|
79
|
+
// 精确匹配优先
|
|
80
|
+
const exact = all.find((r) => r.runId === directRunId);
|
|
81
|
+
if (exact) {
|
|
82
|
+
await openView(exact, ctx.ui.theme, ctx, deps);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// 前缀匹配
|
|
86
|
+
const matched = all.filter((r) => r.runId.startsWith(directRunId));
|
|
87
|
+
if (matched.length === 1) {
|
|
88
|
+
await openView(matched[0], ctx.ui.theme, ctx, deps);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
ctx.ui.notify(`Workflow '${directRunId}' not found`, "error");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 无参——列表选择
|
|
96
|
+
const all = sortedRuns(getRuns());
|
|
97
|
+
if (all.length === 0) {
|
|
98
|
+
ctx.ui.notify("No workflows in current session.", "info");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 单 run 直开
|
|
103
|
+
if (all.length === 1) {
|
|
104
|
+
await openView(all[0], ctx.ui.theme, ctx, deps);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 多 run——select 选择
|
|
109
|
+
const entries = all.map(
|
|
110
|
+
(r) => `${r.spec.scriptName} [${r.state.status}] (${r.runId.slice(0, RUNID_SHORT)})`,
|
|
111
|
+
);
|
|
112
|
+
const selected = await ctx.ui.select("Select workflow:", entries);
|
|
113
|
+
if (!selected) return;
|
|
114
|
+
const idx = entries.indexOf(selected);
|
|
115
|
+
if (idx === -1) return;
|
|
116
|
+
await openView(all[idx], ctx.ui.theme, ctx, deps);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 取 runs 按 status(running/paused 优先)+ startedAt 倒序排序。
|
|
125
|
+
* 复用 main 的 UX 顺序(活跃态在前,新的在前)。
|
|
126
|
+
*/
|
|
127
|
+
function sortedRuns(runs: Map<string, WorkflowRun>): WorkflowRun[] {
|
|
128
|
+
const arr = Array.from(runs.values());
|
|
129
|
+
return arr.sort((a, b) => {
|
|
130
|
+
const sa = STATUS_ORDER[a.state.status] ?? UNKNOWN_STATUS_WEIGHT;
|
|
131
|
+
const sb = STATUS_ORDER[b.state.status] ?? UNKNOWN_STATUS_WEIGHT;
|
|
132
|
+
if (sa !== sb) return sa - sb;
|
|
133
|
+
const ta = a.meta.startedAt ? new Date(a.meta.startedAt).getTime() : 0;
|
|
134
|
+
const tb = b.meta.startedAt ? new Date(b.meta.startedAt).getTime() : 0;
|
|
135
|
+
return tb - ta;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 打开 WorkflowsView(三级导航 TUI),注入 lifecycle ViewActions。
|
|
141
|
+
*
|
|
142
|
+
* ViewActions 通过 deps 调 lifecycle(pause/resume/abort),与 view 解耦——
|
|
143
|
+
* view 单测可注入 mock actions(见 workflows-view.test.ts)。
|
|
144
|
+
*/
|
|
145
|
+
async function openView(
|
|
146
|
+
run: WorkflowRun,
|
|
147
|
+
theme: Theme,
|
|
148
|
+
ctx: ExtensionCommandContext,
|
|
149
|
+
deps: LauncherDeps,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
const actions: ViewActions = {
|
|
152
|
+
pause: (runId: string) => pauseRun(runId, deps),
|
|
153
|
+
resume: (runId: string) => resumeRun(runId, deps),
|
|
154
|
+
abort: (runId: string) => abortRun(runId, deps),
|
|
155
|
+
};
|
|
156
|
+
await createWorkflowsView(run, theme, ctx, actions);
|
|
157
|
+
}
|