From 51e4e8489a428a9c82902998b7a8d01360eb1f6b Mon Sep 17 00:00:00 2001 From: Shirone Date: Wed, 14 Jan 2026 00:40:10 +0100 Subject: [PATCH] fix: use dynamic branch references instead of hardcoded origin/main - Fix handleResolveConflicts to use origin/${worktree.branch} instead of hardcoded origin/main for pull and resolve conflicts - Add defaultBaseBranch prop to CreatePRDialog to use selected branch - Fix branchCardCounts to use primary worktree branch as default - Enable PR status and Address PR Comments for main branch tab - Add automatic PR detection from GitHub for branches without stored metadata This allows users working on release branches (like v0.11.0rc) to properly pull from their branch's remote and see PR status for any branch. Co-Authored-By: Claude Opus 4.5 --- .../server/src/routes/worktree/routes/list.ts | 60 ++++++++++++++++++- apps/ui/src/components/views/board-view.tsx | 36 ++++++----- .../board-view/dialogs/create-pr-dialog.tsx | 11 ++-- .../components/worktree-actions-dropdown.tsx | 35 +++++------ 4 files changed, 99 insertions(+), 43 deletions(-) diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts index bc70a341..a512c308 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 100', + { cwd: projectPath, env: execEnv } + ); + + 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,21 @@ export function createListHandler() { } } - // Add PR info from metadata for each worktree + // Fetch open PRs from GitHub to detect PRs created outside the app + const githubPRs = await fetchGitHubPRs(projectPath); + + // Add PR info from metadata or GitHub for each worktree 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 { + // Fall back to GitHub PR detection + 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 && ( <> {