@ghyper9023/pi-dev-workflow 0.4.1 → 0.4.3
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-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/answers/answer-mpfttme1-20260522-0230.md +13 -0
- package/.pi-dev-output/pi-grill/questions/questions-mpfdz1tz-20260521-1907.json +94 -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-plans/20260522-113000-grill-left-arrow-fix.md +274 -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-review/md/review-20260522-105000.md +166 -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-20260522-113000-grill-left-arrow-fix.json +473 -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/.version/RELEASE-v0.4.2.md +31 -0
- package/.version/RELEASE-v0.4.3.md +42 -0
- package/README.md +21 -3
- package/extensions/dev-prompts.ts +16 -8
- package/extensions/grill-me-agent.ts +74 -8
- package/extensions/ui-helpers.ts +59 -7
- package/extensions/workflow-engine.ts +80 -32
- package/package.json +1 -1
- package/tests/test-loopcount-timeout-fix.mjs +336 -0
- package/themes/oh-my-pi-titanium.json +90 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# 代码审查报告
|
|
2
|
+
|
|
3
|
+
**审查时间**: 2026-05-21 20:45
|
|
4
|
+
**审查范围**: commit `01413c9` 对 `extensions/ui-helpers.ts` 和 `extensions/workflow-engine.ts` 的修改
|
|
5
|
+
**功能需求**: 修复超时时间显示位置、循环次数计数、git diff 解析
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Critical 问题
|
|
10
|
+
|
|
11
|
+
### C1. `executeLoopGroup` 中 `loopCount` 更新后调用 `updateWidgetStep` 但 UI 在 sub-step 运行期间仍不显示循环次数
|
|
12
|
+
|
|
13
|
+
**严重等级**: critical
|
|
14
|
+
**文件**: `extensions/workflow-engine.ts`(executeLoopGroup) & `extensions/ui-helpers.ts`(buildWidgetLines)
|
|
15
|
+
**根因分析**:
|
|
16
|
+
|
|
17
|
+
在 commit `01413c9` 中,`buildWidgetLines` 的 `isRunning` 分支(原本显示"第 1 次循环")被删除,并改为"由 `executeLoopGroup` 通过 `updateWidgetStep` 管理 loopCount"。
|
|
18
|
+
|
|
19
|
+
然而 `executeLoopGroup` 的流程是:
|
|
20
|
+
1. `while (loopCount < maxLoops)` — 进入循环,`loopCount=0`
|
|
21
|
+
2. 重置 sub-step 为 pending
|
|
22
|
+
3. 运行 worker agent(耗时较长)
|
|
23
|
+
4. 运行 reviewer agent(耗时较长)
|
|
24
|
+
5. `loopCount++`(变为 1)
|
|
25
|
+
6. `updateWidgetStep(...)` 设置 `loopCount=1`
|
|
26
|
+
|
|
27
|
+
**问题核心**:在第 3-4 步(worker/reviewer 运行期间),`runAgentWithProgress` 内部会多次调用 `refreshWidget()`(通过 `setWidgetSubStepStatus`、`addWidgetSubStepTool` 等)。但这些 `refreshWidget()` 调用时,widget step 的 `loopCount` 仍为 `undefined`,因为 `updateWidgetStep(loopCount=1)` 还没执行到。
|
|
28
|
+
|
|
29
|
+
因此在整个 sub-step 运行期间(这是 UI 可见时间最长的阶段),**用户完全看不到任何循环次数提示**。只有等 sub-step 全部完成后才更新 loopCount,但此时 UI 已经准备进入下一轮了。
|
|
30
|
+
|
|
31
|
+
**影响**: 用户在整个 worker/reviewer 执行过程中完全看不到"第 N 次循环"提示,这是严重的用户体验 bug。
|
|
32
|
+
|
|
33
|
+
**修复方案**: 在 `executeLoopGroup` 中,**在运行 worker agent 之前**(而不是之后)就更新 loopCount 到 widget。
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// 在 while 循环开始处,运行 agent 之前
|
|
37
|
+
loopCount++;
|
|
38
|
+
state.loopCount = loopCount;
|
|
39
|
+
updateWidgetStep(stepIndex, step.label, "running", {
|
|
40
|
+
loopCount,
|
|
41
|
+
maxLoops: step.maxLoops,
|
|
42
|
+
startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 然后运行 worker agent
|
|
46
|
+
const loopTask = buildTaskForStep(step.loopAgentName!, contextPrompt, planFileRelPath, _workflowCwd);
|
|
47
|
+
let agentResult = await runAgentWithProgress(loopAgent, loopTask, stepIndex, step.loopAgentName!, step.timeoutMs);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
这样在 sub-step 开始运行前,widget 上已经显示正确的 `loopCount`,后续所有 `refreshWidget()` 都能读取到正确的值。
|
|
51
|
+
|
|
52
|
+
同时应保留 `buildWidgetLines` 中 `isRunning` 状态的 fallback 逻辑(`第 1 次循环`)作为兜底。
|
|
53
|
+
|
|
54
|
+
### C2. `buildWidgetLines` 中超时时间在步骤行和子代理行显示逻辑冲突
|
|
55
|
+
|
|
56
|
+
**严重等级**: critical
|
|
57
|
+
**文件**: `extensions/ui-helpers.ts`(buildWidgetLines)
|
|
58
|
+
**根因分析**:
|
|
59
|
+
|
|
60
|
+
当前代码中:
|
|
61
|
+
- **步骤行(step line)**: 对于 loop-group 步骤已移除了 `timeoutMs`(`step.type === "loop-group" ? undefined : step.timeoutMs`),所以步骤行不会显示超时。
|
|
62
|
+
- **子代理行(sub-step line)**: 通过 `sub.detail`(内容如 `超时时间60m`)在 agent 行末尾显示。
|
|
63
|
+
|
|
64
|
+
但当前子代理行的渲染代码(`buildWidgetLines` 中的 sub-step 渲染部分)**已经实现了** 功能需求要求的显示格式:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
|__ ✓ worker · (52.6s/超时时间60m)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
代码中已有的逻辑(查看 ~Line 539-545):
|
|
71
|
+
```typescript
|
|
72
|
+
if (sub.detail && sub.detail.includes("超时时间")) {
|
|
73
|
+
subTimeoutStr = dim(theme, `/${sub.detail}`);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**问题在于**:当前 `sub.detail` 已在 `runAgentWithProgress` 中设置为 `超时时间${formatTimeout(timeoutMs)}`,所以这部分功能本身是**已实现**的。功能需求中描述的超时显示位置问题可能存在于旧的代码版本,commit `01413c9` 已经修复了这部分。
|
|
78
|
+
|
|
79
|
+
但在审查过程中确认:**当前代码中 sub-step 的超时时间显示逻辑是正确的**,不应再改动。需要重点关注的是**确保 detail 中的"超时时间"文本不被作为独立的子项重复显示**。
|
|
80
|
+
|
|
81
|
+
当前代码(~Line 554-556):
|
|
82
|
+
```typescript
|
|
83
|
+
if (childItems.length === 0 && sub.detail && !sub.detail.includes("超时时间")) {
|
|
84
|
+
childItems.push(sub.detail);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
这个逻辑是正确的:`sub.detail` 包含"超时时间"时不会被作为子项重复显示。
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Medium 问题
|
|
93
|
+
|
|
94
|
+
### M1. `getGitDiffChanges` 中 `git status --porcelain` 解析存在潜在 bug
|
|
95
|
+
|
|
96
|
+
**严重等级**: medium
|
|
97
|
+
**文件**: `extensions/workflow-engine.ts`(getGitDiffChanges)
|
|
98
|
+
**根因分析**:
|
|
99
|
+
|
|
100
|
+
当前代码已按实施计划中的方案改用了字符串拆分而非正则:
|
|
101
|
+
```typescript
|
|
102
|
+
const parts = trimmed.split("\t");
|
|
103
|
+
if (parts.length === 2) {
|
|
104
|
+
const status = parts[0]!.trim();
|
|
105
|
+
const filePath = parts[1]!.trim();
|
|
106
|
+
...
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
但 `git status --porcelain` 的解析仍有问题:
|
|
111
|
+
```typescript
|
|
112
|
+
const statusPrefix = trimmed.slice(0, 2);
|
|
113
|
+
const filePath = trimmed.slice(3).trim();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`git status --porcelain` 的实际输出格式为:
|
|
117
|
+
- `?? newfile.ts` — untracked (2 字符状态 + 空格 + 路径)
|
|
118
|
+
- ` M modified.ts` — working tree 修改 (空格 + M + 空格 + 路径)
|
|
119
|
+
- `A staged.ts` — staged 添加 (A + 空格 + 空格 + 路径)
|
|
120
|
+
- `M staged-mod.ts` — staged 修改 (M + 空格 + 空格 + 路径)
|
|
121
|
+
|
|
122
|
+
前两个字符是 `XY`(index 状态 + working tree 状态),后面跟**一个空格**再跟路径。所以 `trimmed.slice(0, 2)` 取到的是完整的 2 字符状态码,`trimmed.slice(3)` 取到的是路径(跳过了第 3 个字符即空格)。
|
|
123
|
+
|
|
124
|
+
**但问题在于**:`trimmed` 是通过 `line.trim()` 得到的,**这已经去掉了首尾空格**。对于 ` M modified.ts`(首字符为空格),`trim()` 后变成了 `M modified.ts`,此时 `statusPrefix = "M "`("M" + 空格),**但原始的 git porcelain 输出中 "M" 前面有一个空格表示 working tree 状态**,trim 后丢失了这个信息。
|
|
125
|
+
|
|
126
|
+
修复方案:**不要对 line 做 `trim()`**,或者使用 `line` 而非 `trimmed` 来解析状态码。
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// 正确的解析方式
|
|
130
|
+
for (const line of statusOutput.split("\n")) {
|
|
131
|
+
if (!line.trim()) continue;
|
|
132
|
+
// 前两个字符是状态
|
|
133
|
+
const statusPrefix = line.slice(0, 2); // 直接使用原始 line,不 trim
|
|
134
|
+
const filePath = line.slice(3).trim();
|
|
135
|
+
// "??" 表示 untracked/new
|
|
136
|
+
// "A " 或 "AM" 或 "M " 表示 tracked 文件被修改
|
|
137
|
+
if (statusPrefix === "??") {
|
|
138
|
+
// untracked = new
|
|
139
|
+
...
|
|
140
|
+
} else if (statusPrefix === "A " || statusPrefix.startsWith("A")) {
|
|
141
|
+
// staged new
|
|
142
|
+
...
|
|
143
|
+
}
|
|
144
|
+
...
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### M2. `executeLoopGroup` 中异常退出重试逻辑可能导致无限循环
|
|
149
|
+
|
|
150
|
+
**严重等级**: medium
|
|
151
|
+
**文件**: `extensions/workflow-engine.ts`(executeLoopGroup)
|
|
152
|
+
**根因分析**:
|
|
153
|
+
|
|
154
|
+
在 `executeLoopGroup` 中,当 agent 异常退出(非零退出码且非超时)时:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
while (agentResult.exitCode !== 0 && !isTimeoutResult(agentResult)) {
|
|
158
|
+
if (mode === "full-auto") {
|
|
159
|
+
throw new Error(...);
|
|
160
|
+
} else {
|
|
161
|
+
const choice = await uiSelect(ctx, ..., [
|
|
162
|
+
"1. 重新执行", "2. 跳过此步骤", "3. 取消工作流",
|
|
163
|
+
]);
|
|
164
|
+
...
|
|
165
|
+
agentResult = await runAgentWithProgress(loopAgent, `[RETRY]\n\n${loopTask}`, ...);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
这里使用了 `while` 循环而非 `if`,但每次重试都会创建新的 `agentResult`。如果 agent **持续**异常退出,理论上用户每次都选"重新执行"就会无限循环。不过用户有选择跳过或取消的权力,所以实际风险较低。
|
|
171
|
+
|
|
172
|
+
建议改为 `do-while` 或加一个最大重试次数限制,但功能需求要求最小改动,可标记为观察项。
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Low 问题
|
|
177
|
+
|
|
178
|
+
### L1. `updateWidgetStep` 中 `startedAt` 回退逻辑可能导致时间错误
|
|
179
|
+
|
|
180
|
+
**严重等级**: low
|
|
181
|
+
**文件**: `extensions/workflow-engine.ts`(executeLoopGroup)
|
|
182
|
+
**根因分析**:
|
|
183
|
+
|
|
184
|
+
在 `executeLoopGroup` 中的 `updateWidgetStep` 调用:
|
|
185
|
+
```typescript
|
|
186
|
+
updateWidgetStep(stepIndex, step.label, "running", {
|
|
187
|
+
loopCount,
|
|
188
|
+
maxLoops: step.maxLoops,
|
|
189
|
+
startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
如果 `_widgetSteps[stepIndex]?.startedAt` 为 `0`(理论上不可能,但类型上 permit),`||` 会回退到 `Date.now()`,这没问题。但如果 `startedAt` 已经有一个较早的值,每次循环都复用同一个 `startedAt` 会导致累计时间异常——步骤行显示的时间是**工作流开始到当前的总时间**,而不是本轮循环的开始时间。
|
|
194
|
+
|
|
195
|
+
然而 `loop-group` 步骤的步骤行显示的是从 `stepStartTime` 开始到现在的总运行时长(因为 step 级别只有一个 `startedAt`),而 sub-step 有自己的 `startedAt`。所以这个行为是预期的,不构成 bug。标记为低优先级观察项。
|
|
196
|
+
|
|
197
|
+
### L2. `buildWidgetLines` 中 `isRunning` 状态检测逻辑冗余
|
|
198
|
+
|
|
199
|
+
**严重等级**: low
|
|
200
|
+
**文件**: `extensions/ui-helpers.ts`(buildWidgetLines)
|
|
201
|
+
**根因分析**:
|
|
202
|
+
|
|
203
|
+
当前代码:
|
|
204
|
+
```typescript
|
|
205
|
+
const isRunning = s.status === "running" || isCurrent;
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`isCurrent` 定义为 `i === state.currentStepIndex && state.status === "running"`。所以 `isRunning` 的检测等价于 `s.status === "running" || (i === currentStepIndex && state.status === "running")`。
|
|
209
|
+
|
|
210
|
+
实际上,如果 `i === currentStepIndex` 且 `state.status === "running"`,那么 `s.status` 应该已经是 `"running"`(由 `updateWidgetStep` 设置)。所以 `isCurrent` 部分通常是冗余的。
|
|
211
|
+
|
|
212
|
+
但在初始状态或过渡状态中可能存在短暂的不一致。保留作为容错是可以的,但可考虑简化。
|
|
213
|
+
|
|
214
|
+
### L3. 测试文件中 `extractLoopGroups` 函数解析逻辑脆弱
|
|
215
|
+
|
|
216
|
+
**严重等级**: low
|
|
217
|
+
**文件**: `tests/test-loopcount-timeout-fix.mjs`
|
|
218
|
+
**根因分析**:
|
|
219
|
+
|
|
220
|
+
`extractLoopGroups` 函数通过逐行扫描和花括号深度来解析 loop-group 配置块,这种方法很脆弱:
|
|
221
|
+
1. 它假定 `type: "loop-group"` 所在行是配置块的开始
|
|
222
|
+
2. 它通过 `}` 后跟 `,` 或 `;` 来检测结束
|
|
223
|
+
3. 如果配置格式稍有变化(如多行注释、属性换行等),解析就会失败
|
|
224
|
+
|
|
225
|
+
实际测试运行发现当前能正确解析所有 7 个 loop-group 配置(断言 `>=6`),但未来修改配置格式时需要同步更新测试解析器。
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 总结
|
|
230
|
+
|
|
231
|
+
| 等级 | 数量 | 说明 |
|
|
232
|
+
|------|------|------|
|
|
233
|
+
| Critical | 2 | loopCount 在 sub-step 运行期间不显示(时序问题);超时显示逻辑需验证 |
|
|
234
|
+
| Medium | 2 | `git status --porcelain` 解析 bug;重试可能无限循环 |
|
|
235
|
+
| Low | 3 | startedAt 回退、运行状态检测冗余、测试解析脆弱 |
|
|
236
|
+
|
|
237
|
+
## 推荐修复优先级
|
|
238
|
+
|
|
239
|
+
1. **修复 C1**: 在 `executeLoopGroup` 中,将 `loopCount++` 和 `updateWidgetStep` 移到运行 worker agent 之前
|
|
240
|
+
2. **修复 M1**: 修正 `git status --porcelain` 解析中 `line.trim()` 导致的状态码丢失问题
|
|
241
|
+
3. 其他问题可在后续迭代中优化
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# 代码审查报告:Grill/Input 文本换行与左方向键返回修复
|
|
2
|
+
|
|
3
|
+
**审查日期**: 2026-05-21 21:45
|
|
4
|
+
**审查范围**: `extensions/grill-me-agent.ts` + `extensions/ui-helpers.ts` 的未提交改动
|
|
5
|
+
**审查目标**: 验证三个需求的实现质量:输入换行 / 选项换行 / ← 左方向键返回
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 文件差异总览
|
|
10
|
+
|
|
11
|
+
| 文件 | 改动行数 | 风险等级 |
|
|
12
|
+
|------|---------|---------|
|
|
13
|
+
| `extensions/grill-me-agent.ts` | +30 / -8 | medium |
|
|
14
|
+
| `extensions/ui-helpers.ts` | +26 / -1 | medium |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. `extensions/grill-me-agent.ts` 审查
|
|
19
|
+
|
|
20
|
+
### 1a) 左方向键拦截 (handleInput)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
if (backable && currentIndex > 1 && matchesKey(data, Key.left)) {
|
|
24
|
+
done("__BACK__");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**问题**: 左方向键的 `matchesKey` 检查先于 `selectList.handleInput(data)`。
|
|
30
|
+
SelectList 只在 handleInput 内部更新 `selectedIndex` 的高亮状态。左方向键被拦截后,
|
|
31
|
+
当前选中的 item 仍然正确(因为 SelectList 的 selectedIndex 没被改变),
|
|
32
|
+
但容器的 `invalidate()` 没有被调用,不过 tui.requestRender() 未执行。
|
|
33
|
+
|
|
34
|
+
**严重等级**: **low** — 当前不会引起可观察的 bug,但应调用 `container.invalidate()` 确保一致性。
|
|
35
|
+
|
|
36
|
+
### 1b) 选项截断 + description (关键逻辑)
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const truncated = truncateToWidth(label, MAX_OPTION_LABEL, "...");
|
|
40
|
+
return {
|
|
41
|
+
value: `opt-${i}`,
|
|
42
|
+
label: truncated,
|
|
43
|
+
description: truncated !== label ? opt : undefined,
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**分析**: `description` 字段会被 `normalizeToSingleLine` 处理(移除换行符),
|
|
48
|
+
且 SelectList 渲染器对 description 也应用 `truncateToWidth`(剩余宽度可能很小)。
|
|
49
|
+
对于超长选项文本,description 列宽度取决于终端剩余列数,如果终端很窄,
|
|
50
|
+
description 仍然会被截断。但两列布局确实提供了更多可见内容。
|
|
51
|
+
|
|
52
|
+
**严重等级**: **low** — 这是 pi-tui 库的设计限制,已经达到库能力范围内最好效果。
|
|
53
|
+
|
|
54
|
+
### 1c) `__back__` 选项与左方向键返回路径的耦合
|
|
55
|
+
|
|
56
|
+
grill-me-agent 中有两个返回上一题的路径:
|
|
57
|
+
1. 通过选择 "← 返回上一题" SelectItem(value 为 `"__back__"`)
|
|
58
|
+
2. 通过按 ← 方向键触发 `done("__BACK__")`
|
|
59
|
+
|
|
60
|
+
调用方代码中:
|
|
61
|
+
```typescript
|
|
62
|
+
if (value === "__back__") return "__BACK__";
|
|
63
|
+
```
|
|
64
|
+
其中 `value` 来自 SelectList 的 `item.value`(即 `"__back__"`),大小写匹配正确。
|
|
65
|
+
路径2直接传递 `"__BACK__"`(大写),由后续 `"__BACK__"` 检查处理。两路径殊途同归。
|
|
66
|
+
|
|
67
|
+
**严重等级**: **low** — 当前功能正确,但若未来修改了 SelectItem 的 value 字符串,
|
|
68
|
+
方向键路径不受影响(因为直接传常量字符串)。
|
|
69
|
+
|
|
70
|
+
### 1d) hint 文案更新
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
? " ↑↓ 导航 • Enter 选择 • ← 返回上一题 • Esc 取消全部评审"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**问题**: 文案正确、清晰。无问题。
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 2. `extensions/ui-helpers.ts` 审查
|
|
81
|
+
|
|
82
|
+
### 2a) 左方向键拦截破坏 Input 光标左移 (CRITICAL)
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
if (backable && matchesKey(data, Key.left)) {
|
|
86
|
+
done(BACK_MARKER);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// ... later:
|
|
90
|
+
input.handleInput(data);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**严重问题**: **所有 `backable=true` 场景下,Input 组件的光标左移功能完全丢失**。
|
|
94
|
+
|
|
95
|
+
**根因分析**:
|
|
96
|
+
1. pi-tui 的 Input 组件中,左方向键绑定 `tui.editor.cursorLeft`(详细见 keybindings: `defaultKeys: ["left", "ctrl+b"]`)
|
|
97
|
+
2. 本实现将 `matchesKey(data, Key.left)` 放在 `input.handleInput(data)` **之前**,且条件满足时直接 return
|
|
98
|
+
3. Input.handleInput 永远不会收到左方向键事件,光标永远无法左移
|
|
99
|
+
|
|
100
|
+
**影响范围**: 所有 `uiInput(ctx, ..., backable=true)` 调用的场景:
|
|
101
|
+
- grill "自定义输入"环节(用户输入长文本后想修改前面的字符)
|
|
102
|
+
- wizard 中可返回的上一步输入(如 dev-feat 的"核心功能描述")
|
|
103
|
+
|
|
104
|
+
**替代方案不充分**: 实施计划提到用户可用 `Ctrl+B` 替代左方向键光标左移。
|
|
105
|
+
但 `Ctrl+B` 不是常见的终端快捷键(Emacs 用户熟悉,但 Vim 用户和普通终端用户不熟悉),
|
|
106
|
+
且实施计划中没有在 UI hint 文本中提示 `Ctrl+B` 替代方案。
|
|
107
|
+
|
|
108
|
+
**修复建议**:
|
|
109
|
+
推荐 **方案 A**(最小改动,保留最佳用户体验): 仅在输入框为空或光标在最左侧时拦截左方向键做"返回",
|
|
110
|
+
否则作为正常光标左移:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// 左方向键 → 光标在最左侧或输入为空时触发返回
|
|
114
|
+
if (backable && matchesKey(data, Key.left)) {
|
|
115
|
+
const cursor = getInputCursor?.(input); // 需要获取 Input 的 cursor 位置
|
|
116
|
+
if (this.cursorPosition === 0) {
|
|
117
|
+
done(BACK_MARKER);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
但 Input 组件没有公开 cursor 属性(private)。替代方案:检查 input.getValue() 是否为空。
|
|
124
|
+
|
|
125
|
+
**推荐方案 B**: 将"返回"功能从裸 `←` 改为 `Alt+←`(pi-tui 中已绑定 `tui.editor.cursorWordLeft`)或 `Ctrl+Shift+←`(已有实现)。
|
|
126
|
+
|
|
127
|
+
**推荐方案 C(最小改动)**: 将 `backable` 左方向键拦截改为仅在 `input.getValue()` 为空时生效:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
if (backable && matchesKey(data, Key.left) && input.getValue().length === 0) {
|
|
131
|
+
done(BACK_MARKER);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
这样用户在输入内容后按 ← 可以编辑,而在空输入时按 ← 返回。
|
|
137
|
+
|
|
138
|
+
### 2b) 预览区域 Text 组件在空文本时仍输出一行
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const previewText = new Text("", 0, 0);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**中等问题 — medium**: **即使输入框为空,预览区域始终额外输出 1 行空白 + 1 Spacer 间距行**。
|
|
145
|
+
|
|
146
|
+
经检查 pi-tui Text 组件的实现:
|
|
147
|
+
```javascript
|
|
148
|
+
// text.js render方法
|
|
149
|
+
if (!this.text || this.text.trim() === "") {
|
|
150
|
+
const result = []; // 早期返回空数组
|
|
151
|
+
...
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
// ...
|
|
155
|
+
return result.length > 0 ? result : [""]; // 如果 result 为空,返回 [""] 一行空行
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
早期返回处返回 `[]`(空数组),但最后一行 `return result.length > 0 ? result : [""]` **不会被执行**
|
|
159
|
+
因为早期 return 已经走了。实际 `setText("")` 后 Text.render 返回 `[]`(输出 0 行)。
|
|
160
|
+
|
|
161
|
+
所以 `new Text("", 0, 0)` 在空字符串时输出0行,但 paddingY=0, paddingX=0 使其没有边距。
|
|
162
|
+
`render()` 实际执行路径:
|
|
163
|
+
1. `!this.text` → `true`(因为 `this.text = ""`,空字符串是 falsy)
|
|
164
|
+
2. 进入早期返回:`const result = []; return result;` → 返回 `[]`(空数组)
|
|
165
|
+
|
|
166
|
+
**结论**: 空文本时 Text 输出 0 行。但后面的 `Spacer(1)` 固定输出 1 行空白间距。
|
|
167
|
+
所以在无输入时 UI 有 **1 行额外空白**,不是 2 行。
|
|
168
|
+
|
|
169
|
+
**严重等级**: 从 medium 降为 **low** — 仅 1 行空白的视觉影响,功能正常。
|
|
170
|
+
|
|
171
|
+
### 2c) wrapTextWithAnsi 在 handleInput 中无条件调用
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const val = input.getValue();
|
|
175
|
+
if (val.length > 0) {
|
|
176
|
+
const wrapped = wrapTextWithAnsi(val, width - 4);
|
|
177
|
+
...
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**问题**: 每次按键都读取 value、换行、拼接 ANSI 字符串。对数百字符的输入性能可忽略,
|
|
182
|
+
但 es module 反复调用 `wrapTextWithAnsi` 对大文本可能产生轻微延迟。
|
|
183
|
+
|
|
184
|
+
**严重等级**: **low** — 性能微优化。
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 3. 整体架构审查
|
|
189
|
+
|
|
190
|
+
### 3a) 左方向键在三个层级的冲突 (CRITICAL)
|
|
191
|
+
|
|
192
|
+
**严重问题**:
|
|
193
|
+
|
|
194
|
+
左方向键在代码中有三种语义:
|
|
195
|
+
1. **Input 原生**: 光标左移一个字符(`tui.editor.cursorLeft`)
|
|
196
|
+
2. **uiInput 层**: 返回上一题(当 `backable=true`)
|
|
197
|
+
3. **showQuestionTUI 层**: 返回上一题(当 `backable && currentIndex > 1`)
|
|
198
|
+
|
|
199
|
+
路径 #3 → #2 → #1,是串联关系。当用户进入自定义输入后:
|
|
200
|
+
- 按 ← → uiInput 拦截 → 返回 BACK_MARKER → showQuestionTUI → 返回上一题
|
|
201
|
+
- 用户本意:在输入框中左移光标修改文字 → 无法实现
|
|
202
|
+
|
|
203
|
+
两处左方向键拦截会同时影响不同的 UI 层级,用户流程交叉时无法区分
|
|
204
|
+
"在 SelectList 中返回上一题"和"在 Input 中光标左移"。
|
|
205
|
+
|
|
206
|
+
### 3b) backable=false 场景保留
|
|
207
|
+
|
|
208
|
+
在 `uiInput` 中,当 `backable=false` 时,左方向键正常转发给 Input 组件(光标左移)。
|
|
209
|
+
这是正确的行为,原有功能没有被破坏。
|
|
210
|
+
|
|
211
|
+
### 3c) Ctrl+Shift+← 保留
|
|
212
|
+
|
|
213
|
+
原有 `Ctrl+Shift+←` 返回功能被保留,与新增裸 ← 键并行。
|
|
214
|
+
两者功能冗余但不冲突。
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 4. 测试覆盖分析
|
|
219
|
+
|
|
220
|
+
| # | 测试场景 | 状态 | 问题 |
|
|
221
|
+
|---|---------|------|------|
|
|
222
|
+
| 1 | Grill 左方向键返回 | ✅ 已实现 | SelectList 层级正常 |
|
|
223
|
+
| 2 | Grill 长选项显示 | ✅ 已实现 | description 在窄终端仍被截断(库限制) |
|
|
224
|
+
| 3 | Grill 自定义输入 ← 返回 | ✅ 路径可达 | **但输入中无法左移光标** |
|
|
225
|
+
| 4 | dev-* 长文本预览 | ✅ 已实现 | 空输入时有 1 行多余间距 |
|
|
226
|
+
| 5 | 普通 Esc 取消 | ✅ 未改变 | — |
|
|
227
|
+
| 6 | Ctrl+Shift+←/→ 导航 | ✅ 保留 | — |
|
|
228
|
+
| 7 | 非 backable 左方向键 | ✅ 保留 | — |
|
|
229
|
+
| 8 | 预览内容实时更新 | ✅ 按 Backspace 测试通过 | — |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 汇总
|
|
234
|
+
|
|
235
|
+
| 等级 | 数量 | 说明 |
|
|
236
|
+
|------|------|------|
|
|
237
|
+
| **critical** | 1 | backable=true 时 uiInput 中左方向键拦截导致 Input 光标左移完全失效 |
|
|
238
|
+
| **medium** | 1 | 预览区域 Spacer 无条件占用行间距(视觉影响) |
|
|
239
|
+
| **low** | 3 | container.invalidate() 缺失;wrapTextWithAnsi 性能微调;__BACK__ 路径耦合 |
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 关键问题详解
|
|
244
|
+
|
|
245
|
+
### Critical 1: uiInput 左方向键拦截破坏光标左移
|
|
246
|
+
|
|
247
|
+
**文件**: `extensions/ui-helpers.ts` 第 ~263 行
|
|
248
|
+
**影响**: 所有 `backable=true` 的 uiInput 场景
|
|
249
|
+
**根本原因**: `matchesKey(data, Key.left)` 在 `input.handleInput(data)` 之前拦截并 return
|
|
250
|
+
|
|
251
|
+
**修复方案推荐**:
|
|
252
|
+
**方案 C(最小改动)**: 仅在输入框为空时拦截 ← 作为返回:
|
|
253
|
+
```typescript
|
|
254
|
+
if (backable && matchesKey(data, Key.left) && input.getValue().length === 0) {
|
|
255
|
+
done(BACK_MARKER);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
这样做的好处:
|
|
260
|
+
- 空输入时 ← 返回(用户无内容需要编辑)
|
|
261
|
+
- 有输入内容时 ← 正常光标左移(用户可以修改文字)
|
|
262
|
+
- 仍可使用 Ctrl+Shift+← 在任何状态下返回
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 总结
|
|
267
|
+
|
|
268
|
+
实现的功能需求基本正确覆盖,但 `uiInput` 中的左方向键拦截过于激进,
|
|
269
|
+
没有考虑 Input 组件本身对左方向键的依赖。其他改动在可接受范围内。
|
|
270
|
+
主要建议修复 `uiInput` 中左方向键拦截的逻辑,增加光标位置或输入内容检查。
|