@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,258 @@
|
|
|
1
|
+
# 修复 git diff 解析与循环计数 Bug — 实施计划
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
修复两个 Bug:
|
|
6
|
+
1. **git diff 解析**:`f98799d` commit 中将 `getGitDiffChanges()` 的 git diff 解析从正则改为简单的 `split("\t")`,导致无法正确处理非 tab 分隔的输出(如 space-padded 格式),且 agent 输出文本被错误解析为文件路径(如 `checkpoint-${planId}.json`、`[],\t\t\toutputs:`)。
|
|
7
|
+
2. **循环计数偏移**:`executeLoopGroup` 中 `loopCount++` 在 reviewer 完成后才执行,导致第二次循环开始时 widget 仍显示旧的 loopCount,造成 "第 1 次循环" 重复出现。
|
|
8
|
+
|
|
9
|
+
## 文件清单
|
|
10
|
+
|
|
11
|
+
### 修改文件
|
|
12
|
+
| 文件路径 | 改动描述 | 风险等级 |
|
|
13
|
+
|---------|---------|---------|
|
|
14
|
+
| `extensions/workflow-engine.ts` | 修复 `getGitDiffChanges` 解析逻辑;修复 `loopCount` 更新时机 | 中 |
|
|
15
|
+
| `extensions/ui-helpers.ts` | 修复 `buildWidgetLines` 中循环计数的 fallback 逻辑 | 低 |
|
|
16
|
+
|
|
17
|
+
## 实施步骤
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### 步骤 1:修复 `getGitDiffChanges` 中 git diff 输出的解析
|
|
22
|
+
|
|
23
|
+
- **前置条件**:无
|
|
24
|
+
- **改动文件**:`extensions/workflow-engine.ts`(函数 `getGitDiffChanges`)
|
|
25
|
+
- **改动内容**:
|
|
26
|
+
|
|
27
|
+
**问题**:`f98799d` 将原来健壮的正则解析 `/^([MAD])\s+(.+)$/` 改为简单的 `split("\t")`。Git 的 `--name-status` 输出格式在不同环境/版本中可能使用 space-padded 格式(如 `"M path/to/file"`),此时 `split("\t")` 只能得到 `parts.length === 1`,导致解析失败。结果文件变更不会被检测到。
|
|
28
|
+
|
|
29
|
+
**修复方案**:将解析改回使用正则 `^([MAD])\s+(.+)$`,同时保留 tab split 作为后备(兼容两种格式):
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// 原来的正则方式(健壮):
|
|
33
|
+
// git diff --name-status output format: X\tfilepath or "X filepath"
|
|
34
|
+
const statusMatch = trimmed.match(/^([MAD])\s+(.+)$/);
|
|
35
|
+
if (statusMatch) {
|
|
36
|
+
const status = statusMatch[1]!.trim();
|
|
37
|
+
const filePath = statusMatch[2]!.trim();
|
|
38
|
+
if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
|
|
39
|
+
seen.add(filePath);
|
|
40
|
+
changes.push({ status: status as "M" | "A" | "D", path: filePath });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 后备:tab split(兼容部分 git 版本输出的 tab 格式)
|
|
44
|
+
else if (trimmed.includes("\t")) {
|
|
45
|
+
const parts = trimmed.split("\t");
|
|
46
|
+
if (parts.length === 2) {
|
|
47
|
+
const status = parts[0]!.trim();
|
|
48
|
+
const filePath = parts[1]!.trim();
|
|
49
|
+
if (filePath && !seen.has(filePath) && (status === "M" || status === "A" || status === "D")) {
|
|
50
|
+
seen.add(filePath);
|
|
51
|
+
changes.push({ status: status as "M" | "A" | "D", path: filePath });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
同时修复 `git status --porcelain` 部分:当前代码用 `trimmed.slice(0, 2)` 和 `trimmed.slice(3)`,但 `--porcelain` 格式是固定的 2 字符状态码 + 1空格 + path。需要更健壮的处理:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// 原来:statusPrefix = trimmed.slice(0, 2); filePath = trimmed.slice(3).trim();
|
|
61
|
+
// 改为正则(更健壮):
|
|
62
|
+
const statusMatch2 = trimmed.match(/^(..)\s+(.+)$/);
|
|
63
|
+
if (statusMatch2) {
|
|
64
|
+
const statusPrefix = statusMatch2[1]!.trim();
|
|
65
|
+
const filePath = statusMatch2[2]!.trim();
|
|
66
|
+
if (filePath && !seen.has(filePath) && (statusPrefix === "??" || statusPrefix === "A " || statusPrefix.startsWith("A"))) {
|
|
67
|
+
seen.add(filePath);
|
|
68
|
+
changes.push({ status: "A", path: filePath });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- **验证方式**:手动执行 `git diff --name-status` 验证输出格式,确认正则能够正确解析。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### 步骤 2:修复 agent 输出文本刮取逻辑中的脏数据
|
|
78
|
+
|
|
79
|
+
- **前置条件**:无
|
|
80
|
+
- **改动文件**:`extensions/workflow-engine.ts`(函数 `runAgentWithProgress` 中的文本刮取部分)
|
|
81
|
+
- **改动内容**:
|
|
82
|
+
|
|
83
|
+
**问题**:五个 `filePatterns` 正则过于宽松,会从 agent 的自然语言输出中误匹配脏数据。具体来说:
|
|
84
|
+
|
|
85
|
+
1. Pattern `/(?:^|\n)\s*(?:edit|new|delete|read|modify|create|update|add|remove)\s*[::]\s*([^\n]+\.[a-zA-Z0-9_]+)/gim` 可以匹配 agent 文本中类似:
|
|
86
|
+
- "M [],\n\t\t\t\toutputs:"(遇到 "M" 不匹配,但 "remove"或其他匹配?不,这个 pattern 需要前面的动词)
|
|
87
|
+
- 实际上,agent 的输出中可能有类似这样的文本:
|
|
88
|
+
```
|
|
89
|
+
modify: [],\n\t\t\t\toutputs: ...
|
|
90
|
+
```
|
|
91
|
+
或进度消息中的其他文本片段。
|
|
92
|
+
|
|
93
|
+
2. 标记代码块的 pattern `` /`([^`]+\.[a-zA-Z0-9_]+)`/g `` 可能匹配到 `` `checkpoint-xxx.json` `` 或 `` `checkpoint-${planId}.json` `` 这样的模板字符串。
|
|
94
|
+
|
|
95
|
+
**修复方案**:在 `filePatterns` 的每个匹配结果后添加更严格的过滤器,排除明显不是文件路径的字符串:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// 在 filePath 验证后添加额外过滤
|
|
99
|
+
// 过滤器:排除包含不合法路径字符或模板表达式的字符串
|
|
100
|
+
if (filePath.includes("${") || filePath.includes("\\n") || filePath.includes("\\t")) continue; // 排除模板字符串和转义字符
|
|
101
|
+
if (filePath.includes("[]") || filePath.includes("{}")) continue; // 排除数组/对象字面量
|
|
102
|
+
if (filePath.match(/^[\s,;)\]}]+$/)) continue; // 排除纯符号
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
关键修改位置:`runAgentWithProgress` 函数中,`const filePath = m[1]!.trim()` 之后的验证逻辑块。
|
|
106
|
+
|
|
107
|
+
- **验证方式**:用包含 `checkpoint-\${planId}.json` 和 `[],\n\t\t\t\toutputs:` 等脏数据的测试文本运行逻辑,确认不会产生误匹配。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### 步骤 3:修复循环计数偏移
|
|
112
|
+
|
|
113
|
+
- **前置条件**:步骤 1 和 2 完成
|
|
114
|
+
- **改动文件**:`extensions/workflow-engine.ts`(函数 `executeLoopGroup`)和 `extensions/ui-helpers.ts`(函数 `buildWidgetLines`)
|
|
115
|
+
- **改动内容**:
|
|
116
|
+
|
|
117
|
+
**根本原因**:`executeLoopGroup` 中的循环计数更新顺序有误。当前的顺序是:
|
|
118
|
+
|
|
119
|
+
1. 进入 while 循环(此时 `loopCount` 还未递增)
|
|
120
|
+
2. 重置 sub-step 为 pending
|
|
121
|
+
3. 执行 worker agent
|
|
122
|
+
4. 执行 reviewer agent
|
|
123
|
+
5. `loopCount++` 并 `state.loopCount = loopCount`
|
|
124
|
+
6. 检查是否需要继续循环
|
|
125
|
+
|
|
126
|
+
当 reviewer 发现 critical 问题需要再次循环时,`loopCount` 已经在步骤 5 增加为 1,所以在第二次循环开始时 widget 显示的是 "第 1 次循环"(因为 `state.loopCount = 1`),但实际上用户期望看到的是 "第 2 次循环"(即将开始第 2 轮)。
|
|
127
|
+
|
|
128
|
+
更准确地说,期望的显示行为是:
|
|
129
|
+
- Pending 时:`第 0 次循环`(表示尚未开始)
|
|
130
|
+
- 第 1 次循环执行中:`第 1 次循环`
|
|
131
|
+
- 第 1 次循环完成,需要第 2 次循环,第二次循环执行中:`第 2 次循环`
|
|
132
|
+
- ...
|
|
133
|
+
|
|
134
|
+
**修复方案 A(推荐,最小改动)**:在 while 循环**开始处**(进入新的一轮循环之前)更新 loopCount。
|
|
135
|
+
|
|
136
|
+
将 `loopCount++` 和 `state.loopCount = loopCount` 从 reviewer 完成之后**移到 while 循环最开头**。这样:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
while (loopCount < maxLoops) {
|
|
140
|
+
loopCount++; // 递增计数,表示"即将开始第 N 次循环"
|
|
141
|
+
state.loopCount = loopCount;
|
|
142
|
+
|
|
143
|
+
// 立即更新 UI
|
|
144
|
+
updateWidgetStep(stepIndex, step.label, "running", {
|
|
145
|
+
loopCount,
|
|
146
|
+
maxLoops: step.maxLoops,
|
|
147
|
+
startedAt: _widgetSteps[stepIndex]?.startedAt || Date.now(),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 重置 sub-step 状态
|
|
151
|
+
setWidgetSubStepStatus(stepIndex, step.loopAgentName!, "pending");
|
|
152
|
+
setWidgetSubStepStatus(stepIndex, step.reviewAgentName!, "pending");
|
|
153
|
+
// ... 后续逻辑 ...
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
同时需要移除原来 reviewer 完成后的 `loopCount++` 和 `state.loopCount = loopCount` 部分(在 `if (reviewSummary?.maxSeverity === "critical" ...)` 判断之前)。
|
|
158
|
+
|
|
159
|
+
**注意**:由于 `loopCount` 现在从 1 开始递增(而不是原来的从 0 开始,在 reviewer 完成后才 ++),所以需要同步修改 `buildWidgetLines` 中的 fallback 逻辑:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// 在 ui-helpers.ts 的 buildWidgetLines 中:
|
|
163
|
+
if (s.maxLoops != null) {
|
|
164
|
+
if (isRunning) {
|
|
165
|
+
// 当 loop-group 开始运行时,loopCount 已经通过 executeLoopGroup 在循环开头设置了,
|
|
166
|
+
// 所以不需要 fallback 显示"第 1 次循环"
|
|
167
|
+
// 直接使用 s.loopCount 的值
|
|
168
|
+
if (s.loopCount == null || s.loopCount === 0) {
|
|
169
|
+
// 安全 fallback(理论上不会走到这里)
|
|
170
|
+
loopStr = dim(theme, ` · 第 1 次循环`);
|
|
171
|
+
}
|
|
172
|
+
} else if (isPending) {
|
|
173
|
+
loopStr = dim(theme, ` · 第 0 次循环`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- **验证方式**:
|
|
179
|
+
1. 启动工作流,观察 loop-group 的循环计数显示
|
|
180
|
+
2. 验证第 1 次循环显示 `第 1 次循环`
|
|
181
|
+
3. 当 reviewer 触发再次循环时,验证显示 `第 2 次循环` 而不是 `第 1 次循环`
|
|
182
|
+
4. 验证第 3 次循环显示 `第 3 次循环`
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### 步骤 4:同步修改 loadCheckpoint 恢复时的循环计数
|
|
187
|
+
|
|
188
|
+
- **前置条件**:步骤 3 完成
|
|
189
|
+
- **改动文件**:`extensions/workflow-engine.ts`(`runWorkflow` 函数中恢复 checkpoint 的逻辑)
|
|
190
|
+
- **改动内容**:
|
|
191
|
+
|
|
192
|
+
由于 `loopCount` 的语义发生变化(从"已完成次数"变为"当前正在进行的轮次"),需要确保从 checkpoint 恢复时,`loopCount` 能正确恢复。
|
|
193
|
+
|
|
194
|
+
当前 checkpoint 中的 `loopCounts[step.id]` 存储的是已完成次数(即原来的语义)。如果 loopCount 现在从 1 开始,则恢复时需要确保:
|
|
195
|
+
|
|
196
|
+
- 如果 checkpoint 中 `loopCounts[step.id] = 1`(已完成 1 次),恢复后应显示 `第 1 次循环` 但不重新执行已完成的工作。
|
|
197
|
+
|
|
198
|
+
但检查代码逻辑:checkpoint 恢复时会跳过 `status === "done"` 的步骤,所以 `loopCounts` 只对**未完成**的 loop-group 步骤有效。对于未完成的步骤,`loopCounts` 为 0 或上一次退出时的值。
|
|
199
|
+
|
|
200
|
+
**实际上不需要修改**,因为 `loopCounts[step.id]` 只作为 `while` 循环的起始值(`let loopCount = loopCounts[step.id] ?? 0;`)。在步骤 3 中,我们将 `loopCount++` 移到了 while 开头,所以:
|
|
201
|
+
|
|
202
|
+
- 恢复后 loopCount = previous_loopCount(已完成次数)
|
|
203
|
+
- while 开始执行时立即 ++,变成 previous_loopCount + 1(当前轮次)
|
|
204
|
+
|
|
205
|
+
这恰好是正确的行为。
|
|
206
|
+
|
|
207
|
+
- **验证方式**:从 checkpoint 恢复工作流,确认循环计数正确。
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 依赖关系
|
|
212
|
+
|
|
213
|
+
- 步骤 1 和 2 相互独立,可并行实施
|
|
214
|
+
- 步骤 3 独立于步骤 1、2
|
|
215
|
+
- 步骤 4 依赖步骤 3
|
|
216
|
+
|
|
217
|
+
## 测试策略
|
|
218
|
+
|
|
219
|
+
### 单元测试(手动验证)
|
|
220
|
+
|
|
221
|
+
1. **git diff 解析测试**:
|
|
222
|
+
- 用以下格式模拟 git diff 输出:
|
|
223
|
+
- `M\tpath/to/file.ts`(tab 分隔)
|
|
224
|
+
- `M path/to/file.ts`(空格分隔,多空格)
|
|
225
|
+
- `A\tnewfile.ts`
|
|
226
|
+
- `D\tdeletedfile.ts`
|
|
227
|
+
- 验证所有格式都能正确解析
|
|
228
|
+
|
|
229
|
+
2. **文本刮取过滤器测试**:
|
|
230
|
+
- 用以下文本测试 `filePatterns` 匹配:
|
|
231
|
+
- `checkpoint-${planId}.json`
|
|
232
|
+
- `[],\n\t\t\t\toutputs:`
|
|
233
|
+
- `edit: src/main.rs`(应匹配)
|
|
234
|
+
- `I've modified src/main.rs`(应匹配)
|
|
235
|
+
- `try`(不应匹配)
|
|
236
|
+
- 验证过滤器正确排除脏数据
|
|
237
|
+
|
|
238
|
+
3. **循环计数测试**:
|
|
239
|
+
- 模拟 loop-group 执行流程:
|
|
240
|
+
```
|
|
241
|
+
pending → 第 0 次循环
|
|
242
|
+
running 第一次循环 → 第 1 次循环
|
|
243
|
+
running 第二次循环 → 第 2 次循环
|
|
244
|
+
running 第三次循环 → 第 3 次循环
|
|
245
|
+
```
|
|
246
|
+
- 验证显示值正确
|
|
247
|
+
|
|
248
|
+
### 集成测试
|
|
249
|
+
|
|
250
|
+
1. 运行一个实际工作流,观察 UI 显示
|
|
251
|
+
2. 手动触发 reviewer 发现 bug,观察再次循环时的显示
|
|
252
|
+
|
|
253
|
+
## 注意事项
|
|
254
|
+
|
|
255
|
+
1. **最小改动原则**:只修改有 bug 的逻辑,不重构其他部分
|
|
256
|
+
2. **向后兼容**:checkpoint 文件格式不变,`loopCounts` 字段语义变化需要确保从旧 checkpoint 恢复时行为正确
|
|
257
|
+
3. **git diff 解析**:改回正则解析的同时保留 tab split 后备,兼容多种 git 输出格式
|
|
258
|
+
4. **文本刮取**:添加的过滤器不应影响正常文件路径的匹配(如 `src/main.rs`、`extensions/workflow-engine.ts` 等)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Grill 左方向键冲突与选项描述显示修复 — 实施计划
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
修复 `dc0d3fa9` 提交引入的两个回归问题:
|
|
6
|
+
|
|
7
|
+
1. **左方向键破坏 Input 光标左移**:`uiInput` 中将裸 `←` 拦截为"返回",导致所有 `backable=true` 的输入场景中,左方向键的"光标左移"功能完全失效。
|
|
8
|
+
2. **Grill 选项截断+description 显示混乱**:选项 label 被截断后,完整文本放入 `description` 字段,但 SelectList 的 description 列不换行,导致用户看到"左边简短被截断 + 右边灰色不换行"的混乱显示。
|
|
9
|
+
|
|
10
|
+
**用户明确要求**:grill 提问环节的向左返回统一改为 `Ctrl+Shift+←`(与输入式一致),输入式的左方向键恢复为光标左移。
|
|
11
|
+
|
|
12
|
+
## 根因分析
|
|
13
|
+
|
|
14
|
+
### Bug 1:左方向键冲突
|
|
15
|
+
|
|
16
|
+
**问题位置**:`extensions/ui-helpers.ts` 第 263-266 行
|
|
17
|
+
|
|
18
|
+
`dc0d3fa9` 提交在 `uiInput.handleInput` 中添加了:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// 左方向键 → 返回(优先于 Input 的光标左移)
|
|
22
|
+
if (backable && matchesKey(data, Key.left)) {
|
|
23
|
+
done(BACK_MARKER);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
这段拦截在 `input.handleInput(data)` 之前执行,Input 组件永远不会收到左方向键事件,导致:
|
|
29
|
+
- Input 内部的 `tui.editor.cursorLeft` 绑定(左方向键)永远不会被触发
|
|
30
|
+
- 用户无法在输入框中左移光标
|
|
31
|
+
- 影响范围:所有 `backable=true` 的 uiInput 调用(grill 自定义输入、dev-* wizard 输入等)
|
|
32
|
+
|
|
33
|
+
同时,`extensions/grill-me-agent.ts` 第 671-674 行的 `showQuestionTUI.handleInput` 也拦截了左方向键:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// 左方向键 → 返回上一题
|
|
37
|
+
if (backable && currentIndex > 1 && matchesKey(data, Key.left)) {
|
|
38
|
+
done("__BACK__");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
两处拦截叠加,用户按 `←` 时:
|
|
44
|
+
- 在 SelectList 中 → 返回上一题(正确,因为 SelectList 不处理 left 键)
|
|
45
|
+
- 在 Input 中 → 返回上一题(错误,期望光标左移)
|
|
46
|
+
|
|
47
|
+
### Bug 2:选项截断+description 显示混乱
|
|
48
|
+
|
|
49
|
+
**问题位置**:`extensions/grill-me-agent.ts` 第 602-618 行
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const MAX_OPTION_LABEL = 50;
|
|
53
|
+
const truncated = truncateToWidth(label, MAX_OPTION_LABEL, "...");
|
|
54
|
+
return {
|
|
55
|
+
value: `opt-${i}`,
|
|
56
|
+
label: truncated,
|
|
57
|
+
description: truncated !== label ? opt : undefined,
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
SelectList 的渲染流程:
|
|
62
|
+
1. `renderItem` 对 `label` 使用 `truncateToWidth` 到主列宽度(截断为 50 字符 + "...")
|
|
63
|
+
2. 如果 `description` 存在,使用两列布局,description 列也被 `truncateToWidth` 到剩余宽度
|
|
64
|
+
|
|
65
|
+
当终端宽度不足以显示完整的 description 文本时,description 列也会被截断且**不换行**,产生用户看到的"左边截断 + 右边灰色不换行"效果。
|
|
66
|
+
|
|
67
|
+
## 修复方案
|
|
68
|
+
|
|
69
|
+
### 方案(根据用户建议)
|
|
70
|
+
|
|
71
|
+
**核心思路**:统一使用 `Ctrl+Shift+←` 作为返回键,恢复裸 `←` 的光标左移功能。
|
|
72
|
+
|
|
73
|
+
**详细改动**:
|
|
74
|
+
|
|
75
|
+
| 位置 | 当前行为(`dc0d3fa9`) | 修复后行为 |
|
|
76
|
+
|------|----------------------|-----------|
|
|
77
|
+
| `grill-me-agent.ts` handleInput | 裸 `←` 拦截为返回 | `Ctrl+Shift+←` 拦截为返回 |
|
|
78
|
+
| `ui-helpers.ts` uiInput handleInput | 裸 `←` 拦截为返回(破坏光标左移) | 移除裸 `←` 拦截 |
|
|
79
|
+
| `ui-helpers.ts` uiInput handleInput | `Ctrl+Shift+←` 返回(保留) | 保留 `Ctrl+Shift+←` |
|
|
80
|
+
| 选项列表(SelectList) | 用户按 `←` → 返回上一题 | 用户按 `Ctrl+Shift+←` → 返回上一题 |
|
|
81
|
+
| 输入框(Input) | 用户按 `←` → 返回上一题 ❌ | 用户按 `←` → 光标左移 ✅ |
|
|
82
|
+
| 输入框(Input) | 用户按 `Ctrl+Shift+←` → 返回上一题 | 保留 |
|
|
83
|
+
|
|
84
|
+
**Bug 2 修复**:直接移除截断+description 方案,恢复完整 label 显示。
|
|
85
|
+
|
|
86
|
+
## 文件清单
|
|
87
|
+
|
|
88
|
+
### 修改文件
|
|
89
|
+
|
|
90
|
+
| 文件路径 | 改动描述 | 风险等级 |
|
|
91
|
+
|---------|---------|---------|
|
|
92
|
+
| `extensions/ui-helpers.ts` | `uiInput.handleInput` 中移除裸 `←` 拦截(第 263-266 行);更新 JSDoc 注释 | 低 |
|
|
93
|
+
| `extensions/grill-me-agent.ts` | 1. `showQuestionTUI.handleInput` 中 `Key.left` → `Key.ctrlShift("left")` 2. 更新 hint 文案 3. 移除选项截断+description 方案,恢复完整 label | 低 |
|
|
94
|
+
|
|
95
|
+
### 无新增/删除文件
|
|
96
|
+
|
|
97
|
+
## 实施步骤
|
|
98
|
+
|
|
99
|
+
### 步骤 1:修复 `ui-helpers.ts` — 移除 uiInput 中的裸 ← 拦截
|
|
100
|
+
|
|
101
|
+
- **前置条件**:无
|
|
102
|
+
- **改动文件**:`extensions/ui-helpers.ts`
|
|
103
|
+
- **改动内容**:
|
|
104
|
+
|
|
105
|
+
**a) 删除裸 ← 拦截(约第 263-266 行)**
|
|
106
|
+
|
|
107
|
+
删除以下代码块:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// 左方向键 → 返回(优先于 Input 的光标左移)
|
|
111
|
+
if (backable && matchesKey(data, Key.left)) {
|
|
112
|
+
done(BACK_MARKER);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
保留 `Ctrl+Shift+←` 和 `Ctrl+Shift+→` 拦截不受影响。
|
|
118
|
+
|
|
119
|
+
**b) 更新 JSDoc 注释(约第 220 行)**
|
|
120
|
+
|
|
121
|
+
将:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
* When backable=true, supports ← for back, Ctrl+Shift+← for back, and Ctrl+Shift+→ for submit+next.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
改为:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
* When backable=true, supports Ctrl+Shift+← for back and Ctrl+Shift+→ for submit+next.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **验证方式**:
|
|
134
|
+
- 运行 `/dev-feat` → 进入"核心功能描述"输入框 → 按 `←` → 确认光标左移
|
|
135
|
+
- 按 `Ctrl+Shift+←` → 确认返回上一题
|
|
136
|
+
- 按 `Ctrl+Shift+→` → 确认跳过并继续
|
|
137
|
+
|
|
138
|
+
### 步骤 2:修复 `grill-me-agent.ts` — 选项列表 ← 改为 Ctrl+Shift+←
|
|
139
|
+
|
|
140
|
+
- **前置条件**:步骤 1 完成
|
|
141
|
+
- **改动文件**:`extensions/grill-me-agent.ts`
|
|
142
|
+
- **改动内容**:
|
|
143
|
+
|
|
144
|
+
**a) 修改 handleInput 中的左方向键拦截(约第 671-674 行)**
|
|
145
|
+
|
|
146
|
+
当前:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// 左方向键 → 返回上一题(SelectList 不处理 left 键,需自行拦截)
|
|
150
|
+
if (backable && currentIndex > 1 && matchesKey(data, Key.left)) {
|
|
151
|
+
done("__BACK__");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
改为:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Ctrl+Shift+← → 返回上一题(SelectList 不处理该键,需自行拦截)
|
|
160
|
+
if (backable && currentIndex > 1 && matchesKey(data, Key.ctrlShift("left"))) {
|
|
161
|
+
done("__BACK__");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**b) 修改 hint 提示文字(约第 660 行)**
|
|
167
|
+
|
|
168
|
+
当前:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const hint = backable && currentIndex > 1
|
|
172
|
+
? " ↑↓ 导航 • Enter 选择 • ← 返回上一题 • Esc 取消全部评审"
|
|
173
|
+
: " ↑↓ 导航 • Enter 选择 • Esc 取消全部评审";
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
改为:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const hint = backable && currentIndex > 1
|
|
180
|
+
? " ↑↓ 导航 • Enter 选择 • Ctrl+Shift+← 返回上一题 • Esc 取消全部评审"
|
|
181
|
+
: " ↑↓ 导航 • Enter 选择 • Esc 取消全部评审";
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- **验证方式**:
|
|
185
|
+
- 运行 `/dev-fix` → 进入 grill 评审
|
|
186
|
+
- 在第二个问题按 `←` → 确认**无反应**(正确,SelectList 不处理 left 键)
|
|
187
|
+
- 按 `Ctrl+Shift+←` → 确认返回到第一个问题
|
|
188
|
+
- 确认第一个问题选择的答案被保留
|
|
189
|
+
|
|
190
|
+
### 步骤 3:修复 `grill-me-agent.ts` — 移除选项截断+description 方案
|
|
191
|
+
|
|
192
|
+
- **前置条件**:步骤 2 完成
|
|
193
|
+
- **改动文件**:`extensions/grill-me-agent.ts`
|
|
194
|
+
- **改动内容**:
|
|
195
|
+
|
|
196
|
+
**a) 修改选项构建逻辑(约第 602-618 行)**
|
|
197
|
+
|
|
198
|
+
当前代码(`dc0d3fa9` 引入):
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const MAX_OPTION_LABEL = 50;
|
|
202
|
+
const selectItems: SelectItem[] = q.options.map((opt, i) => {
|
|
203
|
+
const prefix = `(${String.fromCharCode(97 + i)}) `;
|
|
204
|
+
const label = opt === previousAnswer
|
|
205
|
+
? `${prefix}${opt} - 上次选择`
|
|
206
|
+
: `${prefix}${opt}`;
|
|
207
|
+
const truncated = truncateToWidth(label, MAX_OPTION_LABEL, "...");
|
|
208
|
+
return {
|
|
209
|
+
value: `opt-${i}`,
|
|
210
|
+
label: truncated,
|
|
211
|
+
description: truncated !== label ? opt : undefined,
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
恢复为(还原到 `dc0d3fa9` 之前的状态):
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const selectItems: SelectItem[] = q.options.map((opt, i) => ({
|
|
220
|
+
value: `opt-${i}`,
|
|
221
|
+
label: opt === previousAnswer
|
|
222
|
+
? `(${String.fromCharCode(97 + i)}) ${opt} - 上次选择`
|
|
223
|
+
: `(${String.fromCharCode(97 + i)}) ${opt}`,
|
|
224
|
+
}));
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**b) 清理不再需要的 import(文件顶部约第 21-27 行)**
|
|
228
|
+
|
|
229
|
+
如果 `truncateToWidth` 在文件其他地方没有使用(经 grep 检查仅在步骤 3a 处使用),则删除 import 中的 `truncateToWidth`:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// 删除: truncateToWidth,
|
|
233
|
+
import {
|
|
234
|
+
Container,
|
|
235
|
+
SelectList,
|
|
236
|
+
Text,
|
|
237
|
+
Spacer,
|
|
238
|
+
matchesKey,
|
|
239
|
+
Key,
|
|
240
|
+
type SelectItem,
|
|
241
|
+
} from "@earendil-works/pi-tui";
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **验证方式**:
|
|
245
|
+
- 查看 grill 问题的选项 → 确认显示完整标签文本(而不是截断+description 两列显示)
|
|
246
|
+
- 当选项文本超宽时,SelectList 会自动截断到可用宽度(这是 SelectList 的内部行为,不需要手动干预)
|
|
247
|
+
|
|
248
|
+
## 依赖关系
|
|
249
|
+
|
|
250
|
+
- 步骤 1 和步骤 2、3 无直接依赖,但建议按顺序 1→2→3 执行,以保证逻辑一致性
|
|
251
|
+
- 步骤 3 中需先确认 `truncateToWidth` 是否在其他地方使用,再决定是否清理 import
|
|
252
|
+
|
|
253
|
+
## 测试策略
|
|
254
|
+
|
|
255
|
+
手动测试清单:
|
|
256
|
+
|
|
257
|
+
| 测试场景 | 操作 | 预期结果 |
|
|
258
|
+
|---------|------|---------|
|
|
259
|
+
| 输入框左方向键 | 在 dev-* 输入框中按 `←` | 光标左移 ✅(修复点) |
|
|
260
|
+
| 输入框 Ctrl+Shift+← | 在 dev-* 输入框中按 `Ctrl+Shift+←` | 返回上一题 ✅ |
|
|
261
|
+
| 输入框 Ctrl+Shift+→ | 在 dev-* 输入框中按 `Ctrl+Shift+→` | 跳过并继续 ✅ |
|
|
262
|
+
| Grill 选项列表 ← | 在 grill 第二题按 `←` | 无反应(SelectList 不处理 left 键)✅ |
|
|
263
|
+
| Grill 选项列表 Ctrl+Shift+← | 在 grill 第二题按 `Ctrl+Shift+←` | 返回上一题 ✅(修复点) |
|
|
264
|
+
| Grill 自定义输入 ← | 进入自定义输入框按 `←` | 光标左移 ✅(修复点) |
|
|
265
|
+
| Grill 自定义输入 Ctrl+Shift+← | 在自定义输入框按 `Ctrl+Shift+←` | 返回选项列表 ✅ |
|
|
266
|
+
| Grill 选项完整显示 | 查看长选项文本 | 完整 label 显示 ✅(修复点) |
|
|
267
|
+
| 原有功能 | ↑↓ Enter Esc | 正常导航/选择/取消 ✅ |
|
|
268
|
+
|
|
269
|
+
## 注意事项
|
|
270
|
+
|
|
271
|
+
- **最小改动**:仅修改 2 个文件中涉及的 4 个代码位置,总计约 10-15 行
|
|
272
|
+
- **不修改 pi-tui 库**:所有改动在 extensions/ 目录内完成
|
|
273
|
+
- **兼容性**:原有的 `Ctrl+Shift+←` 和 `Ctrl+Shift+→` 功能完全保留,新增的选项列表 `Ctrl+Shift+←` 与输入框保持一致的按键语义
|
|
274
|
+
- **SelectList 的 `← 返回上一题` 选项**:注意 `showQuestionTUI` 中还有一个 `"← 返回上一题"` 的 `SelectItem`(第 637 行),这是通过选择选项来返回,而非通过按键。此功能应保留,因为用户也可以通过 ↑↓ 导航到该选项后按 Enter 返回。但需要确认它与 `Key.left` 拦截的分离——按键拦截在 handleInput 中,选项选择在 SelectList 的 onSelect 回调中,两者互不干扰。保留该选项作为额外导航方式。
|