@geminixiang/pi-simplify 0.0.7 → 0.0.9

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.
@@ -11,15 +11,15 @@ Repo defaults:
11
11
  - remote: `origin`
12
12
  - repo: `geminixiang/pi-simplify`
13
13
  - npm package: `@geminixiang/pi-simplify`
14
- - use `package.json` version as the git tag and GitHub release title
14
+ - use `package.json` version for npm/package files, and `v<version>` for the git tag and GitHub release title
15
15
  - versions with `-alpha.`, `-beta.`, or `-rc.` are prereleases by default
16
16
 
17
17
  ## Version rules
18
18
 
19
19
  Examples:
20
20
 
21
- - stable: `0.2.1`, `0.3.0`, `1.0.0`
22
- - prerelease: `0.2.0-beta.8`, `0.3.0-rc.1`, `1.0.0-alpha.2`
21
+ - stable package version: `0.2.1`, `0.3.0`, `1.0.0` → release tag/title: `v0.2.1`, `v0.3.0`, `v1.0.0`
22
+ - prerelease package version: `0.2.0-beta.8`, `0.3.0-rc.1`, `1.0.0-alpha.2` → release tag/title: `v0.2.0-beta.8`, `v0.3.0-rc.1`, `v1.0.0-alpha.2`
23
23
 
24
24
  Guidance:
25
25
 
@@ -100,17 +100,17 @@ Write concise notes focused on user-visible changes, usually with:
100
100
  - `### Docs and maintenance`
101
101
  - `### Verification`
102
102
 
103
- Write notes to `/tmp/pi-simplify-release-<version>.md`. Keep them consistent with the CHANGELOG entry from step 3.
103
+ Write notes to `/tmp/pi-simplify-release-<version>.md`. Keep them consistent with the CHANGELOG entry from step 3. Use `v<previous-version>...v<version>` in compare links.
104
104
 
105
105
  ### 6. Create or update release
106
106
 
107
107
  Prerelease:
108
108
 
109
109
  ```bash
110
- gh release create <version> \
110
+ gh release create v<version> \
111
111
  --repo geminixiang/pi-simplify \
112
112
  --target main \
113
- --title <version> \
113
+ --title v<version> \
114
114
  --notes-file /tmp/pi-simplify-release-<version>.md \
115
115
  --prerelease
116
116
  ```
@@ -118,14 +118,14 @@ gh release create <version> \
118
118
  Stable:
119
119
 
120
120
  ```bash
121
- gh release create <version> \
121
+ gh release create v<version> \
122
122
  --repo geminixiang/pi-simplify \
123
123
  --target main \
124
- --title <version> \
124
+ --title v<version> \
125
125
  --notes-file /tmp/pi-simplify-release-<version>.md
126
126
  ```
127
127
 
128
- If it already exists, use `gh release edit <version> ...` and keep prerelease/stable intent consistent.
128
+ If it already exists, use `gh release edit v<version> ...` and keep prerelease/stable intent consistent.
129
129
 
130
130
  ## Report back
131
131
 
package/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ 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.9] - 2026-05-27
10
+
11
+ ### Added
12
+
13
+ - Support simplifying specific folders and files as snapshot reviews.
14
+ - Add a preset selector for choosing uncommitted changes or folder snapshot mode.
15
+
16
+ ### Fixed
17
+
18
+ - Respect configured selection keybindings in simplify TUI selectors.
19
+ - Keep release tags and GitHub release titles v-prefixed.
20
+
21
+ ## [0.0.8] - 2026-05-25
22
+
23
+ ### Added
24
+
25
+ - Support simplifying specific folders and files as snapshot reviews.
26
+ - Add a preset selector for choosing uncommitted changes or folder snapshot mode.
27
+
28
+ ### Fixed
29
+
30
+ - Keep release tags and GitHub release titles v-prefixed.
31
+
9
32
  ## [0.0.7] - 2026-05-25
10
33
 
11
34
  ### Changed
package/README.md CHANGED
@@ -30,12 +30,22 @@ pi install npm:@geminixiang/pi-simplify
30
30
  /simplify
31
31
  ```
32
32
 
33
- Analyzes all git changes and presents cleanup candidates:
33
+ Opens a preset selector. Choose uncommitted changes or a folder snapshot.
34
+
35
+ Uncommitted mode analyzes all git changes and presents cleanup candidates:
34
36
 
35
37
  - **Safe** (green) - auto-selected, will be deleted
36
38
  - **Confirm** (yellow) - delete after user confirms
37
39
  - **Review** (orange) - user should review first
38
40
 
41
+ ### Simplify Folders
42
+
43
+ ```
44
+ /simplify folder src docs
45
+ ```
46
+
47
+ Analyzes the specified folders/files as a snapshot (not a diff), even when there are no local git changes.
48
+
39
49
  ### With Focus
40
50
 
41
51
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminixiang/pi-simplify",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Simplify: Review changed code for reuse, quality, and efficiency",
5
5
  "keywords": [
6
6
  "pi-package"
package/prompt.ts CHANGED
@@ -50,8 +50,8 @@ Before flagging, **search the codebase** (utility directories, shared modules, f
50
50
 
51
51
  ## How to Analyze
52
52
 
53
- 1. Run \`git diff\` to see what changed.
54
- 2. For each change, classify as **Essential** / **Residual** / **Legacy** (don't flag legacy code unless the diff touches it).
53
+ 1. Unless the user explicitly asks for a folder/snapshot review, run \`git diff\` to see what changed. For folder/snapshot review, read the files directly under the requested paths and ignore files outside that scope.
54
+ 2. For each change or in-scope snapshot finding, classify as **Essential** / **Residual** / **Legacy** (don't flag legacy code unless the diff touches it; in folder/snapshot mode, flag only in-scope code that is clearly removable or simplifiable).
55
55
  3. For each finding:
56
56
  - Identify exact file and line(s)
57
57
  - Assign **category** (reuse / quality / efficiency) and **risk**
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);
@@ -119,14 +146,23 @@ export async function showCandidateSelector(
119
146
  );
120
147
  lines.push(theme.fg("muted", `${detailPrefix}Kind: ${current.category}`));
121
148
  lines.push(theme.fg("muted", `${detailPrefix}Fix: ${ACTION_LABEL[current.action]}`));
122
- lines.push(theme.fg("borderMuted", `${detailIndent}└─ space selects this fix`));
149
+ lines.push(
150
+ theme.fg("borderMuted", `${detailIndent}└─ ${rawKeyHint("space", "selects this fix")}`),
151
+ );
123
152
  }
124
153
 
125
154
  lines.push("");
126
155
  lines.push(
127
156
  theme.fg(
128
157
  "muted",
129
- `Selected: ${selectedSet.size} • ↑↓ navigate • space toggle • a select all • enter confirm • esc cancel`,
158
+ [
159
+ `Selected: ${selectedSet.size}`,
160
+ rawKeyHint("↑↓", "navigate"),
161
+ rawKeyHint("space", "toggle"),
162
+ rawKeyHint("a", "select all"),
163
+ keyHint("tui.select.confirm", "confirm"),
164
+ keyHint("tui.select.cancel", "cancel"),
165
+ ].join(" • "),
130
166
  ),
131
167
  );
132
168
  return lines;
@@ -135,35 +171,12 @@ export async function showCandidateSelector(
135
171
  selectList.invalidate();
136
172
  },
137
173
  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
- }
174
+ if (keybindings.matches(data, "tui.select.confirm")) return submitSelection();
175
+ if (keybindings.matches(data, "tui.select.cancel")) return cancelSelection();
176
+
177
+ // Extra shortcuts: these selector-specific actions are not registered pi keybinding ids.
178
+ if (matchesKey(data, "space")) return toggleCurrent();
179
+ if (matchesKey(data, "a")) return toggleAll();
167
180
 
168
181
  const previousCursorPos = cursorPos;
169
182
  selectList.handleInput(data);
package/workflow.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  defineTool,
3
+ DynamicBorder,
4
+ keyHint,
3
5
  type ExtensionAPI,
4
6
  type ExtensionCommandContext,
5
7
  } from "@earendil-works/pi-coding-agent";
8
+ import { Container, SelectList, Text, type SelectItem } from "@earendil-works/pi-tui";
6
9
  import { Type } from "typebox";
7
10
 
8
11
  import { SIMPLIFY_PROMPT } from "./prompt.js";
@@ -26,6 +29,58 @@ const actionSchema = Type.Union([
26
29
  Type.Literal("parallelize"),
27
30
  ]);
28
31
 
32
+ type SimplifyTarget = { type: "uncommitted" } | { type: "folder"; paths: string[] };
33
+ type ParsedArgs = {
34
+ target: SimplifyTarget | null;
35
+ additionalFocus?: string;
36
+ error?: string;
37
+ };
38
+
39
+ const SIMPLIFY_PRESETS = [
40
+ { value: "uncommitted", label: "Simplify uncommitted changes", description: "" },
41
+ { value: "folder", label: "Simplify a folder (or more)", description: "(snapshot, not diff)" },
42
+ ] as const;
43
+
44
+ type SimplifyPresetValue = (typeof SIMPLIFY_PRESETS)[number]["value"];
45
+
46
+ function parseSimplifyPaths(value: string): string[] {
47
+ return value
48
+ .split(/\s+/)
49
+ .map((item) => item.trim())
50
+ .filter((item) => item.length > 0);
51
+ }
52
+
53
+ function parseArgs(args: string): ParsedArgs {
54
+ const trimmed = args.trim();
55
+ if (!trimmed) return { target: null };
56
+
57
+ const [subcommand, ...rest] = trimmed.split(/\s+/);
58
+ if (subcommand?.toLowerCase() !== "folder") {
59
+ return { target: { type: "uncommitted" }, additionalFocus: trimmed };
60
+ }
61
+
62
+ const paths = parseSimplifyPaths(rest.join(" "));
63
+ if (paths.length === 0)
64
+ return { target: null, error: "Usage: /simplify folder <path> [path...]" };
65
+ return { target: { type: "folder", paths } };
66
+ }
67
+
68
+ async function hasUncommittedChanges(pi: ExtensionAPI): Promise<boolean> {
69
+ const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
70
+ return code === 0 && stdout.trim().length > 0;
71
+ }
72
+
73
+ function buildSimplifyPrompt(target: SimplifyTarget, additionalFocus?: string): string {
74
+ let fullPrompt = SIMPLIFY_PROMPT;
75
+
76
+ if (target.type === "folder") {
77
+ fullPrompt += `\n\n## Scope Override: Folder Snapshot\n\nReview only the code in these repository-relative paths: ${target.paths.join(", ")}\n\nThis is a snapshot review, not a git diff review. Read files directly under these paths instead of relying on \`git diff\`. Only return candidates whose \`file\` is inside one of these paths.`;
78
+ }
79
+
80
+ if (additionalFocus?.trim()) fullPrompt += `\n\n## Additional Focus\n\n${additionalFocus.trim()}`;
81
+ return fullPrompt;
82
+ }
83
+
29
84
  function registerSimplifyCandidatesTool(
30
85
  pi: ExtensionAPI,
31
86
  state: {
@@ -107,6 +162,79 @@ function registerSimplifyCandidatesTool(
107
162
  );
108
163
  }
109
164
 
165
+ async function showFolderInput(ctx: ExtensionCommandContext): Promise<SimplifyTarget | null> {
166
+ const result = await ctx.ui.editor(
167
+ "Enter folders/files to simplify (space-separated or one per line):",
168
+ ".",
169
+ );
170
+
171
+ if (!result?.trim()) return null;
172
+ const paths = parseSimplifyPaths(result);
173
+ if (paths.length === 0) return null;
174
+
175
+ return { type: "folder", paths };
176
+ }
177
+
178
+ async function showSimplifyTargetSelector(
179
+ ctx: ExtensionCommandContext,
180
+ pi: ExtensionAPI,
181
+ ): Promise<SimplifyTarget | null> {
182
+ const presetItems: SelectItem[] = SIMPLIFY_PRESETS.map((preset) => ({
183
+ value: preset.value,
184
+ label: preset.label,
185
+ description: preset.description,
186
+ }));
187
+ const smartDefault = (await hasUncommittedChanges(pi)) ? "uncommitted" : "folder";
188
+ const smartDefaultIndex = presetItems.findIndex((item) => item.value === smartDefault);
189
+
190
+ while (true) {
191
+ const result = await ctx.ui.custom<SimplifyPresetValue | null>((tui, theme, _kb, done) => {
192
+ const container = new Container();
193
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
194
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select a simplify preset"))));
195
+
196
+ const selectList = new SelectList(presetItems, Math.min(presetItems.length, 10), {
197
+ selectedPrefix: (text) => theme.fg("accent", text),
198
+ selectedText: (text) => theme.fg("accent", text),
199
+ description: (text) => theme.fg("muted", text),
200
+ scrollInfo: (text) => theme.fg("dim", text),
201
+ noMatch: (text) => theme.fg("warning", text),
202
+ });
203
+
204
+ if (smartDefaultIndex >= 0) selectList.setSelectedIndex(smartDefaultIndex);
205
+ selectList.onSelect = (item) => done(item.value as SimplifyPresetValue);
206
+ selectList.onCancel = () => done(null);
207
+
208
+ container.addChild(selectList);
209
+ container.addChild(
210
+ new Text(
211
+ `${keyHint("tui.select.confirm", "confirm")} ${keyHint("tui.select.cancel", "cancel")}`,
212
+ ),
213
+ );
214
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
215
+
216
+ return {
217
+ render(width: number) {
218
+ return container.render(width);
219
+ },
220
+ invalidate() {
221
+ container.invalidate();
222
+ },
223
+ handleInput(data: string) {
224
+ selectList.handleInput(data);
225
+ tui.requestRender();
226
+ },
227
+ };
228
+ });
229
+
230
+ if (!result) return null;
231
+ if (result === "uncommitted") return { type: "uncommitted" };
232
+
233
+ const target = await showFolderInput(ctx);
234
+ if (target) return target;
235
+ }
236
+ }
237
+
110
238
  async function applyFindings(
111
239
  ctx: ExtensionCommandContext,
112
240
  selected: SimplifyResult[],
@@ -167,7 +295,8 @@ export function registerSimplifyWorkflow(pi: ExtensionAPI) {
167
295
  });
168
296
 
169
297
  pi.registerCommand("simplify", {
170
- description: "Review changed code for reuse, quality, and efficiency, then apply fixes",
298
+ description:
299
+ "Review changed code or folders for reuse, quality, and efficiency, then apply fixes",
171
300
  handler: async (args: string, ctx: ExtensionCommandContext) => {
172
301
  if (!ctx.hasUI) {
173
302
  ctx.ui.notify("Simplify requires interactive mode", "error");
@@ -188,16 +317,29 @@ export function registerSimplifyWorkflow(pi: ExtensionAPI) {
188
317
  return;
189
318
  }
190
319
 
191
- const { stdout: gitStatus } = await pi.exec("git", ["status", "--porcelain"]);
192
- if (!gitStatus.trim()) {
193
- ctx.ui.notify("No git changes found. Simplify reviews uncommitted changes.", "info");
320
+ const parsed = parseArgs(args);
321
+ if (parsed.error) {
322
+ ctx.ui.notify(parsed.error, "error");
323
+ return;
324
+ }
325
+
326
+ let target = parsed.target;
327
+ if (!target) target = await showSimplifyTargetSelector(ctx, pi);
328
+ if (!target) {
329
+ ctx.ui.notify("Simplify cancelled", "info");
330
+ return;
331
+ }
332
+
333
+ if (target.type === "uncommitted" && !(await hasUncommittedChanges(pi))) {
334
+ ctx.ui.notify("No git changes found. Choose folder mode to simplify a snapshot.", "info");
194
335
  return;
195
336
  }
196
337
 
197
- ctx.ui.notify("Analyzing code for review findings...", "info");
338
+ const targetHint =
339
+ target.type === "folder" ? `folders: ${target.paths.join(", ")}` : "uncommitted changes";
340
+ ctx.ui.notify(`Analyzing ${targetHint} for review findings...`, "info");
198
341
 
199
- let fullPrompt = SIMPLIFY_PROMPT;
200
- if (args.trim()) fullPrompt += `\n\n## Additional Focus\n\n${args.trim()}`;
342
+ const fullPrompt = buildSimplifyPrompt(target, parsed.additionalFocus);
201
343
 
202
344
  latestToolCandidates = null;
203
345
  const turnStarted = new Promise<void>((r) => {