@geminixiang/pi-simplify 0.0.8 → 0.0.10
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/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/prompt.ts +16 -4
- package/selector.ts +67 -45
- package/types.ts +6 -1
- package/workflow.ts +27 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.0.10] - 2026-05-30
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Replace each candidate's single `reason` with a three-part cause-and-effect chain — **root issue**, **consequence**, and **benefit after fix** — so findings explain _why_ they matter, not just _what_ to change.
|
|
14
|
+
- Require a real, non-trivial consequence for every finding; items that only make code "slightly shorter" are no longer flagged.
|
|
15
|
+
- Show the full root issue → consequence → benefit chain in the findings selector, and pass the root issue and goal to the apply step.
|
|
16
|
+
|
|
17
|
+
## [0.0.9] - 2026-05-27
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Support simplifying specific folders and files as snapshot reviews.
|
|
22
|
+
- Add a preset selector for choosing uncommitted changes or folder snapshot mode.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Respect configured selection keybindings in simplify TUI selectors.
|
|
27
|
+
- Keep release tags and GitHub release titles v-prefixed.
|
|
28
|
+
|
|
9
29
|
## [0.0.8] - 2026-05-25
|
|
10
30
|
|
|
11
31
|
### Added
|
package/package.json
CHANGED
package/prompt.ts
CHANGED
|
@@ -2,7 +2,7 @@ export const SIMPLIFY_PROMPT = `# Simplify: Review Changed Code for Reuse, Quali
|
|
|
2
2
|
|
|
3
3
|
You are a code review assistant. Review the changed code along three axes — **Reuse**, **Quality**, and **Efficiency** — and surface concrete fixes.
|
|
4
4
|
|
|
5
|
-
Match the user's language for candidate
|
|
5
|
+
Match the user's language for all candidate fields; if the user's request is in Chinese, write the \`rootIssue\`, \`consequence\`, and \`benefit\` fields in Chinese.
|
|
6
6
|
|
|
7
7
|
## What to Find
|
|
8
8
|
|
|
@@ -63,11 +63,21 @@ Before flagging, **search the codebase** (utility directories, shared modules, f
|
|
|
63
63
|
- **confirm**: Apply after user confirms (reuse swaps, over-engineering, hacky patterns, efficiency fixes)
|
|
64
64
|
- **review**: User should look first (commented-out code, ambiguous cases)
|
|
65
65
|
|
|
66
|
+
## Make the Case (REQUIRED for every candidate)
|
|
67
|
+
|
|
68
|
+
A finding is only useful if it convinces the reader to act. For each candidate, build an explicit cause-and-effect chain across three fields:
|
|
69
|
+
|
|
70
|
+
1. **rootIssue** — the underlying flaw in the *current* code, not the fix. For reuse, name the existing symbol and file it duplicates. Be specific about what is wrong.
|
|
71
|
+
2. **consequence** — what this flaw *leads to* if left unchanged: divergent implementations that drift, an N+1 query on every request, untested duplicate logic, a memory leak that grows over time, etc.
|
|
72
|
+
3. **benefit** — the concrete advantage after the fix: single source of truth, "-14 lines", "one query instead of N", "covered by existing tests", "no listener leak".
|
|
73
|
+
|
|
74
|
+
**Discipline:** if you cannot state a real, non-trivial \`consequence\`, do NOT flag the finding. "It's slightly shorter" or "it's a bit cleaner" is not a consequence. Every candidate you return must survive the question *"what actually goes wrong if we leave this?"* — this keeps the list short and every item defensible.
|
|
75
|
+
|
|
66
76
|
## Rules
|
|
67
77
|
|
|
68
78
|
1. When in doubt, mark as "confirm" or "review" — don't change without consent.
|
|
69
|
-
2. For reuse findings, name the existing symbol/file you'd swap to
|
|
70
|
-
3. For efficiency findings,
|
|
79
|
+
2. For reuse findings, name the existing symbol/file you'd swap to in \`rootIssue\`.
|
|
80
|
+
3. For efficiency findings, justify the win in \`benefit\` (e.g., "N+1 → single query", "sequential awaits → Promise.all").
|
|
71
81
|
4. Don't flag necessary code just because it's simple.
|
|
72
82
|
5. Respect existing abstraction boundaries.
|
|
73
83
|
6. Be especially careful with:
|
|
@@ -85,7 +95,9 @@ Field notes:
|
|
|
85
95
|
- \`risk\` — one of: "safe", "confirm", "review"
|
|
86
96
|
- \`file\` — repository-relative path, no backticks, no markdown
|
|
87
97
|
- \`lines\` — line number or range ("42" or "42-57"); empty string if unknown
|
|
88
|
-
- \`
|
|
98
|
+
- \`rootIssue\` — the underlying problem with the current code (for reuse, name the existing symbol + file)
|
|
99
|
+
- \`consequence\` — what goes wrong if left unchanged (no real consequence ⇒ don't flag)
|
|
100
|
+
- \`benefit\` — the concrete advantage gained after the fix
|
|
89
101
|
- \`action\` — one of: "delete", "inline", "refactor", "parallelize"
|
|
90
102
|
|
|
91
103
|
If there are no candidates, call \`simplify_candidates\` with an empty \`candidates\` array.
|
package/selector.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { getSelectListTheme, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "@earendil-works/pi-tui";
|
|
2
|
+
getSelectListTheme,
|
|
3
|
+
keyHint,
|
|
4
|
+
rawKeyHint,
|
|
5
|
+
type ExtensionCommandContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { matchesKey, SelectList, wrapTextWithAnsi, type SelectItem } from "@earendil-works/pi-tui";
|
|
9
8
|
|
|
10
9
|
import type { Action, SimplifyResult } from "./types.js";
|
|
11
10
|
|
|
@@ -64,7 +63,7 @@ export async function showCandidateSelector(
|
|
|
64
63
|
Math.min(selectedIndex - Math.floor(maxVisible / 2), selectableItems.length - maxVisible),
|
|
65
64
|
);
|
|
66
65
|
|
|
67
|
-
const result = await ctx.ui.custom<SimplifyResult[]>((tui, theme,
|
|
66
|
+
const result = await ctx.ui.custom<SimplifyResult[]>((tui, theme, keybindings, done) => {
|
|
68
67
|
const updateListLabel = (index: number) => {
|
|
69
68
|
const item = listItems[index];
|
|
70
69
|
const { candidate } = selectableItems[index];
|
|
@@ -82,6 +81,34 @@ export async function showCandidateSelector(
|
|
|
82
81
|
};
|
|
83
82
|
selectList.onCancel = () => done([]);
|
|
84
83
|
|
|
84
|
+
const submitSelection = () => {
|
|
85
|
+
done(Array.from(selectedSet).map((i) => selectableItems[i].candidate));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const cancelSelection = () => {
|
|
89
|
+
done([]);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const toggleCurrent = () => {
|
|
93
|
+
if (selectedSet.has(cursorPos)) {
|
|
94
|
+
selectedSet.delete(cursorPos);
|
|
95
|
+
} else {
|
|
96
|
+
selectedSet.add(cursorPos);
|
|
97
|
+
}
|
|
98
|
+
updateListLabel(cursorPos);
|
|
99
|
+
tui.requestRender();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const toggleAll = () => {
|
|
103
|
+
if (selectedSet.size === selectableItems.length) {
|
|
104
|
+
selectedSet.clear();
|
|
105
|
+
} else {
|
|
106
|
+
for (let i = 0; i < selectableItems.length; i++) selectedSet.add(i);
|
|
107
|
+
}
|
|
108
|
+
updateAllListLabels();
|
|
109
|
+
tui.requestRender();
|
|
110
|
+
};
|
|
111
|
+
|
|
85
112
|
return {
|
|
86
113
|
render(width: number) {
|
|
87
114
|
selectList.setSelectedIndex(cursorPos);
|
|
@@ -106,12 +133,21 @@ export async function showCandidateSelector(
|
|
|
106
133
|
const detailIndent = " ";
|
|
107
134
|
const detailPrefix = `${detailIndent}│ `;
|
|
108
135
|
const location = current.lines ? `${current.file}:${current.lines}` : current.file;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
136
|
+
const wrapWidth = Math.max(1, width - detailPrefix.length);
|
|
137
|
+
|
|
138
|
+
const pushSection = (heading: string, body: string) => {
|
|
139
|
+
if (!body) return;
|
|
140
|
+
lines.push(theme.fg("borderMuted", `${detailIndent}┌─ ${heading}`));
|
|
141
|
+
lines.push(
|
|
142
|
+
...wrapTextWithAnsi(body, wrapWidth).map((wrappedLine) =>
|
|
143
|
+
theme.fg("muted", `${detailPrefix}${wrappedLine}`),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
pushSection("Root issue", current.rootIssue);
|
|
149
|
+
pushSection("Consequence", current.consequence);
|
|
150
|
+
pushSection("Benefit after fix", current.benefit);
|
|
115
151
|
lines.push(theme.fg("muted", `${detailPrefix}`));
|
|
116
152
|
lines.push(theme.fg("muted", `${detailPrefix}File: ${location}`));
|
|
117
153
|
lines.push(
|
|
@@ -119,14 +155,23 @@ export async function showCandidateSelector(
|
|
|
119
155
|
);
|
|
120
156
|
lines.push(theme.fg("muted", `${detailPrefix}Kind: ${current.category}`));
|
|
121
157
|
lines.push(theme.fg("muted", `${detailPrefix}Fix: ${ACTION_LABEL[current.action]}`));
|
|
122
|
-
lines.push(
|
|
158
|
+
lines.push(
|
|
159
|
+
theme.fg("borderMuted", `${detailIndent}└─ ${rawKeyHint("space", "selects this fix")}`),
|
|
160
|
+
);
|
|
123
161
|
}
|
|
124
162
|
|
|
125
163
|
lines.push("");
|
|
126
164
|
lines.push(
|
|
127
165
|
theme.fg(
|
|
128
166
|
"muted",
|
|
129
|
-
|
|
167
|
+
[
|
|
168
|
+
`Selected: ${selectedSet.size}`,
|
|
169
|
+
rawKeyHint("↑↓", "navigate"),
|
|
170
|
+
rawKeyHint("space", "toggle"),
|
|
171
|
+
rawKeyHint("a", "select all"),
|
|
172
|
+
keyHint("tui.select.confirm", "confirm"),
|
|
173
|
+
keyHint("tui.select.cancel", "cancel"),
|
|
174
|
+
].join(" • "),
|
|
130
175
|
),
|
|
131
176
|
);
|
|
132
177
|
return lines;
|
|
@@ -135,35 +180,12 @@ export async function showCandidateSelector(
|
|
|
135
180
|
selectList.invalidate();
|
|
136
181
|
},
|
|
137
182
|
handleInput(data: string) {
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (matchesKey(data,
|
|
144
|
-
done([]);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (matchesKey(data, Key.space)) {
|
|
148
|
-
if (selectedSet.has(cursorPos)) {
|
|
149
|
-
selectedSet.delete(cursorPos);
|
|
150
|
-
} else {
|
|
151
|
-
selectedSet.add(cursorPos);
|
|
152
|
-
}
|
|
153
|
-
updateListLabel(cursorPos);
|
|
154
|
-
tui.requestRender();
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (matchesKey(data, "a")) {
|
|
158
|
-
if (selectedSet.size === selectableItems.length) {
|
|
159
|
-
selectedSet.clear();
|
|
160
|
-
} else {
|
|
161
|
-
for (let i = 0; i < selectableItems.length; i++) selectedSet.add(i);
|
|
162
|
-
}
|
|
163
|
-
updateAllListLabels();
|
|
164
|
-
tui.requestRender();
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
183
|
+
if (keybindings.matches(data, "tui.select.confirm")) return submitSelection();
|
|
184
|
+
if (keybindings.matches(data, "tui.select.cancel")) return cancelSelection();
|
|
185
|
+
|
|
186
|
+
// Extra shortcuts: these selector-specific actions are not registered pi keybinding ids.
|
|
187
|
+
if (matchesKey(data, "space")) return toggleCurrent();
|
|
188
|
+
if (matchesKey(data, "a")) return toggleAll();
|
|
167
189
|
|
|
168
190
|
const previousCursorPos = cursorPos;
|
|
169
191
|
selectList.handleInput(data);
|
package/types.ts
CHANGED
|
@@ -6,7 +6,12 @@ export type SimplifyResult = {
|
|
|
6
6
|
category: Category;
|
|
7
7
|
file: string;
|
|
8
8
|
lines: string;
|
|
9
|
-
|
|
9
|
+
/** The underlying problem with the current code. */
|
|
10
|
+
rootIssue: string;
|
|
11
|
+
/** What this problem leads to if left unchanged. */
|
|
12
|
+
consequence: string;
|
|
13
|
+
/** The concrete advantage gained after applying the fix. */
|
|
14
|
+
benefit: string;
|
|
10
15
|
risk: Risk;
|
|
11
16
|
action: Action;
|
|
12
17
|
};
|
package/workflow.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineTool,
|
|
3
3
|
DynamicBorder,
|
|
4
|
+
keyHint,
|
|
4
5
|
type ExtensionAPI,
|
|
5
6
|
type ExtensionCommandContext,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -99,7 +100,21 @@ function registerSimplifyCandidatesTool(
|
|
|
99
100
|
pattern: "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$",
|
|
100
101
|
}),
|
|
101
102
|
lines: Type.String({ description: "Line number or range, or empty string if unknown" }),
|
|
102
|
-
|
|
103
|
+
rootIssue: Type.String({
|
|
104
|
+
description:
|
|
105
|
+
"The root problem with the current code. For reuse, name the existing symbol + file it duplicates. State the underlying flaw, not the fix.",
|
|
106
|
+
minLength: 1,
|
|
107
|
+
}),
|
|
108
|
+
consequence: Type.String({
|
|
109
|
+
description:
|
|
110
|
+
"What this problem leads to if left unchanged (e.g. divergent implementations, N+1 queries on every request, untested duplicate logic). If there is no real consequence, do not flag it.",
|
|
111
|
+
minLength: 1,
|
|
112
|
+
}),
|
|
113
|
+
benefit: Type.String({
|
|
114
|
+
description:
|
|
115
|
+
"The concrete advantage after applying the fix (e.g. single source of truth, -14 lines, one query instead of N, covered by existing tests).",
|
|
116
|
+
minLength: 1,
|
|
117
|
+
}),
|
|
103
118
|
action: actionSchema,
|
|
104
119
|
}),
|
|
105
120
|
),
|
|
@@ -205,7 +220,11 @@ async function showSimplifyTargetSelector(
|
|
|
205
220
|
selectList.onCancel = () => done(null);
|
|
206
221
|
|
|
207
222
|
container.addChild(selectList);
|
|
208
|
-
container.addChild(
|
|
223
|
+
container.addChild(
|
|
224
|
+
new Text(
|
|
225
|
+
`${keyHint("tui.select.confirm", "confirm")} ${keyHint("tui.select.cancel", "cancel")}`,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
209
228
|
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
210
229
|
|
|
211
230
|
return {
|
|
@@ -247,7 +266,12 @@ async function applyFindings(
|
|
|
247
266
|
|
|
248
267
|
Apply the following findings. Each item includes a bracketed action (e.g. \`[delete]\`, \`[refactor]\`, \`[parallelize]\`, \`[inline]\`) — follow that action, not a blanket delete.
|
|
249
268
|
|
|
250
|
-
${selected
|
|
269
|
+
${selected
|
|
270
|
+
.map(
|
|
271
|
+
(c) =>
|
|
272
|
+
`- ${c.file} (${c.lines || "?"}) [${c.action}]\n - Root issue: ${c.rootIssue}\n - Goal: ${c.benefit}`,
|
|
273
|
+
)
|
|
274
|
+
.join("\n")}
|
|
251
275
|
|
|
252
276
|
For each item:
|
|
253
277
|
1. Read the file to find the exact location
|