@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminixiang/pi-simplify",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Simplify: Review changed code for reuse, quality, and efficiency",
5
5
  "keywords": [
6
6
  "pi-package"
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 reasons and summaries; if the user's request is in Chinese, write the candidate \`reason\` fields in Chinese.
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, briefly justify the win (e.g., "N+1 → single query", "sequential awaits → Promise.all").
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
- - \`reason\` — single plain-text sentence stating the concrete fix
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
- matchesKey,
4
- Key,
5
- SelectList,
6
- wrapTextWithAnsi,
7
- type SelectItem,
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, _keybindings, done) => {
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
- lines.push(theme.fg("borderMuted", `${detailIndent}┌─ recommendation`));
110
- lines.push(
111
- ...wrapTextWithAnsi(current.reason, Math.max(1, width - detailPrefix.length)).map(
112
- (wrappedLine) => theme.fg("muted", `${detailPrefix}${wrappedLine}`),
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(theme.fg("borderMuted", `${detailIndent}└─ space selects this fix`));
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
- `Selected: ${selectedSet.size} • ↑↓ navigate • space toggle • a select all • enter confirm • esc cancel`,
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 (matchesKey(data, Key.enter)) {
139
- const results = Array.from(selectedSet).map((i) => selectableItems[i].candidate);
140
- done(results);
141
- return;
142
- }
143
- if (matchesKey(data, Key.escape)) {
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
- reason: string;
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
- reason: Type.String({ description: "Concrete fix in one sentence" }),
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(new Text(theme.fg("dim", "Press enter to confirm or esc to cancel")));
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.map((c) => `- ${c.file} (${c.lines || "?"}): ${c.reason} [${c.action}]`).join("\n")}
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