@ghyper9023/pi-dev-workflow 0.3.3 → 0.4.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/README.md +171 -29
- package/agents/review-agent.md +5 -5
- package/agents/workflow/docWriter-agent.md +29 -0
- package/agents/workflow/planner-agent.md +80 -0
- package/agents/workflow/reviewer-agent.md +44 -0
- package/agents/workflow/trimmer-agent.md +34 -0
- package/agents/workflow/worker-agent.md +29 -0
- package/extensions/dev-prompts.ts +375 -75
- package/extensions/git-commands.ts +3 -13
- package/extensions/grill-me-agent.ts +138 -66
- package/extensions/sub-agents.ts +32 -11
- package/extensions/ui-helpers.ts +1030 -0
- package/extensions/workflow-engine.ts +1715 -0
- package/package.json +1 -1
- package/skills/review-html/SKILL.md +2 -2
- package/skills/to-prd/SKILL.md +1 -1
- package/tests/test-grill-json-fix.mjs +243 -0
- package/tests/test-output-directory-structure.mjs +177 -0
- package/tests/test-save-answer-file-workflow.mjs +187 -0
- package/tests/test-workflow-config.mjs +244 -0
- package/tests/test-workflow-engine.mjs +518 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 职责:
|
|
5
5
|
* 1. runGrillPhase() — 启动 sub-agent 生成评审问题,TUI 逐题呈现(选项 + 自定义输入)
|
|
6
|
-
* 2. runPRDPhase() — 启动 sub-agent 生成 PRD,保存到 pi-dev-output/pi-prd/
|
|
6
|
+
* 2. runPRDPhase() — 启动 sub-agent 生成 PRD,保存到 .pi-dev-output/pi-prd/
|
|
7
7
|
*
|
|
8
8
|
* 关键设计决策(修复 #2):
|
|
9
9
|
* sub-agent 通过 `write` 工具将评审问题写入临时文件,主进程事后读取。
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Spacer,
|
|
24
24
|
type SelectItem,
|
|
25
25
|
} from "@earendil-works/pi-tui";
|
|
26
|
+
import { uiSelect, uiConfirm, uiInput } from "./ui-helpers";
|
|
26
27
|
|
|
27
28
|
// ── Types ────────────────────────────────────────────────────
|
|
28
29
|
|
|
@@ -53,51 +54,56 @@ export interface PRDResult {
|
|
|
53
54
|
|
|
54
55
|
// ── Output dirs ──────────────────────────────────────────────
|
|
55
56
|
|
|
56
|
-
const DEV_OUTPUT_DIR = "pi-dev-output";
|
|
57
|
+
const DEV_OUTPUT_DIR = ".pi-dev-output";
|
|
57
58
|
const GRILL_DIRNAME = "pi-grill";
|
|
59
|
+
const GRILL_ANSWERS_DIRNAME = "answers";
|
|
60
|
+
const GRILL_QUESTIONS_DIRNAME = "questions";
|
|
58
61
|
const PRD_DIRNAME = "pi-prd";
|
|
59
62
|
|
|
60
|
-
/** Ensure an output subdirectory exists
|
|
63
|
+
/** Ensure an output subdirectory exists. */
|
|
61
64
|
function ensureOutputDir(cwd: string, subdir: string): string {
|
|
62
65
|
const dir = path.join(cwd, DEV_OUTPUT_DIR, subdir);
|
|
63
66
|
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
-
const gitignorePath = path.join(cwd, DEV_OUTPUT_DIR, ".gitignore");
|
|
65
|
-
try {
|
|
66
|
-
const existing = fs.readFileSync(gitignorePath, "utf-8").trim();
|
|
67
|
-
if (!existing.includes("*")) {
|
|
68
|
-
fs.writeFileSync(gitignorePath, "*\n!.gitignore\n");
|
|
69
|
-
}
|
|
70
|
-
} catch {
|
|
71
|
-
fs.writeFileSync(gitignorePath, "*\n!.gitignore\n");
|
|
72
|
-
}
|
|
73
67
|
return dir;
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
/**
|
|
70
|
+
/** Format current time as YYYYMMDD-HHmm for human-readable timestamps. */
|
|
71
|
+
function formatTimestamp(): string {
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const Y = now.getFullYear().toString();
|
|
74
|
+
const M = (now.getMonth() + 1).toString().padStart(2, "0");
|
|
75
|
+
const D = now.getDate().toString().padStart(2, "0");
|
|
76
|
+
const h = now.getHours().toString().padStart(2, "0");
|
|
77
|
+
const m = now.getMinutes().toString().padStart(2, "0");
|
|
78
|
+
return `${Y}${M}${D}-${h}${m}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Generate a safe temp filename for grill output (pi-grill/questions/questions-<id>-<YYYYMMDD-HHmm>.json). */
|
|
77
82
|
function grillOutputPath(cwd: string): string {
|
|
78
|
-
const dir = ensureOutputDir(cwd, GRILL_DIRNAME);
|
|
83
|
+
const dir = ensureOutputDir(cwd, path.join(GRILL_DIRNAME, GRILL_QUESTIONS_DIRNAME));
|
|
79
84
|
const ts = Date.now().toString(36);
|
|
80
|
-
return path.join(dir, `questions-${ts}.json`);
|
|
85
|
+
return path.join(dir, `questions-${ts}-${formatTimestamp()}.json`);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
/**
|
|
84
|
-
* Save the final assembled prompt to a timestamped answer file.
|
|
89
|
+
* Save the final assembled prompt to a timestamped answer file (pi-grill/answers/answer-<id>-<YYYYMMDD-HHmm>.md).
|
|
85
90
|
* Returns the relative path from cwd (for display in notifications).
|
|
86
91
|
*/
|
|
87
92
|
export function saveAnswerFile(cwd: string, content: string): string {
|
|
88
|
-
const dir = ensureOutputDir(cwd, GRILL_DIRNAME);
|
|
93
|
+
const dir = ensureOutputDir(cwd, path.join(GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME));
|
|
89
94
|
const ts = Date.now().toString(36);
|
|
90
|
-
const filename = `answer-${ts}.md`;
|
|
95
|
+
const filename = `answer-${ts}-${formatTimestamp()}.md`;
|
|
91
96
|
fs.writeFileSync(path.join(dir, filename), content, "utf-8");
|
|
92
|
-
return path.join(DEV_OUTPUT_DIR, GRILL_DIRNAME, filename);
|
|
97
|
+
return path.join(DEV_OUTPUT_DIR, GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME, filename);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
/**
|
|
96
101
|
* Find the most recent answer backup file and read its content.
|
|
97
102
|
* Returns undefined if no backup exists or read fails.
|
|
103
|
+
* Now reads from pi-grill/answers/ subdirectory.
|
|
98
104
|
*/
|
|
99
105
|
export function recoverFromBackup(cwd: string): string | undefined {
|
|
100
|
-
const dir = path.join(cwd, DEV_OUTPUT_DIR, GRILL_DIRNAME);
|
|
106
|
+
const dir = path.join(cwd, DEV_OUTPUT_DIR, GRILL_DIRNAME, GRILL_ANSWERS_DIRNAME);
|
|
101
107
|
try {
|
|
102
108
|
if (!fs.existsSync(dir)) return undefined;
|
|
103
109
|
const files = fs.readdirSync(dir)
|
|
@@ -363,14 +369,15 @@ export async function runGrillPhase(
|
|
|
363
369
|
|
|
364
370
|
const agentDef = options?.agentDef ?? _defaultGrillAgent;
|
|
365
371
|
const confirmTitle = options?.title ?? "🔍 设计方案评审";
|
|
366
|
-
const confirmDesc = options?.description ?? "
|
|
372
|
+
const confirmDesc = options?.description ?? "AI 会从架构、数据流、边界条件、安全等多个维度挑战你的设计。";
|
|
367
373
|
const qTitlePrefix = options?.questionTitle ?? "设计方案评审";
|
|
368
374
|
const loaderLabel = options?.loaderLabel ?? "🧠 AI 子代理正在分析代码并生成评审问题...";
|
|
369
375
|
|
|
370
376
|
// ── Step 1: Confirm entering grill mode ──────────────────
|
|
371
|
-
const enterGrill = await ctx
|
|
377
|
+
const enterGrill = await uiConfirm(ctx, confirmTitle, confirmDesc);
|
|
372
378
|
if (!enterGrill) {
|
|
373
|
-
|
|
379
|
+
// Skip grill but continue the workflow (not a cancellation)
|
|
380
|
+
return defaultResult;
|
|
374
381
|
}
|
|
375
382
|
|
|
376
383
|
// ── Step 2: Prepare output file + enhanced prompt ─────────
|
|
@@ -392,15 +399,12 @@ export async function runGrillPhase(
|
|
|
392
399
|
loader.signal,
|
|
393
400
|
undefined,
|
|
394
401
|
(progress) => {
|
|
395
|
-
// BorderedLoader 内部持有 Loader/CancellableLoader(私有 loader 字段),后者有 setText
|
|
396
402
|
const inner = (loader as unknown as { loader?: { setText?: (t: string) => void } }).loader;
|
|
397
403
|
inner?.setText?.(`🧠 ${progress.slice(0, 60)}`);
|
|
398
404
|
},
|
|
399
405
|
)
|
|
400
406
|
.then((result) => {
|
|
401
|
-
// Primary: read from file (sub-agent wrote via `write` tool)
|
|
402
407
|
let qs = readQuestionsFromFile(outputFilePath);
|
|
403
|
-
// Fallback: parse from NDJSON response text
|
|
404
408
|
if (qs.length === 0) {
|
|
405
409
|
const output = extractFinalOutput(result.output);
|
|
406
410
|
qs = parseGrillQuestions(output);
|
|
@@ -408,7 +412,6 @@ export async function runGrillPhase(
|
|
|
408
412
|
done(qs);
|
|
409
413
|
})
|
|
410
414
|
.catch(() => {
|
|
411
|
-
// On error, still try to read file (sub-agent may have written before error)
|
|
412
415
|
const qs = readQuestionsFromFile(outputFilePath);
|
|
413
416
|
done(qs);
|
|
414
417
|
});
|
|
@@ -418,7 +421,6 @@ export async function runGrillPhase(
|
|
|
418
421
|
|
|
419
422
|
// ── Step 4: Retry dialog if no questions generated ──────
|
|
420
423
|
if (questions.length === 0) {
|
|
421
|
-
// Read the failed file to get error context for the retry prompt
|
|
422
424
|
let failedFileContent = "";
|
|
423
425
|
let parseErrorMsg = "";
|
|
424
426
|
try {
|
|
@@ -430,7 +432,8 @@ export async function runGrillPhase(
|
|
|
430
432
|
parseErrorMsg = (e as Error).message;
|
|
431
433
|
}
|
|
432
434
|
|
|
433
|
-
const choice = await
|
|
435
|
+
const choice = await uiSelect(
|
|
436
|
+
ctx,
|
|
434
437
|
"⚠️ AI 未能成功生成评审问题",
|
|
435
438
|
[
|
|
436
439
|
"🔄 重新尝试生成评审问题",
|
|
@@ -464,7 +467,7 @@ export async function runGrillPhase(
|
|
|
464
467
|
writeToolPromptSuffix(retryPath),
|
|
465
468
|
errorFeedback,
|
|
466
469
|
].filter(Boolean).join("\n\n");
|
|
467
|
-
|
|
470
|
+
const retryQuestions = await ctx.ui.custom<GrillQuestion[]>((tui, theme, _kb, done) => {
|
|
468
471
|
const loader = new BorderedLoader(tui, theme, loaderLabel);
|
|
469
472
|
loader.onAbort = () => done([]);
|
|
470
473
|
spawnSubagent(agentDef, retryPrompt, ctx.cwd, loader.signal, undefined)
|
|
@@ -476,37 +479,91 @@ export async function runGrillPhase(
|
|
|
476
479
|
.catch(() => done([]));
|
|
477
480
|
return loader;
|
|
478
481
|
});
|
|
479
|
-
if (
|
|
480
|
-
ctx.ui.notify("⚠️ 再次尝试仍然失败,跳过 Grill 阶段", "warning");
|
|
482
|
+
if (retryQuestions.length === 0) {
|
|
481
483
|
return defaultResult;
|
|
482
484
|
}
|
|
483
|
-
|
|
485
|
+
// Replace questions with retry results (with back support)
|
|
486
|
+
const pairs: Array<{ question: string; answer: string }> = [];
|
|
487
|
+
let rIdx = 0;
|
|
488
|
+
while (rIdx >= 0 && rIdx < retryQuestions.length) {
|
|
489
|
+
const q = retryQuestions[rIdx]!;
|
|
490
|
+
const previousAnswer = pairs[rIdx]?.answer;
|
|
491
|
+
const answer = await showQuestionTUI(ctx, q, rIdx + 1, retryQuestions.length, qTitlePrefix,
|
|
492
|
+
rIdx > 0, previousAnswer);
|
|
493
|
+
if (answer === null) {
|
|
494
|
+
return { ...defaultResult, cancelled: true, pairs };
|
|
495
|
+
}
|
|
496
|
+
if (answer === "__BACK__") {
|
|
497
|
+
if (rIdx > 0) {
|
|
498
|
+
rIdx--;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
return { ...defaultResult, cancelled: true, pairs };
|
|
502
|
+
}
|
|
503
|
+
// Overwrite if re-answering, otherwise append
|
|
504
|
+
if (rIdx < pairs.length) {
|
|
505
|
+
pairs[rIdx] = { question: q.question, answer };
|
|
506
|
+
} else {
|
|
507
|
+
pairs.push({ question: q.question, answer });
|
|
508
|
+
}
|
|
509
|
+
rIdx++;
|
|
510
|
+
}
|
|
511
|
+
const qaBlock = pairs
|
|
512
|
+
.map((p, i) => `[评审问题 ${i + 1}]\n问题: ${p.question}\n回答: ${p.answer}`)
|
|
513
|
+
.join("\n\n");
|
|
514
|
+
const finalEnhancedPrompt = [
|
|
515
|
+
assembledPrompt,
|
|
516
|
+
"",
|
|
517
|
+
"---",
|
|
518
|
+
"## 设计评审记录",
|
|
519
|
+
"",
|
|
520
|
+
"以下是在开发前进行的设计评审问答,所有决策已确认:",
|
|
521
|
+
"",
|
|
522
|
+
qaBlock,
|
|
523
|
+
].join("\n");
|
|
524
|
+
return {
|
|
525
|
+
cancelled: false,
|
|
526
|
+
pairs,
|
|
527
|
+
enhancedPrompt: finalEnhancedPrompt,
|
|
528
|
+
};
|
|
484
529
|
}
|
|
485
530
|
case "⏭️ 跳过 Grill,直接发送 Prompt":
|
|
486
|
-
ctx.ui.notify("⏭️ 已跳过 Grill 阶段", "info");
|
|
487
531
|
return defaultResult;
|
|
488
532
|
case "❌ 取消 (Esc)":
|
|
489
533
|
default:
|
|
490
|
-
ctx.ui.notify("❌ 操作已取消", "warning");
|
|
491
534
|
return { ...defaultResult, cancelled: true };
|
|
492
535
|
}
|
|
493
536
|
}
|
|
494
537
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
// ── Step 5: TUI — present questions one by one ───────────
|
|
538
|
+
// ── Step 5: TUI — present questions one by one (with back support) ──
|
|
498
539
|
const pairs: Array<{ question: string; answer: string }> = [];
|
|
540
|
+
let qIdx = 0;
|
|
499
541
|
|
|
500
|
-
|
|
501
|
-
const q = questions[
|
|
502
|
-
const
|
|
542
|
+
while (qIdx >= 0 && qIdx < questions.length) {
|
|
543
|
+
const q = questions[qIdx]!;
|
|
544
|
+
const previousAnswer = pairs[qIdx]?.answer;
|
|
545
|
+
const answer = await showQuestionTUI(ctx, q, qIdx + 1, questions.length, qTitlePrefix,
|
|
546
|
+
qIdx > 0, previousAnswer);
|
|
503
547
|
|
|
504
548
|
if (answer === null) {
|
|
505
|
-
ctx.ui.notify("❌ 评审已取消", "warning");
|
|
506
549
|
return { ...defaultResult, cancelled: true, pairs };
|
|
507
550
|
}
|
|
508
551
|
|
|
509
|
-
|
|
552
|
+
if (answer === "__BACK__") {
|
|
553
|
+
if (qIdx > 0) {
|
|
554
|
+
qIdx--;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
return { ...defaultResult, cancelled: true, pairs };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Overwrite if re-answering (back then forward), otherwise append
|
|
561
|
+
if (qIdx < pairs.length) {
|
|
562
|
+
pairs[qIdx] = { question: q.question, answer };
|
|
563
|
+
} else {
|
|
564
|
+
pairs.push({ question: q.question, answer });
|
|
565
|
+
}
|
|
566
|
+
qIdx++;
|
|
510
567
|
}
|
|
511
568
|
|
|
512
569
|
// ── Step 6: Assemble enhanced prompt ─────────────────────
|
|
@@ -525,8 +582,6 @@ export async function runGrillPhase(
|
|
|
525
582
|
qaBlock,
|
|
526
583
|
].join("\n");
|
|
527
584
|
|
|
528
|
-
ctx.ui.notify(`✅ 评审完成,共 ${pairs.length} 道问题`, "success");
|
|
529
|
-
|
|
530
585
|
return {
|
|
531
586
|
cancelled: false,
|
|
532
587
|
pairs,
|
|
@@ -541,17 +596,32 @@ async function showQuestionTUI(
|
|
|
541
596
|
currentIndex: number,
|
|
542
597
|
totalCount: number,
|
|
543
598
|
titlePrefix = "设计方案评审",
|
|
599
|
+
backable = false,
|
|
600
|
+
previousAnswer?: string,
|
|
544
601
|
): Promise<string | null> {
|
|
545
602
|
const selectItems: SelectItem[] = q.options.map((opt, i) => ({
|
|
546
603
|
value: `opt-${i}`,
|
|
547
|
-
label:
|
|
604
|
+
label: opt === previousAnswer
|
|
605
|
+
? `(${String.fromCharCode(97 + i)}) ${opt} - 上次选择`
|
|
606
|
+
: `(${String.fromCharCode(97 + i)}) ${opt}`,
|
|
548
607
|
}));
|
|
608
|
+
|
|
609
|
+
const customLabel = previousAnswer && !q.options.includes(previousAnswer)
|
|
610
|
+
? `✏️ 自定义输入 - 上次选择`
|
|
611
|
+
: `✏️ 自定义输入`;
|
|
549
612
|
selectItems.push({
|
|
550
613
|
value: "__custom__",
|
|
551
|
-
label:
|
|
614
|
+
label: customLabel,
|
|
552
615
|
description: "输入你自己的回答,不受选项限制",
|
|
553
616
|
});
|
|
554
617
|
|
|
618
|
+
if (backable && currentIndex > 1) {
|
|
619
|
+
selectItems.push({
|
|
620
|
+
value: "__back__",
|
|
621
|
+
label: "← 返回上一题",
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
555
625
|
const title = `${titlePrefix} (问题 ${currentIndex}/${totalCount})`;
|
|
556
626
|
|
|
557
627
|
const value = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
@@ -575,8 +645,11 @@ async function showQuestionTUI(
|
|
|
575
645
|
container.addChild(selectList);
|
|
576
646
|
|
|
577
647
|
container.addChild(new Spacer(1));
|
|
648
|
+
const hint = backable && currentIndex > 1
|
|
649
|
+
? " ↑↓ 导航 • Enter 选择 • 选择←返回上一题 • Esc 取消全部评审"
|
|
650
|
+
: " ↑↓ 导航 • Enter 选择 • Esc 取消全部评审";
|
|
578
651
|
container.addChild(
|
|
579
|
-
new Text(theme.fg("dim",
|
|
652
|
+
new Text(theme.fg("dim", hint), 0, 0),
|
|
580
653
|
);
|
|
581
654
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
582
655
|
|
|
@@ -590,13 +663,18 @@ async function showQuestionTUI(
|
|
|
590
663
|
};
|
|
591
664
|
});
|
|
592
665
|
|
|
666
|
+
if (value === "__back__") return "__BACK__";
|
|
593
667
|
if (value === null) return null;
|
|
594
668
|
if (value === "__custom__") {
|
|
595
|
-
const custom = await ctx
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
669
|
+
const custom = await uiInput(ctx, "✏️ 自定义回答",
|
|
670
|
+
previousAnswer && !q.options.includes(previousAnswer)
|
|
671
|
+
? `(上次: ${previousAnswer.slice(0, 60)})`
|
|
672
|
+
: "输入你的回答内容(Esc 取消本题,回到选项)",
|
|
673
|
+
false, true,
|
|
674
|
+
previousAnswer && !q.options.includes(previousAnswer) ? previousAnswer : "",
|
|
675
|
+
);
|
|
676
|
+
if (custom === "__BACK__") return "__BACK__";
|
|
677
|
+
if (custom === undefined) return showQuestionTUI(ctx, q, currentIndex, totalCount, titlePrefix, backable, previousAnswer);
|
|
600
678
|
return custom.trim() || "(空)";
|
|
601
679
|
}
|
|
602
680
|
|
|
@@ -610,7 +688,7 @@ async function showQuestionTUI(
|
|
|
610
688
|
* Run the PRD phase:
|
|
611
689
|
* 1. Ask user if they want to create a PRD
|
|
612
690
|
* 2. Call sub-agent → gets PRD Markdown
|
|
613
|
-
* 3. Save to pi-dev-output/pi-prd/<name>.md
|
|
691
|
+
* 3. Save to .pi-dev-output/pi-prd/<name>.md
|
|
614
692
|
* 4. Ask if user wants to start development
|
|
615
693
|
*/
|
|
616
694
|
export async function runPRDPhase(
|
|
@@ -619,9 +697,10 @@ export async function runPRDPhase(
|
|
|
619
697
|
pi: ExtensionAPI,
|
|
620
698
|
ctx: ExtensionCommandContext,
|
|
621
699
|
): Promise<PRDResult | null> {
|
|
622
|
-
const wantPrd = await
|
|
700
|
+
const wantPrd = await uiConfirm(
|
|
701
|
+
ctx,
|
|
623
702
|
"📋 创建 PRD",
|
|
624
|
-
"
|
|
703
|
+
"PRD 将保存到 .pi-dev-output/pi-prd/ 目录。",
|
|
625
704
|
);
|
|
626
705
|
if (!wantPrd) return null;
|
|
627
706
|
|
|
@@ -648,7 +727,6 @@ export async function runPRDPhase(
|
|
|
648
727
|
});
|
|
649
728
|
|
|
650
729
|
if (!prdContent) {
|
|
651
|
-
ctx.ui.notify("⚠️ PRD 生成失败", "error");
|
|
652
730
|
return null;
|
|
653
731
|
}
|
|
654
732
|
|
|
@@ -657,7 +735,6 @@ export async function runPRDPhase(
|
|
|
657
735
|
const filePath = path.join(DEV_OUTPUT_DIR, PRD_DIRNAME, filename);
|
|
658
736
|
const fullPath = path.join(prdDir, filename);
|
|
659
737
|
fs.writeFileSync(fullPath, prdContent, "utf-8");
|
|
660
|
-
ctx.ui.notify(`✅ PRD 已保存到 ${filePath}`, "success");
|
|
661
738
|
|
|
662
739
|
await askDevelopmentStart(pi, ctx, prdContent, filePath);
|
|
663
740
|
return { content: prdContent, filePath };
|
|
@@ -669,7 +746,8 @@ async function askDevelopmentStart(
|
|
|
669
746
|
prdContent: string,
|
|
670
747
|
prdFilePath: string,
|
|
671
748
|
): Promise<void> {
|
|
672
|
-
const choice = await
|
|
749
|
+
const choice = await uiSelect(
|
|
750
|
+
ctx,
|
|
673
751
|
"🚀 是否开始开发?",
|
|
674
752
|
[
|
|
675
753
|
"是 — 根据 PRD 开始开发",
|
|
@@ -694,17 +772,12 @@ async function askDevelopmentStart(
|
|
|
694
772
|
"请按照上述 PRD 逐步实现。先分析代码库结构,给出实施计划,确认后再编写代码。",
|
|
695
773
|
].join("\n");
|
|
696
774
|
pi.sendUserMessage(devMsg, { deliverAs: "followUp" });
|
|
697
|
-
ctx.ui.notify("🚀 已发送开发指令给主代理", "success");
|
|
698
775
|
break;
|
|
699
776
|
}
|
|
700
777
|
case "否 — 稍后手动开始":
|
|
701
|
-
ctx.ui.notify(`📋 PRD 已保存在 ${prdFilePath},可随时手动引用`, "info");
|
|
702
778
|
break;
|
|
703
779
|
default: {
|
|
704
|
-
const customMsg = await ctx
|
|
705
|
-
placeholder: "输入你的开发指令(将结合 PRD 一起发送给主代理)",
|
|
706
|
-
required: false,
|
|
707
|
-
});
|
|
780
|
+
const customMsg = await uiInput(ctx, "✏️ 自定义开发指令", "输入你的开发指令(将结合 PRD 一起发送给主代理)");
|
|
708
781
|
if (customMsg === undefined) return askDevelopmentStart(pi, ctx, prdContent, prdFilePath);
|
|
709
782
|
const finalMsg = customMsg.trim()
|
|
710
783
|
? [
|
|
@@ -724,7 +797,6 @@ async function askDevelopmentStart(
|
|
|
724
797
|
prdContent,
|
|
725
798
|
].join("\n");
|
|
726
799
|
pi.sendUserMessage(finalMsg, { deliverAs: "followUp" });
|
|
727
|
-
ctx.ui.notify("🚀 已发送自定义开发指令给主代理", "success");
|
|
728
800
|
break;
|
|
729
801
|
}
|
|
730
802
|
}
|
package/extensions/sub-agents.ts
CHANGED
|
@@ -17,6 +17,7 @@ import * as fs from "node:fs";
|
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { Type } from "typebox";
|
|
20
|
+
import { uiSelect } from "./ui-helpers";
|
|
20
21
|
|
|
21
22
|
// ── Configuration ────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -70,11 +71,12 @@ function loadAppendSystem(cwd: string): string | null {
|
|
|
70
71
|
return null;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
/** Find the newest HTML review file in pi-review/ or pi-dev-output/pi-review/ directory. */
|
|
74
|
+
/** Find the newest HTML review file in .pi-dev-output/pi-review/html/, pi-review/, or .pi-dev-output/pi-review/ directory. */
|
|
74
75
|
function findNewestReviewHtml(cwd: string): string {
|
|
75
76
|
const candidates = [
|
|
77
|
+
path.join(cwd, ".pi-dev-output", "pi-review", "html"),
|
|
76
78
|
path.join(cwd, "pi-review"),
|
|
77
|
-
path.join(cwd, "pi-dev-output", "pi-review"),
|
|
79
|
+
path.join(cwd, ".pi-dev-output", "pi-review"),
|
|
78
80
|
];
|
|
79
81
|
|
|
80
82
|
for (const reviewDir of candidates) {
|
|
@@ -552,17 +554,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
552
554
|
|
|
553
555
|
// ── /subagent-stop - 主动终止所有正在运行的 sub-agent ──────
|
|
554
556
|
pi.registerCommand("subagent-stop", {
|
|
555
|
-
description: "Terminate all running sub-agents immediately",
|
|
557
|
+
description: "Terminate all running sub-agents immediately. Also cancels any active workflow.",
|
|
556
558
|
handler: async (_args, ctx) => {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
559
|
+
const childCount = activeChildren.size;
|
|
560
|
+
|
|
561
|
+
// Also try to cancel any active workflow
|
|
562
|
+
let workflowCancelled = false;
|
|
563
|
+
try {
|
|
564
|
+
const { cancelActiveWorkflow, isWorkflowRunning } = await import("./workflow-engine");
|
|
565
|
+
if (isWorkflowRunning()) {
|
|
566
|
+
cancelActiveWorkflow();
|
|
567
|
+
workflowCancelled = true;
|
|
568
|
+
}
|
|
569
|
+
} catch { /* workflow-engine not available, ignore */ }
|
|
570
|
+
|
|
571
|
+
if (childCount === 0 && !workflowCancelled) {
|
|
572
|
+
ctx.ui.notify("i️ 当前没有运行中的 sub-agent 或工作流", "info");
|
|
560
573
|
return;
|
|
561
574
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
575
|
+
|
|
576
|
+
if (childCount > 0) {
|
|
577
|
+
killAllChildren();
|
|
578
|
+
ctx.ui.notify(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, "info");
|
|
579
|
+
ctx.ui.notify(`🛑 已终止 ${childCount} 个 sub-agent 进程`, "warning");
|
|
580
|
+
ctx.ui.notify(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`, "info");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (workflowCancelled) {
|
|
584
|
+
ctx.ui.notify(`🛑 已取消运行中的工作流`, "warning");
|
|
585
|
+
}
|
|
566
586
|
},
|
|
567
587
|
});
|
|
568
588
|
|
|
@@ -670,7 +690,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
670
690
|
|
|
671
691
|
// 对于普通关键词触发的审查请求,询问用户选择模式
|
|
672
692
|
// ctx.ui.select 接受 string[],返回选中的字符串
|
|
673
|
-
const mode = await
|
|
693
|
+
const mode = await uiSelect(
|
|
694
|
+
ctx,
|
|
674
695
|
"🔍 检测到审查意图",
|
|
675
696
|
[
|
|
676
697
|
"1. 后台审查(非阻塞,异步通知)",
|