@geminixiang/pi-simplify 0.0.6 → 0.0.8
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/.pi/skills/release/SKILL.md +146 -0
- package/CHANGELOG.md +35 -0
- package/README.md +6 -11
- package/index.ts +3 -595
- package/package.json +4 -2
- package/prompt.ts +93 -0
- package/selector.ts +176 -0
- package/types.ts +12 -0
- package/workflow.ts +394 -0
package/index.ts
CHANGED
|
@@ -7,602 +7,10 @@
|
|
|
7
7
|
* - Efficiency: redundant work, missed concurrency, hot-path bloat, memory leaks
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ExtensionAPI
|
|
11
|
-
import { matchesKey, Key, truncateToWidth, type SelectItem } from "@mariozechner/pi-tui";
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
type Risk = "safe" | "confirm" | "review";
|
|
15
|
-
type Action = "delete" | "inline" | "refactor" | "parallelize";
|
|
16
|
-
|
|
17
|
-
type SimplifyResult = {
|
|
18
|
-
category: Category;
|
|
19
|
-
file: string;
|
|
20
|
-
lines: string;
|
|
21
|
-
reason: string;
|
|
22
|
-
risk: Risk;
|
|
23
|
-
action: Action;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const SIMPLIFY_PROMPT = `# Simplify: Review Changed Code for Reuse, Quality, and Efficiency
|
|
27
|
-
|
|
28
|
-
You are a code review assistant. Review the changed code along three axes — **Reuse**, **Quality**, and **Efficiency** — and surface concrete fixes.
|
|
29
|
-
|
|
30
|
-
## What to Find
|
|
31
|
-
|
|
32
|
-
### A. Reuse (Confirm Before Replacing)
|
|
33
|
-
|
|
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)
|
|
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
|
|
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:
|
|
79
|
-
- Identify exact file and line(s)
|
|
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.)
|
|
82
|
-
|
|
83
|
-
## Risk Levels
|
|
84
|
-
|
|
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)
|
|
88
|
-
|
|
89
|
-
## Rules
|
|
90
|
-
|
|
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:
|
|
97
|
-
- Error handling code
|
|
98
|
-
- Security-related logic
|
|
99
|
-
- Code that looks "unused" but is called via reflection/eval
|
|
100
|
-
- Database migration files
|
|
101
|
-
|
|
102
|
-
## Output Format (REQUIRED)
|
|
103
|
-
|
|
104
|
-
You may write prose analysis first, but you MUST end your response with a single fenced JSON block containing ALL candidates. This JSON is parsed programmatically — it must be valid JSON and use this exact shape:
|
|
105
|
-
|
|
106
|
-
\`\`\`json
|
|
107
|
-
{
|
|
108
|
-
"candidates": [
|
|
109
|
-
{
|
|
110
|
-
"category": "reuse",
|
|
111
|
-
"risk": "confirm",
|
|
112
|
-
"file": "path/to/file.ext",
|
|
113
|
-
"lines": "12-15",
|
|
114
|
-
"reason": "Replace hand-rolled join with existing pathJoin() in src/utils/path.ts",
|
|
115
|
-
"action": "refactor"
|
|
116
|
-
}
|
|
117
|
-
]
|
|
118
|
-
}
|
|
119
|
-
\`\`\`
|
|
120
|
-
|
|
121
|
-
Field notes:
|
|
122
|
-
- \`category\` — one of: "reuse", "quality", "efficiency"
|
|
123
|
-
- \`risk\` — one of: "safe", "confirm", "review"
|
|
124
|
-
- \`file\` — repository-relative path, no backticks, no markdown
|
|
125
|
-
- \`lines\` — line number or range ("42" or "42-57"); empty string if unknown
|
|
126
|
-
- \`reason\` — single plain-text sentence stating the concrete fix
|
|
127
|
-
- \`action\` — one of: "delete", "inline", "refactor", "parallelize"
|
|
128
|
-
|
|
129
|
-
If there are no candidates, output \`{"candidates": []}\`. Do NOT output anything after the closing \`\`\` fence.
|
|
130
|
-
`;
|
|
12
|
+
import { registerSimplifyWorkflow } from "./workflow.js";
|
|
131
13
|
|
|
132
14
|
export default function simplifyExtension(pi: ExtensionAPI) {
|
|
133
|
-
|
|
134
|
-
// the AI makes tool calls). Overwriting would drop the JSON block if it
|
|
135
|
-
// appears in an earlier message and a later one adds a short confirmation.
|
|
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
|
-
|
|
146
|
-
pi.on("message_end", (event) => {
|
|
147
|
-
const msg = event.message;
|
|
148
|
-
if (msg.role !== "assistant" || !Array.isArray(msg.content)) return;
|
|
149
|
-
const text = (msg.content as { type: string; text?: string }[])
|
|
150
|
-
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
151
|
-
.map((b) => b.text as string)
|
|
152
|
-
.join("\n");
|
|
153
|
-
if (text) assistantTextBuffer += (assistantTextBuffer ? "\n" : "") + text;
|
|
154
|
-
});
|
|
155
|
-
|
|
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
|
-
}
|
|
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;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function stripMarkdown(text: string): string {
|
|
182
|
-
return text.replace(/[`*]/g, "").trim();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Try to parse a JSON candidates block. The prompt requires this format,
|
|
187
|
-
* but we tolerate the AI wrapping it in ```json ... ``` or ``` ... ```,
|
|
188
|
-
* and also handle the case where it embeds the JSON inline without fences.
|
|
189
|
-
* Returns null only when no valid JSON with a `candidates` array is found —
|
|
190
|
-
* a valid empty list returns [] (which means "the AI found nothing").
|
|
191
|
-
*/
|
|
192
|
-
function parseCandidatesJSON(response: string): SimplifyResult[] | null {
|
|
193
|
-
const fenceRe = /```(?:json)?\s*\n([\s\S]*?)\n```/gi;
|
|
194
|
-
const blocks: string[] = [];
|
|
195
|
-
let m: RegExpExecArray | null;
|
|
196
|
-
while ((m = fenceRe.exec(response)) !== null) blocks.push(m[1]);
|
|
197
|
-
|
|
198
|
-
const bareMatch = response.match(/\{[\s\S]*"candidates"[\s\S]*\}/);
|
|
199
|
-
if (bareMatch) blocks.push(bareMatch[0]);
|
|
200
|
-
|
|
201
|
-
// Walk newest-first: the contract puts the JSON at the end of the response.
|
|
202
|
-
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
203
|
-
try {
|
|
204
|
-
const parsed = JSON.parse(blocks[i]);
|
|
205
|
-
const list = parsed?.candidates;
|
|
206
|
-
if (!Array.isArray(list)) continue;
|
|
207
|
-
const out: SimplifyResult[] = [];
|
|
208
|
-
for (const item of list) {
|
|
209
|
-
if (!item || typeof item !== "object") continue;
|
|
210
|
-
const file = stripMarkdown(String(item.file ?? item.location ?? ""));
|
|
211
|
-
// Validate path is within repo (no path traversal)
|
|
212
|
-
if (!file || file.includes("..") || file.startsWith("/")) continue;
|
|
213
|
-
const reason = stripMarkdown(String(item.reason ?? item.description ?? ""));
|
|
214
|
-
const action = normalizeAction(item.action);
|
|
215
|
-
if (!action) continue;
|
|
216
|
-
out.push({
|
|
217
|
-
category: normalizeCategory(item.category),
|
|
218
|
-
file,
|
|
219
|
-
lines: stripMarkdown(String(item.lines ?? "")),
|
|
220
|
-
reason: reason || "(no reason provided)",
|
|
221
|
-
risk: normalizeRisk(item.risk),
|
|
222
|
-
action,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
// A valid candidates array (even empty) is a successful parse
|
|
226
|
-
return out;
|
|
227
|
-
} catch {
|
|
228
|
-
// try next block
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function showCandidateSelector(
|
|
235
|
-
ctx: ExtensionCommandContext,
|
|
236
|
-
candidates: SimplifyResult[],
|
|
237
|
-
): Promise<SimplifyResult[]> {
|
|
238
|
-
if (candidates.length === 0) {
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Categorize by risk level for display
|
|
243
|
-
const safeCandidates = candidates.filter((c) => c.risk === "safe");
|
|
244
|
-
const confirmCandidates = candidates.filter((c) => c.risk === "confirm");
|
|
245
|
-
const reviewCandidates = candidates.filter((c) => c.risk === "review");
|
|
246
|
-
|
|
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
|
-
|
|
261
|
-
const displayItems: SelectItem[] = [];
|
|
262
|
-
const selectableItems: { index: number; candidate: SimplifyResult }[] = [];
|
|
263
|
-
let globalIndex = 0;
|
|
264
|
-
|
|
265
|
-
for (const section of sections) {
|
|
266
|
-
displayItems.push({
|
|
267
|
-
value: `__section__${section.key}__`,
|
|
268
|
-
label: `── ${section.config.label} (${section.total}) ──`,
|
|
269
|
-
description: "",
|
|
270
|
-
});
|
|
271
|
-
globalIndex++;
|
|
272
|
-
for (const c of section.items) {
|
|
273
|
-
displayItems.push({
|
|
274
|
-
value: JSON.stringify(c),
|
|
275
|
-
label: `${c.file} - ${c.reason}`,
|
|
276
|
-
description: section.config.description,
|
|
277
|
-
});
|
|
278
|
-
selectableItems.push({ index: globalIndex++, candidate: c });
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Pre-select all safe items
|
|
283
|
-
const selectedSet = new Set<number>(
|
|
284
|
-
safeCandidates.length > 0 ? Array.from({ length: safeCandidates.length }, (_, i) => i) : [],
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
const maxVisible = 12;
|
|
288
|
-
let scrollOffset = 0;
|
|
289
|
-
let cursorPos = 0;
|
|
290
|
-
|
|
291
|
-
const result = await ctx.ui.custom<SimplifyResult[]>((tui, theme, _keybindings, done) => {
|
|
292
|
-
const renderList = (width: number): string[] => {
|
|
293
|
-
const lines: string[] = [];
|
|
294
|
-
|
|
295
|
-
const visibleItems: { selectableIdx: number | null; item: SelectItem }[] = [];
|
|
296
|
-
let nextSelectableIdx = 0;
|
|
297
|
-
for (const item of displayItems) {
|
|
298
|
-
if (item.value.startsWith("__section__")) {
|
|
299
|
-
visibleItems.push({ selectableIdx: null, item });
|
|
300
|
-
} else {
|
|
301
|
-
visibleItems.push({ selectableIdx: nextSelectableIdx++, item });
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Show visible range with scroll
|
|
306
|
-
const startIdx = Math.max(0, scrollOffset);
|
|
307
|
-
const endIdx = Math.min(visibleItems.length, startIdx + maxVisible);
|
|
308
|
-
|
|
309
|
-
for (let vIdx = startIdx; vIdx < endIdx; vIdx++) {
|
|
310
|
-
const { selectableIdx, item } = visibleItems[vIdx];
|
|
311
|
-
|
|
312
|
-
if (item.value.startsWith("__section__")) {
|
|
313
|
-
// Section header
|
|
314
|
-
lines.push(theme.fg("accent", item.label));
|
|
315
|
-
} else {
|
|
316
|
-
// Selectable item
|
|
317
|
-
const isCursor = selectableIdx === cursorPos;
|
|
318
|
-
const isSelected = selectableIdx !== null && selectedSet.has(selectableIdx);
|
|
319
|
-
const prefix = isCursor ? theme.fg("accent", "> ") : " ";
|
|
320
|
-
const checkbox = isSelected ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
|
|
321
|
-
const truncatedLabel = truncateToWidth(item.label, width - 6);
|
|
322
|
-
const line = `${prefix}${checkbox} ${truncatedLabel}`;
|
|
323
|
-
lines.push(isCursor ? theme.fg("accent", line) : line);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Scroll indicator
|
|
328
|
-
if (visibleItems.length > maxVisible) {
|
|
329
|
-
const scrollInfo = ` (${startIdx + 1}-${endIdx}/${visibleItems.length})`;
|
|
330
|
-
lines.push(theme.fg("dim", scrollInfo));
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return lines;
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
return {
|
|
337
|
-
render(width: number) {
|
|
338
|
-
const lines: string[] = [];
|
|
339
|
-
lines.push(theme.bold("Simplify: Select findings to apply"));
|
|
340
|
-
lines.push(
|
|
341
|
-
theme.fg(
|
|
342
|
-
"muted",
|
|
343
|
-
`Found ${candidates.length} candidates (${safeCandidates.length} safe, ${confirmCandidates.length} confirm, ${reviewCandidates.length} review)`,
|
|
344
|
-
),
|
|
345
|
-
);
|
|
346
|
-
lines.push("");
|
|
347
|
-
lines.push(...renderList(width));
|
|
348
|
-
lines.push("");
|
|
349
|
-
lines.push(
|
|
350
|
-
theme.fg(
|
|
351
|
-
"muted",
|
|
352
|
-
`Selected: ${selectedSet.size} • ↑↓ navigate • space toggle • a select all • enter confirm • esc cancel`,
|
|
353
|
-
),
|
|
354
|
-
);
|
|
355
|
-
return lines;
|
|
356
|
-
},
|
|
357
|
-
invalidate() {},
|
|
358
|
-
handleInput(data: string) {
|
|
359
|
-
if (matchesKey(data, Key.enter)) {
|
|
360
|
-
const results = Array.from(selectedSet).map((i) => selectableItems[i].candidate);
|
|
361
|
-
done(results);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (matchesKey(data, Key.escape)) {
|
|
365
|
-
done([]);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
if (matchesKey(data, Key.space)) {
|
|
369
|
-
// Toggle current selection
|
|
370
|
-
if (selectedSet.has(cursorPos)) {
|
|
371
|
-
selectedSet.delete(cursorPos);
|
|
372
|
-
} else {
|
|
373
|
-
selectedSet.add(cursorPos);
|
|
374
|
-
}
|
|
375
|
-
tui.requestRender();
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
if (matchesKey(data, "a")) {
|
|
379
|
-
// Select/deselect all
|
|
380
|
-
if (selectedSet.size === selectableItems.length) {
|
|
381
|
-
selectedSet.clear();
|
|
382
|
-
} else {
|
|
383
|
-
for (let i = 0; i < selectableItems.length; i++) {
|
|
384
|
-
selectedSet.add(i);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
tui.requestRender();
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Navigation
|
|
392
|
-
if (matchesKey(data, Key.down)) {
|
|
393
|
-
cursorPos = Math.min(selectableItems.length - 1, cursorPos + 1);
|
|
394
|
-
// Ensure cursor is within visible window
|
|
395
|
-
const displayIdx = selectableItems[cursorPos].index;
|
|
396
|
-
if (displayIdx >= scrollOffset + maxVisible) {
|
|
397
|
-
scrollOffset = displayIdx - maxVisible + 1;
|
|
398
|
-
}
|
|
399
|
-
tui.requestRender();
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
if (matchesKey(data, Key.up)) {
|
|
403
|
-
cursorPos = Math.max(0, cursorPos - 1);
|
|
404
|
-
// Ensure cursor is within visible window
|
|
405
|
-
const displayIdx = selectableItems[cursorPos].index;
|
|
406
|
-
if (displayIdx < scrollOffset) {
|
|
407
|
-
// Scroll up to show the item, and include section header if it's just above
|
|
408
|
-
scrollOffset = Math.max(0, displayIdx - 1);
|
|
409
|
-
}
|
|
410
|
-
tui.requestRender();
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
if (matchesKey(data, Key.home)) {
|
|
414
|
-
cursorPos = 0;
|
|
415
|
-
scrollOffset = 0;
|
|
416
|
-
tui.requestRender();
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (matchesKey(data, Key.end)) {
|
|
420
|
-
cursorPos = selectableItems.length - 1;
|
|
421
|
-
const lastDisplayIdx = selectableItems[cursorPos].index;
|
|
422
|
-
scrollOffset = Math.max(0, lastDisplayIdx - maxVisible + 1);
|
|
423
|
-
tui.requestRender();
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
return result ?? [];
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
async function applyFindings(
|
|
434
|
-
ctx: ExtensionCommandContext,
|
|
435
|
-
selected: SimplifyResult[],
|
|
436
|
-
): Promise<void> {
|
|
437
|
-
if (selected.length === 0) {
|
|
438
|
-
ctx.ui.notify("No findings selected to apply", "info");
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const safeItems = selected.filter((c) => c.risk === "safe");
|
|
443
|
-
const confirmItems = selected.filter((c) => c.risk === "confirm");
|
|
444
|
-
|
|
445
|
-
const cleanupPrompt = `# Apply Review Findings
|
|
446
|
-
|
|
447
|
-
Apply the following findings. Each item includes a bracketed action (e.g. \`[delete]\`, \`[refactor]\`, \`[parallelize]\`, \`[inline]\`) — follow that action, not a blanket delete.
|
|
448
|
-
|
|
449
|
-
${selected.map((c) => `- ${c.file} (${c.lines || "?"}): ${c.reason} [${c.action}]`).join("\n")}
|
|
450
|
-
|
|
451
|
-
For each item:
|
|
452
|
-
1. Read the file to find the exact location
|
|
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
|
|
456
|
-
|
|
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`;
|
|
460
|
-
|
|
461
|
-
ctx.ui.notify(
|
|
462
|
-
`Applying ${selected.length} findings (${safeItems.length} safe, ${confirmItems.length} confirmed)`,
|
|
463
|
-
"info",
|
|
464
|
-
);
|
|
465
|
-
|
|
466
|
-
pi.sendUserMessage(cleanupPrompt);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
pi.registerCommand("simplify", {
|
|
470
|
-
description: "Review changed code for reuse, quality, and efficiency, then apply fixes",
|
|
471
|
-
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
472
|
-
if (!ctx.hasUI) {
|
|
473
|
-
ctx.ui.notify("Simplify requires interactive mode", "error");
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (!ctx.isIdle()) {
|
|
478
|
-
ctx.ui.notify(
|
|
479
|
-
"Cannot run simplify while agent is busy. Wait for current task to complete.",
|
|
480
|
-
"warning",
|
|
481
|
-
);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Check if in git repo
|
|
486
|
-
const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
|
|
487
|
-
if (code !== 0) {
|
|
488
|
-
ctx.ui.notify("Not a git repository", "error");
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Check if there are changes
|
|
493
|
-
const { stdout: gitStatus } = await pi.exec("git", ["status", "--porcelain"]);
|
|
494
|
-
if (!gitStatus.trim()) {
|
|
495
|
-
ctx.ui.notify("No git changes found. Simplify reviews uncommitted changes.", "info");
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
ctx.ui.notify("Analyzing code for review findings...", "info");
|
|
500
|
-
|
|
501
|
-
// Build prompt with optional focus
|
|
502
|
-
let fullPrompt = SIMPLIFY_PROMPT;
|
|
503
|
-
if (args.trim()) {
|
|
504
|
-
fullPrompt += `\n\n## Additional Focus\n\n${args.trim()}`;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Reset capture buffer, then send prompt and wait.
|
|
508
|
-
// message_end events append to assistantTextBuffer during the turn.
|
|
509
|
-
//
|
|
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.
|
|
513
|
-
assistantTextBuffer = "";
|
|
514
|
-
const turnStarted = new Promise<void>((r) => {
|
|
515
|
-
resolvePendingTurnStart = r;
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
pi.sendUserMessage(fullPrompt);
|
|
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
|
-
|
|
530
|
-
await ctx.waitForIdle();
|
|
531
|
-
const analysisText = assistantTextBuffer;
|
|
532
|
-
|
|
533
|
-
if (!analysisText) {
|
|
534
|
-
ctx.ui.notify("Could not read analysis — no response captured.", "error");
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const candidates = parseCandidatesJSON(analysisText);
|
|
539
|
-
|
|
540
|
-
if (candidates === null) {
|
|
541
|
-
ctx.ui.notify(
|
|
542
|
-
"Could not parse review findings — the model did not return the expected JSON block.",
|
|
543
|
-
"warning",
|
|
544
|
-
);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (candidates.length === 0) {
|
|
549
|
-
ctx.ui.notify("No review findings found!", "info");
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
ctx.ui.notify(`Found ${candidates.length} findings. Select findings to apply...`, "info");
|
|
554
|
-
|
|
555
|
-
// Show selector
|
|
556
|
-
const selected = await showCandidateSelector(ctx, candidates);
|
|
557
|
-
|
|
558
|
-
if (!selected || selected.length === 0) {
|
|
559
|
-
ctx.ui.notify("Apply findings cancelled", "info");
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Apply selected findings
|
|
564
|
-
await applyFindings(ctx, selected);
|
|
565
|
-
},
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
pi.registerCommand("simplify-quick", {
|
|
569
|
-
description: "Quick cleanup of obviously safe items (debug code, unused exports)",
|
|
570
|
-
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
571
|
-
if (!ctx.hasUI) {
|
|
572
|
-
ctx.ui.notify("Simplify requires interactive mode", "error");
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (!ctx.isIdle()) {
|
|
577
|
-
ctx.ui.notify("Agent is busy. Try again after current task.", "warning");
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const quickPrompt = `# Quick Cleanup
|
|
582
|
-
|
|
583
|
-
Find and delete ONLY the most obviously safe cleanup items:
|
|
584
|
-
|
|
585
|
-
1. \`console.log\`, \`console.warn\`, \`console.error\` statements
|
|
586
|
-
2. \`debugger\` statements
|
|
587
|
-
3. Unused \`import\` statements (imported but never used)
|
|
588
|
-
4. Empty \`catch\` blocks (just \`catch (e) {}\`)
|
|
589
|
-
5. Unused \`const\`/\`let\`/\`var\` declarations
|
|
590
|
-
|
|
591
|
-
DO NOT delete:
|
|
592
|
-
- Commented-out code
|
|
593
|
-
- Over-engineered abstractions
|
|
594
|
-
- Duplicate logic
|
|
595
|
-
- Anything that might be needed
|
|
596
|
-
|
|
597
|
-
For each item found:
|
|
598
|
-
1. Confirm it's truly unused by searching for references
|
|
599
|
-
2. Delete only the specific lines
|
|
600
|
-
3. Report what was deleted
|
|
601
|
-
|
|
602
|
-
Return a summary of what was deleted.`;
|
|
603
|
-
|
|
604
|
-
ctx.ui.notify("Running quick cleanup...", "info");
|
|
605
|
-
pi.sendUserMessage(quickPrompt, { deliverAs: "followUp" });
|
|
606
|
-
},
|
|
607
|
-
});
|
|
15
|
+
registerSimplifyWorkflow(pi);
|
|
608
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geminixiang/pi-simplify",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Simplify: Review changed code for reuse, quality, and efficiency",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"fmt:check": "oxfmt --check"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@
|
|
22
|
+
"@earendil-works/pi-coding-agent": ">=0.74.0",
|
|
23
|
+
"@earendil-works/pi-tui": ">=0.74.0",
|
|
24
|
+
"typebox": "^1.1.38"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
25
27
|
"oxfmt": "^0.45.0",
|