From e74ce993d5bd53bec1819e0a686f5a30acf57b86 Mon Sep 17 00:00:00 2001 From: Tom Rochette Date: Sat, 13 Jun 2026 01:50:11 -0400 Subject: [PATCH] fix: auto-fetch branches when opening create worktree dialog (#1572) * repro: add test reproducing issue #1564 - empty branch list in create worktree dialog The NewWorktreeDialog does not auto-fetch branches when it opens if branches haven't been cached yet. Branches only get populated if the Git tab has been opened or a draft session has been started, both of which call fetchBranches/fetchAll independently. The test demonstrates: 1. Branches are null (empty list) for directories not yet fetched 2. Branches are available after fetchBranches is called 3. The ChatInput's draft session branch fetch explains why the issue says 'list gets populated after starting a draft session' 4. The missing auto-fetch call when dialog opens * fix: auto-fetch branches when opening create worktree dialog --------- Co-authored-by: Reproduction Bot --- .../components/session/NewWorktreeDialog.tsx | 6 + .../ui/src/stores/useGitStore.repro.test.ts | 158 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 packages/ui/src/stores/useGitStore.repro.test.ts diff --git a/packages/ui/src/components/session/NewWorktreeDialog.tsx b/packages/ui/src/components/session/NewWorktreeDialog.tsx index 6c974590..70be749d 100644 --- a/packages/ui/src/components/session/NewWorktreeDialog.tsx +++ b/packages/ui/src/components/session/NewWorktreeDialog.tsx @@ -359,6 +359,12 @@ export function NewWorktreeDialog({ void fetchBranches(projectDirectory, git); }, [projectDirectory, git, fetchBranches]); + React.useEffect(() => { + if (!open || !projectDirectory || !git) return; + if (branches?.all) return; + void fetchBranches(projectDirectory, git); + }, [open, projectDirectory, git, branches?.all, fetchBranches]); + React.useEffect(() => { if (!existingBranchDropdownOpen && !existingBranchPickerOpen) { setExistingBranchQuery(''); diff --git a/packages/ui/src/stores/useGitStore.repro.test.ts b/packages/ui/src/stores/useGitStore.repro.test.ts new file mode 100644 index 00000000..f0566091 --- /dev/null +++ b/packages/ui/src/stores/useGitStore.repro.test.ts @@ -0,0 +1,158 @@ +/** + * Reproduction test for issue #1564: + * Source branch list is sometimes empty when creating a worktree. + * + * Root cause: NewWorktreeDialog relies on useGitBranches(projectDirectory) from + * the Zustand store, but does NOT auto-fetch branches when the dialog opens. + * Branches only get populated if: + * - The Git tab has been opened (GitView's ensureAll) + * - A draft session has been started (ChatInput's branch fetch) + * - The user manually clicks the refresh button in the dialog + * + * This test demonstrates that branches start as null (empty list) for a + * directory that hasn't been explicitly fetched, matching the "sometimes empty" + * behavior reported in the issue. + */ +import { beforeEach, describe, expect, test } from 'bun:test'; +import { useGitStore } from './useGitStore'; + +type GitAPI = Parameters['fetchStatus']>[1]; + +const createGitApi = (branchesResult?: { all: string[]; current: string }): GitAPI => ({ + checkIsGitRepository: async () => true, + getGitStatus: async () => ({ + current: 'main', + tracking: null, + ahead: 0, + behind: 0, + files: [], + isClean: true, + }), + getGitBranches: async () => ({ + all: branchesResult?.all ?? ['main', 'develop', 'remotes/origin/main'], + current: branchesResult?.current ?? 'main', + branches: {}, + }), + getGitLog: async () => ({ all: [], latest: null, total: 0 }), + getCurrentGitIdentity: async () => null, + getGitFileDiff: async (_directory: string, options: { path: string }) => ({ + original: '', + modified: '', + path: options.path, + }), +}); + +describe('Issue #1564 - Source branch list is empty in create worktree dialog', () => { + beforeEach(() => { + // Reset store state - simulating a fresh app load where + // no Git tab has been opened and no draft session started + useGitStore.setState({ + directories: new Map(), + activeDirectory: null, + }); + }); + + test('branches are null (empty list) for a directory that has not been fetched', () => { + // Simulate what happens when NewWorktreeDialog mounts: + // projectDirectory = '/my-project' but no branches were fetched yet + const directory = '/my-project'; + const state = useGitStore.getState().directories.get(directory); + + // The dialog calls useGitBranches(projectDirectory), which returns: + // state.directories.get(directory)?.branches ?? null + // Since the directory doesn't exist in the store, branches is null + expect(state).toBe(undefined); + + // This directly simulates the selector in the dialog: + const branches = useGitStore.getState().directories.get(directory)?.branches ?? null; + expect(branches).toBeNull(); + + // The dialog then computes: + // localBranches = branches?.all?.filter(...) ?? [] + // remoteBranches = branches?.all?.filter(...) ?? [] + // Both are empty arrays, so the dropdown shows "No branches found" + const localBranches = branches?.all?.filter((b) => !b.startsWith('remotes/')) ?? []; + const remoteBranches = branches?.all?.filter((b) => b.startsWith('remotes/')) ?? []; + expect(localBranches).toEqual([]); + expect(remoteBranches).toEqual([]); + }); + + test('branches ARE available AFTER fetchBranches is called', async () => { + const directory = '/my-project'; + const git = createGitApi({ all: ['main', 'develop', 'feature/new-feature', 'remotes/origin/main'], current: 'main' }); + + // Simulate the user clicking the refresh button in the dialog + await useGitStore.getState().fetchBranches(directory, git); + + const dirState = useGitStore.getState().directories.get(directory); + expect(dirState).toBeTruthy(); + expect(dirState?.branches).not.toBeNull(); + expect(dirState?.branches?.all).toEqual(['main', 'develop', 'feature/new-feature', 'remotes/origin/main']); + + const branches = dirState!.branches!; + const localBranches = branches.all.filter((b) => !b.startsWith('remotes/')).sort(); + const remoteBranches = branches.all.filter((b) => b.startsWith('remotes/')).sort(); + + expect(localBranches).toEqual(['develop', 'feature/new-feature', 'main']); + expect(remoteBranches).toEqual(['remotes/origin/main']); + }); + + test('simulates the ChatInput flow: branches become available after draft session start', async () => { + // The ChatInput component fetches branches when showDraftTargetSelectors is true + // This is triggered when a draft session is started + const directory = '/my-project'; + const git = createGitApi({ all: ['main', 'hotfix', 'remotes/origin/main'], current: 'main' }); + + // Before draft session - branches not fetched + expect(useGitStore.getState().directories.get(directory)?.branches).toBe(undefined); + + // Simulate ChatInput's useEffect that runs on draft session start: + // React.useEffect(() => { + // if (!showDraftTargetSelectors || ...) return; + // void fetchBranches(selectedDraftProjectPath, runtimeGit) + // }, [...]); + await useGitStore.getState().fetchBranches(directory, git); + + // After fetch - branches are now available + const branches = useGitStore.getState().directories.get(directory)?.branches; + expect(branches).not.toBeNull(); + expect(branches?.all).toContain('main'); + expect(branches?.all).toContain('hotfix'); + + // This is the exact same state the dialog would now see + const localBranches = branches!.all.filter((b) => !b.startsWith('remotes/')).sort(); + expect(localBranches).toEqual(['hotfix', 'main']); + }); + + test('demonstrates the missing auto-fetch in NewWorktreeDialog', () => { + // The core issue: NewWorktreeDialog should call fetchBranches (or ensureAll) + // when it opens with no branches cached, but it doesn't. + // + // Looking at the dialog code: + // - Line 242: const branches = useGitBranches(projectDirectory); + // - Line 243: const isLoadingBranches = useGitLoadingBranches(projectDirectory); + // - Line 355-360: handleFetchBranches only triggered by manual refresh button + // - Lines 616-647: reset effect on open does NOT call fetchBranches + // + // Compare with ChatInput's approach (lines 3557-3563): + // if (selectedDraftProjectBranches?.all) { return; } + // void fetchBranches(selectedDraftProjectPath, runtimeGit) + // ChatInput auto-fetches if branches aren't available. + // + // The dialog should similarly auto-fetch: + // React.useEffect(() => { + // if (!open || !projectDirectory || !git) return; + // if (branches?.all) return; // already cached + // void fetchBranches(projectDirectory, git); + // }, [open, projectDirectory, git, branches?.all, fetchBranches]); + + // Verify the issue exists by checking that opening the dialog + // doesn't trigger any branch fetching for un-fetched directories + const directory = '/unfetched-project'; + const hasBranches = useGitStore.getState().directories.get(directory)?.branches?.all; + expect(hasBranches).toBe(undefined); + + // The dialog would show "No branches found" in this state + // because branches?.all is undefined -> localBranches = [] && remoteBranches = [] + }); +});