@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
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# 代码审查报告:超时时间显示位置、循环次数计数与 git diff 解析修复
|
|
2
|
+
|
|
3
|
+
**审查日期**: 2026-05-21 23:55
|
|
4
|
+
**审查范围**: `extensions/ui-helpers.ts`, `extensions/workflow-engine.ts`
|
|
5
|
+
**审查基线**: 当前工作区未提交变更(vs HEAD dc0d3fa)
|
|
6
|
+
**审查内容**: 三个功能修复的代码实现质量
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 总体评价
|
|
11
|
+
|
|
12
|
+
本次未提交变更加载了三个功能的修复代码,整体方向正确,核心逻辑已实现。但发现 **2 个严重问题**、**2 个中等问题** 和 **1 个低优先级问题**。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🔴 严重问题
|
|
17
|
+
|
|
18
|
+
### C1. `buildWidgetLines` 子代理行渲染:当 sub-step 处于 pending 状态时,计时/超时行渲染出错
|
|
19
|
+
|
|
20
|
+
**严重度**: critical
|
|
21
|
+
**文件**: `extensions/ui-helpers.ts`
|
|
22
|
+
**位置**: 第 544-574 行(新增的子代理计时/超时渲染块)
|
|
23
|
+
|
|
24
|
+
**问题描述**:
|
|
25
|
+
|
|
26
|
+
新增的渲染逻辑:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
if (isSubRunning || isSubDone) {
|
|
30
|
+
let elapsedMs: number | undefined;
|
|
31
|
+
if (isSubRunning && sub.startedAt) {
|
|
32
|
+
elapsedMs = Date.now() - sub.startedAt;
|
|
33
|
+
} else if (sub.durationMs != null) {
|
|
34
|
+
elapsedMs = sub.durationMs;
|
|
35
|
+
}
|
|
36
|
+
// ...
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
这个 `if` 条件是 `isSubRunning || isSubDone`,但子代理行("|__ worker ·")**每次循环开始时**会在 `executeLoopGroup` 中被重置为 `pending` 状态(第 1192-1193 行)。也就是说,在 pending 状态的短暂窗口期,`if (isSubRunning || isSubDone)` 为 false,因此 `subDurStr` 保持空字符串,计时/超时不在子代理行上显示。
|
|
41
|
+
|
|
42
|
+
**这不是 bug,但存在一个更严重的问题**:
|
|
43
|
+
|
|
44
|
+
看 `setWidgetSubStepStatus` 函数(第 781-788 行):
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
function setWidgetSubStepStatus(stepIndex: number, agentName: string, status: WorkflowSubStepWidgetState["status"]): void {
|
|
48
|
+
const step = _widgetSteps[stepIndex];
|
|
49
|
+
if (!step) return;
|
|
50
|
+
const sub = step.subSteps?.find(s => s.agent === agentName);
|
|
51
|
+
if (sub) {
|
|
52
|
+
sub.status = status;
|
|
53
|
+
refreshWidget();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
这个函数在 `executeLoopGroup` 中调用时,**只重置了 sub-step 的 status 为 "pending"**,但**不清除** `detail`、`startedAt`、`tools`、`outputs` 等字段。
|
|
59
|
+
|
|
60
|
+
更严重的是,`startedAt` 不被清除,而 `addWidgetSubStepTool` 和 `addWidgetSubStepOutput` 也**不清空数组**——新循环会直接 `push` 到旧数组上。
|
|
61
|
+
|
|
62
|
+
**影响**:
|
|
63
|
+
1. 当 sub-step 刚刚被重置为 pending,status = "pending" 时,`buildWidgetLines` 的子代理逻辑进入:
|
|
64
|
+
```
|
|
65
|
+
} else if (isSubPending) {
|
|
66
|
+
childItems.push(dim(theme, "正在排队"));
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
这会显示 "正在排队"——**表面上 UI 看起来没问题**(pending 只显示排队)。
|
|
70
|
+
|
|
71
|
+
2. 但当 sub-step 第二次进入 running 状态时,`runAgentWithProgress` 会复用旧 sub-step(`existing` 存在),只更新 `status="running"`、`startedAt`、`detail`,但 **tools 和 outputs 数组没有被清空**——导致上一轮循环的 tools/outputs 残留。
|
|
72
|
+
|
|
73
|
+
**代码证据**:`runAgentWithProgress` 第 933-939 行:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const existing = step.subSteps.find(s => s.agent === agentName);
|
|
77
|
+
if (!existing) {
|
|
78
|
+
step.subSteps.push({ ... tools: [], outputs: [], ... });
|
|
79
|
+
refreshWidget();
|
|
80
|
+
} else {
|
|
81
|
+
existing.status = "running";
|
|
82
|
+
existing.startedAt = agentStartTime;
|
|
83
|
+
existing.detail = `超时时间${formatTimeout(timeoutMs)}`;
|
|
84
|
+
// ⚠️ 注意:没有清空 existing.tools 和 existing.outputs!
|
|
85
|
+
refreshWidget();
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**修复建议**:
|
|
90
|
+
在 `setWidgetSubStepStatus` 重置为 pending 时,同时清除子步骤的历史数据。
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
function setWidgetSubStepStatus(stepIndex: number, agentName: string, status: WorkflowSubStepWidgetState["status"]): void {
|
|
94
|
+
const step = _widgetSteps[stepIndex];
|
|
95
|
+
if (!step) return;
|
|
96
|
+
const sub = step.subSteps?.find(s => s.agent === agentName);
|
|
97
|
+
if (sub) {
|
|
98
|
+
sub.status = status;
|
|
99
|
+
// 当重置为 pending 时,清除上一轮循环的历史数据
|
|
100
|
+
if (status === "pending") {
|
|
101
|
+
sub.tools = [];
|
|
102
|
+
sub.outputs = [];
|
|
103
|
+
sub.detail = undefined;
|
|
104
|
+
sub.startedAt = undefined;
|
|
105
|
+
sub.durationMs = undefined;
|
|
106
|
+
}
|
|
107
|
+
refreshWidget();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
或者,更安全的做法是在 `executeLoopGroup` 重置 sub-step 时显式清除:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// 每次循环开始时重置 sub-step 状态
|
|
116
|
+
const resetSubStep = (name: string) => {
|
|
117
|
+
const sub = _widgetSteps[stepIndex]?.subSteps?.find(s => s.agent === name);
|
|
118
|
+
if (sub) {
|
|
119
|
+
sub.status = "pending";
|
|
120
|
+
sub.tools = [];
|
|
121
|
+
sub.outputs = [];
|
|
122
|
+
sub.detail = undefined;
|
|
123
|
+
sub.startedAt = undefined;
|
|
124
|
+
sub.durationMs = undefined;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
resetSubStep(step.loopAgentName!);
|
|
128
|
+
resetSubStep(step.reviewAgentName!);
|
|
129
|
+
refreshWidget();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### C2. `git status --porcelain` 解析:`trimmed.slice(3)` 对其他格式(如 `"A "` 后含双空格或制表符的路径)处理不当
|
|
135
|
+
|
|
136
|
+
**严重度**: critical
|
|
137
|
+
**文件**: `extensions/workflow-engine.ts`
|
|
138
|
+
**位置**: 第 328 行
|
|
139
|
+
|
|
140
|
+
**问题描述**:
|
|
141
|
+
|
|
142
|
+
当前的 `git status --porcelain` 解析:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const statusPrefix = trimmed.slice(0, 2); // e.g., "??", " M", "A "
|
|
146
|
+
const filePath = trimmed.slice(3).trim(); // after "?? " or " M " etc.
|
|
147
|
+
if (filePath && !seen.has(filePath) && (statusPrefix === "??" || statusPrefix === "A " || statusPrefix.startsWith("A"))) {
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
这段代码假设状态前缀总是 2 个字符,后面固定 1 个空格。但 `git status --porcelain` 的格式是:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
XY filepath\n
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
其中 X 是 index 状态,Y 是 work tree 状态。分隔符是**单个空格**。
|
|
157
|
+
|
|
158
|
+
**存在的问题**:
|
|
159
|
+
|
|
160
|
+
1. `"A "` 表示 index 中有 A(新增),work tree 未修改。`trimmed.slice(0, 2)` = `"A "`,然后 `trimmed.slice(3)` 跳过 `"A "` 的第三个字符(空格),结果正确。
|
|
161
|
+
|
|
162
|
+
2. 但 `"A "` 的 `trimmed.slice(0, 2)` 就是 `"A "`,而 `statusPrefix === "A "` 匹配。这个 case 看起来正确。
|
|
163
|
+
|
|
164
|
+
3. **真正的问题**:当 `trimmed.slice(3)` 遇到路径包含特殊字符(如空格)时,单个 slice 无法正确处理。但 git status --porcelain 对包含空格的路径会使用引号包裹或其他转义方式。
|
|
165
|
+
|
|
166
|
+
**更关键的问题**:`trimmed.slice(3)` 假设 `trimmed` 至少有 4 个字符。如果输入是 `"A "` 或 `"??"`(极短路径),slice(3) 返回空字符串,trim() 后变成空字符串,被 `if (filePath && ...)` 过滤掉——所以不会 crash,但漏掉了路径。
|
|
167
|
+
|
|
168
|
+
**实际测试**:之前测试脚本显示 `" M modified.ts"` 的 `statusPrefix = " M"`,`filePath = "modified.ts"` —— 但 `" M"` 不匹配 `"A "` 或 `"??"` 或 `startsWith("A")`,所以被正确过滤。
|
|
169
|
+
|
|
170
|
+
但问题是**边界条件**:`trimmed.slice(3)` 假设分隔符总是恰好 1 个空格。git status --porcelain 的规范格式是 `XY path`(一个空格)。但在实际输出中,中文路径或 Unicode 路径可能导致宽度计算偏差吗?不会,因为 slice 是按字节/字符索引,不是显示宽度。
|
|
171
|
+
|
|
172
|
+
**根因分析**:当前实现**实际上对标准格式是正确**的,但代码不够健壮——如果 git 输出了 `^I`(制表符)而非空格,slice(3) 会得到错误结果。但 git 规范保证是空格,所以这不构成实际 bug。
|
|
173
|
+
|
|
174
|
+
不过考虑到需求要求"用简单的 string 处理代替复杂正则",当前的实现**过于简单**,未处理边界情况。需求明确要求:
|
|
175
|
+
|
|
176
|
+
> `git diff --name-status` 直接拿到的信息 `X 空格 filepath 换行` 只需要简单的 string 处理即可
|
|
177
|
+
|
|
178
|
+
用户的意图是:`git diff --name-status` 的格式非常规整(`X\tfilepath\n`),不需要正则。当前实现用了 `split("\t")` 正确实现了这一点。
|
|
179
|
+
|
|
180
|
+
但对于 `git status --porcelain`,原需求**没有提到**这个命令。按"最小改动"原则,这个改动的风险超过了收益。
|
|
181
|
+
|
|
182
|
+
**建议**:`git status --porcelain` 部分恢复到原来的正则实现,或至少添加更健壮的路径提取:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// 更安全的解析:找到第一个空格后的内容
|
|
186
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
187
|
+
const afterStatus = firstSpace >= 0 ? trimmed.slice(firstSpace).trim() : "";
|
|
188
|
+
// 或用正则仅匹配 ?? 和 A (更精确)
|
|
189
|
+
const statusMatch = trimmed.match(/^(\?\?|A .*)\s+(.+)$/);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 🟡 中等问题
|
|
195
|
+
|
|
196
|
+
### M1. 超时时间子代理行渲染:当 sub-step 处于 pending 状态时不显示任何信息
|
|
197
|
+
|
|
198
|
+
**严重度**: medium
|
|
199
|
+
**文件**: `extensions/ui-helpers.ts`
|
|
200
|
+
**位置**: 第 544 行
|
|
201
|
+
|
|
202
|
+
**问题描述**:
|
|
203
|
+
|
|
204
|
+
新加的子代理行渲染逻辑:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
if (isSubRunning || isSubDone) {
|
|
208
|
+
// ... 计算 elapsedMs ...
|
|
209
|
+
if (sub.detail && sub.detail.includes("超时时间")) {
|
|
210
|
+
subTimeoutStr = dim(theme, `/${sub.detail}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
这意味着 sub-step 处于 **pending** 状态时(比如第 2 次循环开始时,worker 正在运行但 reviewer 处于 pending),reviewer 的子代理行显示 `|__ ◦ reviewer ·` 后面没有任何括号信息。
|
|
216
|
+
|
|
217
|
+
**预期行为**:根据实施计划:
|
|
218
|
+
|
|
219
|
+
> ```
|
|
220
|
+
> ▶ ⠏ 🔧 修复代码 → 审查
|
|
221
|
+
> |__ ⠏ worker · (52.6s/超时时间60m )
|
|
222
|
+
> |__ reviewer · (0s/超时时间60m)
|
|
223
|
+
> ```
|
|
224
|
+
|
|
225
|
+
即使 reviewer 是 pending 状态,也应该显示 `(0s/超时时间60m)`,因为超时时间是预设的。
|
|
226
|
+
|
|
227
|
+
**根因分析**:pending 状态的 sub-step 实际上已经预先设置了 `detail` 字段(在 `populatePredefinedSubSteps` 中,sub-step 的 detail 是未设置的)。但 `runAgentWithProgress` 只在 sub-step 进入 running 状态时设置 `detail`。
|
|
228
|
+
|
|
229
|
+
**修复建议**:
|
|
230
|
+
在 `populatePredefinedSubSteps` 中预先设置超时时间到 sub-step 的 detail:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
if (def.type === "loop-group") {
|
|
234
|
+
if (def.loopAgentName) {
|
|
235
|
+
newSubSteps.push({
|
|
236
|
+
agent: def.loopAgentName,
|
|
237
|
+
status: "pending",
|
|
238
|
+
tools: [],
|
|
239
|
+
outputs: [],
|
|
240
|
+
detail: `超时时间${formatTimeout(def.timeoutMs)}`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (def.reviewAgentName) {
|
|
244
|
+
newSubSteps.push({
|
|
245
|
+
agent: def.reviewAgentName,
|
|
246
|
+
status: "pending",
|
|
247
|
+
tools: [],
|
|
248
|
+
outputs: [],
|
|
249
|
+
detail: `超时时间${formatTimeout(def.reviewTimeoutMs ?? def.timeoutMs)}`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
同时,子代理行渲染应**不限制为 isSubRunning || isSubDone**,而是只要有超时时间就显示:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
if (sub.detail && sub.detail.includes("超时时间")) {
|
|
259
|
+
// pending 状态也显示超时时间(0s/超时时间60m)
|
|
260
|
+
if (isSubPending) {
|
|
261
|
+
subDurStr = dim(theme, ` (0s`);
|
|
262
|
+
}
|
|
263
|
+
// ...
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### M2. `executeLoopGroup` 重置 sub-step 后,`updateWidgetStep` 在循环开始前未同步 loopCount
|
|
270
|
+
|
|
271
|
+
**严重度**: medium
|
|
272
|
+
**文件**: `extensions/workflow-engine.ts`
|
|
273
|
+
**位置**: 第 1188-1194 行
|
|
274
|
+
|
|
275
|
+
**问题描述**:
|
|
276
|
+
|
|
277
|
+
当前 `executeLoopGroup` 中重置 sub-step 的代码:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
while (loopCount < maxLoops) {
|
|
281
|
+
// 每次循环开始时重置 sub-step 状态
|
|
282
|
+
setWidgetSubStepStatus(stepIndex, step.loopAgentName!,"pending");
|
|
283
|
+
setWidgetSubStepStatus(stepIndex, step.reviewAgentName!,"pending");
|
|
284
|
+
const loopStartTime = Date.now();
|
|
285
|
+
// Run loop agent...
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
但在此之前的 `updateWidgetStep`(第 1255-1259 行,在 loopCount++ 后)设置了 widget step 的 `loopCount` 属性。
|
|
289
|
+
|
|
290
|
+
在进入新循环时,widget step 的 `loopCount` 虽然已设置为上一次循环的 `loopCount` 值(例如 1),但 `buildWidgetLines` 中的渲染逻辑是:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
if (s.loopCount != null && s.loopCount > 0) {
|
|
294
|
+
loopStr = dim(theme, ` · 第 ${s.loopCount} 次循环`);
|
|
295
|
+
} else if (s.maxLoops != null) {
|
|
296
|
+
if (isRunning) {
|
|
297
|
+
loopStr = dim(theme, ` · 第 1 次循环`);
|
|
298
|
+
} else if (isPending) {
|
|
299
|
+
loopStr = dim(theme, ` · 第 0 次循环`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
对于第 2 次循环:
|
|
305
|
+
- 进入循环前,`s.loopCount = 1`(上一轮循环结束时设置)
|
|
306
|
+
- 第 2 次循环执行 worker(running 状态),显示 "第 1 次循环"——**这是对的**
|
|
307
|
+
- worker 完成后,`loopCount++`(变成 2),`updateWidgetStep` 设置 `loopCount=2`
|
|
308
|
+
- 然后 `reviewSummary?.maxSeverity === "critical"` 条件决定是否继续
|
|
309
|
+
|
|
310
|
+
**问题**:如果 reviewSummary 没有 critical 问题,`break` 跳过后续循环,最终 `loopCount=2` 被正确保存到 widget step 的 `loopCount`。所以最终状态是正确的。
|
|
311
|
+
|
|
312
|
+
但**在循环过程中**:
|
|
313
|
+
- 第 1 次循环 worker 运行时:`s.loopCount` 为 null(初始未设置),走 `else if (isRunning)` 分支显示 "第 1 次循环"——**正确**
|
|
314
|
+
- 第 2 次循环 worker 运行时:`s.loopCount = 1`(上一轮循环结束时设置),走 `if (s.loopCount > 0)` 分支显示 "第 1 次循环"——**这里是不对的,应该显示第 2 次循环**
|
|
315
|
+
|
|
316
|
+
**根因分析**:`loopCount++` 在第 1214 行(loop 末尾),而 `updateWidgetStep` 在第 1216 行。这发生在 worker 的 `runAgentWithProgress` 完成并设置了 sub-step tool/output 之后,且 `refreshWidget` 被多次调用。但 `updateWidgetStep` 只在 worker+reviewer 完成后才调用一次,所以**在 worker 运行期间**,`s.loopCount` 还是旧值。
|
|
317
|
+
|
|
318
|
+
**影响**:第 2 次循环的 worker 运行时,UI 还是显示 "第 1 次循环"。第 3 次循环时显示 "第 2 次循环",依此类推——**始终落后一次循环**。
|
|
319
|
+
|
|
320
|
+
这与实施计划的需求(`第 1 次循环`、`第 2 次循环` 应实时更新)**不完全一致**。
|
|
321
|
+
|
|
322
|
+
**修复建议**:
|
|
323
|
+
在进入新循环时,在 worker 启动前预先传入正确的 loopCount 值:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
while (loopCount < maxLoops) {
|
|
327
|
+
// 每次循环开始时重置 sub-step 状态
|
|
328
|
+
setWidgetSubStepStatus(stepIndex, step.loopAgentName!,"pending");
|
|
329
|
+
setWidgetSubStepStatus(stepIndex, step.reviewAgentName!,"pending");
|
|
330
|
+
|
|
331
|
+
// 在 worker 启动前更新循环次数 UI
|
|
332
|
+
// 新循环的 loopCount = 当前 loopCount + 1(即将开始的是第 N+1 次循环)
|
|
333
|
+
const nextLoopNum = loopCount + 1;
|
|
334
|
+
updateWidgetStep(stepIndex, step.label, "running", {
|
|
335
|
+
maxLoops: step.maxLoops,
|
|
336
|
+
startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const loopStartTime = Date.now();
|
|
340
|
+
// Run loop agent...
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 🟢 低优先级问题
|
|
346
|
+
|
|
347
|
+
### L1. `setWidgetSubStepStatus` 中 `"pending"` 参数前缺少空格,与测试断言不匹配
|
|
348
|
+
|
|
349
|
+
**严重度**: low
|
|
350
|
+
**文件**: `extensions/workflow-engine.ts`
|
|
351
|
+
**位置**: 第 1192-1193 行
|
|
352
|
+
|
|
353
|
+
**问题描述**:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
setWidgetSubStepStatus(stepIndex, step.loopAgentName!,"pending");
|
|
357
|
+
setWidgetSubStepStatus(stepIndex, step.reviewAgentName!,"pending");
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
逗号后没有空格。测试 `test-loopcount-timeout-fix.mjs` 第 310 行断言:
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
weContent.includes('setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending")')
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
注意测试字符串中是 `, "pending"`(逗号+空格+引号),但代码中是 `!,"pending"`(叹号+逗号+无空格+引号)。这导致测试 9.2 和 9.3 失败。
|
|
367
|
+
|
|
368
|
+
**影响**:测试失败会干扰 CI,但功能正确。
|
|
369
|
+
|
|
370
|
+
**修复建议**:统一加空格(`!, "pending"`)保持代码风格一致,同时修复测试。
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
### L2. `buildWidgetLines` 中 `childItems` 为空的 sub-step pending 状态不显示超时时间
|
|
375
|
+
|
|
376
|
+
**严重度**: low
|
|
377
|
+
**文件**: `extensions/ui-helpers.ts`
|
|
378
|
+
**位置**: 第 590-593 行
|
|
379
|
+
|
|
380
|
+
**问题描述**:
|
|
381
|
+
|
|
382
|
+
当前对于 pending 状态的 sub-step:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
if (isSubPending) {
|
|
386
|
+
childItems.push(dim(theme, "正在排队"));
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
如果子代理处于 pending 状态,只显示 "正在排队"。但超时信息(如 "超时时间60m")已经设置到 `sub.detail`(通过 `populatePredefinedSubSteps` 的预填充——但当前尚未实现预填充超时),应该在 pending 状态时也显示。
|
|
391
|
+
|
|
392
|
+
这在 M1 中已详细分析。
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 测试结果
|
|
397
|
+
|
|
398
|
+
运行 `node tests/test-loopcount-timeout-fix.mjs`:
|
|
399
|
+
- 43 通过,**2 失败**(均为测试 9,因代码空格风格不匹配)
|
|
400
|
+
|
|
401
|
+
## 建议汇总
|
|
402
|
+
|
|
403
|
+
### 必须修复(严重)
|
|
404
|
+
|
|
405
|
+
1. **C1**:`setWidgetSubStepStatus` 重置为 pending 时应清除 tools/outputs/startedAt/detail,避免跨循环数据残留
|
|
406
|
+
2. **C2**:`git status --porcelain` 的解析改用更健壮的 string 处理方式,遗留正则或改进 slice
|
|
407
|
+
|
|
408
|
+
### 建议修复(中等)
|
|
409
|
+
|
|
410
|
+
3. **M1**:在 `populatePredefinedSubSteps` 中预先设置 sub-step 的超时时间 detail,并在 pending 状态下也显示
|
|
411
|
+
4. **M2**:在 `executeLoopGroup` 进入新循环时提前更新 loopCount UI,避免循环计数落后一次
|
|
412
|
+
|
|
413
|
+
### 可选修复(低)
|
|
414
|
+
|
|
415
|
+
5. **L1**:统一代码空格风格,使测试通过
|
|
416
|
+
6. **L2**:pending 状态的 sub-step 显示超时时间(与 M1 关联)
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 审查结论
|
|
421
|
+
|
|
422
|
+
代码改动方向正确,三个需求的核心功能(超时显示位置、循环计数、git diff string 解析)均已实现基础逻辑。但存在 **跨循环数据残留(C1)** 和 **git status 解析健壮性不足(C2)** 两个严重问题,必须修复后才能工作正常。
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# 代码审查报告:Esc 双击确认停止工作流
|
|
2
|
+
|
|
3
|
+
**审查日期**: 2026-05-22 00:00
|
|
4
|
+
**审查范围**: `extensions/workflow-engine.ts`
|
|
5
|
+
**审查基线**: HEAD (commit f98799d1) + 当前未提交变更
|
|
6
|
+
**审查内容**: Esc 双击确认停止工作流功能实现质量
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 总体评价
|
|
11
|
+
|
|
12
|
+
本次变更实现了 Esc 双击确认停止工作流的核心逻辑,整体方向正确,代码改动量小(约 15 行),符合"最小改动"原则。核心逻辑(第一次 Esc 提示 → 5s 内第二次确认 → 取消 / 超时重置)已正确实现。
|
|
13
|
+
|
|
14
|
+
存在 **0 个严重问题**、**2 个中等问题** 和 **3 个低优先级问题**。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🟡 中等问题
|
|
19
|
+
|
|
20
|
+
### M1. 5 秒超时后提示信息未主动清除(需求合规性)
|
|
21
|
+
|
|
22
|
+
**严重度**: medium
|
|
23
|
+
**文件**: `extensions/workflow-engine.ts`
|
|
24
|
+
**位置**: 第 1715-1726 行
|
|
25
|
+
|
|
26
|
+
**问题描述**:
|
|
27
|
+
|
|
28
|
+
需求要求:
|
|
29
|
+
> 5s 之后,提示内容"再次按下 ecs 键,停止 Workflow"去掉
|
|
30
|
+
|
|
31
|
+
当前实现使用 `ctx.ui.notify()` 显示提示信息。当用户第一次按 Esc 后显示提示,如果 5 秒内没有第二次按 Esc:
|
|
32
|
+
|
|
33
|
+
- `_lastEscPressTime` 被设为第一次按下时间 `now`
|
|
34
|
+
- 5 秒后没有主动清除提示的逻辑
|
|
35
|
+
- 通知栏可能仍然显示 "再次按下 Esc 键,停止 Workflow"
|
|
36
|
+
- 用户在 10 秒后按 Esc → `now - _lastEscPressTime >= 5000` → 走 else 分支重新显示提示
|
|
37
|
+
- **提示从未被主动移除**
|
|
38
|
+
|
|
39
|
+
如果 `ctx.ui.notify()` 内部有自动消失机制(如 3 秒后自动消失),则当前行为可能符合预期。但当 `notify` 是持久性通知时,就不符合需求要求。
|
|
40
|
+
|
|
41
|
+
**影响**:用户可能以为提示仍然有效(误以为 5 秒窗口还在继续),造成困惑。
|
|
42
|
+
|
|
43
|
+
**修复建议**:
|
|
44
|
+
在第一次 Esc 按下时设置一个 5 秒定时器来清除提示:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// First Esc press (or expired) → show hint
|
|
48
|
+
_lastEscPressTime = now;
|
|
49
|
+
ctx.ui.notify("再次按下 Esc 键,停止 Workflow", "warning");
|
|
50
|
+
// 5 秒后清除提示(如果用户未在窗口内确认)
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
if (_lastEscPressTime === now) {
|
|
53
|
+
ctx.ui.notify("", "info"); // 覆盖旧提示
|
|
54
|
+
}
|
|
55
|
+
}, 5000);
|
|
56
|
+
return { consume: true };
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
需要确认 `ctx.ui.notify("", "info")` 是否能清除旧通知。如果不行,可能需要:
|
|
60
|
+
1. 检查 `ctx.ui` 是否有 `clearNotify()` 或类似 API
|
|
61
|
+
2. 或使用 widget 文本显示提示而不是 notify
|
|
62
|
+
|
|
63
|
+
### M2. 模块级 `_terminalInputUnsubscribe` 变量在新旧工作流切换时有监听器引用丢失风险
|
|
64
|
+
|
|
65
|
+
**严重度**: medium
|
|
66
|
+
**文件**: `extensions/workflow-engine.ts`
|
|
67
|
+
**位置**: 第 822 行、第 1712 行
|
|
68
|
+
|
|
69
|
+
**问题描述**:
|
|
70
|
+
|
|
71
|
+
`_terminalInputUnsubscribe` 是模块级变量(第 822 行)。工作流正常完成后,`executeWorkflowBackground` 末尾设置 `_workflowRunning = false` 并启动 5 秒延迟的 `cleanupWidget()`。
|
|
72
|
+
|
|
73
|
+
如果用户在旧工作流完成后立即启动新工作流:
|
|
74
|
+
|
|
75
|
+
1. 旧工作流 A 完成 → `_workflowRunning = false` → `_cleanupTimer` 在 5 秒后调用 `cleanupWidget()`
|
|
76
|
+
2. 用户立即启动工作流 B → `runWorkflow` 重新赋值 `_terminalInputUnsubscribe = ctx.ui.onTerminalInput(...)` → **旧监听器的引用被覆盖且未被解除注册**
|
|
77
|
+
3. 5 秒后 `cleanupWidget()` 调用 `_terminalInputUnsubscribe()` → **解除的是工作流 B 的新监听器**
|
|
78
|
+
|
|
79
|
+
**后果**:
|
|
80
|
+
- 工作流 A 的旧监听器泄漏(永久驻留在 `ctx.ui` 上)
|
|
81
|
+
- 工作流 B 在运行 5 秒后失去 Esc 监听器(因为被 `cleanupWidget()` 错误地解除了)
|
|
82
|
+
- 工作流 A 的残留监听器仍接收 Esc 事件(但 `_workflowRunning` 为 false,不触发取消)
|
|
83
|
+
|
|
84
|
+
**影响面**:需要用户在工作流结束后 5 秒内手动启动新工作流才能触发。在正常使用中罕见,但一旦触发会导致工作流 B 后半段无法通过 Esc 取消。
|
|
85
|
+
|
|
86
|
+
**修复建议**:
|
|
87
|
+
在 `runWorkflow` 中赋值新监听器之前,先主动清理旧的:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
if (ctx.hasUI) {
|
|
91
|
+
// 清理旧监听器,避免与延迟 cleanupWidget() 冲突
|
|
92
|
+
if (_terminalInputUnsubscribe) {
|
|
93
|
+
_terminalInputUnsubscribe();
|
|
94
|
+
_terminalInputUnsubscribe = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let _lastEscPressTime = 0;
|
|
98
|
+
_terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
|
|
99
|
+
// ... 现有逻辑
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🟢 低优先级问题
|
|
107
|
+
|
|
108
|
+
### L1. `_lastEscPressTime = 0` 在 `cancelWorkflow()` 后为无效代码
|
|
109
|
+
|
|
110
|
+
**严重度**: low
|
|
111
|
+
**文件**: `extensions/workflow-engine.ts`
|
|
112
|
+
**位置**: 第 1720 行
|
|
113
|
+
|
|
114
|
+
**问题描述**:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
if (_lastEscPressTime > 0 && now - _lastEscPressTime < 5000) {
|
|
118
|
+
ctx.ui.notify("⏹️ 正在停止工作流...", "warning");
|
|
119
|
+
cancelWorkflow();
|
|
120
|
+
_lastEscPressTime = 0; // ← 这行在 cancelWorkflow() 后没有实际效果
|
|
121
|
+
return { consume: true };
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`cancelWorkflow()` 已设置 `_workflowRunning = false`,后续任何 Esc 事件都会被第 1714 行的 `if (_workflowRunning && ...)` 过滤。`_lastEscPressTime = 0` 不会产生可观测的行为变化。
|
|
126
|
+
|
|
127
|
+
**建议**:可以保留作为防御性编程(无害),也可移除。
|
|
128
|
+
|
|
129
|
+
### L2. 提示文案使用 "Esc" 而非需求中的 "ecs"
|
|
130
|
+
|
|
131
|
+
**严重度**: low
|
|
132
|
+
**文件**: `extensions/workflow-engine.ts`
|
|
133
|
+
**位置**: 第 1725 行
|
|
134
|
+
|
|
135
|
+
**问题描述**:
|
|
136
|
+
|
|
137
|
+
需求中使用 "ecs",代码中使用 "Esc"。`Esc` 是标准写法,`ecs` 是需求文档中的笔误。建议保留 `Esc`。
|
|
138
|
+
|
|
139
|
+
### L3. 中英文混排提示风格与其他通知不统一
|
|
140
|
+
|
|
141
|
+
**严重度**: low
|
|
142
|
+
**文件**: `extensions/workflow-engine.ts`
|
|
143
|
+
**位置**: 第 1725 行
|
|
144
|
+
|
|
145
|
+
**问题描述**:
|
|
146
|
+
|
|
147
|
+
提示 "再次按下 Esc 键,停止 Workflow" 中英文混排。其他 `ctx.ui.notify()` 调用使用全中文(如 "⏹️ 用户取消工作流")或全英文。
|
|
148
|
+
|
|
149
|
+
**建议**:改为全中文 "⏹️ 再次按下 Esc 键停止工作流"(更统一)。
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 其他观察
|
|
154
|
+
|
|
155
|
+
### 工作流取消后的 `notify` 文案
|
|
156
|
+
|
|
157
|
+
第二次确认时的通知文案从原来的 `"⏹️ 用户取消工作流"` 改为 `"⏹️ 正在停止工作流..."`。这是合理的改动 —— 前者只描述事实,后者通知用户正在执行操作(与 `cancelWorkflow()` 的实际行为一致)。
|
|
158
|
+
|
|
159
|
+
### SIGINT/SIGTERM 不受影响
|
|
160
|
+
|
|
161
|
+
`onSigint` 和 `onSigterm` 处理函数仍然直接调用 `cancelWorkflow()`,没有加入双击确认逻辑。这是正确的 —— 信号处理不应要求用户二次确认(SIGINT 通常用于强制终止)。
|
|
162
|
+
|
|
163
|
+
### 功能正确性验证
|
|
164
|
+
|
|
165
|
+
对所有 Esc 处理场景的验证结果:
|
|
166
|
+
|
|
167
|
+
| 场景 | 预期行为 | 实际行为 | 正确 |
|
|
168
|
+
|------|---------|---------|:---:|
|
|
169
|
+
| 工作流运行中,第一次按 Esc | 显示提示 | 显示提示 | ✅ |
|
|
170
|
+
| 5s 内第二次按 Esc | 取消工作流 | 取消工作流 | ✅ |
|
|
171
|
+
| 超过 5s 后按 Esc | 重置为第一次状态 | 重置为第一次状态 | ✅ |
|
|
172
|
+
| 工作流未运行,按 Esc | 无行为 | `_workflowRunning` 为 false → 无行为 | ✅ |
|
|
173
|
+
| 工作流取消后,再按 Esc | 无行为 | `_workflowRunning` 为 false → 无行为 | ✅ |
|
|
174
|
+
| 快速连按三次 Esc | 第二次时取消,第三次时 | 第二次时 `_workflowRunning=false` → 第三次无行为 | ✅ |
|
|
175
|
+
| 双击确认后,工作流正常取消 | checkpoint 归档、widget 清理 | 通过 `cancelWorkflow()` 回调处理 | ✅ |
|
|
176
|
+
|
|
177
|
+
### 与上次审查的关系
|
|
178
|
+
|
|
179
|
+
上次审查(`review-20260521-235500.md`)发现的问题包括:
|
|
180
|
+
- **C1**: 跨循环 sub-step 数据残留(`setWidgetSubStepStatus` 未清除 tools/outputs)→ **与本次 Esc 功能无关,仍未修复**
|
|
181
|
+
- **C2**: `git status --porcelain` 解析健壮性 → **与本次 Esc 功能无关,仍未修复**
|
|
182
|
+
|
|
183
|
+
本次 Esc 双击确认功能未引入新的严重问题,但上次审查的 C1 和 C2 作为独立的问题仍存在。
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 测试建议
|
|
188
|
+
|
|
189
|
+
### 人工测试用例
|
|
190
|
+
1. **正常双击取消**:启动工作流 → 按 Esc → 显示提示 → 5s 内再按 Esc → 工作流取消,widget 清理
|
|
191
|
+
2. **超时重置**:启动工作流 → 按 Esc → 显示提示 → 等待 6s → 按 Esc → 显示提示(未取消,已重置)
|
|
192
|
+
3. **超时后不取消**:启动工作流 → 按 Esc → 等待 6s → 不操作 → 工作流正常运行
|
|
193
|
+
4. **快速连按**:快速按 3 次 Esc → 第二次时取消,第三次无效果
|
|
194
|
+
5. **工作流结束后按 Esc**:工作流自然完成 → 按 Esc → 无反应
|
|
195
|
+
6. **信号叠加**:按 Esc(提示)→ Ctrl+C → SIGINT 直接取消工作流(不等待第二次确认)
|
|
196
|
+
|
|
197
|
+
### 单元测试建议
|
|
198
|
+
- 隔离测试 `onTerminalInput` 回调的时间窗口逻辑
|
|
199
|
+
- Mock `matchesKey`、`_workflowRunning`、`_workflowAbortController` 等模块变量
|
|
200
|
+
- 验证:`_lastEscPressTime = 0`(初始)、`_lastEscPressTime > 0`且<5s(确认)、`_lastEscPressTime > 0`且>=5s(重置)
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 审查结论
|
|
205
|
+
|
|
206
|
+
| 等级 | 数量 | 核心问题 |
|
|
207
|
+
|:---:|:---:|---------|
|
|
208
|
+
| 🔴 critical | 0 | 无严重问题 |
|
|
209
|
+
| 🟡 medium | 2 | M1: 超时后提示未清除;M2: 监听器引用丢失风险 |
|
|
210
|
+
| 🟢 low | 3 | L1-L3: 代码风格和文案细节 |
|
|
211
|
+
|
|
212
|
+
Esc 双击确认功能的核心逻辑实现正确,改动量小。未发现会导致功能异常或数据丢失的严重 bug。建议按顺序修复 M1(通知清除)和 M2(监听器保护)。
|