@geminixiang/pi-simplify 0.0.5 → 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 (2) hide show
  1. package/index.ts +128 -130
  2. package/package.json +2 -2
package/index.ts CHANGED
@@ -1,83 +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
10
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
13
11
  import { matchesKey, Key, truncateToWidth, type SelectItem } from "@mariozechner/pi-tui";
14
12
 
13
+ type Category = "reuse" | "quality" | "efficiency";
14
+ type Risk = "safe" | "confirm" | "review";
15
+ type Action = "delete" | "inline" | "refactor" | "parallelize";
16
+
15
17
  type SimplifyResult = {
18
+ category: Category;
16
19
  file: string;
17
20
  lines: string;
18
21
  reason: string;
19
- risk: "safe" | "confirm" | "review";
22
+ risk: Risk;
23
+ action: Action;
20
24
  };
21
25
 
22
- const SIMPLIFY_PROMPT = `# Simplify: Clean Up Leftover Code
26
+ const SIMPLIFY_PROMPT = `# Simplify: Review Changed Code for Reuse, Quality, and Efficiency
23
27
 
24
- 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.
25
29
 
26
30
  ## What to Find
27
31
 
28
- ### 1. Dead Code (Safe to Delete)
29
- - **Unused exports**: Functions/classes defined but never imported elsewhere
30
- - **Orphan files**: Files created but never referenced
31
- - **Zombie variables**: Variables assigned but never used
32
- - **Empty blocks**: try/catch/if blocks with no logic
33
-
34
- ### 2. Debug Remnants (Safe to Delete)
35
- - console.log, console.warn, console.error statements
36
- - debugger statements
37
- - Temporary feature flags (e.g., \`ENABLE_DEBUG\`, \`DEBUG_MODE\`)
38
- - Temporary todo comments that are now implemented
39
-
40
- ### 3. Commented-out Code (Review Before Deleting)
41
- - Old logic left in comments
42
- - Disabled features (commented out rather than deleted)
43
- - Copy-pasted templates never customized
44
-
45
- ### 4. Over-engineering (Confirm Before Deleting)
46
- - Abstractions created "for future use" but never used
47
- - Helper functions with single call sites (could be inlined)
48
- - Layers of indirection that add no value
49
-
50
- ### 5. Duplicate Logic (Confirm Before Deleting)
51
- - Repeated if-else blocks doing the same thing
52
- - Copy-paste code with minor variations
53
- - Duplicate utility functions
32
+ ### A. Reuse (Confirm Before Replacing)
54
33
 
55
- ## 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
47
+
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)
56
65
 
57
- 1. Run \`git diff\` to see what changed
58
- 2. For each change, determine if it's:
59
- - **Essential**: Required for the feature to work
60
- - **Residual**: Left over from development/debugging
61
- - **Legacy**: Old code not touched by this change
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
62
73
 
63
- 3. For each residual item:
74
+ ## How to Analyze
75
+
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:
64
79
  - Identify exact file and line(s)
65
- - Assess risk level
66
- - 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.)
67
82
 
68
83
  ## Risk Levels
69
84
 
70
- - **safe**: Definitely can be deleted (unused, debug code)
71
- - **confirm**: Delete after user confirms (over-engineered, duplicates)
72
- - **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)
73
88
 
74
89
  ## Rules
75
90
 
76
- 1. When in doubt, mark as "confirm" or "review" - don't delete without consent
77
- 2. For inline candidates, show the alternative
78
- 3. Don't flag necessary code just because it's simple
79
- 4. Respect existing abstraction boundaries
80
- 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:
81
97
  - Error handling code
82
98
  - Security-related logic
83
99
  - Code that looks "unused" but is called via reflection/eval
@@ -91,22 +107,24 @@ You may write prose analysis first, but you MUST end your response with a single
91
107
  {
92
108
  "candidates": [
93
109
  {
94
- "risk": "safe",
110
+ "category": "reuse",
111
+ "risk": "confirm",
95
112
  "file": "path/to/file.ext",
96
113
  "lines": "12-15",
97
- "reason": "Why this should be removed",
98
- "action": "delete"
114
+ "reason": "Replace hand-rolled join with existing pathJoin() in src/utils/path.ts",
115
+ "action": "refactor"
99
116
  }
100
117
  ]
101
118
  }
102
119
  \`\`\`
103
120
 
104
121
  Field notes:
122
+ - \`category\` — one of: "reuse", "quality", "efficiency"
105
123
  - \`risk\` — one of: "safe", "confirm", "review"
106
124
  - \`file\` — repository-relative path, no backticks, no markdown
107
125
  - \`lines\` — line number or range ("42" or "42-57"); empty string if unknown
108
- - \`reason\` — single plain-text sentence
109
- - \`action\` — one of: "delete", "inline", "confirm" (appended to reason)
126
+ - \`reason\` — single plain-text sentence stating the concrete fix
127
+ - \`action\` — one of: "delete", "inline", "refactor", "parallelize"
110
128
 
111
129
  If there are no candidates, output \`{"candidates": []}\`. Do NOT output anything after the closing \`\`\` fence.
112
130
  `;
@@ -135,17 +153,33 @@ export default function simplifyExtension(pi: ExtensionAPI) {
135
153
  if (text) assistantTextBuffer += (assistantTextBuffer ? "\n" : "") + text;
136
154
  });
137
155
 
138
- 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"]);
139
159
 
140
- function normalizeRisk(value: unknown): "safe" | "confirm" | "review" {
160
+ function normalizeCategory(value: unknown): Category {
141
161
  const v = String(value ?? "")
142
162
  .toLowerCase()
143
163
  .trim();
144
- return RISK_VALUES.has(v) ? (v as "safe" | "confirm" | "review") : "review";
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
+ }
173
+
174
+ function normalizeAction(value: unknown): Action | null {
175
+ const v = String(value ?? "")
176
+ .toLowerCase()
177
+ .trim();
178
+ return ACTION_VALUES.has(v as Action) ? (v as Action) : null;
145
179
  }
146
180
 
147
181
  function stripMarkdown(text: string): string {
148
- return text.replace(/[`*_]/g, "").trim();
182
+ return text.replace(/[`*]/g, "").trim();
149
183
  }
150
184
 
151
185
  /**
@@ -177,12 +211,15 @@ export default function simplifyExtension(pi: ExtensionAPI) {
177
211
  // Validate path is within repo (no path traversal)
178
212
  if (!file || file.includes("..") || file.startsWith("/")) continue;
179
213
  const reason = stripMarkdown(String(item.reason ?? item.description ?? ""));
180
- const action = item.action ? ` [${stripMarkdown(String(item.action))}]` : "";
214
+ const action = normalizeAction(item.action);
215
+ if (!action) continue;
181
216
  out.push({
217
+ category: normalizeCategory(item.category),
182
218
  file,
183
219
  lines: stripMarkdown(String(item.lines ?? "")),
184
- reason: reason ? reason + action : action.trim() || "(no reason provided)",
220
+ reason: reason || "(no reason provided)",
185
221
  risk: normalizeRisk(item.risk),
222
+ action,
186
223
  });
187
224
  }
188
225
  // A valid candidates array (even empty) is a successful parse
@@ -194,9 +231,6 @@ export default function simplifyExtension(pi: ExtensionAPI) {
194
231
  return null;
195
232
  }
196
233
 
197
- /**
198
- * Show selection dialog for candidates with proper multi-select support
199
- */
200
234
  async function showCandidateSelector(
201
235
  ctx: ExtensionCommandContext,
202
236
  candidates: SimplifyResult[],
@@ -211,9 +245,9 @@ export default function simplifyExtension(pi: ExtensionAPI) {
211
245
  const reviewCandidates = candidates.filter((c) => c.risk === "review");
212
246
 
213
247
  const RISK_CONFIG = {
214
- safe: { label: "Safe to delete", description: "Will be deleted", selected: true },
215
- confirm: { label: "Needs confirmation", description: "Select to delete", selected: false },
216
- review: { label: "Needs review", description: "Review before deleting", selected: false },
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" },
217
251
  } as const;
218
252
 
219
253
  const sections = [
@@ -255,21 +289,16 @@ export default function simplifyExtension(pi: ExtensionAPI) {
255
289
  let cursorPos = 0;
256
290
 
257
291
  const result = await ctx.ui.custom<SimplifyResult[]>((tui, theme, _keybindings, done) => {
258
- // Render the multi-select list manually
259
292
  const renderList = (width: number): string[] => {
260
293
  const lines: string[] = [];
261
294
 
262
- // Calculate which items are visible
263
295
  const visibleItems: { selectableIdx: number | null; item: SelectItem }[] = [];
264
- for (let i = 0; i < displayItems.length; i++) {
265
- const item = displayItems[i];
296
+ let nextSelectableIdx = 0;
297
+ for (const item of displayItems) {
266
298
  if (item.value.startsWith("__section__")) {
267
299
  visibleItems.push({ selectableIdx: null, item });
268
300
  } else {
269
- const selectableIdx = selectableItems.findIndex((si) => si.index === i);
270
- if (selectableIdx >= 0) {
271
- visibleItems.push({ selectableIdx, item: displayItems[i] });
272
- }
301
+ visibleItems.push({ selectableIdx: nextSelectableIdx++, item });
273
302
  }
274
303
  }
275
304
 
@@ -307,7 +336,7 @@ export default function simplifyExtension(pi: ExtensionAPI) {
307
336
  return {
308
337
  render(width: number) {
309
338
  const lines: string[] = [];
310
- lines.push(theme.bold("Simplify: Select items to remove"));
339
+ lines.push(theme.bold("Simplify: Select findings to apply"));
311
340
  lines.push(
312
341
  theme.fg(
313
342
  "muted",
@@ -401,58 +430,44 @@ export default function simplifyExtension(pi: ExtensionAPI) {
401
430
  return result ?? [];
402
431
  }
403
432
 
404
- /**
405
- * Execute the cleanup by sending deletion commands
406
- */
407
- async function executeCleanup(
433
+ async function applyFindings(
408
434
  ctx: ExtensionCommandContext,
409
435
  selected: SimplifyResult[],
410
436
  ): Promise<void> {
411
437
  if (selected.length === 0) {
412
- ctx.ui.notify("No items selected for cleanup", "info");
438
+ ctx.ui.notify("No findings selected to apply", "info");
413
439
  return;
414
440
  }
415
441
 
416
442
  const safeItems = selected.filter((c) => c.risk === "safe");
417
443
  const confirmItems = selected.filter((c) => c.risk === "confirm");
418
444
 
419
- // Build cleanup prompt with verification step
420
- const cleanupPrompt = `# Cleanup Instructions
445
+ const cleanupPrompt = `# Apply Review Findings
421
446
 
422
- 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.
423
448
 
424
- ${selected.map((c) => `- ${c.file}: ${c.reason}`).join("\n")}
449
+ ${selected.map((c) => `- ${c.file} (${c.lines || "?"}): ${c.reason} [${c.action}]`).join("\n")}
425
450
 
426
451
  For each item:
427
452
  1. Read the file to find the exact location
428
- 2. Remove only the specified code (not surrounding code unless instructed)
429
- 3. If the removal affects other code, stop and report the issue
430
- 4. After all deletions, verify the code still works by running any existing tests
431
-
432
- IMPORTANT: After completing deletions:
433
- - Run \`npm test\` or equivalent test command
434
- - If tests fail, report which tests failed and whether it's related to the cleanup
435
- - If there are no tests, at least verify the files parse correctly (e.g., \`node --check\`)
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
436
456
 
437
- Report:
438
- - What was deleted
439
- - Test results (pass/fail)
440
- - Any issues encountered
441
- - 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`;
442
460
 
443
461
  ctx.ui.notify(
444
- `Starting cleanup of ${selected.length} items (${safeItems.length} safe, ${confirmItems.length} confirmed)`,
462
+ `Applying ${selected.length} findings (${safeItems.length} safe, ${confirmItems.length} confirmed)`,
445
463
  "info",
446
464
  );
447
465
 
448
466
  pi.sendUserMessage(cleanupPrompt);
449
467
  }
450
468
 
451
- /**
452
- * Main command handler
453
- */
454
469
  pi.registerCommand("simplify", {
455
- description: "Clean up leftover code (dead code, debug remnants, over-engineering)",
470
+ description: "Review changed code for reuse, quality, and efficiency, then apply fixes",
456
471
  handler: async (args: string, ctx: ExtensionCommandContext) => {
457
472
  if (!ctx.hasUI) {
458
473
  ctx.ui.notify("Simplify requires interactive mode", "error");
@@ -481,7 +496,7 @@ Report:
481
496
  return;
482
497
  }
483
498
 
484
- ctx.ui.notify("Analyzing code for cleanup candidates...", "info");
499
+ ctx.ui.notify("Analyzing code for review findings...", "info");
485
500
 
486
501
  // Build prompt with optional focus
487
502
  let fullPrompt = SIMPLIFY_PROMPT;
@@ -524,35 +539,32 @@ Report:
524
539
 
525
540
  if (candidates === null) {
526
541
  ctx.ui.notify(
527
- "Could not parse cleanup candidates — the model did not return the expected JSON block.",
542
+ "Could not parse review findings — the model did not return the expected JSON block.",
528
543
  "warning",
529
544
  );
530
545
  return;
531
546
  }
532
547
 
533
548
  if (candidates.length === 0) {
534
- ctx.ui.notify("No cleanup candidates found!", "info");
549
+ ctx.ui.notify("No review findings found!", "info");
535
550
  return;
536
551
  }
537
552
 
538
- 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");
539
554
 
540
555
  // Show selector
541
556
  const selected = await showCandidateSelector(ctx, candidates);
542
557
 
543
558
  if (!selected || selected.length === 0) {
544
- ctx.ui.notify("Cleanup cancelled", "info");
559
+ ctx.ui.notify("Apply findings cancelled", "info");
545
560
  return;
546
561
  }
547
562
 
548
- // Execute cleanup
549
- await executeCleanup(ctx, selected);
563
+ // Apply selected findings
564
+ await applyFindings(ctx, selected);
550
565
  },
551
566
  });
552
567
 
553
- /**
554
- * Quick simplify - auto-clean safe items only
555
- */
556
568
  pi.registerCommand("simplify-quick", {
557
569
  description: "Quick cleanup of obviously safe items (debug code, unused exports)",
558
570
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
@@ -593,18 +605,4 @@ Return a summary of what was deleted.`;
593
605
  pi.sendUserMessage(quickPrompt, { deliverAs: "followUp" });
594
606
  },
595
607
  });
596
-
597
- /**
598
- * Status indicator on startup
599
- */
600
- /**pi.on("session_start", async (_event, ctx) => {
601
- const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
602
- if (code === 0) {
603
- ctx.ui.setStatus(
604
- "simplify",
605
- `${ctx.ui.theme.fg("accent", "simplify")} ${ctx.ui.theme.fg("muted", "ready (try /simplify)")}`,
606
- );
607
- }
608
- });
609
- */
610
608
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@geminixiang/pi-simplify",
3
- "version": "0.0.5",
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
  ],