@ghyper9023/pi-dev-workflow 0.4.0 → 0.4.2
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/.pi-dev-output/pi-grill/answers/answer-mpds3by7-20260520-1606.md +14 -0
- package/.pi-dev-output/pi-grill/answers/answer-mpfe77f1-20260521-1913.md +58 -0
- package/.pi-dev-output/pi-grill/answers/answer-mpfh37wu-20260521-2034.md +13 -0
- package/.pi-dev-output/pi-grill/answers/answer-mpfi5q4c-20260521-2104.md +13 -0
- package/.pi-dev-output/pi-grill/answers/answer-mpfizccb-20260521-2127.md +13 -0
- package/.pi-dev-output/pi-grill/answers/answer-mpfjk78k-20260521-2143.md +13 -0
- package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +94 -0
- package/.pi-dev-output/pi-plans/20260520-153000-fix-workflow-engine-bugs.md +150 -0
- package/.pi-dev-output/pi-plans/20260521-113000-fix-loopcount-timeout.md +215 -0
- package/.pi-dev-output/pi-plans/20260521-1730-grill-input-wrap-back-fix.md +240 -0
- package/.pi-dev-output/pi-plans/20260521-230000-fix-timeout-display-loopcount-gitdiff.md +253 -0
- package/.pi-dev-output/pi-plans/20260521-230500-esc-double-press-confirm-workflow.md +137 -0
- package/.pi-dev-output/pi-plans/20260521-235000-fix-gitdiff-loopcount.md +258 -0
- package/.pi-dev-output/pi-review/html/20260521-2305-review-workflow-index.html +196 -0
- package/.pi-dev-output/pi-review/md/review-20260520-100000.md +91 -0
- package/.pi-dev-output/pi-review/md/review-20260521-140000.md +191 -0
- package/.pi-dev-output/pi-review/md/review-20260521-190000.md +189 -0
- package/.pi-dev-output/pi-review/md/review-20260521-204500.md +241 -0
- package/.pi-dev-output/pi-review/md/review-20260521-214500.md +270 -0
- package/.pi-dev-output/pi-review/md/review-20260521-215158.md +214 -0
- package/.pi-dev-output/pi-review/md/review-20260521-234500.md +201 -0
- package/.pi-dev-output/pi-review/md/review-20260521-235500.md +422 -0
- package/.pi-dev-output/pi-review/md/review-20260522-000000.md +212 -0
- package/.pi-dev-output/pi-review/md/review-20260522-003000.md +377 -0
- package/.pi-dev-output/pi-review/md/review-20260522-003500.md +296 -0
- package/.pi-dev-output/pi-workflow/checkpoint-20260520-153000-fix-workflow-engine-bugs.json +108 -0
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-113000-fix-loopcount-timeout.json +402 -0
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-1730-grill-input-wrap-back-fix.json +447 -0
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-230000-fix-timeout-display-loopcount-gitdiff.json +708 -0
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-230500-esc-double-press-confirm-workflow.json +365 -0
- package/.pi-dev-output/pi-workflow/checkpoint-20260521-235000-fix-gitdiff-loopcount.json +395 -0
- package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfhyxc5.json +30 -0
- package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi2unc.json +49 -0
- package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi382e.json +59 -0
- package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi5r22.json +76 -0
- package/extensions/dev-prompts.ts +16 -8
- package/extensions/grill-me-agent.ts +23 -7
- package/extensions/ui-helpers.ts +59 -8
- package/extensions/workflow-engine.ts +116 -35
- package/package.json +1 -1
- package/tests/test-loopcount-timeout-fix.mjs +336 -0
- package/tests/test-workflow-engine-bugs.mjs +349 -0
- package/themes/oh-my-pi-titanium.json +90 -0
|
@@ -381,7 +381,8 @@ const FEAT_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
381
381
|
loopAgentName: "worker",
|
|
382
382
|
reviewAgentName: "reviewer",
|
|
383
383
|
maxLoops: 3,
|
|
384
|
-
timeoutMs:
|
|
384
|
+
timeoutMs: 1_800_000,
|
|
385
|
+
reviewTimeoutMs: 900_000,
|
|
385
386
|
},
|
|
386
387
|
{
|
|
387
388
|
id: "trimmer-reviewer",
|
|
@@ -390,7 +391,8 @@ const FEAT_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
390
391
|
loopAgentName: "trimmer",
|
|
391
392
|
reviewAgentName: "reviewer",
|
|
392
393
|
maxLoops: 3,
|
|
393
|
-
timeoutMs:
|
|
394
|
+
timeoutMs: 1_200_000,
|
|
395
|
+
reviewTimeoutMs: 900_000,
|
|
394
396
|
},
|
|
395
397
|
{
|
|
396
398
|
id: "docWriter",
|
|
@@ -416,7 +418,8 @@ const FIX_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
416
418
|
loopAgentName: "worker",
|
|
417
419
|
reviewAgentName: "reviewer",
|
|
418
420
|
maxLoops: 3,
|
|
419
|
-
timeoutMs:
|
|
421
|
+
timeoutMs: 1_800_000,
|
|
422
|
+
reviewTimeoutMs: 900_000,
|
|
420
423
|
},
|
|
421
424
|
{
|
|
422
425
|
id: "docWriter",
|
|
@@ -442,7 +445,8 @@ const REFACTOR_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
442
445
|
loopAgentName: "worker",
|
|
443
446
|
reviewAgentName: "reviewer",
|
|
444
447
|
maxLoops: 3,
|
|
445
|
-
timeoutMs:
|
|
448
|
+
timeoutMs: 1_800_000,
|
|
449
|
+
reviewTimeoutMs: 900_000,
|
|
446
450
|
},
|
|
447
451
|
{
|
|
448
452
|
id: "trimmer-reviewer",
|
|
@@ -451,7 +455,8 @@ const REFACTOR_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
451
455
|
loopAgentName: "trimmer",
|
|
452
456
|
reviewAgentName: "reviewer",
|
|
453
457
|
maxLoops: 3,
|
|
454
|
-
timeoutMs:
|
|
458
|
+
timeoutMs: 1_200_000,
|
|
459
|
+
reviewTimeoutMs: 900_000,
|
|
455
460
|
},
|
|
456
461
|
];
|
|
457
462
|
|
|
@@ -470,7 +475,8 @@ const PERF_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
470
475
|
loopAgentName: "worker",
|
|
471
476
|
reviewAgentName: "reviewer",
|
|
472
477
|
maxLoops: 3,
|
|
473
|
-
timeoutMs:
|
|
478
|
+
timeoutMs: 1_800_000,
|
|
479
|
+
reviewTimeoutMs: 900_000,
|
|
474
480
|
},
|
|
475
481
|
];
|
|
476
482
|
|
|
@@ -489,7 +495,8 @@ const TEST_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
489
495
|
loopAgentName: "worker",
|
|
490
496
|
reviewAgentName: "reviewer",
|
|
491
497
|
maxLoops: 3,
|
|
492
|
-
timeoutMs:
|
|
498
|
+
timeoutMs: 1_800_000,
|
|
499
|
+
reviewTimeoutMs: 900_000,
|
|
493
500
|
},
|
|
494
501
|
];
|
|
495
502
|
|
|
@@ -518,7 +525,8 @@ const STYLE_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
518
525
|
loopAgentName: "trimmer",
|
|
519
526
|
reviewAgentName: "reviewer",
|
|
520
527
|
maxLoops: 2,
|
|
521
|
-
timeoutMs:
|
|
528
|
+
timeoutMs: 1_200_000,
|
|
529
|
+
reviewTimeoutMs: 900_000,
|
|
522
530
|
},
|
|
523
531
|
];
|
|
524
532
|
|
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
SelectList,
|
|
22
22
|
Text,
|
|
23
23
|
Spacer,
|
|
24
|
+
matchesKey,
|
|
25
|
+
Key,
|
|
26
|
+
truncateToWidth,
|
|
24
27
|
type SelectItem,
|
|
25
28
|
} from "@earendil-works/pi-tui";
|
|
26
29
|
import { uiSelect, uiConfirm, uiInput } from "./ui-helpers";
|
|
@@ -599,12 +602,20 @@ async function showQuestionTUI(
|
|
|
599
602
|
backable = false,
|
|
600
603
|
previousAnswer?: string,
|
|
601
604
|
): Promise<string | null> {
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
605
|
+
const MAX_OPTION_LABEL = 50;
|
|
606
|
+
const selectItems: SelectItem[] = q.options.map((opt, i) => {
|
|
607
|
+
const prefix = `(${String.fromCharCode(97 + i)}) `;
|
|
608
|
+
const label = opt === previousAnswer
|
|
609
|
+
? `${prefix}${opt} - 上次选择`
|
|
610
|
+
: `${prefix}${opt}`;
|
|
611
|
+
const truncated = truncateToWidth(label, MAX_OPTION_LABEL, "...");
|
|
612
|
+
return {
|
|
613
|
+
value: `opt-${i}`,
|
|
614
|
+
label: truncated,
|
|
615
|
+
// 只有被截断时才提供 description,展示完整文本
|
|
616
|
+
description: truncated !== label ? opt : undefined,
|
|
617
|
+
};
|
|
618
|
+
});
|
|
608
619
|
|
|
609
620
|
const customLabel = previousAnswer && !q.options.includes(previousAnswer)
|
|
610
621
|
? `✏️ 自定义输入 - 上次选择`
|
|
@@ -646,7 +657,7 @@ async function showQuestionTUI(
|
|
|
646
657
|
|
|
647
658
|
container.addChild(new Spacer(1));
|
|
648
659
|
const hint = backable && currentIndex > 1
|
|
649
|
-
? " ↑↓ 导航 • Enter 选择 •
|
|
660
|
+
? " ↑↓ 导航 • Enter 选择 • ← 返回上一题 • Esc 取消全部评审"
|
|
650
661
|
: " ↑↓ 导航 • Enter 选择 • Esc 取消全部评审";
|
|
651
662
|
container.addChild(
|
|
652
663
|
new Text(theme.fg("dim", hint), 0, 0),
|
|
@@ -657,6 +668,11 @@ async function showQuestionTUI(
|
|
|
657
668
|
render: (w) => container.render(w),
|
|
658
669
|
invalidate: () => container.invalidate(),
|
|
659
670
|
handleInput: (data) => {
|
|
671
|
+
// 左方向键 → 返回上一题(SelectList 不处理 left 键,需自行拦截)
|
|
672
|
+
if (backable && currentIndex > 1 && matchesKey(data, Key.left)) {
|
|
673
|
+
done("__BACK__");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
660
676
|
selectList.handleInput(data);
|
|
661
677
|
tui.requestRender();
|
|
662
678
|
},
|
package/extensions/ui-helpers.ts
CHANGED
|
@@ -207,7 +207,7 @@ export function uiConfirm(
|
|
|
207
207
|
/**
|
|
208
208
|
* Show an input dialog with proper wrapping.
|
|
209
209
|
* Returns the entered string, or BACK_MARKER on back, or undefined on cancel.
|
|
210
|
-
* When backable=true, supports Ctrl+Shift+← for back and Ctrl+Shift+→ for submit+next.
|
|
210
|
+
* When backable=true, supports ← for back, Ctrl+Shift+← for back, and Ctrl+Shift+→ for submit+next.
|
|
211
211
|
*/
|
|
212
212
|
export function uiInput(
|
|
213
213
|
ctx: ExtensionCommandContext,
|
|
@@ -229,6 +229,11 @@ export function uiInput(
|
|
|
229
229
|
}
|
|
230
230
|
container.addChild(new Spacer(1));
|
|
231
231
|
|
|
232
|
+
// 实时换行预览区域(在输入框上方)
|
|
233
|
+
const previewText = new Text("", 0, 0);
|
|
234
|
+
container.addChild(previewText);
|
|
235
|
+
container.addChild(new Spacer(1));
|
|
236
|
+
|
|
232
237
|
const input = new Input(placeholder ?? "", width - 2);
|
|
233
238
|
if (initialValue) {
|
|
234
239
|
input.setValue(initialValue);
|
|
@@ -255,6 +260,12 @@ export function uiInput(
|
|
|
255
260
|
render: (w) => container.render(w),
|
|
256
261
|
invalidate: () => container.invalidate(),
|
|
257
262
|
handleInput: (data) => {
|
|
263
|
+
// 左方向键 → 返回(优先于 Input 的光标左移)
|
|
264
|
+
if (backable && matchesKey(data, Key.left)) {
|
|
265
|
+
done(BACK_MARKER);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
258
269
|
// Intercept back/next keys before passing to Input
|
|
259
270
|
if (backable) {
|
|
260
271
|
// Ctrl+Shift+← → go back to previous question
|
|
@@ -269,6 +280,19 @@ export function uiInput(
|
|
|
269
280
|
}
|
|
270
281
|
}
|
|
271
282
|
input.handleInput(data);
|
|
283
|
+
|
|
284
|
+
// 读取更新后的 value,更新预览
|
|
285
|
+
const val = input.getValue();
|
|
286
|
+
if (val.length > 0) {
|
|
287
|
+
const wrapped = wrapTextWithAnsi(val, width - 4);
|
|
288
|
+
const previewContent = wrapped
|
|
289
|
+
.map(l => theme.fg("dim", ` ${l}`))
|
|
290
|
+
.join("\n");
|
|
291
|
+
previewText.setText(previewContent);
|
|
292
|
+
} else {
|
|
293
|
+
previewText.setText("");
|
|
294
|
+
}
|
|
295
|
+
|
|
272
296
|
tui.requestRender();
|
|
273
297
|
},
|
|
274
298
|
};
|
|
@@ -362,7 +386,7 @@ function formatDurationFull(ms: number): string {
|
|
|
362
386
|
return `${m}m${s}s`;
|
|
363
387
|
}
|
|
364
388
|
|
|
365
|
-
function formatTimeout(ms: number): string {
|
|
389
|
+
export function formatTimeout(ms: number): string {
|
|
366
390
|
const m = Math.floor(ms / 60000);
|
|
367
391
|
const s = Math.floor((ms % 60000) / 1000);
|
|
368
392
|
return s > 0 ? `${m}m${s}s` : `${m}m`;
|
|
@@ -462,8 +486,13 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
|
|
|
462
486
|
loopStr = dim(theme, ` · 第 ${s.loopCount} 次循环`);
|
|
463
487
|
} else if (s.maxLoops != null) {
|
|
464
488
|
if (isRunning) {
|
|
465
|
-
//
|
|
466
|
-
|
|
489
|
+
// 当 loop-group 开始运行时,loopCount 已经通过 executeLoopGroup 在循环开头设置了,
|
|
490
|
+
// 所以不需要 fallback 显示"第 1 次循环"
|
|
491
|
+
// 直接使用 s.loopCount 的值
|
|
492
|
+
if (s.loopCount == null || s.loopCount === 0) {
|
|
493
|
+
// 安全 fallback(理论上不会走到这里)
|
|
494
|
+
loopStr = dim(theme, ` · 第 1 次循环`);
|
|
495
|
+
}
|
|
467
496
|
} else if (isPending) {
|
|
468
497
|
loopStr = dim(theme, ` · 第 0 次循环`);
|
|
469
498
|
}
|
|
@@ -516,9 +545,32 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
|
|
|
516
545
|
? theme.fg("accent", spinnerFrame())
|
|
517
546
|
: dim(theme, "◦");
|
|
518
547
|
|
|
519
|
-
// Agent line: " |__ ✓ worker ·"
|
|
548
|
+
// Agent line: " |__ ✓ worker · (52.6s/超时时间60m)"
|
|
549
|
+
// Build inline duration and timeout info for sub-step
|
|
550
|
+
let subDurStr = "";
|
|
551
|
+
let subTimeoutStr = "";
|
|
552
|
+
let subDurClose = "";
|
|
553
|
+
let elapsedMs: number | undefined;
|
|
554
|
+
if (sub.startedAt) {
|
|
555
|
+
elapsedMs = Date.now() - sub.startedAt;
|
|
556
|
+
} else if (sub.durationMs != null) {
|
|
557
|
+
elapsedMs = sub.durationMs;
|
|
558
|
+
}
|
|
559
|
+
if (elapsedMs != null) {
|
|
560
|
+
subDurStr = dim(theme, ` (${formatDurationFull(elapsedMs)}`);
|
|
561
|
+
} else if (isSubRunning) {
|
|
562
|
+
subDurStr = dim(theme, ` (0s`);
|
|
563
|
+
}
|
|
564
|
+
// Extract timeout info from sub.detail (e.g. "超时时间60m")
|
|
565
|
+
// detail is set in runAgentWithProgress as `超时时间${formatTimeout(timeoutMs)}`
|
|
566
|
+
if (sub.detail && sub.detail.includes("超时时间")) {
|
|
567
|
+
subTimeoutStr = dim(theme, `/${sub.detail}`);
|
|
568
|
+
}
|
|
569
|
+
if (subDurStr) {
|
|
570
|
+
subDurClose = dim(theme, ")");
|
|
571
|
+
}
|
|
520
572
|
const agentConnector = dim(theme, "|__");
|
|
521
|
-
lines.push(`${agentIndent}${agentConnector} ${subIcon} ${sub.agent}
|
|
573
|
+
lines.push(`${agentIndent}${agentConnector} ${subIcon} ${sub.agent} ·${subDurStr}${subTimeoutStr}${subDurClose}`);
|
|
522
574
|
|
|
523
575
|
// ── Children (tools, outputs, or "正在排队") ──
|
|
524
576
|
const childItems: string[] = [];
|
|
@@ -536,7 +588,7 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
|
|
|
536
588
|
childItems.push(`output:${o}`);
|
|
537
589
|
}
|
|
538
590
|
}
|
|
539
|
-
if (childItems.length === 0 && sub.detail) {
|
|
591
|
+
if (childItems.length === 0 && sub.detail && !sub.detail.includes("超时时间")) {
|
|
540
592
|
childItems.push(sub.detail);
|
|
541
593
|
}
|
|
542
594
|
}
|
|
@@ -585,7 +637,6 @@ function buildWidgetLines(state: WorkflowWidgetState, theme: Theme, expanded: bo
|
|
|
585
637
|
} else {
|
|
586
638
|
lines.push(` ${dim(theme, "Ctrl+O 折叠详情")} ${dim(theme, "|")} ${gold("Escape 取消")}`);
|
|
587
639
|
}
|
|
588
|
-
lines.push(` ${gold("Ctrl+O 展开详情")} ${dim(theme, "|")} ${gold("Escape 取消")}`);
|
|
589
640
|
}
|
|
590
641
|
|
|
591
642
|
return lines;
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
updateWorkflowWidget,
|
|
32
32
|
buildWidgetState,
|
|
33
33
|
sendWorkflowResult,
|
|
34
|
+
formatTimeout,
|
|
34
35
|
setWorkflowCancelCallback,
|
|
35
36
|
cancelWorkflow,
|
|
36
37
|
BACK_MARKER,
|
|
@@ -55,6 +56,8 @@ export interface WorkflowStepDef {
|
|
|
55
56
|
reviewAgentName?: string;
|
|
56
57
|
maxLoops?: number;
|
|
57
58
|
timeoutMs: number;
|
|
59
|
+
/** 独立于 loopAgent 的 reviewer 超时时间(ms),默认使用 timeoutMs */
|
|
60
|
+
reviewTimeoutMs?: number;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
interface WorkflowStepState {
|
|
@@ -291,38 +294,52 @@ function getGitDiffChanges(cwd: string): GitFileChange[] {
|
|
|
291
294
|
|
|
292
295
|
try {
|
|
293
296
|
// 1. `git diff --name-status` — shows modified (M) and deleted (D) vs HEAD
|
|
297
|
+
// Format: "M\tpath/to/file" (tab-separated) or "M path/to/file" (spaces)
|
|
294
298
|
const diffOutput = execSync("git diff --name-status", { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
295
299
|
if (diffOutput) {
|
|
296
300
|
for (const line of diffOutput.split("\n")) {
|
|
297
301
|
const trimmed = line.trim();
|
|
298
302
|
if (!trimmed) continue;
|
|
299
|
-
//
|
|
300
|
-
const
|
|
301
|
-
if (
|
|
302
|
-
const status =
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
seen.add(
|
|
306
|
-
changes.push({ status, path });
|
|
303
|
+
// 使用正则解析:支持 tab 分隔("M\tpath")和空格填充("M path")两种格式
|
|
304
|
+
const statusMatch = trimmed.match(/^([MAD])\s+(.+)$/);
|
|
305
|
+
if (statusMatch) {
|
|
306
|
+
const status = statusMatch[1]!.trim();
|
|
307
|
+
const filePath = statusMatch[2]!.trim();
|
|
308
|
+
if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
|
|
309
|
+
seen.add(filePath);
|
|
310
|
+
changes.push({ status: status as "M" | "A" | "D", path: filePath });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// 后备:tab split(兼容部分 git 版本输出的 tab 格式)
|
|
314
|
+
else if (trimmed.includes("\t")) {
|
|
315
|
+
const parts = trimmed.split("\t");
|
|
316
|
+
if (parts.length === 2) {
|
|
317
|
+
const status = parts[0]!.trim();
|
|
318
|
+
const filePath = parts[1]!.trim();
|
|
319
|
+
if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
|
|
320
|
+
seen.add(filePath);
|
|
321
|
+
changes.push({ status: status as "M" | "A" | "D", path: filePath });
|
|
322
|
+
}
|
|
307
323
|
}
|
|
308
324
|
}
|
|
309
325
|
}
|
|
310
326
|
}
|
|
311
327
|
|
|
312
328
|
// 2. `git status --porcelain` — find untracked files (??) missing from git diff
|
|
329
|
+
// Format: "XY filepath" (e.g., " M .gitignore", "?? newfile.ts", "A filepath")
|
|
313
330
|
const statusOutput = execSync("git status --porcelain", { cwd, encoding: "utf8", timeout: 5000 }).trim();
|
|
314
331
|
if (statusOutput) {
|
|
315
332
|
for (const line of statusOutput.split("\n")) {
|
|
316
333
|
const trimmed = line.trim();
|
|
317
334
|
if (!trimmed) continue;
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
324
|
-
seen.add(
|
|
325
|
-
changes.push({ status: "A", path });
|
|
335
|
+
// 使用正则解析 --porcelain 格式:前 2 字符状态码 + 空格 + 路径
|
|
336
|
+
const statusMatch2 = trimmed.match(/^(..)\s+(.+)$/);
|
|
337
|
+
if (statusMatch2) {
|
|
338
|
+
const statusPrefix = statusMatch2[1]!.trim();
|
|
339
|
+
const filePath = statusMatch2[2]!.trim();
|
|
340
|
+
if (filePath && !seen.has(filePath) && (statusPrefix === "??" || statusPrefix === "A " || statusPrefix.startsWith("A"))) {
|
|
341
|
+
seen.add(filePath);
|
|
342
|
+
changes.push({ status: "A", path: filePath });
|
|
326
343
|
}
|
|
327
344
|
}
|
|
328
345
|
}
|
|
@@ -355,7 +372,7 @@ function toGitStatus(toolType: string): string {
|
|
|
355
372
|
*/
|
|
356
373
|
function hasContentChanged(cwd: string, path: string, baselineHash: string): boolean {
|
|
357
374
|
try {
|
|
358
|
-
const currentHash =
|
|
375
|
+
const currentHash = require('child_process').spawnSync('git', ['hash-object', path], { cwd, encoding: 'utf8', timeout: 3000 }).stdout?.trim() || "";
|
|
359
376
|
return currentHash !== baselineHash;
|
|
360
377
|
} catch {
|
|
361
378
|
// file deleted or inaccessible — consider changed
|
|
@@ -604,6 +621,7 @@ let _widgetStartTime = 0;
|
|
|
604
621
|
let _widgetExtraToolCount = 0;
|
|
605
622
|
let _widgetExtraTokenCount = 0;
|
|
606
623
|
let _workflowRunning = false;
|
|
624
|
+
let _cleanupTimer: ReturnType<typeof setTimeout> | null = null;
|
|
607
625
|
|
|
608
626
|
function refreshWidget(): void {
|
|
609
627
|
if (!_lastWorkflowCtx) return;
|
|
@@ -635,6 +653,10 @@ function initWidget(ctx: ExtensionCommandContext, mode: WorkflowMode, stepsCount
|
|
|
635
653
|
_widgetStartTime = Date.now();
|
|
636
654
|
_widgetExtraToolCount = 0;
|
|
637
655
|
_widgetExtraTokenCount = 0;
|
|
656
|
+
if (_cleanupTimer) {
|
|
657
|
+
clearTimeout(_cleanupTimer);
|
|
658
|
+
_cleanupTimer = null;
|
|
659
|
+
}
|
|
638
660
|
_lastWorkflowCtx = ctx;
|
|
639
661
|
_workflowRunning = true;
|
|
640
662
|
refreshWidget();
|
|
@@ -773,13 +795,13 @@ function addWidgetSubStepOutput(stepIndex: number, agentName: string, output: st
|
|
|
773
795
|
}
|
|
774
796
|
|
|
775
797
|
function setWidgetSubStepStatus(stepIndex: number, agentName: string, status: WorkflowSubStepWidgetState["status"]): void {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
798
|
+
const step = _widgetSteps[stepIndex];
|
|
799
|
+
if (!step) return;
|
|
800
|
+
const sub = step.subSteps?.find(s => s.agent === agentName);
|
|
801
|
+
if (sub) {
|
|
780
802
|
sub.status = status;
|
|
781
|
-
|
|
782
|
-
|
|
803
|
+
refreshWidget();
|
|
804
|
+
}
|
|
783
805
|
}
|
|
784
806
|
|
|
785
807
|
function setWidgetCurrentStep(index: number): void {
|
|
@@ -788,6 +810,10 @@ function setWidgetCurrentStep(index: number): void {
|
|
|
788
810
|
}
|
|
789
811
|
|
|
790
812
|
function cleanupWidget(): void {
|
|
813
|
+
if (_cleanupTimer) {
|
|
814
|
+
clearTimeout(_cleanupTimer);
|
|
815
|
+
_cleanupTimer = null;
|
|
816
|
+
}
|
|
791
817
|
_workflowRunning = false;
|
|
792
818
|
if (_lastWorkflowCtx) {
|
|
793
819
|
updateWorkflowWidget(_lastWorkflowCtx, null);
|
|
@@ -920,12 +946,14 @@ async function runAgentWithProgress(
|
|
|
920
946
|
tools: [],
|
|
921
947
|
outputs: [],
|
|
922
948
|
startedAt: agentStartTime,
|
|
949
|
+
detail: `超时时间${formatTimeout(timeoutMs)}`,
|
|
923
950
|
});
|
|
924
951
|
refreshWidget();
|
|
925
952
|
} else {
|
|
926
|
-
// Update existing sub-step status and
|
|
953
|
+
// Update existing sub-step status, startedAt, and detail
|
|
927
954
|
existing.status = "running";
|
|
928
955
|
existing.startedAt = agentStartTime;
|
|
956
|
+
existing.detail = `超时时间${formatTimeout(timeoutMs)}`;
|
|
929
957
|
refreshWidget();
|
|
930
958
|
}
|
|
931
959
|
}
|
|
@@ -1006,6 +1034,10 @@ async function runAgentWithProgress(
|
|
|
1006
1034
|
if (filePath.startsWith("http")) continue;
|
|
1007
1035
|
if (filePath.length < 6 && !filePath.includes("/")) continue;
|
|
1008
1036
|
|
|
1037
|
+
// 额外过滤器:排除明显不是文件路径的脏数据
|
|
1038
|
+
if (filePath.includes("${") || filePath.includes("\\n") || filePath.includes("\\t")) continue; // 排除模板字符串和转义字符
|
|
1039
|
+
if (filePath.includes("[]") || filePath.includes("{}")) continue; // 排除数组/对象字面量
|
|
1040
|
+
if (filePath.match(/^[\s,;)\]}]+$/)) continue; // 排除纯符号
|
|
1009
1041
|
seenTools.add(filePath);
|
|
1010
1042
|
const fullMatch = m[0]!.toLowerCase();
|
|
1011
1043
|
// Determine operation type and convert to git status
|
|
@@ -1169,8 +1201,21 @@ async function executeLoopGroup(
|
|
|
1169
1201
|
const maxLoops = step.maxLoops ?? 3;
|
|
1170
1202
|
let loopCount = loopCounts[step.id] ?? 0;
|
|
1171
1203
|
let contextPrompt = prompt;
|
|
1204
|
+
const reviewTimeoutMs = step.reviewTimeoutMs ?? step.timeoutMs;
|
|
1172
1205
|
|
|
1173
1206
|
while (loopCount < maxLoops) {
|
|
1207
|
+
loopCount++;
|
|
1208
|
+
// 立即更新 UI 显示当前循环次数
|
|
1209
|
+
state.loopCount = loopCount;
|
|
1210
|
+
updateWidgetStep(stepIndex, step.label, "running", {
|
|
1211
|
+
loopCount,
|
|
1212
|
+
maxLoops: step.maxLoops,
|
|
1213
|
+
startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// 每次循环开始时重置 sub-step 状态
|
|
1217
|
+
setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
|
|
1218
|
+
setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");
|
|
1174
1219
|
const loopStartTime = Date.now();
|
|
1175
1220
|
|
|
1176
1221
|
// Run loop agent
|
|
@@ -1178,6 +1223,21 @@ async function executeLoopGroup(
|
|
|
1178
1223
|
|
|
1179
1224
|
let agentResult = await runAgentWithProgress(loopAgent, loopTask, stepIndex, step.loopAgentName!, step.timeoutMs);
|
|
1180
1225
|
|
|
1226
|
+
// 检查 agent 是否异常退出(非超时非零退出码)
|
|
1227
|
+
while (agentResult.exitCode !== 0 && !isTimeoutResult(agentResult)) {
|
|
1228
|
+
if (mode === "full-auto") {
|
|
1229
|
+
throw new Error(`Agent ${step.loopAgentName} 异常退出 (exit ${agentResult.exitCode}): ${agentResult.stderr.slice(0, 200)}`);
|
|
1230
|
+
} else {
|
|
1231
|
+
const choice = await uiSelect(ctx, `❌ ${step.loopAgentName} 异常退出 (exit ${agentResult.exitCode})`, [
|
|
1232
|
+
"1. 重新执行", "2. 跳过此步骤", "3. 取消工作流",
|
|
1233
|
+
]);
|
|
1234
|
+
if (!choice || choice.startsWith("3")) { cancelWorkflow(); return; }
|
|
1235
|
+
if (choice.startsWith("2")) { state.status = "skipped"; return; }
|
|
1236
|
+
// 重新执行
|
|
1237
|
+
agentResult = await runAgentWithProgress(loopAgent, `[RETRY]\n\n${loopTask}`, stepIndex, step.loopAgentName!, step.timeoutMs);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1181
1241
|
if (isTimeoutResult(agentResult)) {
|
|
1182
1242
|
if (mode === "full-auto") {
|
|
1183
1243
|
contextPrompt = `[TIMEOUT_WARNING] 上一个 ${step.loopAgentName} 执行超时。\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd)}`;
|
|
@@ -1203,7 +1263,7 @@ async function executeLoopGroup(
|
|
|
1203
1263
|
? contextPrompt
|
|
1204
1264
|
: buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd);
|
|
1205
1265
|
|
|
1206
|
-
const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!,
|
|
1266
|
+
const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, reviewTimeoutMs);
|
|
1207
1267
|
|
|
1208
1268
|
const extractedOutput = extractFinalOutput(reviewResult.output) || reviewResult.output;
|
|
1209
1269
|
const combinedOutput = extractedOutput + "\n" + reviewResult.stderr;
|
|
@@ -1216,8 +1276,6 @@ async function executeLoopGroup(
|
|
|
1216
1276
|
}
|
|
1217
1277
|
}
|
|
1218
1278
|
|
|
1219
|
-
loopCount++;
|
|
1220
|
-
|
|
1221
1279
|
if (reviewSummary?.maxSeverity === "critical" && loopCount < maxLoops) {
|
|
1222
1280
|
if (mode === "full-auto") {
|
|
1223
1281
|
contextPrompt = [prompt, "", "## 上次审查发现的问题",
|
|
@@ -1378,7 +1436,11 @@ async function executeWorkflowBackground(
|
|
|
1378
1436
|
// ── Execute (timer starts NOW, after all user confirmations) ──
|
|
1379
1437
|
state.status = "running";
|
|
1380
1438
|
const stepStartTime = Date.now();
|
|
1381
|
-
updateWidgetStep(currentStepIndex, step.label, "running", {
|
|
1439
|
+
updateWidgetStep(currentStepIndex, step.label, "running", {
|
|
1440
|
+
timeoutMs: step.type === "loop-group" ? undefined : step.timeoutMs,
|
|
1441
|
+
maxLoops: step.maxLoops,
|
|
1442
|
+
startedAt: stepStartTime,
|
|
1443
|
+
});
|
|
1382
1444
|
|
|
1383
1445
|
try {
|
|
1384
1446
|
if (step.type === "loop-group") {
|
|
@@ -1395,7 +1457,7 @@ async function executeWorkflowBackground(
|
|
|
1395
1457
|
durationMs: state.durationMs,
|
|
1396
1458
|
loopCount: state.loopCount,
|
|
1397
1459
|
maxLoops: step.maxLoops,
|
|
1398
|
-
timeoutMs: step.timeoutMs,
|
|
1460
|
+
timeoutMs: step.type === "loop-group" ? undefined : step.timeoutMs,
|
|
1399
1461
|
});
|
|
1400
1462
|
} catch (err) {
|
|
1401
1463
|
state.status = "failed";
|
|
@@ -1406,6 +1468,7 @@ async function executeWorkflowBackground(
|
|
|
1406
1468
|
error: state.error,
|
|
1407
1469
|
loopCount: state.loopCount,
|
|
1408
1470
|
});
|
|
1471
|
+
break;
|
|
1409
1472
|
}
|
|
1410
1473
|
|
|
1411
1474
|
setWidgetCurrentStep(currentStepIndex + 1);
|
|
@@ -1432,7 +1495,11 @@ async function executeWorkflowBackground(
|
|
|
1432
1495
|
sendWorkflowResult(pi, finalState, prompt, _workflowType);
|
|
1433
1496
|
|
|
1434
1497
|
// Cleanup widget after delay
|
|
1435
|
-
|
|
1498
|
+
if (_cleanupTimer) clearTimeout(_cleanupTimer);
|
|
1499
|
+
_cleanupTimer = setTimeout(() => {
|
|
1500
|
+
_cleanupTimer = null;
|
|
1501
|
+
cleanupWidget();
|
|
1502
|
+
}, 5000);
|
|
1436
1503
|
|
|
1437
1504
|
function buildCp(): CheckpointData {
|
|
1438
1505
|
return {
|
|
@@ -1588,7 +1655,7 @@ export async function runWorkflow(
|
|
|
1588
1655
|
const isDoneState = stepStates[i]?.status === "done";
|
|
1589
1656
|
updateWidgetStep(i, steps[i]!.label, isDoneState ? "done" : "pending", {
|
|
1590
1657
|
maxLoops: steps[i]!.maxLoops,
|
|
1591
|
-
timeoutMs: steps[i]!.timeoutMs,
|
|
1658
|
+
timeoutMs: steps[i]!.type === "loop-group" ? undefined : steps[i]!.timeoutMs,
|
|
1592
1659
|
});
|
|
1593
1660
|
// Pre-populate sub-steps for all steps (shows queued agents)
|
|
1594
1661
|
populatePredefinedSubSteps(i);
|
|
@@ -1644,20 +1711,34 @@ export async function runWorkflow(
|
|
|
1644
1711
|
|
|
1645
1712
|
// ── Archive checkpoint on cancel too ──
|
|
1646
1713
|
archiveCheckpointFile(_workflowCwd, _workflowPlanFileRelPath);
|
|
1647
|
-
|
|
1714
|
+
if (_cleanupTimer) clearTimeout(_cleanupTimer);
|
|
1715
|
+
_cleanupTimer = setTimeout(() => {
|
|
1716
|
+
_cleanupTimer = null;
|
|
1717
|
+
cleanupWidget();
|
|
1718
|
+
}, 5000);
|
|
1648
1719
|
}
|
|
1649
1720
|
});
|
|
1650
1721
|
|
|
1651
1722
|
// Collapse tools to show widget
|
|
1652
1723
|
ctx.ui.setToolsExpanded(false);
|
|
1653
1724
|
|
|
1654
|
-
// ── Register terminal input handler (Esc to cancel) ──
|
|
1725
|
+
// ── Register terminal input handler (Esc to cancel, with double-press confirmation) ──
|
|
1655
1726
|
if (ctx.hasUI) {
|
|
1727
|
+
let _lastEscPressTime = 0;
|
|
1656
1728
|
_terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
|
|
1657
1729
|
if (!matchesKey(data, Key.escape)) return undefined;
|
|
1658
1730
|
if (_workflowRunning && _workflowAbortController && !_workflowAbortController.signal.aborted) {
|
|
1659
|
-
|
|
1660
|
-
|
|
1731
|
+
const now = Date.now();
|
|
1732
|
+
if (_lastEscPressTime > 0 && now - _lastEscPressTime < 3000) {
|
|
1733
|
+
// Second Esc press within 5s → confirm cancel
|
|
1734
|
+
ctx.ui.notify("⏹️ 正在停止工作流...", "warning");
|
|
1735
|
+
cancelWorkflow();
|
|
1736
|
+
_lastEscPressTime = 0;
|
|
1737
|
+
return { consume: true };
|
|
1738
|
+
}
|
|
1739
|
+
// First Esc press (or expired) → show hint
|
|
1740
|
+
_lastEscPressTime = now;
|
|
1741
|
+
ctx.ui.notify("再次按下 Esc 键,停止 Workflow - 3秒内按下有效", "warning");
|
|
1661
1742
|
return { consume: true };
|
|
1662
1743
|
}
|
|
1663
1744
|
return undefined;
|