Commit 9a3257d483e8
dots/pi/agent/extensions/github/actions/pr.ts
@@ -25,6 +25,7 @@ import {
getReviewDecisionText,
formatRelativeDate,
execGh,
+ findPRTemplate,
} from "../utils";
const PR_LIST_FIELDS = "number,title,state,author,headRefName,baseRefName,url,isDraft,labels,reviewDecision,additions,deletions,changedFiles,createdAt,updatedAt";
@@ -240,9 +241,15 @@ export async function handlePRCreate(
};
}
+ // Find PR template
+ const template = await findPRTemplate(pi, ctx);
+
// APPROVAL GATE
if (ctx.hasUI) {
- const confirmMessage = buildPRCreateConfirmation(params);
+ let confirmMessage = buildPRCreateConfirmation(params);
+ if (template) {
+ confirmMessage += `\n\nTemplate: ${template}`;
+ }
const confirmed = await ctx.ui.confirm("Create Pull Request?", confirmMessage);
if (!confirmed) {
ctx.ui.notify("PR creation cancelled", "info");
@@ -255,6 +262,11 @@ export async function handlePRCreate(
const args = ["pr", "create", "--title", params.title];
+ // Add template if found and body not explicitly provided
+ if (template && !params.body) {
+ args.push("--template", template);
+ }
+
if (params.body) args.push("--body", params.body);
if (params.base) args.push("--base", params.base);
if (params.draft) args.push("--draft");
@@ -270,8 +282,22 @@ export async function handlePRCreate(
const result = await execGh(pi, ctx, args, { signal, timeout: 30000 });
if (result.code !== 0) {
+ let errorMsg = getErrorMessage(result.stderr, "Create PR");
+
+ // Add helpful hints for common issues
+ if (result.stderr.includes("No commits between")) {
+ errorMsg += "\n\n💡 Tip: Make sure you have committed and pushed your changes to the branch.";
+ errorMsg += "\n If working in a worktree, ensure you're on the correct branch.";
+ }
+ if (result.stderr.includes("uncommitted changes")) {
+ errorMsg += "\n\n💡 Tip: Commit or stash your changes before creating a PR.";
+ }
+ if (result.stderr.includes("head repository")) {
+ errorMsg += "\n\n💡 Tip: Make sure you've pushed your branch to your fork.";
+ }
+
return {
- content: [{ type: "text", text: getErrorMessage(result.stderr, "Create PR") }],
+ content: [{ type: "text", text: errorMsg }],
details: { action: "pr-create", error: result.stderr } as GhDetails,
isError: true,
};
dots/pi/agent/extensions/github/utils.ts
@@ -10,6 +10,7 @@ import type { GhPR, GhIssue, GhCheck, GhRun, GhReview, GhReviewComment, GhReview
// ============================================================================
let cachedGitRoot: string | null = null;
+let cachedWorktreeRoot: string | null = null;
/**
* Find the git repository root directory.
@@ -30,16 +31,72 @@ async function findGitRoot(pi: ExtensionAPI, cwd: string): Promise<string | null
return null;
}
+/**
+ * Detect if we're working in a git worktree context.
+ * Checks recent bash operations to find where actual work is happening.
+ * Returns the worktree path if detected, null otherwise.
+ */
+async function detectWorktreeContext(
+ pi: ExtensionAPI,
+ ctx: ExtensionContext
+): Promise<string | null> {
+ if (cachedWorktreeRoot !== null) return cachedWorktreeRoot;
+
+ // Strategy 1: Check if ctx.cwd itself is a worktree
+ const gitDirResult = await pi.exec("git", ["rev-parse", "--git-dir"], { cwd: ctx.cwd, timeout: 5000 });
+ if (gitDirResult.code === 0 && gitDirResult.stdout.includes("/worktrees/")) {
+ // We're already in a worktree
+ const toplevel = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: ctx.cwd, timeout: 5000 });
+ if (toplevel.code === 0 && toplevel.stdout.trim()) {
+ cachedWorktreeRoot = toplevel.stdout.trim();
+ return cachedWorktreeRoot;
+ }
+ }
+
+ // Strategy 2: Check for recent git operations in session messages
+ // Look for bash tool results with git commands that mention worktree paths
+ try {
+ const entries = ctx.sessionManager.getBranch();
+ for (const entry of entries.reverse()) { // Start from most recent
+ if (entry.type !== "message") continue;
+ const msg = entry.message;
+ if (msg.role !== "toolResult" || msg.toolName !== "bash") continue;
+
+ const details = msg.details as any;
+ if (!details?.cwd) continue;
+
+ // Check if this command was run in a worktree directory
+ if (details.cwd.includes("/.local/share/worktrees/")) {
+ // Verify it's still a valid git directory
+ const checkResult = await pi.exec("git", ["rev-parse", "--show-toplevel"], {
+ cwd: details.cwd,
+ timeout: 5000
+ });
+ if (checkResult.code === 0 && checkResult.stdout.trim()) {
+ cachedWorktreeRoot = checkResult.stdout.trim();
+ return cachedWorktreeRoot;
+ }
+ }
+ }
+ } catch {
+ // Ignore errors in session scanning
+ }
+
+ return null;
+}
+
/**
* Reset cached git root (call on session change)
*/
export function resetGitRoot() {
cachedGitRoot = null;
+ cachedWorktreeRoot = null;
}
/**
* Execute gh command with correct working directory.
* Automatically uses git repository root if available.
+ * Detects and prefers worktree context when available.
*/
export async function execGh(
pi: ExtensionAPI,
@@ -47,11 +104,47 @@ export async function execGh(
args: string[],
options?: { signal?: AbortSignal; timeout?: number }
) {
+ // First check if we're working in a worktree context
+ const worktree = await detectWorktreeContext(pi, ctx);
+ if (worktree) {
+ return await pi.exec("gh", args, { ...options, cwd: worktree });
+ }
+
+ // Fall back to regular git root detection
const root = await findGitRoot(pi, ctx.cwd);
const cwd = root ?? ctx.cwd;
return await pi.exec("gh", args, { ...options, cwd });
}
+/**
+ * Find PR template in common locations.
+ * Returns the path to the template file if found, null otherwise.
+ */
+export async function findPRTemplate(
+ pi: ExtensionAPI,
+ ctx: ExtensionContext
+): Promise<string | null> {
+ // Determine the correct working directory (worktree-aware)
+ const worktree = await detectWorktreeContext(pi, ctx);
+ const gitRoot = worktree ?? await findGitRoot(pi, ctx.cwd) ?? ctx.cwd;
+
+ const templatePaths = [
+ ".github/PULL_REQUEST_TEMPLATE.md",
+ ".github/pull_request_template.md",
+ "docs/PULL_REQUEST_TEMPLATE.md",
+ "PULL_REQUEST_TEMPLATE.md",
+ ];
+
+ for (const templatePath of templatePaths) {
+ const result = await pi.exec("test", ["-f", templatePath], { cwd: gitRoot });
+ if (result.code === 0) {
+ return templatePath;
+ }
+ }
+
+ return null;
+}
+
// ============================================================================
// Parsing: gh CLI JSON output → typed objects
// ============================================================================