diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts index 93d93dad..785a5a88 100644 --- a/apps/server/src/routes/worktree/routes/list.ts +++ b/apps/server/src/routes/worktree/routes/list.ts @@ -74,8 +74,23 @@ export function createListHandler() { } else if (line.startsWith('branch ')) { current.branch = line.slice(7).replace('refs/heads/', ''); } else if (line === '') { - if (current.path && current.branch) { + if (current.path) { const isMainWorktree = isFirst; + + // If branch is missing (can happen for main worktree in some git states), + // fall back to getCurrentBranch() for the main worktree + let branchName = current.branch; + if (!branchName && isMainWorktree) { + // For main worktree, use the current branch we already fetched + branchName = currentBranch || ''; + } + + // Skip if we still don't have a branch name (shouldn't happen, but be safe) + if (!branchName) { + current = {}; + continue; + } + // Check if the worktree directory actually exists // Skip checking/pruning the main worktree (projectPath itself) let worktreeExists = false; @@ -89,15 +104,15 @@ export function createListHandler() { // Worktree directory doesn't exist - it was manually deleted removedWorktrees.push({ path: current.path, - branch: current.branch, + branch: branchName, }); } else { // Worktree exists (or is main worktree), add it to the list worktrees.push({ path: current.path, - branch: current.branch, + branch: branchName, isMain: isMainWorktree, - isCurrent: current.branch === currentBranch, + isCurrent: branchName === currentBranch, hasWorktree: true, }); isFirst = false; @@ -107,6 +122,48 @@ export function createListHandler() { } } + // Handle the last worktree entry if output doesn't end with blank line + if (current.path) { + const isMainWorktree = isFirst; + + // If branch is missing (can happen for main worktree in some git states), + // fall back to getCurrentBranch() for the main worktree + let branchName = current.branch; + if (!branchName && isMainWorktree) { + // For main worktree, use the current branch we already fetched + branchName = currentBranch || ''; + } + + // Only add if we have a branch name + if (branchName) { + // Check if the worktree directory actually exists + // Skip checking/pruning the main worktree (projectPath itself) + let worktreeExists = false; + try { + await secureFs.access(current.path); + worktreeExists = true; + } catch { + worktreeExists = false; + } + if (!isMainWorktree && !worktreeExists) { + // Worktree directory doesn't exist - it was manually deleted + removedWorktrees.push({ + path: current.path, + branch: branchName, + }); + } else { + // Worktree exists (or is main worktree), add it to the list + worktrees.push({ + path: current.path, + branch: branchName, + isMain: isMainWorktree, + isCurrent: branchName === currentBranch, + hasWorktree: true, + }); + } + } + } + // Prune removed worktrees from git (only if any were detected) if (removedWorktrees.length > 0) { try { diff --git a/apps/ui/tests/git/worktree-integration.spec.ts b/apps/ui/tests/git/worktree-integration.spec.ts index 65300029..421590fa 100644 --- a/apps/ui/tests/git/worktree-integration.spec.ts +++ b/apps/ui/tests/git/worktree-integration.spec.ts @@ -14,7 +14,6 @@ import { setupProjectWithPath, waitForBoardView, authenticateForTests, - handleLoginScreenIfPresent, } from '../utils'; const TEST_TEMP_DIR = createTempDirPath('worktree-tests'); @@ -55,10 +54,16 @@ test.describe('Worktree Integration', () => { await waitForNetworkIdle(page); await waitForBoardView(page); + // Wait for the worktree selector to appear (indicates API call completed) const branchLabel = page.getByText('Branch:'); await expect(branchLabel).toBeVisible({ timeout: 10000 }); + // Wait for the main branch button to appear + // This ensures the worktree API has returned data with the main branch const mainBranchButton = page.locator('[data-testid="worktree-branch-main"]'); await expect(mainBranchButton).toBeVisible({ timeout: 15000 }); + + // Verify the branch name is displayed + await expect(mainBranchButton).toContainText('main'); }); });