Commit bbd8aa24e4e7
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/review.ts
@@ -328,7 +328,7 @@ type ReviewTarget =
| { type: "baseBranch"; branch: string }
| { type: "commit"; sha: string; title?: string }
| { type: "custom"; instructions: string }
- | { type: "pullRequest"; prNumber: number; baseBranch: string; title: string }
+ | { type: "pullRequest"; prNumber: number; baseBranch: string; title: string; repo?: string }
| { type: "folder"; paths: string[] };
// Prompts (adapted from Codex)
@@ -541,30 +541,38 @@ async function hasPendingChanges(pi: ExtensionAPI): Promise<boolean> {
}
/**
- * Parse a PR reference (URL, number, or owner/repo#number) and return the PR number
+ * Parsed PR reference with optional repository context
*/
-function parsePrReference(ref: string): number | null {
+type PrReference = {
+ number: number;
+ repo?: string; // "owner/repo" format, undefined means current repo
+};
+
+/**
+ * Parse a PR reference (URL, number, or owner/repo#number) and return the PR number + optional repo
+ */
+function parsePrReference(ref: string): PrReference | null {
const trimmed = ref.trim();
// Try as a number first
const num = parseInt(trimmed, 10);
if (!isNaN(num) && num > 0) {
- return num;
+ return { number: num };
}
// Try to extract from GitHub URL
// Formats: https://github.com/owner/repo/pull/123
// github.com/owner/repo/pull/123
- const urlMatch = trimmed.match(/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
+ const urlMatch = trimmed.match(/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
if (urlMatch) {
- return parseInt(urlMatch[1], 10);
+ return { number: parseInt(urlMatch[2], 10), repo: urlMatch[1] };
}
// Try to extract from owner/repo#number format
// Format: tektoncd/pipeline#1234
- const repoMatch = trimmed.match(/^[^/]+\/[^#]+#(\d+)$/);
+ const repoMatch = trimmed.match(/^([^/]+\/[^#]+)#(\d+)$/);
if (repoMatch) {
- return parseInt(repoMatch[1], 10);
+ return { number: parseInt(repoMatch[2], 10), repo: repoMatch[1] };
}
return null;
@@ -573,11 +581,12 @@ function parsePrReference(ref: string): number | null {
/**
* Get PR information from GitHub CLI
*/
-async function getPrInfo(pi: ExtensionAPI, prNumber: number): Promise<{ baseBranch: string; title: string; headBranch: string } | null> {
- const { stdout, code } = await pi.exec("gh", [
- "pr", "view", String(prNumber),
- "--json", "baseRefName,title,headRefName",
- ]);
+async function getPrInfo(pi: ExtensionAPI, prNumber: number, repo?: string): Promise<{ baseBranch: string; title: string; headBranch: string } | null> {
+ const args = ["pr", "view", String(prNumber), "--json", "baseRefName,title,headRefName"];
+ if (repo) {
+ args.push("-R", repo);
+ }
+ const { stdout, code } = await pi.exec("gh", args);
if (code !== 0) return null;
@@ -596,8 +605,12 @@ async function getPrInfo(pi: ExtensionAPI, prNumber: number): Promise<{ baseBran
/**
* Checkout a PR using GitHub CLI
*/
-async function checkoutPr(pi: ExtensionAPI, prNumber: number): Promise<{ success: boolean; error?: string }> {
- const { stdout, stderr, code } = await pi.exec("gh", ["pr", "checkout", String(prNumber)]);
+async function checkoutPr(pi: ExtensionAPI, prNumber: number, repo?: string): Promise<{ success: boolean; error?: string }> {
+ const args = ["pr", "checkout", String(prNumber)];
+ if (repo) {
+ args.push("-R", repo);
+ }
+ const { stdout, stderr, code } = await pi.exec("gh", args);
if (code !== 0) {
return { success: false, error: stderr || stdout || "Failed to checkout PR" };
@@ -701,7 +714,8 @@ function getUserFacingHint(target: ReviewTarget): string {
case "pullRequest": {
const shortTitle = target.title.length > 30 ? target.title.slice(0, 27) + "..." : target.title;
- return `PR #${target.prNumber}: ${shortTitle}`;
+ const repoPrefix = target.repo ? `${target.repo}#` : "PR #";
+ return `${repoPrefix}${target.prNumber}: ${shortTitle}`;
}
case "folder": {
@@ -1210,29 +1224,33 @@ export default function reviewExtension(pi: ExtensionAPI) {
// Manual entry fallback
let prNumber: number;
+ let prRepo: string | undefined;
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):",
+ "Enter PR number, owner/repo#number, or URL (e.g. 123, tektoncd/pipeline#456, 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");
+ ctx.ui.notify("Invalid PR reference. Enter a number, owner/repo#number, or GitHub PR URL.", "error");
return null;
}
- prNumber = parsed;
+ prNumber = parsed.number;
+ prRepo = parsed.repo;
} else {
prNumber = parseInt(selected, 10);
}
+ const repoLabel = prRepo ? ` (${prRepo})` : "";
+
// Get PR info from GitHub
- ctx.ui.notify(`Fetching PR #${prNumber} info...`, "info");
- const prInfo = await getPrInfo(pi, prNumber);
+ ctx.ui.notify(`Fetching PR #${prNumber} info${repoLabel}...`, "info");
+ const prInfo = await getPrInfo(pi, prNumber, prRepo);
if (!prInfo) {
- ctx.ui.notify(`Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`, "error");
+ ctx.ui.notify(`Could not find PR #${prNumber}${repoLabel}. Make sure gh is authenticated and the PR exists.`, "error");
return null;
}
@@ -1243,21 +1261,22 @@ export default function reviewExtension(pi: ExtensionAPI) {
}
// Checkout the PR
- ctx.ui.notify(`Checking out PR #${prNumber}...`, "info");
- const checkoutResult = await checkoutPr(pi, prNumber);
+ ctx.ui.notify(`Checking out PR #${prNumber}${repoLabel}...`, "info");
+ const checkoutResult = await checkoutPr(pi, prNumber, prRepo);
if (!checkoutResult.success) {
ctx.ui.notify(`Failed to checkout PR: ${checkoutResult.error}`, "error");
return null;
}
- ctx.ui.notify(`Checked out PR #${prNumber} (${prInfo.headBranch})`, "info");
+ ctx.ui.notify(`Checked out PR #${prNumber} (${prInfo.headBranch})${repoLabel}`, "info");
return {
type: "pullRequest",
prNumber,
baseBranch: prInfo.baseBranch,
title: prInfo.title,
+ repo: prRepo,
};
}
@@ -1366,15 +1385,16 @@ export default function reviewExtension(pi: ExtensionAPI) {
* Parse command arguments for direct invocation
* Returns the target or a special marker for PR that needs async handling
*/
- function parseArgs(args: string | undefined): ReviewTarget | { type: "pr"; ref: string } | null {
+ function parseArgs(args: string | undefined): ReviewTarget | { type: "pr"; ref: string; repo?: string } | null {
if (!args?.trim()) return null;
const trimmed = args.trim();
// Check if it looks like a PR reference (URL, number, or owner/repo#number)
// This allows `/review 123`, `/review https://...`, or `/review tektoncd/pipeline#1234`
- if (parsePrReference(trimmed) !== null) {
- return { type: "pr", ref: trimmed };
+ const prRef = parsePrReference(trimmed);
+ if (prRef !== null) {
+ return { type: "pr", ref: trimmed, repo: prRef.repo };
}
const parts = trimmed.split(/\s+/);
@@ -1412,7 +1432,8 @@ export default function reviewExtension(pi: ExtensionAPI) {
case "pr": {
const ref = parts[1];
if (!ref) return { type: "pr", ref: "__select__" };
- return { type: "pr", ref };
+ const parsed = parsePrReference(ref);
+ return { type: "pr", ref, repo: parsed?.repo };
}
default:
@@ -1423,44 +1444,49 @@ export default function reviewExtension(pi: ExtensionAPI) {
/**
* Handle PR checkout and return a ReviewTarget (or null on failure)
*/
- async function handlePrCheckout(ctx: ExtensionContext, ref: string): Promise<ReviewTarget | null> {
+ async function handlePrCheckout(ctx: ExtensionContext, ref: string, repo?: string): Promise<ReviewTarget | null> {
// First check for pending changes
if (await hasPendingChanges(pi)) {
ctx.ui.notify("Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.", "error");
return null;
}
- const prNumber = parsePrReference(ref);
- if (!prNumber) {
+ const prRef = parsePrReference(ref);
+ if (!prRef) {
ctx.ui.notify("Invalid PR reference. Enter a number or GitHub PR URL.", "error");
return null;
}
+ // Use repo from the parsed reference if not explicitly provided
+ const effectiveRepo = repo ?? prRef.repo;
+ const repoLabel = effectiveRepo ? ` (${effectiveRepo})` : "";
+
// Get PR info
- ctx.ui.notify(`Fetching PR #${prNumber} info...`, "info");
- const prInfo = await getPrInfo(pi, prNumber);
+ ctx.ui.notify(`Fetching PR #${prRef.number} info${repoLabel}...`, "info");
+ const prInfo = await getPrInfo(pi, prRef.number, effectiveRepo);
if (!prInfo) {
- ctx.ui.notify(`Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`, "error");
+ ctx.ui.notify(`Could not find PR #${prRef.number}${repoLabel}. Make sure gh is authenticated and the PR exists.`, "error");
return null;
}
// Checkout the PR
- ctx.ui.notify(`Checking out PR #${prNumber}...`, "info");
- const checkoutResult = await checkoutPr(pi, prNumber);
+ ctx.ui.notify(`Checking out PR #${prRef.number}${repoLabel}...`, "info");
+ const checkoutResult = await checkoutPr(pi, prRef.number, effectiveRepo);
if (!checkoutResult.success) {
ctx.ui.notify(`Failed to checkout PR: ${checkoutResult.error}`, "error");
return null;
}
- ctx.ui.notify(`Checked out PR #${prNumber} (${prInfo.headBranch})`, "info");
+ ctx.ui.notify(`Checked out PR #${prRef.number} (${prInfo.headBranch})${repoLabel}`, "info");
return {
type: "pullRequest",
- prNumber,
+ prNumber: prRef.number,
baseBranch: prInfo.baseBranch,
title: prInfo.title,
+ repo: effectiveRepo,
};
}
@@ -1498,7 +1524,7 @@ export default function reviewExtension(pi: ExtensionAPI) {
target = await showPrInput(ctx);
} else {
// `/review pr 123` or `/review 123` — direct checkout
- target = await handlePrCheckout(ctx, parsed.ref);
+ target = await handlePrCheckout(ctx, parsed.ref, parsed.repo);
}
if (!target) {
ctx.ui.notify("PR review failed. Returning to review menu.", "warning");