@geminixiang/pi-simplify 0.0.4 → 0.0.6
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/README.md +2 -0
- package/index.ts +178 -316
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pi-simplify
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@geminixiang/pi-simplify)
|
|
4
|
+
|
|
3
5
|
A [pi coding agent](https://github.com/mariozechner/pi) extension that cleans up leftover code after feature implementation.
|
|
4
6
|
|
|
5
7
|
## What it does
|
package/index.ts
CHANGED
|
@@ -1,87 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-simplify: Code Cleanup Extension for pi
|
|
2
|
+
* pi-simplify: Code Review & Cleanup Extension for pi
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* - Over-engineering ("might use later" abstractions)
|
|
9
|
-
* - Duplicate logic
|
|
4
|
+
* Reviews changed code for reuse, quality, and efficiency:
|
|
5
|
+
* - Reuse: replace newly-written code with existing utilities
|
|
6
|
+
* - Quality: dead code, debug remnants, commented-out code, over-engineering, duplicate logic
|
|
7
|
+
* - Efficiency: redundant work, missed concurrency, hot-path bloat, memory leaks
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
|
-
import { writeFileSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
|
|
16
10
|
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
17
11
|
import { matchesKey, Key, truncateToWidth, type SelectItem } from "@mariozechner/pi-tui";
|
|
18
12
|
|
|
13
|
+
type Category = "reuse" | "quality" | "efficiency";
|
|
14
|
+
type Risk = "safe" | "confirm" | "review";
|
|
15
|
+
type Action = "delete" | "inline" | "refactor" | "parallelize";
|
|
16
|
+
|
|
19
17
|
type SimplifyResult = {
|
|
18
|
+
category: Category;
|
|
20
19
|
file: string;
|
|
21
20
|
lines: string;
|
|
22
21
|
reason: string;
|
|
23
|
-
risk:
|
|
22
|
+
risk: Risk;
|
|
23
|
+
action: Action;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
const SIMPLIFY_PROMPT = `# Simplify:
|
|
26
|
+
const SIMPLIFY_PROMPT = `# Simplify: Review Changed Code for Reuse, Quality, and Efficiency
|
|
27
27
|
|
|
28
|
-
You are a code
|
|
28
|
+
You are a code review assistant. Review the changed code along three axes — **Reuse**, **Quality**, and **Efficiency** — and surface concrete fixes.
|
|
29
29
|
|
|
30
30
|
## What to Find
|
|
31
31
|
|
|
32
|
-
###
|
|
33
|
-
- **Unused exports**: Functions/classes defined but never imported elsewhere
|
|
34
|
-
- **Orphan files**: Files created but never referenced
|
|
35
|
-
- **Zombie variables**: Variables assigned but never used
|
|
36
|
-
- **Empty blocks**: try/catch/if blocks with no logic
|
|
37
|
-
|
|
38
|
-
### 2. Debug Remnants (Safe to Delete)
|
|
39
|
-
- console.log, console.warn, console.error statements
|
|
40
|
-
- debugger statements
|
|
41
|
-
- Temporary feature flags (e.g., \`ENABLE_DEBUG\`, \`DEBUG_MODE\`)
|
|
42
|
-
- Temporary todo comments that are now implemented
|
|
43
|
-
|
|
44
|
-
### 3. Commented-out Code (Review Before Deleting)
|
|
45
|
-
- Old logic left in comments
|
|
46
|
-
- Disabled features (commented out rather than deleted)
|
|
47
|
-
- Copy-pasted templates never customized
|
|
48
|
-
|
|
49
|
-
### 4. Over-engineering (Confirm Before Deleting)
|
|
50
|
-
- Abstractions created "for future use" but never used
|
|
51
|
-
- Helper functions with single call sites (could be inlined)
|
|
52
|
-
- Layers of indirection that add no value
|
|
53
|
-
|
|
54
|
-
### 5. Duplicate Logic (Confirm Before Deleting)
|
|
55
|
-
- Repeated if-else blocks doing the same thing
|
|
56
|
-
- Copy-paste code with minor variations
|
|
57
|
-
- Duplicate utility functions
|
|
32
|
+
### A. Reuse (Confirm Before Replacing)
|
|
58
33
|
|
|
59
|
-
|
|
34
|
+
Before flagging, **search the codebase** (utility directories, shared modules, files adjacent to the change) for existing helpers. Quote the existing symbol you'd use.
|
|
35
|
+
|
|
36
|
+
- **Duplicates an existing function**: a newly written helper does what an existing one already does
|
|
37
|
+
- **Inline logic that has a utility**: hand-rolled string manipulation, manual path handling, custom env checks, ad-hoc type guards
|
|
38
|
+
- **Reinvented framework primitive**: re-implementing something the language/stdlib/framework already provides
|
|
39
|
+
|
|
40
|
+
### B. Quality
|
|
41
|
+
|
|
42
|
+
#### B1. Dead Code (Safe to Delete)
|
|
43
|
+
- Unused exports, orphan files, zombie variables, empty try/catch/if blocks
|
|
44
|
+
|
|
45
|
+
#### B2. Debug Remnants (Safe to Delete)
|
|
46
|
+
- \`console.log\` / \`console.warn\` / \`console.error\`, \`debugger\`, temporary feature flags, stale TODO comments
|
|
60
47
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
#### B3. Commented-out Code (Review)
|
|
49
|
+
- Old logic left in comments, disabled features, uncustomized templates
|
|
50
|
+
|
|
51
|
+
#### B4. Over-engineering (Confirm)
|
|
52
|
+
- Abstractions created "for future use" but unused, single-call-site helpers that should be inlined, useless indirection
|
|
53
|
+
|
|
54
|
+
#### B5. Hacky Patterns (Confirm)
|
|
55
|
+
- **Redundant state**: state that duplicates other state, cached values that could be derived, observers that could be direct calls
|
|
56
|
+
- **Parameter sprawl**: piling new parameters onto a function instead of restructuring
|
|
57
|
+
- **Copy-paste with variation**: near-duplicate blocks that should share an abstraction
|
|
58
|
+
- **Leaky abstractions**: exposing internals or breaking existing boundaries
|
|
59
|
+
- **Stringly-typed code**: raw strings where an existing constant/enum/union exists
|
|
60
|
+
- **Unnecessary wrapper elements**: JSX/DOM wrappers that add no layout value
|
|
61
|
+
- **Nested conditionals**: ternary chains or if/else nested 3+ levels deep — flatten with early returns or a lookup table
|
|
62
|
+
- **Useless comments**: comments restating WHAT the code does, narrating the change, or referencing the task/caller — keep only non-obvious WHY
|
|
63
|
+
|
|
64
|
+
### C. Efficiency (Confirm)
|
|
65
|
+
|
|
66
|
+
- **Unnecessary work**: redundant computations, repeated file reads, duplicate API calls, N+1 patterns
|
|
67
|
+
- **Missed concurrency**: independent operations run sequentially when they could run in parallel
|
|
68
|
+
- **Hot-path bloat**: blocking work added to startup or per-request/per-render hot paths
|
|
69
|
+
- **Recurring no-op updates**: state writes inside loops/intervals/handlers that fire unconditionally — add a change-detection guard
|
|
70
|
+
- **Unnecessary existence checks**: pre-checking file/resource existence before operating (TOCTOU) — operate directly and handle the error
|
|
71
|
+
- **Memory**: unbounded data structures, missing cleanup, event listener leaks
|
|
72
|
+
- **Overly broad operations**: reading whole files when a slice would do, loading all items when filtering for one
|
|
73
|
+
|
|
74
|
+
## How to Analyze
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
1. Run \`git diff\` to see what changed.
|
|
77
|
+
2. For each change, classify as **Essential** / **Residual** / **Legacy** (don't flag legacy code unless the diff touches it).
|
|
78
|
+
3. For each finding:
|
|
68
79
|
- Identify exact file and line(s)
|
|
69
|
-
-
|
|
70
|
-
-
|
|
80
|
+
- Assign **category** (reuse / quality / efficiency) and **risk**
|
|
81
|
+
- State the concrete fix (which existing utility to call, which lines to delete, how to parallelize, etc.)
|
|
71
82
|
|
|
72
83
|
## Risk Levels
|
|
73
84
|
|
|
74
|
-
- **safe**: Definitely
|
|
75
|
-
- **confirm**:
|
|
76
|
-
- **review**: User should
|
|
85
|
+
- **safe**: Definitely apply (dead code, debug remnants)
|
|
86
|
+
- **confirm**: Apply after user confirms (reuse swaps, over-engineering, hacky patterns, efficiency fixes)
|
|
87
|
+
- **review**: User should look first (commented-out code, ambiguous cases)
|
|
77
88
|
|
|
78
89
|
## Rules
|
|
79
90
|
|
|
80
|
-
1. When in doubt, mark as "confirm" or "review"
|
|
81
|
-
2. For
|
|
82
|
-
3.
|
|
83
|
-
4.
|
|
84
|
-
5.
|
|
91
|
+
1. When in doubt, mark as "confirm" or "review" — don't change without consent.
|
|
92
|
+
2. For reuse findings, name the existing symbol/file you'd swap to.
|
|
93
|
+
3. For efficiency findings, briefly justify the win (e.g., "N+1 → single query", "sequential awaits → Promise.all").
|
|
94
|
+
4. Don't flag necessary code just because it's simple.
|
|
95
|
+
5. Respect existing abstraction boundaries.
|
|
96
|
+
6. Be especially careful with:
|
|
85
97
|
- Error handling code
|
|
86
98
|
- Security-related logic
|
|
87
99
|
- Code that looks "unused" but is called via reflection/eval
|
|
@@ -95,24 +107,24 @@ You may write prose analysis first, but you MUST end your response with a single
|
|
|
95
107
|
{
|
|
96
108
|
"candidates": [
|
|
97
109
|
{
|
|
98
|
-
"
|
|
110
|
+
"category": "reuse",
|
|
111
|
+
"risk": "confirm",
|
|
99
112
|
"file": "path/to/file.ext",
|
|
100
113
|
"lines": "12-15",
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"action": "delete"
|
|
114
|
+
"reason": "Replace hand-rolled join with existing pathJoin() in src/utils/path.ts",
|
|
115
|
+
"action": "refactor"
|
|
104
116
|
}
|
|
105
117
|
]
|
|
106
118
|
}
|
|
107
119
|
\`\`\`
|
|
108
120
|
|
|
109
121
|
Field notes:
|
|
122
|
+
- \`category\` — one of: "reuse", "quality", "efficiency"
|
|
110
123
|
- \`risk\` — one of: "safe", "confirm", "review"
|
|
111
124
|
- \`file\` — repository-relative path, no backticks, no markdown
|
|
112
125
|
- \`lines\` — line number or range ("42" or "42-57"); empty string if unknown
|
|
113
|
-
- \`
|
|
114
|
-
- \`
|
|
115
|
-
- \`action\` — one of: "delete", "inline", "confirm"
|
|
126
|
+
- \`reason\` — single plain-text sentence stating the concrete fix
|
|
127
|
+
- \`action\` — one of: "delete", "inline", "refactor", "parallelize"
|
|
116
128
|
|
|
117
129
|
If there are no candidates, output \`{"candidates": []}\`. Do NOT output anything after the closing \`\`\` fence.
|
|
118
130
|
`;
|
|
@@ -122,6 +134,15 @@ export default function simplifyExtension(pi: ExtensionAPI) {
|
|
|
122
134
|
// the AI makes tool calls). Overwriting would drop the JSON block if it
|
|
123
135
|
// appears in an earlier message and a later one adds a short confirmation.
|
|
124
136
|
let assistantTextBuffer = "";
|
|
137
|
+
let resolvePendingTurnStart: (() => void) | null = null;
|
|
138
|
+
|
|
139
|
+
pi.on("turn_start", () => {
|
|
140
|
+
if (!resolvePendingTurnStart) return;
|
|
141
|
+
const resolve = resolvePendingTurnStart;
|
|
142
|
+
resolvePendingTurnStart = null;
|
|
143
|
+
resolve();
|
|
144
|
+
});
|
|
145
|
+
|
|
125
146
|
pi.on("message_end", (event) => {
|
|
126
147
|
const msg = event.message;
|
|
127
148
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) return;
|
|
@@ -132,17 +153,33 @@ export default function simplifyExtension(pi: ExtensionAPI) {
|
|
|
132
153
|
if (text) assistantTextBuffer += (assistantTextBuffer ? "\n" : "") + text;
|
|
133
154
|
});
|
|
134
155
|
|
|
135
|
-
const
|
|
156
|
+
const CATEGORY_VALUES = new Set<Category>(["reuse", "quality", "efficiency"]);
|
|
157
|
+
const RISK_VALUES = new Set<Risk>(["safe", "confirm", "review"]);
|
|
158
|
+
const ACTION_VALUES = new Set<Action>(["delete", "inline", "refactor", "parallelize"]);
|
|
159
|
+
|
|
160
|
+
function normalizeCategory(value: unknown): Category {
|
|
161
|
+
const v = String(value ?? "")
|
|
162
|
+
.toLowerCase()
|
|
163
|
+
.trim();
|
|
164
|
+
return CATEGORY_VALUES.has(v as Category) ? (v as Category) : "quality";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeRisk(value: unknown): Risk {
|
|
168
|
+
const v = String(value ?? "")
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.trim();
|
|
171
|
+
return RISK_VALUES.has(v as Risk) ? (v as Risk) : "review";
|
|
172
|
+
}
|
|
136
173
|
|
|
137
|
-
function
|
|
174
|
+
function normalizeAction(value: unknown): Action | null {
|
|
138
175
|
const v = String(value ?? "")
|
|
139
176
|
.toLowerCase()
|
|
140
177
|
.trim();
|
|
141
|
-
return
|
|
178
|
+
return ACTION_VALUES.has(v as Action) ? (v as Action) : null;
|
|
142
179
|
}
|
|
143
180
|
|
|
144
181
|
function stripMarkdown(text: string): string {
|
|
145
|
-
return text.replace(/[`*
|
|
182
|
+
return text.replace(/[`*]/g, "").trim();
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
/**
|
|
@@ -171,14 +208,18 @@ export default function simplifyExtension(pi: ExtensionAPI) {
|
|
|
171
208
|
for (const item of list) {
|
|
172
209
|
if (!item || typeof item !== "object") continue;
|
|
173
210
|
const file = stripMarkdown(String(item.file ?? item.location ?? ""));
|
|
174
|
-
|
|
211
|
+
// Validate path is within repo (no path traversal)
|
|
212
|
+
if (!file || file.includes("..") || file.startsWith("/")) continue;
|
|
175
213
|
const reason = stripMarkdown(String(item.reason ?? item.description ?? ""));
|
|
176
|
-
const action =
|
|
214
|
+
const action = normalizeAction(item.action);
|
|
215
|
+
if (!action) continue;
|
|
177
216
|
out.push({
|
|
217
|
+
category: normalizeCategory(item.category),
|
|
178
218
|
file,
|
|
179
219
|
lines: stripMarkdown(String(item.lines ?? "")),
|
|
180
|
-
reason: reason
|
|
220
|
+
reason: reason || "(no reason provided)",
|
|
181
221
|
risk: normalizeRisk(item.risk),
|
|
222
|
+
action,
|
|
182
223
|
});
|
|
183
224
|
}
|
|
184
225
|
// A valid candidates array (even empty) is a successful parse
|
|
@@ -190,134 +231,6 @@ export default function simplifyExtension(pi: ExtensionAPI) {
|
|
|
190
231
|
return null;
|
|
191
232
|
}
|
|
192
233
|
|
|
193
|
-
/**
|
|
194
|
-
* Tolerant markdown parser — fallback for responses that don't emit the JSON block.
|
|
195
|
-
* Accepts many header/bullet variations that LLMs produce in practice.
|
|
196
|
-
*/
|
|
197
|
-
function parseCandidatesMarkdown(response: string): SimplifyResult[] {
|
|
198
|
-
const candidates: SimplifyResult[] = [];
|
|
199
|
-
const lines = response.split("\n");
|
|
200
|
-
|
|
201
|
-
// Match `[safe]` / `[confirm]` / `[review]` anywhere on a "header-ish" line.
|
|
202
|
-
// A line counts as a header if the risk tag is preceded only by markdown noise:
|
|
203
|
-
// whitespace, #, *, -, digits, dots, parens, colons, backticks, underscores.
|
|
204
|
-
const HEADER_PREFIX = /^[\s#*\-–—•▸→0-9.)\]:`_]*$/;
|
|
205
|
-
const RISK_TAG = /\[(safe|confirm|review)\]/i;
|
|
206
|
-
|
|
207
|
-
let current: (Partial<SimplifyResult> & { _reason?: string; _action?: string }) | null = null;
|
|
208
|
-
|
|
209
|
-
const commit = () => {
|
|
210
|
-
if (!current) return;
|
|
211
|
-
if (!current.risk || !current.file) {
|
|
212
|
-
current = null;
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
const reason = current._reason?.trim() || current.lines?.trim() || "(no reason provided)";
|
|
216
|
-
const action = current._action ? ` [${current._action.trim()}]` : "";
|
|
217
|
-
candidates.push({
|
|
218
|
-
file: current.file,
|
|
219
|
-
lines: current.lines || "",
|
|
220
|
-
reason: reason + action,
|
|
221
|
-
risk: current.risk as "safe" | "confirm" | "review",
|
|
222
|
-
});
|
|
223
|
-
current = null;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
for (const rawLine of lines) {
|
|
227
|
-
const line = rawLine;
|
|
228
|
-
const tagMatch = line.match(RISK_TAG);
|
|
229
|
-
if (tagMatch) {
|
|
230
|
-
const prefix = line.slice(0, line.indexOf(tagMatch[0]));
|
|
231
|
-
if (HEADER_PREFIX.test(prefix)) {
|
|
232
|
-
// New candidate — commit the previous one
|
|
233
|
-
commit();
|
|
234
|
-
const risk = normalizeRisk(tagMatch[1]);
|
|
235
|
-
// Everything after the tag on this line
|
|
236
|
-
const rest = stripMarkdown(
|
|
237
|
-
line.slice(line.indexOf(tagMatch[0]) + tagMatch[0].length).replace(/^[\s\-–—:]+/, ""),
|
|
238
|
-
);
|
|
239
|
-
// Try to extract a file path (with optional :line or :line-line)
|
|
240
|
-
const fileMatch = rest.match(/([^\s()`*,]+?\.[a-zA-Z0-9]+)(?::(\d+(?:-\d+)?))?/);
|
|
241
|
-
current = { risk };
|
|
242
|
-
if (fileMatch) {
|
|
243
|
-
current.file = fileMatch[1];
|
|
244
|
-
current.lines = fileMatch[2] || "";
|
|
245
|
-
// Remaining text becomes a description/reason fallback
|
|
246
|
-
const desc = rest
|
|
247
|
-
.replace(fileMatch[0], "")
|
|
248
|
-
.replace(/^[\s\-–—:]+/, "")
|
|
249
|
-
.trim();
|
|
250
|
-
if (desc) current.lines = current.lines ? `${current.lines}: ${desc}` : desc;
|
|
251
|
-
} else {
|
|
252
|
-
// No recognizable file on header — treat rest as description
|
|
253
|
-
current.lines = rest;
|
|
254
|
-
}
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (!current) continue;
|
|
260
|
-
|
|
261
|
-
// Field extraction: tolerate `- Key:`, `* Key:`, `**Key:**`, or bare `Key:`.
|
|
262
|
-
const fieldMatch = line.match(
|
|
263
|
-
/^\s*(?:[-*•]\s*)?\*{0,2}(File|Location|Path|Reason|Why|Action|Lines?)\*{0,2}\s*:?\s*[:\-–—]?\s*(.+?)\s*$/i,
|
|
264
|
-
);
|
|
265
|
-
if (fieldMatch) {
|
|
266
|
-
const key = fieldMatch[1].toLowerCase();
|
|
267
|
-
const value = stripMarkdown(fieldMatch[2]);
|
|
268
|
-
if (key === "file" || key === "location" || key === "path") {
|
|
269
|
-
// Split `path.ts:12-15` into file + lines if current.file is still missing/weak
|
|
270
|
-
const withLine = value.match(/^(.+?)(?::(\d+(?:-\d+)?))?$/);
|
|
271
|
-
if (withLine) {
|
|
272
|
-
current.file = current.file || withLine[1];
|
|
273
|
-
if (withLine[2]) current.lines = withLine[2];
|
|
274
|
-
} else {
|
|
275
|
-
current.file = current.file || value;
|
|
276
|
-
}
|
|
277
|
-
} else if (key === "reason" || key === "why") {
|
|
278
|
-
current._reason = value;
|
|
279
|
-
} else if (key === "action") {
|
|
280
|
-
current._action = value;
|
|
281
|
-
} else if (key === "line" || key === "lines") {
|
|
282
|
-
current.lines = value;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
commit();
|
|
288
|
-
return candidates;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Write raw AI output to a debug file so the user can see what the model actually returned. */
|
|
292
|
-
function dumpDebug(response: string): string {
|
|
293
|
-
const path = join(tmpdir(), `pi-simplify-debug-${Date.now()}.md`);
|
|
294
|
-
try {
|
|
295
|
-
writeFileSync(path, response, "utf8");
|
|
296
|
-
return path;
|
|
297
|
-
} catch {
|
|
298
|
-
return "";
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
type ParseOutcome = { kind: "ok"; candidates: SimplifyResult[] } | { kind: "unparseable" };
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Parse the AI's response to extract cleanup candidates.
|
|
306
|
-
* - "ok" with non-empty list → show selector
|
|
307
|
-
* - "ok" with empty list → AI legitimately found nothing
|
|
308
|
-
* - "unparseable" → AI output didn't match any known format; dump for debug
|
|
309
|
-
*/
|
|
310
|
-
function parseCandidates(response: string): ParseOutcome {
|
|
311
|
-
const json = parseCandidatesJSON(response);
|
|
312
|
-
if (json !== null) return { kind: "ok", candidates: json };
|
|
313
|
-
const md = parseCandidatesMarkdown(response);
|
|
314
|
-
if (md.length > 0) return { kind: "ok", candidates: md };
|
|
315
|
-
return { kind: "unparseable" };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Show selection dialog for candidates with proper multi-select support
|
|
320
|
-
*/
|
|
321
234
|
async function showCandidateSelector(
|
|
322
235
|
ctx: ExtensionCommandContext,
|
|
323
236
|
candidates: SimplifyResult[],
|
|
@@ -331,94 +244,61 @@ export default function simplifyExtension(pi: ExtensionAPI) {
|
|
|
331
244
|
const confirmCandidates = candidates.filter((c) => c.risk === "confirm");
|
|
332
245
|
const reviewCandidates = candidates.filter((c) => c.risk === "review");
|
|
333
246
|
|
|
334
|
-
|
|
247
|
+
const RISK_CONFIG = {
|
|
248
|
+
safe: { label: "Safe to apply", description: "Will be applied" },
|
|
249
|
+
confirm: { label: "Needs confirmation", description: "Select to apply" },
|
|
250
|
+
review: { label: "Needs review", description: "Review before applying" },
|
|
251
|
+
} as const;
|
|
252
|
+
|
|
253
|
+
const sections = [
|
|
254
|
+
{ key: "safe" as const, items: safeCandidates },
|
|
255
|
+
{ key: "confirm" as const, items: confirmCandidates },
|
|
256
|
+
{ key: "review" as const, items: reviewCandidates },
|
|
257
|
+
]
|
|
258
|
+
.map(({ key, items }) => ({ key, items, config: RISK_CONFIG[key], total: items.length }))
|
|
259
|
+
.filter((s) => s.total > 0);
|
|
260
|
+
|
|
335
261
|
const displayItems: SelectItem[] = [];
|
|
336
262
|
const selectableItems: { index: number; candidate: SimplifyResult }[] = [];
|
|
337
263
|
let globalIndex = 0;
|
|
338
264
|
|
|
339
|
-
|
|
265
|
+
for (const section of sections) {
|
|
340
266
|
displayItems.push({
|
|
341
|
-
value:
|
|
342
|
-
label: `──
|
|
267
|
+
value: `__section__${section.key}__`,
|
|
268
|
+
label: `── ${section.config.label} (${section.total}) ──`,
|
|
343
269
|
description: "",
|
|
344
270
|
});
|
|
345
271
|
globalIndex++;
|
|
346
|
-
for (const c of
|
|
272
|
+
for (const c of section.items) {
|
|
347
273
|
displayItems.push({
|
|
348
274
|
value: JSON.stringify(c),
|
|
349
275
|
label: `${c.file} - ${c.reason}`,
|
|
350
|
-
description:
|
|
276
|
+
description: section.config.description,
|
|
351
277
|
});
|
|
352
|
-
selectableItems.push({ index: globalIndex
|
|
353
|
-
globalIndex++;
|
|
278
|
+
selectableItems.push({ index: globalIndex++, candidate: c });
|
|
354
279
|
}
|
|
355
280
|
}
|
|
356
281
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
description: "",
|
|
362
|
-
});
|
|
363
|
-
globalIndex++;
|
|
364
|
-
for (const c of confirmCandidates) {
|
|
365
|
-
displayItems.push({
|
|
366
|
-
value: JSON.stringify(c),
|
|
367
|
-
label: `${c.file} - ${c.reason}`,
|
|
368
|
-
description: "Select to delete",
|
|
369
|
-
});
|
|
370
|
-
selectableItems.push({ index: globalIndex, candidate: c });
|
|
371
|
-
globalIndex++;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (reviewCandidates.length > 0) {
|
|
376
|
-
displayItems.push({
|
|
377
|
-
value: "__section__review__",
|
|
378
|
-
label: `── Needs review (${reviewCandidates.length}) ──`,
|
|
379
|
-
description: "",
|
|
380
|
-
});
|
|
381
|
-
globalIndex++;
|
|
382
|
-
for (const c of reviewCandidates) {
|
|
383
|
-
displayItems.push({
|
|
384
|
-
value: JSON.stringify(c),
|
|
385
|
-
label: `${c.file} - ${c.reason}`,
|
|
386
|
-
description: "Review before deleting",
|
|
387
|
-
});
|
|
388
|
-
selectableItems.push({ index: globalIndex, candidate: c });
|
|
389
|
-
globalIndex++;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Track selected selectable items (by their selectableItems index)
|
|
394
|
-
const selectedSet = new Set<number>();
|
|
395
|
-
|
|
396
|
-
// Pre-select all safe items (indices 0 to safeCandidates.length - 1 in selectableItems)
|
|
397
|
-
for (let i = 0; i < safeCandidates.length; i++) {
|
|
398
|
-
selectedSet.add(i);
|
|
399
|
-
}
|
|
282
|
+
// Pre-select all safe items
|
|
283
|
+
const selectedSet = new Set<number>(
|
|
284
|
+
safeCandidates.length > 0 ? Array.from({ length: safeCandidates.length }, (_, i) => i) : [],
|
|
285
|
+
);
|
|
400
286
|
|
|
401
287
|
const maxVisible = 12;
|
|
402
288
|
let scrollOffset = 0;
|
|
403
289
|
let cursorPos = 0;
|
|
404
290
|
|
|
405
291
|
const result = await ctx.ui.custom<SimplifyResult[]>((tui, theme, _keybindings, done) => {
|
|
406
|
-
// Render the multi-select list manually
|
|
407
292
|
const renderList = (width: number): string[] => {
|
|
408
293
|
const lines: string[] = [];
|
|
409
294
|
|
|
410
|
-
// Calculate which items are visible
|
|
411
295
|
const visibleItems: { selectableIdx: number | null; item: SelectItem }[] = [];
|
|
412
|
-
|
|
413
|
-
|
|
296
|
+
let nextSelectableIdx = 0;
|
|
297
|
+
for (const item of displayItems) {
|
|
414
298
|
if (item.value.startsWith("__section__")) {
|
|
415
299
|
visibleItems.push({ selectableIdx: null, item });
|
|
416
300
|
} else {
|
|
417
|
-
|
|
418
|
-
if (selectableInfo) {
|
|
419
|
-
const selectableIdx = selectableItems.indexOf(selectableInfo);
|
|
420
|
-
visibleItems.push({ selectableIdx, item });
|
|
421
|
-
}
|
|
301
|
+
visibleItems.push({ selectableIdx: nextSelectableIdx++, item });
|
|
422
302
|
}
|
|
423
303
|
}
|
|
424
304
|
|
|
@@ -456,7 +336,7 @@ export default function simplifyExtension(pi: ExtensionAPI) {
|
|
|
456
336
|
return {
|
|
457
337
|
render(width: number) {
|
|
458
338
|
const lines: string[] = [];
|
|
459
|
-
lines.push(theme.bold("Simplify: Select
|
|
339
|
+
lines.push(theme.bold("Simplify: Select findings to apply"));
|
|
460
340
|
lines.push(
|
|
461
341
|
theme.fg(
|
|
462
342
|
"muted",
|
|
@@ -550,52 +430,44 @@ export default function simplifyExtension(pi: ExtensionAPI) {
|
|
|
550
430
|
return result ?? [];
|
|
551
431
|
}
|
|
552
432
|
|
|
553
|
-
|
|
554
|
-
* Execute the cleanup by sending deletion commands
|
|
555
|
-
*/
|
|
556
|
-
async function executeCleanup(
|
|
433
|
+
async function applyFindings(
|
|
557
434
|
ctx: ExtensionCommandContext,
|
|
558
435
|
selected: SimplifyResult[],
|
|
559
436
|
): Promise<void> {
|
|
560
437
|
if (selected.length === 0) {
|
|
561
|
-
ctx.ui.notify("No
|
|
438
|
+
ctx.ui.notify("No findings selected to apply", "info");
|
|
562
439
|
return;
|
|
563
440
|
}
|
|
564
441
|
|
|
565
442
|
const safeItems = selected.filter((c) => c.risk === "safe");
|
|
566
443
|
const confirmItems = selected.filter((c) => c.risk === "confirm");
|
|
567
444
|
|
|
568
|
-
|
|
569
|
-
const cleanupPrompt = `# Cleanup Instructions
|
|
445
|
+
const cleanupPrompt = `# Apply Review Findings
|
|
570
446
|
|
|
571
|
-
|
|
447
|
+
Apply the following findings. Each item includes a bracketed action (e.g. \`[delete]\`, \`[refactor]\`, \`[parallelize]\`, \`[inline]\`) — follow that action, not a blanket delete.
|
|
572
448
|
|
|
573
|
-
${selected.map((c) => `- ${c.file}: ${c.reason}`).join("\n")}
|
|
449
|
+
${selected.map((c) => `- ${c.file} (${c.lines || "?"}): ${c.reason} [${c.action}]`).join("\n")}
|
|
574
450
|
|
|
575
451
|
For each item:
|
|
576
452
|
1. Read the file to find the exact location
|
|
577
|
-
2.
|
|
578
|
-
3.
|
|
579
|
-
4.
|
|
453
|
+
2. Apply only the specified change (not surrounding code unless instructed)
|
|
454
|
+
3. For refactor/inline/parallelize: preserve behavior; if the change has visible side effects, stop and report
|
|
455
|
+
4. If the change affects other code, stop and report the issue
|
|
580
456
|
|
|
581
|
-
|
|
582
|
-
-
|
|
583
|
-
-
|
|
584
|
-
- Files that may need further attention`;
|
|
457
|
+
After applying all changes:
|
|
458
|
+
- Run \`npm test\` or equivalent; if there are no tests, verify the files parse/type-check
|
|
459
|
+
- Report what changed, test results, and any items skipped with reasons`;
|
|
585
460
|
|
|
586
461
|
ctx.ui.notify(
|
|
587
|
-
`
|
|
462
|
+
`Applying ${selected.length} findings (${safeItems.length} safe, ${confirmItems.length} confirmed)`,
|
|
588
463
|
"info",
|
|
589
464
|
);
|
|
590
465
|
|
|
591
466
|
pi.sendUserMessage(cleanupPrompt);
|
|
592
467
|
}
|
|
593
468
|
|
|
594
|
-
/**
|
|
595
|
-
* Main command handler
|
|
596
|
-
*/
|
|
597
469
|
pi.registerCommand("simplify", {
|
|
598
|
-
description: "
|
|
470
|
+
description: "Review changed code for reuse, quality, and efficiency, then apply fixes",
|
|
599
471
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
600
472
|
if (!ctx.hasUI) {
|
|
601
473
|
ctx.ui.notify("Simplify requires interactive mode", "error");
|
|
@@ -624,7 +496,7 @@ Report:
|
|
|
624
496
|
return;
|
|
625
497
|
}
|
|
626
498
|
|
|
627
|
-
ctx.ui.notify("Analyzing code for
|
|
499
|
+
ctx.ui.notify("Analyzing code for review findings...", "info");
|
|
628
500
|
|
|
629
501
|
// Build prompt with optional focus
|
|
630
502
|
let fullPrompt = SIMPLIFY_PROMPT;
|
|
@@ -635,17 +507,26 @@ Report:
|
|
|
635
507
|
// Reset capture buffer, then send prompt and wait.
|
|
636
508
|
// message_end events append to assistantTextBuffer during the turn.
|
|
637
509
|
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
510
|
+
// Wait for the queued analysis turn to actually begin before waiting for
|
|
511
|
+
// idle; otherwise waitForIdle() can return immediately while the agent is
|
|
512
|
+
// still idle and the queued prompt has not started yet.
|
|
641
513
|
assistantTextBuffer = "";
|
|
642
|
-
let resolveTurnStarted: () => void;
|
|
643
514
|
const turnStarted = new Promise<void>((r) => {
|
|
644
|
-
|
|
515
|
+
resolvePendingTurnStart = r;
|
|
645
516
|
});
|
|
646
|
-
|
|
517
|
+
|
|
647
518
|
pi.sendUserMessage(fullPrompt);
|
|
648
|
-
|
|
519
|
+
|
|
520
|
+
const started = await Promise.race<boolean>([
|
|
521
|
+
turnStarted.then(() => true),
|
|
522
|
+
new Promise<boolean>((r) => setTimeout(() => r(false), 10000)),
|
|
523
|
+
]);
|
|
524
|
+
if (!started) {
|
|
525
|
+
resolvePendingTurnStart = null;
|
|
526
|
+
ctx.ui.notify("Analysis did not start within 10 seconds. Please try again.", "warning");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
649
530
|
await ctx.waitForIdle();
|
|
650
531
|
const analysisText = assistantTextBuffer;
|
|
651
532
|
|
|
@@ -654,42 +535,36 @@ Report:
|
|
|
654
535
|
return;
|
|
655
536
|
}
|
|
656
537
|
|
|
657
|
-
const
|
|
538
|
+
const candidates = parseCandidatesJSON(analysisText);
|
|
658
539
|
|
|
659
|
-
if (
|
|
660
|
-
const debugPath = dumpDebug(analysisText);
|
|
661
|
-
const suffix = debugPath ? ` Raw response saved to ${debugPath}` : "";
|
|
540
|
+
if (candidates === null) {
|
|
662
541
|
ctx.ui.notify(
|
|
663
|
-
|
|
542
|
+
"Could not parse review findings — the model did not return the expected JSON block.",
|
|
664
543
|
"warning",
|
|
665
544
|
);
|
|
666
545
|
return;
|
|
667
546
|
}
|
|
668
547
|
|
|
669
|
-
const candidates = outcome.candidates;
|
|
670
548
|
if (candidates.length === 0) {
|
|
671
|
-
ctx.ui.notify("No
|
|
549
|
+
ctx.ui.notify("No review findings found!", "info");
|
|
672
550
|
return;
|
|
673
551
|
}
|
|
674
552
|
|
|
675
|
-
ctx.ui.notify(`Found ${candidates.length}
|
|
553
|
+
ctx.ui.notify(`Found ${candidates.length} findings. Select findings to apply...`, "info");
|
|
676
554
|
|
|
677
555
|
// Show selector
|
|
678
556
|
const selected = await showCandidateSelector(ctx, candidates);
|
|
679
557
|
|
|
680
558
|
if (!selected || selected.length === 0) {
|
|
681
|
-
ctx.ui.notify("
|
|
559
|
+
ctx.ui.notify("Apply findings cancelled", "info");
|
|
682
560
|
return;
|
|
683
561
|
}
|
|
684
562
|
|
|
685
|
-
//
|
|
686
|
-
await
|
|
563
|
+
// Apply selected findings
|
|
564
|
+
await applyFindings(ctx, selected);
|
|
687
565
|
},
|
|
688
566
|
});
|
|
689
567
|
|
|
690
|
-
/**
|
|
691
|
-
* Quick simplify - auto-clean safe items only
|
|
692
|
-
*/
|
|
693
568
|
pi.registerCommand("simplify-quick", {
|
|
694
569
|
description: "Quick cleanup of obviously safe items (debug code, unused exports)",
|
|
695
570
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -730,17 +605,4 @@ Return a summary of what was deleted.`;
|
|
|
730
605
|
pi.sendUserMessage(quickPrompt, { deliverAs: "followUp" });
|
|
731
606
|
},
|
|
732
607
|
});
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Status indicator on startup
|
|
736
|
-
*/
|
|
737
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
738
|
-
const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
|
|
739
|
-
if (code === 0) {
|
|
740
|
-
ctx.ui.setStatus(
|
|
741
|
-
"simplify",
|
|
742
|
-
`${ctx.ui.theme.fg("accent", "simplify")} ${ctx.ui.theme.fg("muted", "ready (try /simplify)")}`,
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
});
|
|
746
608
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geminixiang/pi-simplify",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Simplify:
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Simplify: Review changed code for reuse, quality, and efficiency",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
7
7
|
],
|