@ghyper9023/pi-dev-workflow 0.4.1 → 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.
Files changed (39) hide show
  1. package/.pi-dev-output/pi-grill/answers/answer-mpfe77f1-20260521-1913.md +58 -0
  2. package/.pi-dev-output/pi-grill/answers/answer-mpfh37wu-20260521-2034.md +13 -0
  3. package/.pi-dev-output/pi-grill/answers/answer-mpfi5q4c-20260521-2104.md +13 -0
  4. package/.pi-dev-output/pi-grill/answers/answer-mpfizccb-20260521-2127.md +13 -0
  5. package/.pi-dev-output/pi-grill/answers/answer-mpfjk78k-20260521-2143.md +13 -0
  6. package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +94 -0
  7. package/.pi-dev-output/pi-plans/20260521-113000-fix-loopcount-timeout.md +215 -0
  8. package/.pi-dev-output/pi-plans/20260521-1730-grill-input-wrap-back-fix.md +240 -0
  9. package/.pi-dev-output/pi-plans/20260521-230000-fix-timeout-display-loopcount-gitdiff.md +253 -0
  10. package/.pi-dev-output/pi-plans/20260521-230500-esc-double-press-confirm-workflow.md +137 -0
  11. package/.pi-dev-output/pi-plans/20260521-235000-fix-gitdiff-loopcount.md +258 -0
  12. package/.pi-dev-output/pi-review/html/20260521-2305-review-workflow-index.html +196 -0
  13. package/.pi-dev-output/pi-review/md/review-20260520-100000.md +91 -0
  14. package/.pi-dev-output/pi-review/md/review-20260521-140000.md +191 -0
  15. package/.pi-dev-output/pi-review/md/review-20260521-190000.md +189 -0
  16. package/.pi-dev-output/pi-review/md/review-20260521-204500.md +241 -0
  17. package/.pi-dev-output/pi-review/md/review-20260521-214500.md +270 -0
  18. package/.pi-dev-output/pi-review/md/review-20260521-215158.md +214 -0
  19. package/.pi-dev-output/pi-review/md/review-20260521-234500.md +201 -0
  20. package/.pi-dev-output/pi-review/md/review-20260521-235500.md +422 -0
  21. package/.pi-dev-output/pi-review/md/review-20260522-000000.md +212 -0
  22. package/.pi-dev-output/pi-review/md/review-20260522-003000.md +377 -0
  23. package/.pi-dev-output/pi-review/md/review-20260522-003500.md +296 -0
  24. package/.pi-dev-output/pi-workflow/checkpoint-20260521-113000-fix-loopcount-timeout.json +402 -0
  25. package/.pi-dev-output/pi-workflow/checkpoint-20260521-1730-grill-input-wrap-back-fix.json +447 -0
  26. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230000-fix-timeout-display-loopcount-gitdiff.json +708 -0
  27. package/.pi-dev-output/pi-workflow/checkpoint-20260521-230500-esc-double-press-confirm-workflow.json +365 -0
  28. package/.pi-dev-output/pi-workflow/checkpoint-20260521-235000-fix-gitdiff-loopcount.json +395 -0
  29. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfhyxc5.json +30 -0
  30. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi2unc.json +49 -0
  31. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi382e.json +59 -0
  32. package/.pi-dev-output/pi-workflow/checkpoint-archive-mpfi5r22.json +76 -0
  33. package/extensions/dev-prompts.ts +16 -8
  34. package/extensions/grill-me-agent.ts +23 -7
  35. package/extensions/ui-helpers.ts +59 -7
  36. package/extensions/workflow-engine.ts +80 -32
  37. package/package.json +1 -1
  38. package/tests/test-loopcount-timeout-fix.mjs +336 -0
  39. 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: 900_000,
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: 300_000,
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: 900_000,
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: 900_000,
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: 300_000,
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: 900_000,
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: 900_000,
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: 300_000,
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 selectItems: SelectItem[] = q.options.map((opt, i) => ({
603
- value: `opt-${i}`,
604
- label: opt === previousAnswer
605
- ? `(${String.fromCharCode(97 + i)}) ${opt} - 上次选择`
606
- : `(${String.fromCharCode(97 + i)}) ${opt}`,
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 选择 • 选择←返回上一题 • Esc 取消全部评审"
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
  },
@@ -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
- // Immediately show 1 次循环 when loop-group starts
466
- loopStr = dim(theme, ` · 第 1 次循环`);
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
  }
@@ -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
- // Format: "M\tpath/to/file" or "D\tpath/to/file"
300
- const match = trimmed.match(/^([MAD])\s+(.+)$/);
301
- if (match) {
302
- const status = match[1]! as "M" | "A" | "D";
303
- const path = match[2]!.trim();
304
- if (path && !seen.has(path)) {
305
- seen.add(path);
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
- // "?? path" means untracked/new
319
- // Also catch "A path" for staged new files
320
- const match = trimmed.match(/^(\?\?|A\s)\s+(.+)$/);
321
- if (match) {
322
- const path = match[2]!.trim();
323
- if (path && !seen.has(path)) {
324
- seen.add(path);
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
  }
@@ -778,13 +795,13 @@ function addWidgetSubStepOutput(stepIndex: number, agentName: string, output: st
778
795
  }
779
796
 
780
797
  function setWidgetSubStepStatus(stepIndex: number, agentName: string, status: WorkflowSubStepWidgetState["status"]): void {
781
- const step = _widgetSteps[stepIndex];
782
- if (!step) return;
783
- const sub = step.subSteps?.find(s => s.agent === agentName);
784
- if (sub) {
798
+ const step = _widgetSteps[stepIndex];
799
+ if (!step) return;
800
+ const sub = step.subSteps?.find(s => s.agent === agentName);
801
+ if (sub) {
785
802
  sub.status = status;
786
- refreshWidget();
787
- }
803
+ refreshWidget();
804
+ }
788
805
  }
789
806
 
790
807
  function setWidgetCurrentStep(index: number): void {
@@ -929,12 +946,14 @@ async function runAgentWithProgress(
929
946
  tools: [],
930
947
  outputs: [],
931
948
  startedAt: agentStartTime,
949
+ detail: `超时时间${formatTimeout(timeoutMs)}`,
932
950
  });
933
951
  refreshWidget();
934
952
  } else {
935
- // Update existing sub-step status and startedAt
953
+ // Update existing sub-step status, startedAt, and detail
936
954
  existing.status = "running";
937
955
  existing.startedAt = agentStartTime;
956
+ existing.detail = `超时时间${formatTimeout(timeoutMs)}`;
938
957
  refreshWidget();
939
958
  }
940
959
  }
@@ -1015,6 +1034,10 @@ async function runAgentWithProgress(
1015
1034
  if (filePath.startsWith("http")) continue;
1016
1035
  if (filePath.length < 6 && !filePath.includes("/")) continue;
1017
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; // 排除纯符号
1018
1041
  seenTools.add(filePath);
1019
1042
  const fullMatch = m[0]!.toLowerCase();
1020
1043
  // Determine operation type and convert to git status
@@ -1178,8 +1201,21 @@ async function executeLoopGroup(
1178
1201
  const maxLoops = step.maxLoops ?? 3;
1179
1202
  let loopCount = loopCounts[step.id] ?? 0;
1180
1203
  let contextPrompt = prompt;
1204
+ const reviewTimeoutMs = step.reviewTimeoutMs ?? step.timeoutMs;
1181
1205
 
1182
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");
1183
1219
  const loopStartTime = Date.now();
1184
1220
 
1185
1221
  // Run loop agent
@@ -1227,7 +1263,7 @@ async function executeLoopGroup(
1227
1263
  ? contextPrompt
1228
1264
  : buildReviewTask(contextPrompt, planFileRelPath, _workflowCwd);
1229
1265
 
1230
- const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, step.timeoutMs);
1266
+ const reviewResult = await runAgentWithProgress(reviewAgent, reviewTask, stepIndex, step.reviewAgentName!, reviewTimeoutMs);
1231
1267
 
1232
1268
  const extractedOutput = extractFinalOutput(reviewResult.output) || reviewResult.output;
1233
1269
  const combinedOutput = extractedOutput + "\n" + reviewResult.stderr;
@@ -1240,8 +1276,6 @@ async function executeLoopGroup(
1240
1276
  }
1241
1277
  }
1242
1278
 
1243
- loopCount++;
1244
-
1245
1279
  if (reviewSummary?.maxSeverity === "critical" && loopCount < maxLoops) {
1246
1280
  if (mode === "full-auto") {
1247
1281
  contextPrompt = [prompt, "", "## 上次审查发现的问题",
@@ -1402,7 +1436,11 @@ async function executeWorkflowBackground(
1402
1436
  // ── Execute (timer starts NOW, after all user confirmations) ──
1403
1437
  state.status = "running";
1404
1438
  const stepStartTime = Date.now();
1405
- updateWidgetStep(currentStepIndex, step.label, "running", { timeoutMs: step.timeoutMs, maxLoops: step.maxLoops, startedAt: stepStartTime });
1439
+ updateWidgetStep(currentStepIndex, step.label, "running", {
1440
+ timeoutMs: step.type === "loop-group" ? undefined : step.timeoutMs,
1441
+ maxLoops: step.maxLoops,
1442
+ startedAt: stepStartTime,
1443
+ });
1406
1444
 
1407
1445
  try {
1408
1446
  if (step.type === "loop-group") {
@@ -1419,7 +1457,7 @@ async function executeWorkflowBackground(
1419
1457
  durationMs: state.durationMs,
1420
1458
  loopCount: state.loopCount,
1421
1459
  maxLoops: step.maxLoops,
1422
- timeoutMs: step.timeoutMs,
1460
+ timeoutMs: step.type === "loop-group" ? undefined : step.timeoutMs,
1423
1461
  });
1424
1462
  } catch (err) {
1425
1463
  state.status = "failed";
@@ -1617,7 +1655,7 @@ export async function runWorkflow(
1617
1655
  const isDoneState = stepStates[i]?.status === "done";
1618
1656
  updateWidgetStep(i, steps[i]!.label, isDoneState ? "done" : "pending", {
1619
1657
  maxLoops: steps[i]!.maxLoops,
1620
- timeoutMs: steps[i]!.timeoutMs,
1658
+ timeoutMs: steps[i]!.type === "loop-group" ? undefined : steps[i]!.timeoutMs,
1621
1659
  });
1622
1660
  // Pre-populate sub-steps for all steps (shows queued agents)
1623
1661
  populatePredefinedSubSteps(i);
@@ -1684,13 +1722,23 @@ export async function runWorkflow(
1684
1722
  // Collapse tools to show widget
1685
1723
  ctx.ui.setToolsExpanded(false);
1686
1724
 
1687
- // ── Register terminal input handler (Esc to cancel) ──
1725
+ // ── Register terminal input handler (Esc to cancel, with double-press confirmation) ──
1688
1726
  if (ctx.hasUI) {
1727
+ let _lastEscPressTime = 0;
1689
1728
  _terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
1690
1729
  if (!matchesKey(data, Key.escape)) return undefined;
1691
1730
  if (_workflowRunning && _workflowAbortController && !_workflowAbortController.signal.aborted) {
1692
- ctx.ui.notify("⏹️ 用户取消工作流", "warning");
1693
- cancelWorkflow();
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");
1694
1742
  return { consume: true };
1695
1743
  }
1696
1744
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghyper9023/pi-dev-workflow",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "keywords": [
5
5
  "pi-package"
6
6
  ],