diff --git a/apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx b/apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx index 4d1ee520..361b4f65 100644 --- a/apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx +++ b/apps/app/src/components/views/board-view/dialogs/create-pr-dialog.tsx @@ -53,6 +53,16 @@ export function CreatePRDialog({ // Reset state when dialog opens or worktree changes useEffect(() => { if (open) { + // Only reset form fields, not the result states (prUrl, browserUrl, showBrowserFallback) + // These are set by the API response and should persist until dialog closes + setTitle(""); + setBody(""); + setCommitMessage(""); + setBaseBranch("main"); + setIsDraft(false); + setError(null); + } else { + // Reset everything when dialog closes setTitle(""); setBody(""); setCommitMessage(""); @@ -98,21 +108,22 @@ export function CreatePRDialog({ onCreated(); } else { // Branch was pushed successfully - toast.success("Branch pushed", { - description: result.result.committed - ? `Commit ${result.result.commitHash} pushed to ${result.result.branch}` - : `Branch ${result.result.branch} pushed`, - }); + const prError = result.result.prError; + const hasBrowserUrl = !!result.result.browserUrl; // Check if we should show browser fallback - if (!result.result.prCreated && result.result.browserUrl) { - const prError = result.result.prError; - + if (!result.result.prCreated && hasBrowserUrl) { // If gh CLI is not available, show browser fallback UI if (prError === "gh_cli_not_available" || !result.result.ghCliAvailable) { setBrowserUrl(result.result.browserUrl); setShowBrowserFallback(true); - onCreated(); + toast.success("Branch pushed", { + description: result.result.committed + ? `Commit ${result.result.commitHash} pushed to ${result.result.branch}` + : `Branch ${result.result.branch} pushed`, + }); + // Don't call onCreated() here - we want to keep the dialog open to show the browser URL + setIsLoading(false); return; // Don't close dialog, show browser fallback UI } @@ -135,16 +146,27 @@ export function CreatePRDialog({ description: errorMessage, duration: 8000, }); - onCreated(); + // Don't call onCreated() here - we want to keep the dialog open to show the browser URL + setIsLoading(false); return; } } + // Show success toast for push + toast.success("Branch pushed", { + description: result.result.committed + ? `Commit ${result.result.commitHash} pushed to ${result.result.branch}` + : `Branch ${result.result.branch} pushed`, + }); + // No browser URL available, just close if (!result.result.prCreated) { - toast.info("PR not created", { - description: "GitHub CLI (gh) may not be installed or authenticated", - }); + if (!hasBrowserUrl) { + toast.info("PR not created", { + description: "Could not determine repository URL. GitHub CLI (gh) may not be installed or authenticated.", + duration: 8000, + }); + } } onCreated(); onOpenChange(false); @@ -177,6 +199,8 @@ export function CreatePRDialog({ if (!worktree) return null; + const shouldShowBrowserFallback = showBrowserFallback && browserUrl; + return ( @@ -212,7 +236,7 @@ export function CreatePRDialog({ View Pull Request - ) : showBrowserFallback && browserUrl ? ( + ) : shouldShowBrowserFallback ? (
@@ -225,17 +249,30 @@ export function CreatePRDialog({ Click below to create a pull request in your browser.

-
+
+
+ {browserUrl} +

Tip: Install the GitHub CLI (gh) to create PRs directly from the app

+ + +
) : ( diff --git a/apps/server/src/routes/worktree/routes/create-pr.ts b/apps/server/src/routes/worktree/routes/create-pr.ts index 8a7acd75..3a956b85 100644 --- a/apps/server/src/routes/worktree/routes/create-pr.ts +++ b/apps/server/src/routes/worktree/routes/create-pr.ts @@ -11,13 +11,34 @@ const execAsync = promisify(exec); // Extended PATH to include common tool installation locations // This is needed because Electron apps don't inherit the user's shell PATH +const pathSeparator = process.platform === "win32" ? ";" : ":"; +const additionalPaths: string[] = []; + +if (process.platform === "win32") { + // Windows paths + if (process.env.LOCALAPPDATA) { + additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`); + } + if (process.env.PROGRAMFILES) { + additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`); + } + if (process.env["ProgramFiles(x86)"]) { + additionalPaths.push(`${process.env["ProgramFiles(x86)"]}\\Git\\cmd`); + } +} else { + // Unix/Mac paths + additionalPaths.push( + "/opt/homebrew/bin", // Homebrew on Apple Silicon + "/usr/local/bin", // Homebrew on Intel Mac, common Linux location + "/home/linuxbrew/.linuxbrew/bin", // Linuxbrew + `${process.env.HOME}/.local/bin`, // pipx, other user installs + ); +} + const extendedPath = [ process.env.PATH, - "/opt/homebrew/bin", // Homebrew on Apple Silicon - "/usr/local/bin", // Homebrew on Intel Mac, common Linux location - "/home/linuxbrew/.linuxbrew/bin", // Linuxbrew - `${process.env.HOME}/.local/bin`, // pipx, other user installs -].filter(Boolean).join(":"); + ...additionalPaths.filter(Boolean), +].filter(Boolean).join(pathSeparator); const execEnv = { ...process.env, @@ -122,9 +143,12 @@ export function createCreatePRHandler() { let browserUrl: string | null = null; let ghCliAvailable = false; - // Check if gh CLI is available + // Check if gh CLI is available (cross-platform) try { - await execAsync("command -v gh", { env: execEnv }); + const checkCommand = process.platform === "win32" + ? "where gh" + : "command -v gh"; + await execAsync(checkCommand, { env: execEnv }); ghCliAvailable = true; } catch { ghCliAvailable = false; @@ -141,9 +165,22 @@ export function createCreatePRHandler() { }); // Parse remotes to detect fork workflow and get repo URL - const lines = remotes.split("\n"); + const lines = remotes.split(/\r?\n/); // Handle both Unix and Windows line endings for (const line of lines) { - const match = line.match(/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/); + // Try multiple patterns to match different remote URL formats + // Pattern 1: git@github.com:owner/repo.git (fetch) + // Pattern 2: https://github.com/owner/repo.git (fetch) + // Pattern 3: https://github.com/owner/repo (fetch) + let match = line.match(/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/); + if (!match) { + // Try SSH format: git@github.com:owner/repo.git + match = line.match(/^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/); + } + if (!match) { + // Try HTTPS format: https://github.com/owner/repo.git + match = line.match(/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/); + } + if (match) { const [, remoteName, owner, repo] = match; if (remoteName === "upstream") { @@ -157,8 +194,30 @@ export function createCreatePRHandler() { } } } - } catch { - // Couldn't parse remotes + } catch (error) { + // Couldn't parse remotes - will try fallback + } + + // Fallback: Try to get repo URL from git config if remote parsing failed + if (!repoUrl) { + try { + const { stdout: originUrl } = await execAsync("git config --get remote.origin.url", { + cwd: worktreePath, + env: execEnv, + }); + const url = originUrl.trim(); + + // Parse URL to extract owner/repo + // Handle both SSH (git@github.com:owner/repo.git) and HTTPS (https://github.com/owner/repo.git) + let match = url.match(/[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/); + if (match) { + const [, owner, repo] = match; + originOwner = owner; + repoUrl = `https://github.com/${owner}/${repo}`; + } + } catch (error) { + // Failed to get repo URL from config + } } // Construct browser URL for PR creation @@ -192,7 +251,6 @@ export function createCreatePRHandler() { prCmd += ` --title "${title.replace(/"/g, '\\"')}" --body "${body.replace(/"/g, '\\"')}" ${draftFlag}`; prCmd = prCmd.trim(); - console.log("[CreatePR] Running:", prCmd); const { stdout: prOutput } = await execAsync(prCmd, { cwd: worktreePath, env: execEnv, @@ -202,11 +260,9 @@ export function createCreatePRHandler() { // gh CLI failed const err = ghError as { stderr?: string; message?: string }; prError = err.stderr || err.message || "PR creation failed"; - console.warn("[CreatePR] gh CLI error:", prError); } } else { prError = "gh_cli_not_available"; - console.log("[CreatePR] gh CLI not available, returning browser URL"); } // Return result with browser fallback URL