mitsupi 1.0.0
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/LICENSE +201 -0
- package/README.md +95 -0
- package/TODO.md +11 -0
- package/commands/handoff.md +100 -0
- package/commands/make-release.md +75 -0
- package/commands/pickup.md +30 -0
- package/commands/update-changelog.md +78 -0
- package/package.json +22 -0
- package/pi-extensions/answer.ts +527 -0
- package/pi-extensions/codex-tuning.ts +632 -0
- package/pi-extensions/commit.ts +248 -0
- package/pi-extensions/cwd-history.ts +237 -0
- package/pi-extensions/issues.ts +548 -0
- package/pi-extensions/loop.ts +446 -0
- package/pi-extensions/qna.ts +167 -0
- package/pi-extensions/reveal.ts +689 -0
- package/pi-extensions/review.ts +807 -0
- package/pi-themes/armin.json +81 -0
- package/pi-themes/nightowl.json +82 -0
- package/skills/anachb/SKILL.md +183 -0
- package/skills/anachb/departures.sh +79 -0
- package/skills/anachb/disruptions.sh +53 -0
- package/skills/anachb/route.sh +87 -0
- package/skills/anachb/search.sh +43 -0
- package/skills/ghidra/SKILL.md +254 -0
- package/skills/ghidra/scripts/find-ghidra.sh +54 -0
- package/skills/ghidra/scripts/ghidra-analyze.sh +239 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportAll.java +278 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportCalls.java +148 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportDecompiled.java +84 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportFunctions.java +114 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportStrings.java +123 -0
- package/skills/ghidra/scripts/ghidra_scripts/ExportSymbols.java +135 -0
- package/skills/github/SKILL.md +47 -0
- package/skills/improve-skill/SKILL.md +155 -0
- package/skills/improve-skill/scripts/extract-session.js +349 -0
- package/skills/oebb-scotty/SKILL.md +429 -0
- package/skills/oebb-scotty/arrivals.sh +83 -0
- package/skills/oebb-scotty/departures.sh +83 -0
- package/skills/oebb-scotty/disruptions.sh +33 -0
- package/skills/oebb-scotty/search-station.sh +36 -0
- package/skills/oebb-scotty/trip.sh +119 -0
- package/skills/openscad/SKILL.md +232 -0
- package/skills/openscad/examples/parametric_box.scad +92 -0
- package/skills/openscad/examples/phone_stand.scad +95 -0
- package/skills/openscad/tools/common.sh +50 -0
- package/skills/openscad/tools/export-stl.sh +56 -0
- package/skills/openscad/tools/extract-params.sh +147 -0
- package/skills/openscad/tools/multi-preview.sh +68 -0
- package/skills/openscad/tools/preview.sh +74 -0
- package/skills/openscad/tools/render-with-params.sh +91 -0
- package/skills/openscad/tools/validate.sh +46 -0
- package/skills/pi-share/SKILL.md +105 -0
- package/skills/pi-share/fetch-session.mjs +322 -0
- package/skills/sentry/SKILL.md +239 -0
- package/skills/sentry/lib/auth.js +99 -0
- package/skills/sentry/scripts/fetch-event.js +329 -0
- package/skills/sentry/scripts/fetch-issue.js +356 -0
- package/skills/sentry/scripts/list-issues.js +239 -0
- package/skills/sentry/scripts/search-events.js +291 -0
- package/skills/sentry/scripts/search-logs.js +240 -0
- package/skills/tmux/SKILL.md +105 -0
- package/skills/tmux/scripts/find-sessions.sh +112 -0
- package/skills/tmux/scripts/wait-for-text.sh +83 -0
- package/skills/web-browser/SKILL.md +91 -0
- package/skills/web-browser/scripts/cdp.js +210 -0
- package/skills/web-browser/scripts/dismiss-cookies.js +373 -0
- package/skills/web-browser/scripts/eval.js +68 -0
- package/skills/web-browser/scripts/logs-tail.js +69 -0
- package/skills/web-browser/scripts/nav.js +65 -0
- package/skills/web-browser/scripts/net-summary.js +94 -0
- package/skills/web-browser/scripts/package-lock.json +33 -0
- package/skills/web-browser/scripts/package.json +6 -0
- package/skills/web-browser/scripts/pick.js +165 -0
- package/skills/web-browser/scripts/screenshot.js +52 -0
- package/skills/web-browser/scripts/start.js +80 -0
- package/skills/web-browser/scripts/watch.js +266 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Review Extension (inspired by Codex's review feature)
|
|
3
|
+
*
|
|
4
|
+
* Provides a `/review` command that prompts the agent to review code changes.
|
|
5
|
+
* Supports multiple review modes:
|
|
6
|
+
* - Review against a base branch (PR style)
|
|
7
|
+
* - Review uncommitted changes
|
|
8
|
+
* - Review a specific commit
|
|
9
|
+
* - Custom review instructions
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* - `/review` - show interactive selector
|
|
13
|
+
* - `/review uncommitted` - review uncommitted changes directly
|
|
14
|
+
* - `/review branch main` - review against main branch
|
|
15
|
+
* - `/review commit abc123` - review specific commit
|
|
16
|
+
* - `/review custom "check for security issues"` - custom instructions
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
20
|
+
import { DynamicBorder, BorderedLoader } from "@mariozechner/pi-coding-agent";
|
|
21
|
+
import { Container, type SelectItem, SelectList, Text, Key } from "@mariozechner/pi-tui";
|
|
22
|
+
|
|
23
|
+
// State to track fresh session review (where we branched from).
|
|
24
|
+
// Module-level state means only one review can be active at a time.
|
|
25
|
+
// This is intentional - the UI and /end-review command assume a single active review.
|
|
26
|
+
let reviewOriginId: string | undefined = undefined;
|
|
27
|
+
|
|
28
|
+
// Review target types (matching Codex's approach)
|
|
29
|
+
type ReviewTarget =
|
|
30
|
+
| { type: "uncommitted" }
|
|
31
|
+
| { type: "baseBranch"; branch: string }
|
|
32
|
+
| { type: "commit"; sha: string; title?: string }
|
|
33
|
+
| { type: "custom"; instructions: string };
|
|
34
|
+
|
|
35
|
+
// Prompts (adapted from Codex)
|
|
36
|
+
const UNCOMMITTED_PROMPT =
|
|
37
|
+
"Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.";
|
|
38
|
+
|
|
39
|
+
const BASE_BRANCH_PROMPT_WITH_MERGE_BASE =
|
|
40
|
+
"Review the code changes against the base branch '{baseBranch}'. The merge base commit for this comparison is {mergeBaseSha}. Run `git diff {mergeBaseSha}` to inspect the changes relative to {baseBranch}. Provide prioritized, actionable findings.";
|
|
41
|
+
|
|
42
|
+
const BASE_BRANCH_PROMPT_FALLBACK =
|
|
43
|
+
"Review the code changes against the base branch '{branch}'. Start by finding the merge diff between the current branch and {branch}'s upstream e.g. (`git merge-base HEAD \"$(git rev-parse --abbrev-ref \"{branch}@{upstream}\")\"`), then run `git diff` against that SHA to see what changes we would merge into the {branch} branch. Provide prioritized, actionable findings.";
|
|
44
|
+
|
|
45
|
+
const COMMIT_PROMPT_WITH_TITLE =
|
|
46
|
+
'Review the code changes introduced by commit {sha} ("{title}"). Provide prioritized, actionable findings.';
|
|
47
|
+
|
|
48
|
+
const COMMIT_PROMPT = "Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings.";
|
|
49
|
+
|
|
50
|
+
// The detailed review rubric (adapted from Codex's review_prompt.md)
|
|
51
|
+
const REVIEW_RUBRIC = `# Review Guidelines
|
|
52
|
+
|
|
53
|
+
You are acting as a code reviewer for a proposed code change.
|
|
54
|
+
|
|
55
|
+
## Determining what to flag
|
|
56
|
+
|
|
57
|
+
Flag issues that:
|
|
58
|
+
1. Meaningfully impact the accuracy, performance, security, or maintainability of the code.
|
|
59
|
+
2. Are discrete and actionable (not general issues or multiple combined issues).
|
|
60
|
+
3. Don't demand rigor inconsistent with the rest of the codebase.
|
|
61
|
+
4. Were introduced in the changes being reviewed (not pre-existing bugs).
|
|
62
|
+
5. The author would likely fix if aware of them.
|
|
63
|
+
6. Don't rely on unstated assumptions about the codebase or author's intent.
|
|
64
|
+
7. Have provable impact on other parts of the code (not speculation).
|
|
65
|
+
8. Are clearly not intentional changes by the author.
|
|
66
|
+
9. Be particularly careful with untrusted user input and follow the specific guidelines to review.
|
|
67
|
+
|
|
68
|
+
## Untrusted User Input
|
|
69
|
+
|
|
70
|
+
1. Be careful with open redirects, they must always be checked to only go to trusted domains (?next_page=...)
|
|
71
|
+
2. Always flag SQL that is not parametrized
|
|
72
|
+
3. In systems with user supplied URL input, http fetches always need to be protected against access to local resources (intercept DNS resolver!)
|
|
73
|
+
4. Escape, don't sanitize if you have the option (eg: HTML escaping)
|
|
74
|
+
|
|
75
|
+
## Comment guidelines
|
|
76
|
+
|
|
77
|
+
1. Be clear about why the issue is a problem.
|
|
78
|
+
2. Communicate severity appropriately - don't exaggerate.
|
|
79
|
+
3. Be brief - at most 1 paragraph.
|
|
80
|
+
4. Keep code snippets under 3 lines, wrapped in inline code or code blocks.
|
|
81
|
+
5. Explicitly state scenarios/environments where the issue arises.
|
|
82
|
+
6. Use a matter-of-fact tone - helpful AI assistant, not accusatory.
|
|
83
|
+
7. Write for quick comprehension without close reading.
|
|
84
|
+
8. Avoid excessive flattery or unhelpful phrases like "Great job...".
|
|
85
|
+
|
|
86
|
+
## Review priorities
|
|
87
|
+
|
|
88
|
+
1. Call out newly added dependencies explicitly and explain why they're needed.
|
|
89
|
+
2. Prefer simple, direct solutions over wrappers or abstractions without clear value.
|
|
90
|
+
3. Favor fail-fast behavior; avoid logging-and-continue patterns that hide errors.
|
|
91
|
+
4. Prefer predictable production behavior; crashing is better than silent degradation.
|
|
92
|
+
5. Treat back pressure handling as critical to system stability.
|
|
93
|
+
6. Apply system-level thinking; flag changes that increase operational risk or on-call wakeups.
|
|
94
|
+
7. Ensure that errors are always checked against codes or stable identifiers, never error messages.
|
|
95
|
+
|
|
96
|
+
## Priority levels
|
|
97
|
+
|
|
98
|
+
Tag each finding with a priority level in the title:
|
|
99
|
+
- [P0] - Drop everything to fix. Blocking release/operations. Only for universal issues.
|
|
100
|
+
- [P1] - Urgent. Should be addressed in the next cycle.
|
|
101
|
+
- [P2] - Normal. To be fixed eventually.
|
|
102
|
+
- [P3] - Low. Nice to have.
|
|
103
|
+
|
|
104
|
+
## Output format
|
|
105
|
+
|
|
106
|
+
Provide your findings in a clear, structured format:
|
|
107
|
+
1. List each finding with its priority tag, file location, and explanation.
|
|
108
|
+
2. Keep line references as short as possible (avoid ranges over 5-10 lines).
|
|
109
|
+
3. At the end, provide an overall verdict: "correct" (no blocking issues) or "needs attention" (has blocking issues).
|
|
110
|
+
4. Ignore trivial style issues unless they obscure meaning or violate documented standards.
|
|
111
|
+
|
|
112
|
+
Output all findings the author would fix if they knew about them. If there are no qualifying findings, explicitly state the code looks good. Don't stop at the first finding - list every qualifying issue.`;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get the merge base between HEAD and a branch
|
|
116
|
+
*/
|
|
117
|
+
async function getMergeBase(
|
|
118
|
+
pi: ExtensionAPI,
|
|
119
|
+
branch: string,
|
|
120
|
+
): Promise<string | null> {
|
|
121
|
+
try {
|
|
122
|
+
// First try to get the upstream tracking branch
|
|
123
|
+
const { stdout: upstream, code: upstreamCode } = await pi.exec("git", [
|
|
124
|
+
"rev-parse",
|
|
125
|
+
"--abbrev-ref",
|
|
126
|
+
`${branch}@{upstream}`,
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
if (upstreamCode === 0 && upstream.trim()) {
|
|
130
|
+
const { stdout: mergeBase, code } = await pi.exec("git", ["merge-base", "HEAD", upstream.trim()]);
|
|
131
|
+
if (code === 0 && mergeBase.trim()) {
|
|
132
|
+
return mergeBase.trim();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Fall back to using the branch directly
|
|
137
|
+
const { stdout: mergeBase, code } = await pi.exec("git", ["merge-base", "HEAD", branch]);
|
|
138
|
+
if (code === 0 && mergeBase.trim()) {
|
|
139
|
+
return mergeBase.trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get list of local branches
|
|
150
|
+
*/
|
|
151
|
+
async function getLocalBranches(pi: ExtensionAPI): Promise<string[]> {
|
|
152
|
+
const { stdout, code } = await pi.exec("git", ["branch", "--format=%(refname:short)"]);
|
|
153
|
+
if (code !== 0) return [];
|
|
154
|
+
return stdout
|
|
155
|
+
.trim()
|
|
156
|
+
.split("\n")
|
|
157
|
+
.filter((b) => b.trim());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get list of recent commits
|
|
162
|
+
*/
|
|
163
|
+
async function getRecentCommits(pi: ExtensionAPI, limit: number = 10): Promise<Array<{ sha: string; title: string }>> {
|
|
164
|
+
const { stdout, code } = await pi.exec("git", ["log", `--oneline`, `-n`, `${limit}`]);
|
|
165
|
+
if (code !== 0) return [];
|
|
166
|
+
|
|
167
|
+
return stdout
|
|
168
|
+
.trim()
|
|
169
|
+
.split("\n")
|
|
170
|
+
.filter((line) => line.trim())
|
|
171
|
+
.map((line) => {
|
|
172
|
+
const [sha, ...rest] = line.trim().split(" ");
|
|
173
|
+
return { sha, title: rest.join(" ") };
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if there are uncommitted changes (staged, unstaged, or untracked)
|
|
179
|
+
*/
|
|
180
|
+
async function hasUncommittedChanges(pi: ExtensionAPI): Promise<boolean> {
|
|
181
|
+
const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
|
|
182
|
+
return code === 0 && stdout.trim().length > 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the current branch name
|
|
187
|
+
*/
|
|
188
|
+
async function getCurrentBranch(pi: ExtensionAPI): Promise<string | null> {
|
|
189
|
+
const { stdout, code } = await pi.exec("git", ["branch", "--show-current"]);
|
|
190
|
+
if (code === 0 && stdout.trim()) {
|
|
191
|
+
return stdout.trim();
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get the default branch (main or master)
|
|
198
|
+
*/
|
|
199
|
+
async function getDefaultBranch(pi: ExtensionAPI): Promise<string> {
|
|
200
|
+
// Try to get from remote HEAD
|
|
201
|
+
const { stdout, code } = await pi.exec("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
|
|
202
|
+
if (code === 0 && stdout.trim()) {
|
|
203
|
+
return stdout.trim().replace("origin/", "");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fall back to checking if main or master exists
|
|
207
|
+
const branches = await getLocalBranches(pi);
|
|
208
|
+
if (branches.includes("main")) return "main";
|
|
209
|
+
if (branches.includes("master")) return "master";
|
|
210
|
+
|
|
211
|
+
return "main"; // Default fallback
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build the review prompt based on target
|
|
216
|
+
*/
|
|
217
|
+
async function buildReviewPrompt(pi: ExtensionAPI, target: ReviewTarget): Promise<string> {
|
|
218
|
+
switch (target.type) {
|
|
219
|
+
case "uncommitted":
|
|
220
|
+
return UNCOMMITTED_PROMPT;
|
|
221
|
+
|
|
222
|
+
case "baseBranch": {
|
|
223
|
+
const mergeBase = await getMergeBase(pi, target.branch);
|
|
224
|
+
if (mergeBase) {
|
|
225
|
+
return BASE_BRANCH_PROMPT_WITH_MERGE_BASE.replace(/{baseBranch}/g, target.branch).replace(
|
|
226
|
+
/{mergeBaseSha}/g,
|
|
227
|
+
mergeBase,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return BASE_BRANCH_PROMPT_FALLBACK.replace(/{branch}/g, target.branch);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case "commit":
|
|
234
|
+
if (target.title) {
|
|
235
|
+
return COMMIT_PROMPT_WITH_TITLE.replace("{sha}", target.sha).replace("{title}", target.title);
|
|
236
|
+
}
|
|
237
|
+
return COMMIT_PROMPT.replace("{sha}", target.sha);
|
|
238
|
+
|
|
239
|
+
case "custom":
|
|
240
|
+
return target.instructions;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get user-facing hint for the review target
|
|
246
|
+
*/
|
|
247
|
+
function getUserFacingHint(target: ReviewTarget): string {
|
|
248
|
+
switch (target.type) {
|
|
249
|
+
case "uncommitted":
|
|
250
|
+
return "current changes";
|
|
251
|
+
case "baseBranch":
|
|
252
|
+
return `changes against '${target.branch}'`;
|
|
253
|
+
case "commit": {
|
|
254
|
+
const shortSha = target.sha.slice(0, 7);
|
|
255
|
+
return target.title ? `commit ${shortSha}: ${target.title}` : `commit ${shortSha}`;
|
|
256
|
+
}
|
|
257
|
+
case "custom":
|
|
258
|
+
return target.instructions.length > 40 ? target.instructions.slice(0, 37) + "..." : target.instructions;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Review preset options for the selector
|
|
263
|
+
const REVIEW_PRESETS = [
|
|
264
|
+
{ value: "baseBranch", label: "Review against a base branch", description: "(PR Style)" },
|
|
265
|
+
{ value: "uncommitted", label: "Review uncommitted changes", description: "" },
|
|
266
|
+
{ value: "commit", label: "Review a commit", description: "" },
|
|
267
|
+
{ value: "custom", label: "Custom review instructions", description: "" },
|
|
268
|
+
] as const;
|
|
269
|
+
|
|
270
|
+
export default function reviewExtension(pi: ExtensionAPI) {
|
|
271
|
+
/**
|
|
272
|
+
* Determine the smart default review type based on git state
|
|
273
|
+
*/
|
|
274
|
+
async function getSmartDefault(): Promise<"uncommitted" | "baseBranch" | "commit"> {
|
|
275
|
+
// Priority 1: If there are uncommitted changes, default to reviewing them
|
|
276
|
+
if (await hasUncommittedChanges(pi)) {
|
|
277
|
+
return "uncommitted";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Priority 2: If on a feature branch (not the default branch), default to PR-style review
|
|
281
|
+
const currentBranch = await getCurrentBranch(pi);
|
|
282
|
+
const defaultBranch = await getDefaultBranch(pi);
|
|
283
|
+
if (currentBranch && currentBranch !== defaultBranch) {
|
|
284
|
+
return "baseBranch";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Priority 3: Default to reviewing a specific commit
|
|
288
|
+
return "commit";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Show the review preset selector
|
|
293
|
+
*/
|
|
294
|
+
async function showReviewSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
295
|
+
// Determine smart default and reorder items
|
|
296
|
+
const smartDefault = await getSmartDefault();
|
|
297
|
+
const items: SelectItem[] = REVIEW_PRESETS
|
|
298
|
+
.slice() // copy to avoid mutating original
|
|
299
|
+
.sort((a, b) => {
|
|
300
|
+
// Put smart default first
|
|
301
|
+
if (a.value === smartDefault) return -1;
|
|
302
|
+
if (b.value === smartDefault) return 1;
|
|
303
|
+
return 0;
|
|
304
|
+
})
|
|
305
|
+
.map((preset) => ({
|
|
306
|
+
value: preset.value,
|
|
307
|
+
label: preset.label,
|
|
308
|
+
description: preset.description,
|
|
309
|
+
}));
|
|
310
|
+
|
|
311
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
312
|
+
const container = new Container();
|
|
313
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
314
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Select a review preset"))));
|
|
315
|
+
|
|
316
|
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
317
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
318
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
319
|
+
description: (text) => theme.fg("muted", text),
|
|
320
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
321
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
selectList.onSelect = (item) => done(item.value);
|
|
325
|
+
selectList.onCancel = () => done(null);
|
|
326
|
+
|
|
327
|
+
container.addChild(selectList);
|
|
328
|
+
container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to go back")));
|
|
329
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
render(width: number) {
|
|
333
|
+
return container.render(width);
|
|
334
|
+
},
|
|
335
|
+
invalidate() {
|
|
336
|
+
container.invalidate();
|
|
337
|
+
},
|
|
338
|
+
handleInput(data: string) {
|
|
339
|
+
selectList.handleInput(data);
|
|
340
|
+
tui.requestRender();
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (!result) return null;
|
|
346
|
+
|
|
347
|
+
// Handle each preset type
|
|
348
|
+
switch (result) {
|
|
349
|
+
case "uncommitted":
|
|
350
|
+
return { type: "uncommitted" };
|
|
351
|
+
|
|
352
|
+
case "baseBranch":
|
|
353
|
+
return await showBranchSelector(ctx);
|
|
354
|
+
|
|
355
|
+
case "commit":
|
|
356
|
+
return await showCommitSelector(ctx);
|
|
357
|
+
|
|
358
|
+
case "custom":
|
|
359
|
+
return await showCustomInput(ctx);
|
|
360
|
+
|
|
361
|
+
default:
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Show branch selector for base branch review
|
|
368
|
+
*/
|
|
369
|
+
async function showBranchSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
370
|
+
const branches = await getLocalBranches(pi);
|
|
371
|
+
const defaultBranch = await getDefaultBranch(pi);
|
|
372
|
+
|
|
373
|
+
if (branches.length === 0) {
|
|
374
|
+
ctx.ui.notify("No branches found", "error");
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Sort branches with default branch first
|
|
379
|
+
const sortedBranches = branches.sort((a, b) => {
|
|
380
|
+
if (a === defaultBranch) return -1;
|
|
381
|
+
if (b === defaultBranch) return 1;
|
|
382
|
+
return a.localeCompare(b);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const items: SelectItem[] = sortedBranches.map((branch) => ({
|
|
386
|
+
value: branch,
|
|
387
|
+
label: branch,
|
|
388
|
+
description: branch === defaultBranch ? "(default)" : "",
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
392
|
+
const container = new Container();
|
|
393
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
394
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Select base branch"))));
|
|
395
|
+
|
|
396
|
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
397
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
398
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
399
|
+
description: (text) => theme.fg("muted", text),
|
|
400
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
401
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Enable search
|
|
405
|
+
selectList.searchable = true;
|
|
406
|
+
|
|
407
|
+
selectList.onSelect = (item) => done(item.value);
|
|
408
|
+
selectList.onCancel = () => done(null);
|
|
409
|
+
|
|
410
|
+
container.addChild(selectList);
|
|
411
|
+
container.addChild(new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")));
|
|
412
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
render(width: number) {
|
|
416
|
+
return container.render(width);
|
|
417
|
+
},
|
|
418
|
+
invalidate() {
|
|
419
|
+
container.invalidate();
|
|
420
|
+
},
|
|
421
|
+
handleInput(data: string) {
|
|
422
|
+
selectList.handleInput(data);
|
|
423
|
+
tui.requestRender();
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
if (!result) return null;
|
|
429
|
+
return { type: "baseBranch", branch: result };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Show commit selector
|
|
434
|
+
*/
|
|
435
|
+
async function showCommitSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
436
|
+
const commits = await getRecentCommits(pi, 20);
|
|
437
|
+
|
|
438
|
+
if (commits.length === 0) {
|
|
439
|
+
ctx.ui.notify("No commits found", "error");
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const items: SelectItem[] = commits.map((commit) => ({
|
|
444
|
+
value: commit.sha,
|
|
445
|
+
label: `${commit.sha.slice(0, 7)} ${commit.title}`,
|
|
446
|
+
description: "",
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
const result = await ctx.ui.custom<{ sha: string; title: string } | null>((tui, theme, _kb, done) => {
|
|
450
|
+
const container = new Container();
|
|
451
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
452
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Select commit to review"))));
|
|
453
|
+
|
|
454
|
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
455
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
456
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
457
|
+
description: (text) => theme.fg("muted", text),
|
|
458
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
459
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Enable search
|
|
463
|
+
selectList.searchable = true;
|
|
464
|
+
|
|
465
|
+
selectList.onSelect = (item) => {
|
|
466
|
+
const commit = commits.find((c) => c.sha === item.value);
|
|
467
|
+
if (commit) {
|
|
468
|
+
done(commit);
|
|
469
|
+
} else {
|
|
470
|
+
done(null);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
selectList.onCancel = () => done(null);
|
|
474
|
+
|
|
475
|
+
container.addChild(selectList);
|
|
476
|
+
container.addChild(new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")));
|
|
477
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
render(width: number) {
|
|
481
|
+
return container.render(width);
|
|
482
|
+
},
|
|
483
|
+
invalidate() {
|
|
484
|
+
container.invalidate();
|
|
485
|
+
},
|
|
486
|
+
handleInput(data: string) {
|
|
487
|
+
selectList.handleInput(data);
|
|
488
|
+
tui.requestRender();
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (!result) return null;
|
|
494
|
+
return { type: "commit", sha: result.sha, title: result.title };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Show custom instructions input
|
|
499
|
+
*/
|
|
500
|
+
async function showCustomInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
501
|
+
const result = await ctx.ui.editor(
|
|
502
|
+
"Enter review instructions:",
|
|
503
|
+
"Review the code for security vulnerabilities and potential bugs...",
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
if (!result?.trim()) return null;
|
|
507
|
+
return { type: "custom", instructions: result.trim() };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Execute the review
|
|
512
|
+
*/
|
|
513
|
+
async function executeReview(ctx: ExtensionCommandContext, target: ReviewTarget, useFreshSession: boolean): Promise<void> {
|
|
514
|
+
// Check if we're already in a review
|
|
515
|
+
if (reviewOriginId) {
|
|
516
|
+
ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Handle fresh session mode
|
|
521
|
+
if (useFreshSession) {
|
|
522
|
+
// Store current position (where we'll return to)
|
|
523
|
+
reviewOriginId = ctx.sessionManager.getLeafId() ?? undefined;
|
|
524
|
+
|
|
525
|
+
// Find the first user message in the session
|
|
526
|
+
const entries = ctx.sessionManager.getEntries();
|
|
527
|
+
const firstUserMessage = entries.find(
|
|
528
|
+
(e) => e.type === "message" && e.message.role === "user",
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
if (!firstUserMessage) {
|
|
532
|
+
ctx.ui.notify("No user message found in session", "error");
|
|
533
|
+
reviewOriginId = undefined;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Navigate to first user message to create a new branch from that point
|
|
538
|
+
// Label it as "code-review" so it's visible in the tree
|
|
539
|
+
try {
|
|
540
|
+
const result = await ctx.navigateTree(firstUserMessage.id, { summarize: false, label: "code-review" });
|
|
541
|
+
if (result.cancelled) {
|
|
542
|
+
reviewOriginId = undefined;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
// Clean up state if navigation fails
|
|
547
|
+
reviewOriginId = undefined;
|
|
548
|
+
ctx.ui.notify(`Failed to start review: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Clear the editor (navigating to user message fills it with the message text)
|
|
553
|
+
ctx.ui.setEditorText("");
|
|
554
|
+
|
|
555
|
+
// Show widget indicating review is active
|
|
556
|
+
ctx.ui.setWidget("review", (_tui, theme) => {
|
|
557
|
+
const text = new Text(theme.fg("warning", "Review session active, return with /end-review"), 0, 0);
|
|
558
|
+
return {
|
|
559
|
+
render(width: number) {
|
|
560
|
+
return text.render(width);
|
|
561
|
+
},
|
|
562
|
+
invalidate() {
|
|
563
|
+
text.invalidate();
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const prompt = await buildReviewPrompt(pi, target);
|
|
570
|
+
const hint = getUserFacingHint(target);
|
|
571
|
+
|
|
572
|
+
// Combine the review rubric with the specific prompt
|
|
573
|
+
const fullPrompt = `${REVIEW_RUBRIC}\n\n---\n\nPlease perform a code review with the following focus:\n\n${prompt}`;
|
|
574
|
+
|
|
575
|
+
const modeHint = useFreshSession ? " (fresh session)" : "";
|
|
576
|
+
ctx.ui.notify(`Starting review: ${hint}${modeHint}`, "info");
|
|
577
|
+
|
|
578
|
+
// Send as a user message that triggers a turn
|
|
579
|
+
pi.sendUserMessage(fullPrompt);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Parse command arguments for direct invocation
|
|
584
|
+
*/
|
|
585
|
+
function parseArgs(args: string | undefined): ReviewTarget | null {
|
|
586
|
+
if (!args?.trim()) return null;
|
|
587
|
+
|
|
588
|
+
const parts = args.trim().split(/\s+/);
|
|
589
|
+
const subcommand = parts[0]?.toLowerCase();
|
|
590
|
+
|
|
591
|
+
switch (subcommand) {
|
|
592
|
+
case "uncommitted":
|
|
593
|
+
return { type: "uncommitted" };
|
|
594
|
+
|
|
595
|
+
case "branch": {
|
|
596
|
+
const branch = parts[1];
|
|
597
|
+
if (!branch) return null;
|
|
598
|
+
return { type: "baseBranch", branch };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
case "commit": {
|
|
602
|
+
const sha = parts[1];
|
|
603
|
+
if (!sha) return null;
|
|
604
|
+
const title = parts.slice(2).join(" ") || undefined;
|
|
605
|
+
return { type: "commit", sha, title };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
case "custom": {
|
|
609
|
+
const instructions = parts.slice(1).join(" ");
|
|
610
|
+
if (!instructions) return null;
|
|
611
|
+
return { type: "custom", instructions };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
default:
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Register the /review command
|
|
620
|
+
pi.registerCommand("review", {
|
|
621
|
+
description: "Review code changes (uncommitted, branch, commit, or custom)",
|
|
622
|
+
handler: async (args, ctx) => {
|
|
623
|
+
if (!ctx.hasUI) {
|
|
624
|
+
ctx.ui.notify("Review requires interactive mode", "error");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Check if we're already in a review
|
|
629
|
+
if (reviewOriginId) {
|
|
630
|
+
ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Check if we're in a git repository
|
|
635
|
+
const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
|
|
636
|
+
if (code !== 0) {
|
|
637
|
+
ctx.ui.notify("Not a git repository", "error");
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Try to parse direct arguments
|
|
642
|
+
let target = parseArgs(args);
|
|
643
|
+
|
|
644
|
+
// If no args or invalid args, show selector
|
|
645
|
+
if (!target) {
|
|
646
|
+
target = await showReviewSelector(ctx);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (!target) {
|
|
650
|
+
ctx.ui.notify("Review cancelled", "info");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Determine if we should use fresh session mode
|
|
655
|
+
// Check if this is a new session (no messages yet)
|
|
656
|
+
const entries = ctx.sessionManager.getEntries();
|
|
657
|
+
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
658
|
+
|
|
659
|
+
let useFreshSession = false;
|
|
660
|
+
|
|
661
|
+
if (messageCount > 0) {
|
|
662
|
+
// Existing session - ask user which mode they want
|
|
663
|
+
const choice = await ctx.ui.select("Start review in:", ["Empty branch", "Current session"]);
|
|
664
|
+
|
|
665
|
+
if (choice === undefined) {
|
|
666
|
+
ctx.ui.notify("Review cancelled", "info");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
useFreshSession = choice === "Empty branch";
|
|
671
|
+
}
|
|
672
|
+
// If messageCount === 0, useFreshSession stays false (current session mode)
|
|
673
|
+
|
|
674
|
+
await executeReview(ctx, target, useFreshSession);
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Custom prompt for review summaries - focuses on capturing review findings
|
|
679
|
+
const REVIEW_SUMMARY_PROMPT = `We are switching to a coding session to continue working on the code.
|
|
680
|
+
Create a structured summary of this review branch for context when returning later.
|
|
681
|
+
|
|
682
|
+
You MUST summarize the code review that was performed in this branch so that the user can act on it.
|
|
683
|
+
|
|
684
|
+
1. What was reviewed (files, changes, scope)
|
|
685
|
+
2. Key findings and their priority levels (P0-P3)
|
|
686
|
+
3. The overall verdict (correct vs needs attention)
|
|
687
|
+
4. Any action items or recommendations
|
|
688
|
+
|
|
689
|
+
YOU MUST append a message with this EXACT format at the end of your summary:
|
|
690
|
+
|
|
691
|
+
## Next Steps
|
|
692
|
+
1. [What should happen next to act on the review]
|
|
693
|
+
|
|
694
|
+
## Constraints & Preferences
|
|
695
|
+
- [Any constraints, preferences, or requirements mentioned]
|
|
696
|
+
- [Or "(none)" if none were mentioned]
|
|
697
|
+
|
|
698
|
+
## Code Review Findings
|
|
699
|
+
|
|
700
|
+
[P0] Short Title
|
|
701
|
+
|
|
702
|
+
File: path/to/file.ext:line_number
|
|
703
|
+
|
|
704
|
+
\`\`\`
|
|
705
|
+
affected code snippet
|
|
706
|
+
\`\`\`
|
|
707
|
+
|
|
708
|
+
Preserve exact file paths, function names, and error messages.
|
|
709
|
+
`;
|
|
710
|
+
|
|
711
|
+
// Register the /end-review command
|
|
712
|
+
pi.registerCommand("end-review", {
|
|
713
|
+
description: "Complete review and return to original position",
|
|
714
|
+
handler: async (args, ctx) => {
|
|
715
|
+
if (!ctx.hasUI) {
|
|
716
|
+
ctx.ui.notify("End-review requires interactive mode", "error");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Check if we're in a fresh session review
|
|
721
|
+
if (!reviewOriginId) {
|
|
722
|
+
ctx.ui.notify("Not in a review branch (use /review first, or review was started in current session mode)", "info");
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Ask about summarization (Summarize is default/first option)
|
|
727
|
+
const summaryChoice = await ctx.ui.select("Summarize review branch?", [
|
|
728
|
+
"Summarize",
|
|
729
|
+
"No summary",
|
|
730
|
+
]);
|
|
731
|
+
|
|
732
|
+
if (summaryChoice === undefined) {
|
|
733
|
+
// User cancelled - keep state so they can call /end-review again
|
|
734
|
+
ctx.ui.notify("Cancelled. Use /end-review to try again.", "info");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const wantsSummary = summaryChoice === "Summarize";
|
|
739
|
+
const originId = reviewOriginId;
|
|
740
|
+
|
|
741
|
+
if (wantsSummary) {
|
|
742
|
+
// Show spinner while summarizing
|
|
743
|
+
const result = await ctx.ui.custom<{ cancelled: boolean; error?: string } | null>((tui, theme, _kb, done) => {
|
|
744
|
+
const loader = new BorderedLoader(tui, theme, "Summarizing review branch...");
|
|
745
|
+
loader.onAbort = () => done(null);
|
|
746
|
+
|
|
747
|
+
ctx.navigateTree(originId!, {
|
|
748
|
+
summarize: true,
|
|
749
|
+
customInstructions: REVIEW_SUMMARY_PROMPT,
|
|
750
|
+
replaceInstructions: true,
|
|
751
|
+
})
|
|
752
|
+
.then(done)
|
|
753
|
+
.catch((err) => done({ cancelled: false, error: err instanceof Error ? err.message : String(err) }));
|
|
754
|
+
|
|
755
|
+
return loader;
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
if (result === null) {
|
|
759
|
+
// User aborted - keep state so they can try again
|
|
760
|
+
ctx.ui.notify("Summarization cancelled. Use /end-review to try again.", "info");
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (result.error) {
|
|
765
|
+
// Real error - keep state so they can try again
|
|
766
|
+
ctx.ui.notify(`Summarization failed: ${result.error}`, "error");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Clear state only on success
|
|
771
|
+
ctx.ui.setWidget("review", undefined);
|
|
772
|
+
reviewOriginId = undefined;
|
|
773
|
+
|
|
774
|
+
if (result.cancelled) {
|
|
775
|
+
ctx.ui.notify("Navigation cancelled", "info");
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Pre-fill prompt if editor is empty
|
|
780
|
+
if (!ctx.ui.getEditorText().trim()) {
|
|
781
|
+
ctx.ui.setEditorText("Act on the code review");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
ctx.ui.notify("Review complete! Returned to original position.", "info");
|
|
785
|
+
} else {
|
|
786
|
+
// No summary - just navigate back
|
|
787
|
+
try {
|
|
788
|
+
const result = await ctx.navigateTree(originId!, { summarize: false });
|
|
789
|
+
|
|
790
|
+
if (result.cancelled) {
|
|
791
|
+
// Keep state so they can try again
|
|
792
|
+
ctx.ui.notify("Navigation cancelled. Use /end-review to try again.", "info");
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Clear state only on success
|
|
797
|
+
ctx.ui.setWidget("review", undefined);
|
|
798
|
+
reviewOriginId = undefined;
|
|
799
|
+
ctx.ui.notify("Review complete! Returned to original position.", "info");
|
|
800
|
+
} catch (error) {
|
|
801
|
+
// Keep state so they can try again
|
|
802
|
+
ctx.ui.notify(`Failed to return: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
}
|