@ghyper9023/pi-dev-workflow 0.4.2 → 0.5.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/.doc/AGENT-FRONTMATTER-REFERENCE.md +198 -0
- package/.version/RELEASE-v0.4.2.md +31 -0
- package/.version/RELEASE-v0.4.3.md +42 -0
- package/.version/RELEASE-v0.5.0.md +82 -0
- package/README.md +23 -3
- package/agents/git-agent.md +7 -0
- package/agents/grill/dev-doc-grill-agent.md +7 -0
- package/agents/grill/dev-fix-grill-agent.md +7 -0
- package/agents/grill/dev-grill-agent.md +7 -0
- package/agents/grill/dev-perf-grill-agent.md +7 -0
- package/agents/grill/dev-prd-agent.md +7 -0
- package/agents/grill/dev-refactor-grill-agent.md +7 -0
- package/agents/grill/dev-test-grill-agent.md +7 -0
- package/agents/review-agent.md +7 -0
- package/agents/workflow/docWriter-agent.md +13 -2
- package/agents/workflow/planner-agent.md +16 -3
- package/agents/workflow/reviewer-agent.md +21 -4
- package/agents/workflow/trimmer-agent.md +12 -1
- package/agents/workflow/worker-agent.md +17 -2
- package/extensions/grill-me-agent.ts +58 -8
- package/extensions/sub-agents.ts +133 -16
- package/extensions/ui-helpers.ts +27 -27
- package/extensions/workflow-engine.ts +323 -16
- package/package.json +1 -1
- package/tests/test-workflow-engine-bugs.mjs +317 -0
- package/.pi-dev-output/pi-grill/answers/answer-mpds3by7-20260520-1606.md +0 -14
- package/.pi-dev-output/pi-grill/answers/answer-mpfe77f1-20260521-1913.md +0 -58
- package/.pi-dev-output/pi-grill/answers/answer-mpfh37wu-20260521-2034.md +0 -13
- package/.pi-dev-output/pi-grill/answers/answer-mpfi5q4c-20260521-2104.md +0 -13
- package/.pi-dev-output/pi-grill/answers/answer-mpfizccb-20260521-2127.md +0 -13
- package/.pi-dev-output/pi-grill/answers/answer-mpfjk78k-20260521-2143.md +0 -13
- package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +0 -94
- package/.pi-dev-output/pi-plans/20260520-153000-fix-workflow-engine-bugs.md +0 -150
- package/.pi-dev-output/pi-plans/20260521-113000-fix-loopcount-timeout.md +0 -215
- package/.pi-dev-output/pi-plans/20260521-1730-grill-input-wrap-back-fix.md +0 -240
- package/.pi-dev-output/pi-plans/20260521-230000-fix-timeout-display-loopcount-gitdiff.md +0 -253
- package/.pi-dev-output/pi-plans/20260521-230500-esc-double-press-confirm-workflow.md +0 -137
- package/.pi-dev-output/pi-plans/20260521-235000-fix-gitdiff-loopcount.md +0 -258
- package/.pi-dev-output/pi-review/html/20260521-2305-review-workflow-index.html +0 -196
- package/.pi-dev-output/pi-review/md/review-20260520-100000.md +0 -91
- package/.pi-dev-output/pi-review/md/review-20260521-140000.md +0 -191
- package/.pi-dev-output/pi-review/md/review-20260521-190000.md +0 -189
- package/.pi-dev-output/pi-review/md/review-20260521-204500.md +0 -241
- package/.pi-dev-output/pi-review/md/review-20260521-214500.md +0 -270
- package/.pi-dev-output/pi-review/md/review-20260521-215158.md +0 -214
- package/.pi-dev-output/pi-review/md/review-20260521-234500.md +0 -201
- package/.pi-dev-output/pi-review/md/review-20260521-235500.md +0 -422
- package/.pi-dev-output/pi-review/md/review-20260522-000000.md +0 -212
- package/.pi-dev-output/pi-review/md/review-20260522-003000.md +0 -377
- package/.pi-dev-output/pi-review/md/review-20260522-003500.md +0 -296
- package/.pi-dev-output/pi-workflow/checkpoint-20260520-153000-fix-workflow-engine-bugs.json +0 -108
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-113000-fix-loopcount-timeout.json +0 -402
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-1730-grill-input-wrap-back-fix.json +0 -447
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-230000-fix-timeout-display-loopcount-gitdiff.json +0 -708
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-230500-esc-double-press-confirm-workflow.json +0 -365
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-235000-fix-gitdiff-loopcount.json +0 -395
- package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfhyxc5.json +0 -30
- package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi2unc.json +0 -49
- package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi382e.json +0 -59
- package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi5r22.json +0 -76
|
@@ -20,6 +20,41 @@
|
|
|
20
20
|
|
|
21
21
|
import * as fs from "node:fs";
|
|
22
22
|
import * as path from "node:path";
|
|
23
|
+
import { randomBytes } from "node:crypto";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a UUID v7 (time-ordered) string.
|
|
27
|
+
* Format: tttttttt-tttt-7xxx-yxxx-xxxxxxxxxxxx
|
|
28
|
+
* - t = Unix timestamp (ms) as 48-bit big-endian
|
|
29
|
+
* - 7 = version (0111)
|
|
30
|
+
* - y = variant (10xx = 8, 9, a, b)
|
|
31
|
+
* - x = random
|
|
32
|
+
*/
|
|
33
|
+
function uuidv7(): string {
|
|
34
|
+
const ts = BigInt(Date.now());
|
|
35
|
+
const buf = new Uint8Array(16);
|
|
36
|
+
// 48-bit timestamp (big-endian) in bytes 0-5
|
|
37
|
+
buf[0] = Number((ts >> 40n) & 0xFFn);
|
|
38
|
+
buf[1] = Number((ts >> 32n) & 0xFFn);
|
|
39
|
+
buf[2] = Number((ts >> 24n) & 0xFFn);
|
|
40
|
+
buf[3] = Number((ts >> 16n) & 0xFFn);
|
|
41
|
+
buf[4] = Number((ts >> 8n) & 0xFFn);
|
|
42
|
+
buf[5] = Number(ts & 0xFFn);
|
|
43
|
+
// 10 random bytes for version/variant/rand
|
|
44
|
+
const r = randomBytes(10);
|
|
45
|
+
// Byte 6: version (0111) | rand_a high 4 bits
|
|
46
|
+
buf[6] = 0x70 | (r[0] >> 4);
|
|
47
|
+
// Byte 7: rand_a low 8 bits
|
|
48
|
+
buf[7] = (r[0] << 4) | (r[1] >> 4);
|
|
49
|
+
// Byte 8: variant (10) | rand_b high 6 bits
|
|
50
|
+
buf[8] = 0x80 | (r[2] >> 2);
|
|
51
|
+
// Bytes 9-15: remaining rand_b (56 bits)
|
|
52
|
+
buf[9] = r[3]; buf[10] = r[4]; buf[11] = r[5];
|
|
53
|
+
buf[12] = r[6]; buf[13] = r[7]; buf[14] = r[8]; buf[15] = r[9];
|
|
54
|
+
// Format as 8-4-4-4-12 hex UUID
|
|
55
|
+
const hex = Array.from(buf, b => b.toString(16).padStart(2, '0')).join('');
|
|
56
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
57
|
+
}
|
|
23
58
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
24
59
|
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
25
60
|
import { execSync } from "child_process";
|
|
@@ -95,6 +130,8 @@ interface BaselineEntry {
|
|
|
95
130
|
|
|
96
131
|
interface CheckpointData {
|
|
97
132
|
version: 2;
|
|
133
|
+
/** UUID that uniquely identifies this workflow run across all output files */
|
|
134
|
+
workflowId: string;
|
|
98
135
|
createdAt: string;
|
|
99
136
|
updatedAt: string;
|
|
100
137
|
prompt: string;
|
|
@@ -372,7 +409,7 @@ function toGitStatus(toolType: string): string {
|
|
|
372
409
|
*/
|
|
373
410
|
function hasContentChanged(cwd: string, path: string, baselineHash: string): boolean {
|
|
374
411
|
try {
|
|
375
|
-
const currentHash =
|
|
412
|
+
const currentHash = execSync(`git hash-object "${path}"`, { cwd, encoding: 'utf8', timeout: 3000 }).trim();
|
|
376
413
|
return currentHash !== baselineHash;
|
|
377
414
|
} catch {
|
|
378
415
|
// file deleted or inaccessible — consider changed
|
|
@@ -486,16 +523,17 @@ export function loadCheckpointFromFile(cwd: string): CheckpointData | null {
|
|
|
486
523
|
}
|
|
487
524
|
|
|
488
525
|
/**
|
|
489
|
-
* Archive checkpoint after completion: rename to checkpoint-<plan-id>.json
|
|
526
|
+
* Archive checkpoint after completion: rename to checkpoint-<uuid>-<plan-id>.json
|
|
490
527
|
*/
|
|
491
528
|
export function archiveCheckpointFile(cwd: string, planFileRelPath?: string): void {
|
|
492
529
|
try {
|
|
493
530
|
const cpPath = path.join(cwd, CHECKPOINT_FILE);
|
|
494
531
|
if (!fs.existsSync(cpPath)) return;
|
|
532
|
+
const uuidPart = _workflowId ? `-${_workflowId}` : "";
|
|
495
533
|
const planId = planFileRelPath
|
|
496
534
|
? path.basename(planFileRelPath, ".md").replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
497
535
|
: `archive-${Date.now().toString(36)}`;
|
|
498
|
-
const archiveName = `checkpoint-${planId}.json`;
|
|
536
|
+
const archiveName = `checkpoint${uuidPart}-${planId}.json`;
|
|
499
537
|
const archiveDir = path.join(cwd, DEV_OUTPUT_DIR, "pi-workflow");
|
|
500
538
|
fs.renameSync(cpPath, path.join(archiveDir, archiveName));
|
|
501
539
|
} catch { /* ignore */ }
|
|
@@ -510,25 +548,43 @@ function buildTaskForStep(
|
|
|
510
548
|
prompt: string,
|
|
511
549
|
planFileRelPath: string | undefined,
|
|
512
550
|
cwd: string,
|
|
551
|
+
workflowId: string,
|
|
552
|
+
chainContext?: string,
|
|
513
553
|
): string {
|
|
514
554
|
if (agentName === "planner") {
|
|
515
555
|
return [
|
|
516
556
|
"请根据以下功能需求,分析代码库结构,生成详细的实施计划,并写入 .pi-dev-output/pi-plans/ 目录。",
|
|
517
557
|
"",
|
|
558
|
+
"## 文件名格式",
|
|
559
|
+
`<YYYYMMDD-HHmmss>-<简短功能名>-${workflowId}.md`,
|
|
560
|
+
"请在文件名末尾添加工作流 UUID,以便同一工作流的所有文件可关联追溯。",
|
|
561
|
+
"",
|
|
518
562
|
"## 功能需求",
|
|
519
563
|
prompt,
|
|
564
|
+
...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
|
|
520
565
|
].join("\n");
|
|
521
566
|
}
|
|
522
567
|
if (agentName === "worker") {
|
|
523
568
|
const planContent = planFileRelPath ? readFileContent(cwd, planFileRelPath) : undefined;
|
|
569
|
+
const planLocation = planFileRelPath
|
|
570
|
+
? `\n实施计划文件位于: ${planFileRelPath}\n(可在 .pi-dev-output/pi-plans/ 目录中 grep UUID "${workflowId}" 找到)`
|
|
571
|
+
: "";
|
|
524
572
|
if (planContent) {
|
|
525
573
|
return [
|
|
526
574
|
"请根据以下实施计划逐步实现代码改动。",
|
|
527
575
|
"",
|
|
528
576
|
"## 实施计划",
|
|
529
577
|
planContent,
|
|
578
|
+
planLocation,
|
|
579
|
+
"",
|
|
580
|
+
"## 原始需求与修改反馈",
|
|
581
|
+
prompt,
|
|
582
|
+
...(chainContext ? ["", chainContext] : []),
|
|
530
583
|
"",
|
|
531
584
|
"请严格按照计划中的步骤实施,不要做计划外的修改。",
|
|
585
|
+
"",
|
|
586
|
+
"实施完成后,在输出中列出你修改的所有文件,供后续审查者参考。",
|
|
587
|
+
...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
|
|
532
588
|
].join("\n");
|
|
533
589
|
}
|
|
534
590
|
return [
|
|
@@ -536,17 +592,26 @@ function buildTaskForStep(
|
|
|
536
592
|
"",
|
|
537
593
|
"## 功能需求",
|
|
538
594
|
prompt,
|
|
595
|
+
planLocation,
|
|
596
|
+
...(chainContext ? ["", chainContext] : []),
|
|
539
597
|
"",
|
|
540
598
|
"请先分析代码库,制定简要计划,再逐步实施。",
|
|
599
|
+
"实施完成后,在输出中列出你修改的所有文件,供后续审查者参考。",
|
|
600
|
+
...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
|
|
541
601
|
].join("\n");
|
|
542
602
|
}
|
|
543
603
|
if (agentName === "trimmer") {
|
|
604
|
+
const planContent = planFileRelPath ? readFileContent(cwd, planFileRelPath) : undefined;
|
|
544
605
|
return [
|
|
545
606
|
"请精简当前代码库的代码。",
|
|
546
607
|
"缩短不必要的冗长行,优化可读性,消除可合并的重复逻辑。",
|
|
608
|
+
"注意:以下实施计划列出了本次工作流的核心新增内容,精简时请确保不影响这些改动。",
|
|
547
609
|
"",
|
|
548
610
|
"## 原始功能需求",
|
|
549
611
|
prompt,
|
|
612
|
+
...(planContent ? ["", "## 实施计划(改动范围)", planContent] : []),
|
|
613
|
+
...(chainContext ? ["", chainContext] : []),
|
|
614
|
+
...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
|
|
550
615
|
].join("\n");
|
|
551
616
|
}
|
|
552
617
|
if (agentName === "docWriter") {
|
|
@@ -559,6 +624,8 @@ function buildTaskForStep(
|
|
|
559
624
|
"## 功能需求",
|
|
560
625
|
prompt,
|
|
561
626
|
planContent,
|
|
627
|
+
...(chainContext ? ["", chainContext] : []),
|
|
628
|
+
...(buildWorkflowInfoBlock() ? ["", buildWorkflowInfoBlock()] : []),
|
|
562
629
|
].join("\n");
|
|
563
630
|
}
|
|
564
631
|
return prompt;
|
|
@@ -568,21 +635,35 @@ function buildReviewTask(
|
|
|
568
635
|
prompt: string,
|
|
569
636
|
planFileRelPath: string | undefined,
|
|
570
637
|
cwd: string,
|
|
638
|
+
workflowId: string,
|
|
639
|
+
chainContext?: string,
|
|
571
640
|
): string {
|
|
572
641
|
const planContent = planFileRelPath ? readFileContent(cwd, planFileRelPath) : undefined;
|
|
573
642
|
const parts = [
|
|
574
643
|
"请审查当前代码库中针对以下功能的实现。",
|
|
575
644
|
"检查是否有 bug、逻辑错误、未完成的功能、代码质量问题。",
|
|
576
645
|
"将详细审查报告写入 .pi-dev-output/pi-review/md/ 目录。",
|
|
646
|
+
"",
|
|
647
|
+
"## 文件名格式",
|
|
648
|
+
`review-<YYYYMMDD-HHmmss>-${workflowId}.md`,
|
|
649
|
+
"请在文件名末尾添加工作流 UUID,以便同一工作流的所有文件可关联追溯。",
|
|
650
|
+
"",
|
|
577
651
|
"在回复末尾输出以下格式的结构化摘要(必须包含):",
|
|
578
652
|
"[REVIEW_SUMMARY]",
|
|
579
653
|
'{"maxSeverity":"critical|medium|low","critical":N,"medium":N,"low":N}',
|
|
580
654
|
"[/REVIEW_SUMMARY]",
|
|
581
655
|
"",
|
|
656
|
+
"⚠️ 重要:这个 JSON 摘要不是可选的,而是强制性的。",
|
|
657
|
+
"如果缺少此 JSON 摘要,工作流将无法判断是否需要继续修复循环。",
|
|
658
|
+
"请在回复的末尾单独输出,确保前后无其他文本(除换行符外)。",
|
|
659
|
+
"注意最大严重级别字段名是 maxSeverity(注意大小写)。",
|
|
660
|
+
"",
|
|
582
661
|
"## 功能需求",
|
|
583
662
|
prompt,
|
|
584
663
|
];
|
|
585
664
|
if (planContent) parts.push("", "## 实施计划", planContent);
|
|
665
|
+
if (chainContext) parts.push("", chainContext);
|
|
666
|
+
if (buildWorkflowInfoBlock()) parts.push("", buildWorkflowInfoBlock());
|
|
586
667
|
return parts.join("\n");
|
|
587
668
|
}
|
|
588
669
|
|
|
@@ -613,6 +694,10 @@ let _workflowAgentRunHistory: AgentRunEntry[] = [];
|
|
|
613
694
|
let _workflowStepDefs: WorkflowStepDef[] = [];
|
|
614
695
|
/** Baseline snapshot: git hashes of dirty files at workflow start */
|
|
615
696
|
let _workflowBaseline: BaselineEntry[] = [];
|
|
697
|
+
/** Workflow UUID for cross-file traceability */
|
|
698
|
+
let _workflowId = "";
|
|
699
|
+
/** Chain context: keyed parts of previous agent output to pass to next agent */
|
|
700
|
+
let _chainContextParts: Record<string, string> = {};
|
|
616
701
|
|
|
617
702
|
let _widgetMode: WorkflowMode = "attended";
|
|
618
703
|
let _widgetSteps: WorkflowStepWidgetState[] = [];
|
|
@@ -623,6 +708,58 @@ let _widgetExtraTokenCount = 0;
|
|
|
623
708
|
let _workflowRunning = false;
|
|
624
709
|
let _cleanupTimer: ReturnType<typeof setTimeout> | null = null;
|
|
625
710
|
|
|
711
|
+
// ── Chain context helpers ──────────────────────────────────────
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Update a named part of the chain context. Each key is a section header;
|
|
715
|
+
* calling with the same key overwrites the previous value.
|
|
716
|
+
*/
|
|
717
|
+
function updateChainContext(key: string, content: string): void {
|
|
718
|
+
if (content) {
|
|
719
|
+
_chainContextParts[key] = content;
|
|
720
|
+
} else {
|
|
721
|
+
delete _chainContextParts[key];
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Build the chain context string from all parts, for injection into
|
|
727
|
+
* buildTaskForStep / buildReviewTask prompts.
|
|
728
|
+
*/
|
|
729
|
+
function buildChainContext(): string {
|
|
730
|
+
const parts = Object.entries(_chainContextParts)
|
|
731
|
+
.filter(([_, v]) => v)
|
|
732
|
+
.map(([k, v]) => `## ${k}\n${v}`);
|
|
733
|
+
return parts.length > 0 ? parts.join("\n\n") : "";
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Reset chain context — called at the start of a new workflow.
|
|
738
|
+
*/
|
|
739
|
+
function resetChainContext(): void {
|
|
740
|
+
_chainContextParts = {};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Build a standard Workflow Info block injected into every subagent prompt.
|
|
745
|
+
*/
|
|
746
|
+
function buildWorkflowInfoBlock(): string {
|
|
747
|
+
if (!_workflowId) return "";
|
|
748
|
+
const modeLabel = _widgetMode === "full-auto" ? "全自动"
|
|
749
|
+
: _widgetMode === "full-attended" ? "完全值守"
|
|
750
|
+
: "值守";
|
|
751
|
+
return [
|
|
752
|
+
"## 工作流信息",
|
|
753
|
+
`- 工作流 UUID: ${_workflowId}`,
|
|
754
|
+
`- 工作流类型: ${_workflowType ?? "通用"}`,
|
|
755
|
+
`- 工作流启动时间: ${_workflowCreatedAt}`,
|
|
756
|
+
`- 工作流模式: ${modeLabel}`,
|
|
757
|
+
"",
|
|
758
|
+
"提示:如果需要在 .pi-dev-output/ 下查找属于本工作流的文件,",
|
|
759
|
+
`请在工作流输出目录中搜索工作流 UUID "${_workflowId}"。`,
|
|
760
|
+
].join("\n");
|
|
761
|
+
}
|
|
762
|
+
|
|
626
763
|
function refreshWidget(): void {
|
|
627
764
|
if (!_lastWorkflowCtx) return;
|
|
628
765
|
const taskSummary = extractTaskSummary(_workflowPrompt);
|
|
@@ -637,6 +774,7 @@ function refreshWidget(): void {
|
|
|
637
774
|
"running",
|
|
638
775
|
{ toolCount: _widgetExtraToolCount, tokenCount: _widgetExtraTokenCount },
|
|
639
776
|
taskSummary,
|
|
777
|
+
_workflowId,
|
|
640
778
|
);
|
|
641
779
|
updateWorkflowWidget(_lastWorkflowCtx, widgetState);
|
|
642
780
|
}
|
|
@@ -694,12 +832,19 @@ function populatePredefinedSubSteps(stepIndex: number): void {
|
|
|
694
832
|
if (step.subSteps && step.subSteps.length > 0) return; // already populated
|
|
695
833
|
|
|
696
834
|
const def = _workflowStepDefs[stepIndex]!;
|
|
835
|
+
const agents = discoverAgents();
|
|
836
|
+
|
|
837
|
+
function agentThinkingLevel(name: string): string | undefined {
|
|
838
|
+
return agents.find(a => a.name === name)?.thinkingLevel;
|
|
839
|
+
}
|
|
840
|
+
|
|
697
841
|
const newSubSteps: WorkflowSubStepWidgetState[] = [];
|
|
698
842
|
|
|
699
843
|
if (def.type === "loop-group") {
|
|
700
844
|
if (def.loopAgentName) {
|
|
701
845
|
newSubSteps.push({
|
|
702
846
|
agent: def.loopAgentName,
|
|
847
|
+
thinkingLevel: agentThinkingLevel(def.loopAgentName),
|
|
703
848
|
status: "pending",
|
|
704
849
|
tools: [],
|
|
705
850
|
outputs: [],
|
|
@@ -708,6 +853,7 @@ function populatePredefinedSubSteps(stepIndex: number): void {
|
|
|
708
853
|
if (def.reviewAgentName) {
|
|
709
854
|
newSubSteps.push({
|
|
710
855
|
agent: def.reviewAgentName,
|
|
856
|
+
thinkingLevel: agentThinkingLevel(def.reviewAgentName),
|
|
711
857
|
status: "pending",
|
|
712
858
|
tools: [],
|
|
713
859
|
outputs: [],
|
|
@@ -716,6 +862,7 @@ function populatePredefinedSubSteps(stepIndex: number): void {
|
|
|
716
862
|
} else if (def.agentName) {
|
|
717
863
|
newSubSteps.push({
|
|
718
864
|
agent: def.agentName,
|
|
865
|
+
thinkingLevel: agentThinkingLevel(def.agentName),
|
|
719
866
|
status: "pending",
|
|
720
867
|
tools: [],
|
|
721
868
|
outputs: [],
|
|
@@ -804,6 +951,24 @@ function setWidgetSubStepStatus(stepIndex: number, agentName: string, status: Wo
|
|
|
804
951
|
}
|
|
805
952
|
}
|
|
806
953
|
|
|
954
|
+
/**
|
|
955
|
+
* 重置子步骤为 pending 状态并清除计时信息(durationMs/startedAt)。
|
|
956
|
+
* 用于循环组中开启新循环时,清除上一轮的计时数据。
|
|
957
|
+
*/
|
|
958
|
+
function resetWidgetSubStepTimers(stepIndex: number, agentName: string): void {
|
|
959
|
+
const step = _widgetSteps[stepIndex];
|
|
960
|
+
if (!step) return;
|
|
961
|
+
const sub = step.subSteps?.find(s => s.agent === agentName);
|
|
962
|
+
if (sub) {
|
|
963
|
+
sub.status = "pending";
|
|
964
|
+
sub.durationMs = undefined;
|
|
965
|
+
sub.startedAt = undefined;
|
|
966
|
+
sub.tools = [];
|
|
967
|
+
sub.outputs = [];
|
|
968
|
+
}
|
|
969
|
+
refreshWidget();
|
|
970
|
+
}
|
|
971
|
+
|
|
807
972
|
function setWidgetCurrentStep(index: number): void {
|
|
808
973
|
_widgetCurrentIdx = index;
|
|
809
974
|
refreshWidget();
|
|
@@ -984,7 +1149,7 @@ async function runAgentWithProgress(
|
|
|
984
1149
|
addWidgetSubStepOutput(stepIndex, agentName, pathCandidate);
|
|
985
1150
|
}
|
|
986
1151
|
}
|
|
987
|
-
});
|
|
1152
|
+
}, _workflowId ? { workflowId: _workflowId } : undefined);
|
|
988
1153
|
|
|
989
1154
|
const agentDuration = Date.now() - agentStartTime;
|
|
990
1155
|
|
|
@@ -1128,6 +1293,14 @@ async function runAgentWithProgress(
|
|
|
1128
1293
|
"done";
|
|
1129
1294
|
setWidgetSubStepStatus(stepIndex, agentName, subStatus);
|
|
1130
1295
|
|
|
1296
|
+
// ⭐ 修复:代理完成后设置最终持续时长并清除 startedAt
|
|
1297
|
+
// 确保 UI 使用记录的 durationMs 而非实时 Date.now() - startedAt
|
|
1298
|
+
const completedSub = _widgetSteps[stepIndex]?.subSteps?.find(s => s.agent === agentName);
|
|
1299
|
+
if (completedSub) {
|
|
1300
|
+
completedSub.durationMs = agentDuration;
|
|
1301
|
+
completedSub.startedAt = undefined;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1131
1304
|
return result;
|
|
1132
1305
|
}
|
|
1133
1306
|
|
|
@@ -1149,7 +1322,7 @@ async function executeSingleStep(
|
|
|
1149
1322
|
const agent = agentMap.get(agentName);
|
|
1150
1323
|
if (!agent) throw new Error(`未找到 agent: ${agentName}`);
|
|
1151
1324
|
|
|
1152
|
-
const task = buildTaskForStep(agentName, prompt, planFileRelPath, _workflowCwd);
|
|
1325
|
+
const task = buildTaskForStep(agentName, prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext());
|
|
1153
1326
|
let retried = false;
|
|
1154
1327
|
|
|
1155
1328
|
let result = await runAgentWithProgress(agent, task, stepIndex, agentName, step.timeoutMs);
|
|
@@ -1176,6 +1349,35 @@ async function executeSingleStep(
|
|
|
1176
1349
|
if (result.exitCode !== 0 && result.stderr) {
|
|
1177
1350
|
throw new Error(`Agent 错误 (exit ${result.exitCode}): ${result.stderr.slice(0, 500)}`);
|
|
1178
1351
|
}
|
|
1352
|
+
|
|
1353
|
+
// ── Capture chain context for single-step agents ──
|
|
1354
|
+
const agentChanges = _workflowFileChanges
|
|
1355
|
+
.filter(c => c.stepIndex === stepIndex && c.agent === agentName)
|
|
1356
|
+
.map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
|
|
1357
|
+
|
|
1358
|
+
let chainKey: string;
|
|
1359
|
+
if (agentName === "planner") chainKey = "计划制定摘要";
|
|
1360
|
+
else if (agentName === "docWriter") chainKey = "文档更新摘要";
|
|
1361
|
+
else chainKey = `${agentName} 执行摘要`;
|
|
1362
|
+
|
|
1363
|
+
if (agentChanges.length > 0) {
|
|
1364
|
+
const editCount = agentChanges.filter(c => c.startsWith("M")).length;
|
|
1365
|
+
const newCount = agentChanges.filter(c => c.startsWith("A")).length;
|
|
1366
|
+
const delCount = agentChanges.filter(c => c.startsWith("D")).length;
|
|
1367
|
+
const statsParts: string[] = [];
|
|
1368
|
+
if (editCount > 0) statsParts.push(`修改 ${editCount} 个`);
|
|
1369
|
+
if (newCount > 0) statsParts.push(`新增 ${newCount} 个`);
|
|
1370
|
+
if (delCount > 0) statsParts.push(`删除 ${delCount} 个`);
|
|
1371
|
+
const statsLine = statsParts.length > 0 ? `改动统计: ${statsParts.join(",")}\n\n` : "";
|
|
1372
|
+
|
|
1373
|
+
updateChainContext(chainKey,
|
|
1374
|
+
`${agentName} 已完成。\n` +
|
|
1375
|
+
statsLine +
|
|
1376
|
+
`变更文件列表:\n${agentChanges.join("\n")}`
|
|
1377
|
+
);
|
|
1378
|
+
} else {
|
|
1379
|
+
updateChainContext(chainKey, `${agentName} 已完成执行,未检测到文件变更。`);
|
|
1380
|
+
}
|
|
1179
1381
|
}
|
|
1180
1382
|
|
|
1181
1383
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1213,13 +1415,13 @@ async function executeLoopGroup(
|
|
|
1213
1415
|
startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
|
|
1214
1416
|
});
|
|
1215
1417
|
|
|
1216
|
-
// 每次循环开始时重置 sub-step
|
|
1217
|
-
|
|
1218
|
-
|
|
1418
|
+
// 每次循环开始时重置 sub-step 状态(清除上一轮的计时和工具记录)
|
|
1419
|
+
resetWidgetSubStepTimers(stepIndex, step.loopAgentName!);
|
|
1420
|
+
resetWidgetSubStepTimers(stepIndex, step.reviewAgentName!);
|
|
1219
1421
|
const loopStartTime = Date.now();
|
|
1220
1422
|
|
|
1221
|
-
// Run loop agent
|
|
1222
|
-
const loopTask = buildTaskForStep(step.loopAgentName!, contextPrompt, planFileRelPath, _workflowCwd);
|
|
1423
|
+
// ── Run loop agent (worker / trimmer) ──
|
|
1424
|
+
const loopTask = buildTaskForStep(step.loopAgentName!, contextPrompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext());
|
|
1223
1425
|
|
|
1224
1426
|
let agentResult = await runAgentWithProgress(loopAgent, loopTask, stepIndex, step.loopAgentName!, step.timeoutMs);
|
|
1225
1427
|
|
|
@@ -1238,9 +1440,40 @@ async function executeLoopGroup(
|
|
|
1238
1440
|
}
|
|
1239
1441
|
}
|
|
1240
1442
|
|
|
1443
|
+
// ── After loop agent completes: capture changes + stats for chain context ──
|
|
1444
|
+
const loopAgentChanges = _workflowFileChanges
|
|
1445
|
+
.filter(c => c.stepIndex === stepIndex && c.agent === step.loopAgentName)
|
|
1446
|
+
.map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
|
|
1447
|
+
|
|
1448
|
+
// Use agent-specific keys so worker and trimmer changes coexist without overwriting
|
|
1449
|
+
const chainKey = step.loopAgentName === "worker" ? "代码实施摘要" : "代码精简摘要";
|
|
1450
|
+
|
|
1451
|
+
if (loopAgentChanges.length > 0) {
|
|
1452
|
+
const editCount = loopAgentChanges.filter(c => c.startsWith("M")).length;
|
|
1453
|
+
const newCount = loopAgentChanges.filter(c => c.startsWith("A")).length;
|
|
1454
|
+
const delCount = loopAgentChanges.filter(c => c.startsWith("D")).length;
|
|
1455
|
+
const statsParts: string[] = [];
|
|
1456
|
+
if (editCount > 0) statsParts.push(`修改 ${editCount} 个`);
|
|
1457
|
+
if (newCount > 0) statsParts.push(`新增 ${newCount} 个`);
|
|
1458
|
+
if (delCount > 0) statsParts.push(`删除 ${delCount} 个`);
|
|
1459
|
+
const statsLine = statsParts.length > 0 ? `改动统计: ${statsParts.join(",")}\n\n` : "";
|
|
1460
|
+
|
|
1461
|
+
updateChainContext(chainKey,
|
|
1462
|
+
`${step.loopAgentName === "worker" ? "代码实施" : "代码精简"}已完成。\n` +
|
|
1463
|
+
statsLine +
|
|
1464
|
+
`变更文件列表:\n${loopAgentChanges.join("\n")}` +
|
|
1465
|
+
(planFileRelPath ? `\n\n实施计划: ${planFileRelPath}\n(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)` : "")
|
|
1466
|
+
);
|
|
1467
|
+
} else {
|
|
1468
|
+
updateChainContext(chainKey,
|
|
1469
|
+
`${step.loopAgentName} 已完成执行,未检测到文件变更。\n` +
|
|
1470
|
+
(planFileRelPath ? `实施计划: ${planFileRelPath}\n(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)` : "")
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1241
1474
|
if (isTimeoutResult(agentResult)) {
|
|
1242
1475
|
if (mode === "full-auto") {
|
|
1243
|
-
contextPrompt = `[TIMEOUT_WARNING] 上一个 ${step.loopAgentName} 执行超时。\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
|
|
1476
|
+
contextPrompt = `[TIMEOUT_WARNING] 上一个 ${step.loopAgentName} 执行超时。\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
|
|
1244
1477
|
} else {
|
|
1245
1478
|
const choice = await uiSelect(ctx, `⏰ ${step.loopAgentName} 执行超时`, [
|
|
1246
1479
|
"1. 重新执行", "2. 进入审查阶段", "3. 跳过此步骤", "4. 取消工作流",
|
|
@@ -1248,20 +1481,20 @@ async function executeLoopGroup(
|
|
|
1248
1481
|
if (!choice || choice.startsWith("4")) { cancelWorkflow(); return; }
|
|
1249
1482
|
if (choice.startsWith("3")) { state.status = "skipped"; return; }
|
|
1250
1483
|
if (choice.startsWith("2")) {
|
|
1251
|
-
contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
|
|
1484
|
+
contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
|
|
1252
1485
|
} else {
|
|
1253
1486
|
agentResult = await runAgentWithProgress(loopAgent, `[RETRY]\n\n${loopTask}`, stepIndex, step.loopAgentName!, step.timeoutMs);
|
|
1254
1487
|
if (isTimeoutResult(agentResult)) {
|
|
1255
|
-
contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
|
|
1488
|
+
contextPrompt = `[TIMEOUT_WARNING]\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
|
|
1256
1489
|
}
|
|
1257
1490
|
}
|
|
1258
1491
|
}
|
|
1259
1492
|
}
|
|
1260
1493
|
|
|
1261
|
-
// Run reviewer
|
|
1494
|
+
// ── Run reviewer ──
|
|
1262
1495
|
const reviewTask = contextPrompt.includes("[TIMEOUT_WARNING]")
|
|
1263
1496
|
? contextPrompt
|
|
1264
|
-
: buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd);
|
|
1497
|
+
: buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext());
|
|
1265
1498
|
|
|
1266
1499
|
const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, reviewTimeoutMs);
|
|
1267
1500
|
|
|
@@ -1276,11 +1509,31 @@ async function executeLoopGroup(
|
|
|
1276
1509
|
}
|
|
1277
1510
|
}
|
|
1278
1511
|
|
|
1512
|
+
// ── After reviewer: capture review context for next loop ──
|
|
1513
|
+
// Use agent-specific key so worker-reviewer and trimmer-reviewer feedback don't interfere
|
|
1514
|
+
const reviewChainKey = step.loopAgentName === "worker" ? "代码审查反馈" : "精简审查反馈";
|
|
1515
|
+
if (reviewSummary) {
|
|
1516
|
+
const reviewCountParts: string[] = [];
|
|
1517
|
+
if (reviewSummary.critical > 0) reviewCountParts.push(`严重 ${reviewSummary.critical} 个`);
|
|
1518
|
+
if (reviewSummary.medium > 0) reviewCountParts.push(`中等 ${reviewSummary.medium} 个`);
|
|
1519
|
+
if (reviewSummary.low > 0) reviewCountParts.push(`低 ${reviewSummary.low} 个`);
|
|
1520
|
+
const reviewStats = reviewCountParts.length > 0 ? `发现 ${reviewCountParts.join(",")} 问题。` : "未发现问题。";
|
|
1521
|
+
|
|
1522
|
+
updateChainContext(reviewChainKey,
|
|
1523
|
+
`${reviewStats}\n` +
|
|
1524
|
+
`完整审查报告在 .pi-dev-output/pi-review/md/ 目录中,\n` +
|
|
1525
|
+
`请在工作流输出目录中 grep UUID "${_workflowId}" 查找最新报告。`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// ── Decide whether to loop ──
|
|
1279
1530
|
if (reviewSummary?.maxSeverity === "critical" && loopCount < maxLoops) {
|
|
1531
|
+
const reviewReportHint = `\n完整审查报告在 .pi-dev-output/pi-review/md/(grep UUID "${_workflowId}" 查找)。`;
|
|
1280
1532
|
if (mode === "full-auto") {
|
|
1281
1533
|
contextPrompt = [prompt, "", "## 上次审查发现的问题",
|
|
1282
1534
|
`审查摘要: ${JSON.stringify(reviewSummary)}`,
|
|
1283
1535
|
`请修复 ${reviewSummary.critical} 个严重问题后重新运行。`,
|
|
1536
|
+
reviewReportHint,
|
|
1284
1537
|
].join("\n");
|
|
1285
1538
|
continue;
|
|
1286
1539
|
} else {
|
|
@@ -1290,6 +1543,7 @@ async function executeLoopGroup(
|
|
|
1290
1543
|
contextPrompt = [prompt, "", "## 上次审查发现的问题",
|
|
1291
1544
|
`审查摘要: ${JSON.stringify(reviewSummary)}`,
|
|
1292
1545
|
`请修复这些严重问题后重新运行。`,
|
|
1546
|
+
reviewReportHint,
|
|
1293
1547
|
].join("\n");
|
|
1294
1548
|
continue;
|
|
1295
1549
|
}
|
|
@@ -1433,6 +1687,40 @@ async function executeWorkflowBackground(
|
|
|
1433
1687
|
}
|
|
1434
1688
|
}
|
|
1435
1689
|
|
|
1690
|
+
// ── Before executing docWriter, rebuild chain context with only relevant info ──
|
|
1691
|
+
if (step.agentName === "docWriter") {
|
|
1692
|
+
// Clear stale context (review feedback from worker/trimmer loops is noise for docWriter)
|
|
1693
|
+
resetChainContext();
|
|
1694
|
+
// Restore plan location
|
|
1695
|
+
if (planFileRelPathInner) {
|
|
1696
|
+
updateChainContext("实施计划",
|
|
1697
|
+
`计划文件: ${planFileRelPathInner}\n` +
|
|
1698
|
+
`(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
// Build comprehensive change summary from ALL file changes
|
|
1702
|
+
const workerChanges = _workflowFileChanges
|
|
1703
|
+
.filter(c => c.type !== "read" && c.agent === "worker")
|
|
1704
|
+
.map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
|
|
1705
|
+
const trimmerChanges = _workflowFileChanges
|
|
1706
|
+
.filter(c => c.type !== "read" && c.agent === "trimmer")
|
|
1707
|
+
.map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
|
|
1708
|
+
const allChangeLines: string[] = [];
|
|
1709
|
+
if (workerChanges.length > 0) {
|
|
1710
|
+
allChangeLines.push(`🔧 Worker (代码实施) - ${workerChanges.length} 个文件:`);
|
|
1711
|
+
allChangeLines.push(...workerChanges);
|
|
1712
|
+
}
|
|
1713
|
+
if (trimmerChanges.length > 0) {
|
|
1714
|
+
allChangeLines.push(`✂️ Trimmer (代码精简) - ${trimmerChanges.length} 个文件:`);
|
|
1715
|
+
allChangeLines.push(...trimmerChanges);
|
|
1716
|
+
}
|
|
1717
|
+
if (allChangeLines.length > 0) {
|
|
1718
|
+
updateChainContext("本次工作流全部变更",
|
|
1719
|
+
`以下文件已被本次工作流修改:\n${allChangeLines.join("\n")}`
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1436
1724
|
// ── Execute (timer starts NOW, after all user confirmations) ──
|
|
1437
1725
|
state.status = "running";
|
|
1438
1726
|
const stepStartTime = Date.now();
|
|
@@ -1449,6 +1737,13 @@ async function executeWorkflowBackground(
|
|
|
1449
1737
|
await executeSingleStep(ctx, step, state, agentMap, prompt, planFileRelPathInner, mode, currentStepIndex);
|
|
1450
1738
|
if (step.agentName === "planner") {
|
|
1451
1739
|
planFileRelPathInner = findLatestPlanFile(_workflowCwd);
|
|
1740
|
+
// Update chain context so subsequent agents know where the plan is
|
|
1741
|
+
if (planFileRelPathInner) {
|
|
1742
|
+
updateChainContext("实施计划",
|
|
1743
|
+
`计划文件: ${planFileRelPathInner}\n` +
|
|
1744
|
+
`(可在 .pi-dev-output/pi-plans/ 中 grep UUID ${_workflowId} 找到)`
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1452
1747
|
}
|
|
1453
1748
|
}
|
|
1454
1749
|
state.status = "done";
|
|
@@ -1491,6 +1786,7 @@ async function executeWorkflowBackground(
|
|
|
1491
1786
|
stepStates.every(s => s.status === "done" || s.status === "skipped") ? "done" : "failed",
|
|
1492
1787
|
{ toolCount: _widgetExtraToolCount, tokenCount: _widgetExtraTokenCount },
|
|
1493
1788
|
taskSummary,
|
|
1789
|
+
_workflowId,
|
|
1494
1790
|
);
|
|
1495
1791
|
sendWorkflowResult(pi, finalState, prompt, _workflowType);
|
|
1496
1792
|
|
|
@@ -1504,6 +1800,7 @@ async function executeWorkflowBackground(
|
|
|
1504
1800
|
function buildCp(): CheckpointData {
|
|
1505
1801
|
return {
|
|
1506
1802
|
version: 2,
|
|
1803
|
+
workflowId: _workflowId,
|
|
1507
1804
|
createdAt: existingCp?.createdAt ?? new Date().toISOString(),
|
|
1508
1805
|
updatedAt: new Date().toISOString(),
|
|
1509
1806
|
prompt,
|
|
@@ -1622,6 +1919,12 @@ export async function runWorkflow(
|
|
|
1622
1919
|
_workflowFileChanges = existingCp?.fileChanges ? [...existingCp.fileChanges] : [];
|
|
1623
1920
|
_workflowAgentRunHistory = existingCp?.agentRunHistory ? [...existingCp.agentRunHistory] : [];
|
|
1624
1921
|
|
|
1922
|
+
// Initialize or restore workflow UUID
|
|
1923
|
+
_workflowId = existingCp?.workflowId ?? uuidv7();
|
|
1924
|
+
|
|
1925
|
+
// Reset chain context for new workflow session
|
|
1926
|
+
resetChainContext();
|
|
1927
|
+
|
|
1625
1928
|
// ── Baseline snapshot: restore from checkpoint or capture fresh ──
|
|
1626
1929
|
if (resumeFlow && existingCp?.baseline) {
|
|
1627
1930
|
_workflowBaseline = [...existingCp.baseline];
|
|
@@ -1631,6 +1934,7 @@ export async function runWorkflow(
|
|
|
1631
1934
|
|
|
1632
1935
|
saveCheckpoint(ctx.cwd, {
|
|
1633
1936
|
version: 2,
|
|
1937
|
+
workflowId: _workflowId,
|
|
1634
1938
|
createdAt: existingCp?.createdAt ?? new Date().toISOString(),
|
|
1635
1939
|
updatedAt: new Date().toISOString(),
|
|
1636
1940
|
prompt,
|
|
@@ -1673,13 +1977,16 @@ export async function runWorkflow(
|
|
|
1673
1977
|
_widgetCurrentIdx,
|
|
1674
1978
|
_widgetStartTime,
|
|
1675
1979
|
"cancelled",
|
|
1980
|
+
undefined,
|
|
1676
1981
|
extractTaskSummary(_workflowPrompt),
|
|
1982
|
+
_workflowId,
|
|
1677
1983
|
);
|
|
1678
1984
|
updateWorkflowWidget(_lastWorkflowCtx, finalState);
|
|
1679
1985
|
|
|
1680
1986
|
// ── Save final checkpoint before archiving ──
|
|
1681
1987
|
const cancelCp: CheckpointData = {
|
|
1682
1988
|
version: 2,
|
|
1989
|
+
workflowId: _workflowId,
|
|
1683
1990
|
createdAt: _workflowCreatedAt,
|
|
1684
1991
|
updatedAt: new Date().toISOString(),
|
|
1685
1992
|
prompt: _workflowPrompt,
|
|
@@ -1730,7 +2037,7 @@ export async function runWorkflow(
|
|
|
1730
2037
|
if (_workflowRunning && _workflowAbortController && !_workflowAbortController.signal.aborted) {
|
|
1731
2038
|
const now = Date.now();
|
|
1732
2039
|
if (_lastEscPressTime > 0 && now - _lastEscPressTime < 3000) {
|
|
1733
|
-
// Second Esc press within
|
|
2040
|
+
// Second Esc press within 3s → confirm cancel
|
|
1734
2041
|
ctx.ui.notify("⏹️ 正在停止工作流...", "warning");
|
|
1735
2042
|
cancelWorkflow();
|
|
1736
2043
|
_lastEscPressTime = 0;
|