diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts index bc70a341..a7c12f98 100644 --- a/apps/server/src/routes/worktree/routes/list.ts +++ b/apps/server/src/routes/worktree/routes/list.ts @@ -13,7 +13,7 @@ import { promisify } from 'util'; import path from 'path'; import * as secureFs from '../../../lib/secure-fs.js'; import { isGitRepo } from '@automaker/git-utils'; -import { getErrorMessage, logError, normalizePath } from '../common.js'; +import { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js'; import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js'; import { createLogger } from '@automaker/utils'; @@ -121,6 +121,52 @@ async function scanWorktreesDirectory( return discovered; } +/** + * Fetch open PRs from GitHub and create a map of branch name to PR info. + * This allows detecting PRs that were created outside the app. + */ +async function fetchGitHubPRs(projectPath: string): Promise> { + const prMap = new Map(); + + try { + // Check if gh CLI is available + const ghAvailable = await isGhCliAvailable(); + if (!ghAvailable) { + return prMap; + } + + // Fetch open PRs from GitHub + const { stdout } = await execAsync( + 'gh pr list --state open --json number,title,url,state,headRefName,createdAt --limit 1000', + { cwd: projectPath, env: execEnv, timeout: 15000 } + ); + + const prs = JSON.parse(stdout || '[]') as Array<{ + number: number; + title: string; + url: string; + state: string; + headRefName: string; + createdAt: string; + }>; + + for (const pr of prs) { + prMap.set(pr.headRefName, { + number: pr.number, + url: pr.url, + title: pr.title, + state: pr.state, + createdAt: pr.createdAt, + }); + } + } catch (error) { + // Silently fail - PR detection is optional + logger.warn(`Failed to fetch GitHub PRs: ${getErrorMessage(error)}`); + } + + return prMap; +} + export function createListHandler() { return async (req: Request, res: Response): Promise => { try { @@ -241,11 +287,23 @@ export function createListHandler() { } } - // Add PR info from metadata for each worktree + // Add PR info from metadata or GitHub for each worktree + // Only fetch GitHub PRs if includeDetails is requested (performance optimization) + const githubPRs = includeDetails + ? await fetchGitHubPRs(projectPath) + : new Map(); + for (const worktree of worktrees) { const metadata = allMetadata.get(worktree.branch); if (metadata?.pr) { + // Use stored metadata (more complete info) worktree.pr = metadata.pr; + } else if (includeDetails) { + // Fall back to GitHub PR detection only when includeDetails is requested + const githubPR = githubPRs.get(worktree.branch); + if (githubPR) { + worktree.pr = githubPR; + } } } diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index d2df1f40..849b4e0b 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -328,20 +328,6 @@ export function BoardView() { fetchBranches(); }, [currentProject, worktreeRefreshKey]); - // Calculate unarchived card counts per branch - const branchCardCounts = useMemo(() => { - return hookFeatures.reduce( - (counts, feature) => { - if (feature.status !== 'completed') { - const branch = feature.branchName ?? 'main'; - counts[branch] = (counts[branch] || 0) + 1; - } - return counts; - }, - {} as Record - ); - }, [hookFeatures]); - // Custom collision detection that prioritizes columns over cards const collisionDetectionStrategy = useCallback((args: any) => { // First, check if pointer is within a column @@ -426,6 +412,22 @@ export function BoardView() { const selectedWorktreeBranch = currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main'; + // Calculate unarchived card counts per branch + const branchCardCounts = useMemo(() => { + // Use primary worktree branch as default for features without branchName + const primaryBranch = worktrees.find((w) => w.isMain)?.branch || 'main'; + return hookFeatures.reduce( + (counts, feature) => { + if (feature.status !== 'completed') { + const branch = feature.branchName ?? primaryBranch; + counts[branch] = (counts[branch] || 0) + 1; + } + return counts; + }, + {} as Record + ); + }, [hookFeatures, worktrees]); + // Helper function to add and select a worktree const addAndSelectWorktree = useCallback( (worktreeResult: { path: string; branch: string }) => { @@ -724,10 +726,11 @@ export function BoardView() { [handleAddFeature, handleStartImplementation, defaultSkipTests] ); - // Handler for resolving conflicts - creates a feature to pull from origin/main and resolve conflicts + // Handler for resolving conflicts - creates a feature to pull from the remote branch and resolve conflicts const handleResolveConflicts = useCallback( async (worktree: WorktreeInfo) => { - const description = `Pull latest from origin/main and resolve conflicts. Merge origin/main into the current branch (${worktree.branch}), resolving any merge conflicts that arise. After resolving conflicts, ensure the code compiles and tests pass.`; + const remoteBranch = `origin/${worktree.branch}`; + const description = `Pull latest from ${remoteBranch} and resolve conflicts. Merge ${remoteBranch} into the current branch (${worktree.branch}), resolving any merge conflicts that arise. After resolving conflicts, ensure the code compiles and tests pass.`; // Create the feature const featureData = { @@ -1710,6 +1713,7 @@ export function BoardView() { onOpenChange={setShowCreatePRDialog} worktree={selectedWorktreeForAction} projectPath={currentProject?.path || null} + defaultBaseBranch={selectedWorktreeBranch} onCreated={(prUrl) => { // If a PR was created and we have the worktree branch, update all features on that branch with the PR URL if (prUrl && selectedWorktreeForAction?.branch) { diff --git a/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx index 3abbb75f..a4a7dbed 100644 --- a/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx @@ -30,6 +30,8 @@ interface CreatePRDialogProps { worktree: WorktreeInfo | null; projectPath: string | null; onCreated: (prUrl?: string) => void; + /** Default base branch for the PR (defaults to 'main' if not provided) */ + defaultBaseBranch?: string; } export function CreatePRDialog({ @@ -38,10 +40,11 @@ export function CreatePRDialog({ worktree, projectPath, onCreated, + defaultBaseBranch = 'main', }: CreatePRDialogProps) { const [title, setTitle] = useState(''); const [body, setBody] = useState(''); - const [baseBranch, setBaseBranch] = useState('main'); + const [baseBranch, setBaseBranch] = useState(defaultBaseBranch); const [commitMessage, setCommitMessage] = useState(''); const [isDraft, setIsDraft] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -59,7 +62,7 @@ export function CreatePRDialog({ setTitle(''); setBody(''); setCommitMessage(''); - setBaseBranch('main'); + setBaseBranch(defaultBaseBranch); setIsDraft(false); setError(null); // Also reset result states when opening for a new worktree @@ -74,7 +77,7 @@ export function CreatePRDialog({ setTitle(''); setBody(''); setCommitMessage(''); - setBaseBranch('main'); + setBaseBranch(defaultBaseBranch); setIsDraft(false); setError(null); setPrUrl(null); @@ -82,7 +85,7 @@ export function CreatePRDialog({ setShowBrowserFallback(false); operationCompletedRef.current = false; } - }, [open, worktree?.path]); + }, [open, worktree?.path, defaultBaseBranch]); const handleCreate = async () => { if (!worktree) return; diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx index 20880a6f..05597ada 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx @@ -217,27 +217,20 @@ export function WorktreeActionsDropdown({ )} - {!worktree.isMain && ( - + canPerformGitOps && onResolveConflicts(worktree)} + disabled={!canPerformGitOps} + className={cn( + 'text-xs text-purple-500 focus:text-purple-600', + !canPerformGitOps && 'opacity-50 cursor-not-allowed' + )} > - canPerformGitOps && onResolveConflicts(worktree)} - disabled={!canPerformGitOps} - className={cn( - 'text-xs text-purple-500 focus:text-purple-600', - !canPerformGitOps && 'opacity-50 cursor-not-allowed' - )} - > - - Pull & Resolve Conflicts - {!canPerformGitOps && ( - - )} - - - )} + + Pull & Resolve Conflicts + {!canPerformGitOps && } + + {/* Open in editor - split button: click main area for default, chevron for other options */} {effectiveDefaultEditor && ( @@ -332,7 +325,7 @@ export function WorktreeActionsDropdown({ )} {/* Show PR info and Address Comments button if PR exists */} - {!worktree.isMain && hasPR && worktree.pr && ( + {hasPR && worktree.pr && ( <> {