Commit 2cf079dbf030
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/review.ts
@@ -1020,7 +1020,49 @@ export default function reviewExtension(pi: ExtensionAPI) {
}
/**
- * Show PR input and handle checkout
+ * Fetch open PRs from GitHub for the selector
+ */
+ async function fetchOpenPRs(): Promise<Array<{ number: number; title: string; author: string; branch: string; base: string; isDraft: boolean; reviewDecision: string }>> {
+ const { stdout, code } = await pi.exec("gh", [
+ "pr", "list",
+ "--state", "open",
+ "--json", "number,title,author,headRefName,baseRefName,isDraft,reviewDecision",
+ "--limit", "30",
+ ]);
+
+ if (code !== 0) return [];
+
+ try {
+ const data = JSON.parse(stdout);
+ if (!Array.isArray(data)) return [];
+ return data.map((item: any) => ({
+ number: item.number ?? 0,
+ title: item.title ?? "",
+ author: item.author?.login ?? "",
+ branch: item.headRefName ?? "",
+ base: item.baseRefName ?? "",
+ isDraft: item.isDraft ?? false,
+ reviewDecision: item.reviewDecision ?? "",
+ }));
+ } catch {
+ return [];
+ }
+ }
+
+ /**
+ * Get review decision label for display
+ */
+ function reviewDecisionLabel(decision: string): string {
+ switch (decision) {
+ case "APPROVED": return " ✓approved";
+ case "CHANGES_REQUESTED": return " ✗changes requested";
+ case "REVIEW_REQUIRED": return " ⏳review needed";
+ default: return "";
+ }
+ }
+
+ /**
+ * Show PR selector with list of open PRs + manual entry option
*/
async function showPrInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
// First check for pending changes that would prevent branch switching
@@ -1029,18 +1071,95 @@ export default function reviewExtension(pi: ExtensionAPI) {
return null;
}
- // Get PR reference from user
- const prRef = await ctx.ui.editor(
- "Enter PR number or URL (e.g. 123 or https://github.com/owner/repo/pull/123):",
- "",
- );
+ // Fetch open PRs
+ ctx.ui.notify("Fetching open PRs...", "info");
+ const prs = await fetchOpenPRs();
- if (!prRef?.trim()) return null;
+ // Try to get current user for smart ordering
+ let currentUser = "";
+ const { stdout: userOut, code: userCode } = await pi.exec("gh", ["api", "user", "--jq", ".login"], { timeout: 5000 });
+ if (userCode === 0) currentUser = userOut.trim();
- const prNumber = parsePrReference(prRef);
- if (!prNumber) {
- ctx.ui.notify("Invalid PR reference. Enter a number or GitHub PR URL.", "error");
- return null;
+ // Sort: PRs needing your review first, then your own, then others
+ const sorted = prs.slice().sort((a, b) => {
+ const aScore = a.reviewDecision === "REVIEW_REQUIRED" && a.author !== currentUser ? 0
+ : a.author === currentUser ? 1
+ : 2;
+ const bScore = b.reviewDecision === "REVIEW_REQUIRED" && b.author !== currentUser ? 0
+ : b.author === currentUser ? 1
+ : 2;
+ return aScore - bScore;
+ });
+
+ // Build select items
+ const items: SelectItem[] = sorted.map((pr) => {
+ const draft = pr.isDraft ? " [draft]" : "";
+ const review = reviewDecisionLabel(pr.reviewDecision);
+ return {
+ value: String(pr.number),
+ label: `#${pr.number} ${pr.title}${draft}${review}`,
+ description: `@${pr.author} ${pr.branch} → ${pr.base}`,
+ };
+ });
+
+ // Add manual entry option at the bottom
+ items.push({
+ value: "__manual__",
+ label: "Enter PR number manually…",
+ description: "(for cross-repo or closed PRs)",
+ });
+
+ // Show selector
+ const selected = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
+ const container = new Container();
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select a pull request to review"))));
+
+ const selectList = new SelectList(items, Math.min(items.length, 12), {
+ selectedPrefix: (text) => theme.fg("accent", text),
+ selectedText: (text) => theme.fg("accent", text),
+ description: (text) => theme.fg("muted", text),
+ scrollInfo: (text) => theme.fg("dim", text),
+ noMatch: (text) => theme.fg("warning", text),
+ });
+
+ selectList.searchable = true;
+ selectList.onSelect = (item) => done(item.value);
+ selectList.onCancel = () => done(null);
+
+ container.addChild(selectList);
+ container.addChild(new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")));
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
+
+ return {
+ render(width: number) { return container.render(width); },
+ invalidate() { container.invalidate(); },
+ handleInput(data: string) {
+ selectList.handleInput(data);
+ tui.requestRender();
+ },
+ };
+ });
+
+ if (!selected) return null;
+
+ // Manual entry fallback
+ let prNumber: number;
+ if (selected === "__manual__") {
+ const prRef = await ctx.ui.editor(
+ "Enter PR number or URL (e.g. 123 or https://github.com/owner/repo/pull/123):",
+ "",
+ );
+ if (!prRef?.trim()) return null;
+
+ const parsed = parsePrReference(prRef);
+ if (!parsed) {
+ ctx.ui.notify("Invalid PR reference. Enter a number or GitHub PR URL.", "error");
+ return null;
+ }
+ prNumber = parsed;
+ } else {
+ prNumber = parseInt(selected, 10);
}
// Get PR info from GitHub
@@ -1227,7 +1346,7 @@ export default function reviewExtension(pi: ExtensionAPI) {
case "pr": {
const ref = parts[1];
- if (!ref) return null;
+ if (!ref) return { type: "pr", ref: "__select__" };
return { type: "pr", ref };
}
@@ -1309,8 +1428,13 @@ export default function reviewExtension(pi: ExtensionAPI) {
if (parsed) {
if (parsed.type === "pr") {
- // Handle PR checkout (async operation)
- target = await handlePrCheckout(ctx, parsed.ref);
+ if (parsed.ref === "__select__") {
+ // `/review pr` with no number — show PR selector
+ target = await showPrInput(ctx);
+ } else {
+ // `/review pr 123` or `/review 123` — direct checkout
+ target = await handlePrCheckout(ctx, parsed.ref);
+ }
if (!target) {
ctx.ui.notify("PR review failed. Returning to review menu.", "warning");
}