@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/index.ts +178 -316
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # pi-simplify
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@geminixiang/pi-simplify.svg)](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
- * Removes leftover code after feature implementation:
5
- * - Dead code (unused exports, orphaned files)
6
- * - Debug remnants (console.log, debugger, temp flags)
7
- * - Commented-out code
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: "safe" | "confirm" | "review";
22
+ risk: Risk;
23
+ action: Action;
24
24
  };
25
25
 
26
- const SIMPLIFY_PROMPT = `# Simplify: Clean Up Leftover Code
26
+ const SIMPLIFY_PROMPT = `# Simplify: Review Changed Code for Reuse, Quality, and Efficiency
27
27
 
28
- You are a code cleanup assistant. Your job is to identify and remove unnecessary code left over after feature implementation.
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
- ### 1. Dead Code (Safe to Delete)
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
- ## How to Analyze
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
- 1. Run \`git diff\` to see what changed
62
- 2. For each change, determine if it's:
63
- - **Essential**: Required for the feature to work
64
- - **Residual**: Left over from development/debugging
65
- - **Legacy**: Old code not touched by this change
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
- 3. For each residual item:
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
- - Assess risk level
70
- - Provide clear reason for removal
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 can be deleted (unused, debug code)
75
- - **confirm**: Delete after user confirms (over-engineered, duplicates)
76
- - **review**: User should review before action (commented code, ambiguous)
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" - don't delete without consent
81
- 2. For inline candidates, show the alternative
82
- 3. Don't flag necessary code just because it's simple
83
- 4. Respect existing abstraction boundaries
84
- 5. Be especially careful with:
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
- "risk": "safe",
110
+ "category": "reuse",
111
+ "risk": "confirm",
99
112
  "file": "path/to/file.ext",
100
113
  "lines": "12-15",
101
- "type": "debug",
102
- "reason": "Why this should be removed",
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
- - \`type\` — one of: "dead-code", "debug", "commented", "over-eng", "duplicate"
114
- - \`reason\` — single plain-text sentence
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 RISK_VALUES = new Set(["safe", "confirm", "review"]);
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 normalizeRisk(value: unknown): "safe" | "confirm" | "review" {
174
+ function normalizeAction(value: unknown): Action | null {
138
175
  const v = String(value ?? "")
139
176
  .toLowerCase()
140
177
  .trim();
141
- return RISK_VALUES.has(v) ? (v as "safe" | "confirm" | "review") : "review";
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(/[`*_]/g, "").trim();
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
- if (!file) continue;
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 = item.action ? ` [${stripMarkdown(String(item.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 ? reason + action : action.trim() || "(no reason provided)",
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
- // Build display items with section headers
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
- if (safeCandidates.length > 0) {
265
+ for (const section of sections) {
340
266
  displayItems.push({
341
- value: "__section__safe__",
342
- label: `── Safe to delete (${safeCandidates.length}) ──`,
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 safeCandidates) {
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: "Will be deleted",
276
+ description: section.config.description,
351
277
  });
352
- selectableItems.push({ index: globalIndex, candidate: c });
353
- globalIndex++;
278
+ selectableItems.push({ index: globalIndex++, candidate: c });
354
279
  }
355
280
  }
356
281
 
357
- if (confirmCandidates.length > 0) {
358
- displayItems.push({
359
- value: "__section__confirm__",
360
- label: `── Needs confirmation (${confirmCandidates.length}) ──`,
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
- for (let i = 0; i < displayItems.length; i++) {
413
- const item = displayItems[i];
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
- const selectableInfo = selectableItems.find((si) => si.index === i);
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 items to remove"));
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 items selected for cleanup", "info");
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
- // Build cleanup prompt
569
- const cleanupPrompt = `# Cleanup Instructions
445
+ const cleanupPrompt = `# Apply Review Findings
570
446
 
571
- Delete the following code:
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. Remove only the specified code (not surrounding code unless instructed)
578
- 3. If the removal affects other code, stop and report the issue
579
- 4. After all deletions, verify the code still works by running any existing tests
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
- Report:
582
- - What was deleted
583
- - Any issues encountered
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
- `Starting cleanup of ${selected.length} items (${safeItems.length} safe, ${confirmItems.length} confirmed)`,
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: "Clean up leftover code (dead code, debug remnants, over-engineering)",
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 cleanup candidates...", "info");
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
- // Race condition fix: pi.sendUserMessage() queues the message, but
639
- // ctx.waitForIdle() resolves immediately if the agent is still in idle.
640
- // We must wait for the agent to transition out of idle before waiting.
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
- resolveTurnStarted = r;
515
+ resolvePendingTurnStart = r;
645
516
  });
646
- pi.on("turn_start", () => resolveTurnStarted!());
517
+
647
518
  pi.sendUserMessage(fullPrompt);
648
- await turnStarted;
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 outcome = parseCandidates(analysisText);
538
+ const candidates = parseCandidatesJSON(analysisText);
658
539
 
659
- if (outcome.kind === "unparseable") {
660
- const debugPath = dumpDebug(analysisText);
661
- const suffix = debugPath ? ` Raw response saved to ${debugPath}` : "";
540
+ if (candidates === null) {
662
541
  ctx.ui.notify(
663
- `Could not parse cleanup candidates — the model did not return the expected JSON block.${suffix}`,
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 cleanup candidates found!", "info");
549
+ ctx.ui.notify("No review findings found!", "info");
672
550
  return;
673
551
  }
674
552
 
675
- ctx.ui.notify(`Found ${candidates.length} candidates. Select items to remove...`, "info");
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("Cleanup cancelled", "info");
559
+ ctx.ui.notify("Apply findings cancelled", "info");
682
560
  return;
683
561
  }
684
562
 
685
- // Execute cleanup
686
- await executeCleanup(ctx, selected);
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",
4
- "description": "Simplify: Clean up leftover code after feature implementation",
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
  ],