Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/components/ConfigureWorktree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const ConfigureWorktree: React.FC<ConfigureWorktreeProps> = ({onComplete}) => {
const [autoUseDefaultBranch, setAutoUseDefaultBranch] = useState(
worktreeConfig.autoUseDefaultBranch ?? false,
);
const [includeRemoteBranches, setIncludeRemoteBranches] = useState(
worktreeConfig.includeRemoteBranches ?? false,
);
const [editMode, setEditMode] = useState<EditMode>('menu');
const [tempPattern, setTempPattern] = useState(pattern);

Expand Down Expand Up @@ -79,6 +82,10 @@ const ConfigureWorktree: React.FC<ConfigureWorktreeProps> = ({onComplete}) => {
label: `Auto Use Default Branch: ${autoUseDefaultBranch ? '✅ Enabled' : '❌ Disabled'}`,
value: 'toggleAutoUseDefault',
},
{
label: `Include Remote Branches: ${includeRemoteBranches ? '✅ Enabled' : '❌ Disabled'}`,
value: 'toggleIncludeRemote',
},
{
label: '💾 Save Changes',
value: 'save',
Expand Down Expand Up @@ -107,6 +114,9 @@ const ConfigureWorktree: React.FC<ConfigureWorktreeProps> = ({onComplete}) => {
case 'toggleAutoUseDefault':
setAutoUseDefaultBranch(!autoUseDefaultBranch);
break;
case 'toggleIncludeRemote':
setIncludeRemoteBranches(!includeRemoteBranches);
break;
case 'save':
// Save the configuration
configEditor.setWorktreeConfig({
Expand All @@ -115,6 +125,7 @@ const ConfigureWorktree: React.FC<ConfigureWorktreeProps> = ({onComplete}) => {
copySessionData,
sortByLastSession,
autoUseDefaultBranch,
includeRemoteBranches,
});
onComplete();
break;
Expand Down
62 changes: 51 additions & 11 deletions src/components/NewWorktree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {configReader} from '../services/config/configReader.js';
import {generateWorktreeDirectory} from '../utils/worktreeUtils.js';
import {WorktreeService} from '../services/worktreeService.js';
import {useSearchMode} from '../hooks/useSearchMode.js';
import {useDynamicLimit} from '../hooks/useDynamicLimit.js';
import SearchableList from './SearchableList.js';
import {Effect} from 'effect';
import type {AppError} from '../types/errors.js';
Expand Down Expand Up @@ -68,7 +69,7 @@ const NewWorktree: React.FC<NewWorktreeProps> = ({
const presetsConfig = configReader.getCommandPresets();
const isAutoDirectory = worktreeConfig.autoDirectory;
const isAutoUseDefaultBranch = worktreeConfig.autoUseDefaultBranch ?? false;
const limit = 10;
const includeRemoteBranches = worktreeConfig.includeRemoteBranches ?? false;

const getInitialStep = (): Step => {
if (isAutoDirectory) {
Expand All @@ -94,15 +95,23 @@ const NewWorktree: React.FC<NewWorktreeProps> = ({
const [isLoadingBranches, setIsLoadingBranches] = useState(true);
const [branchLoadError, setBranchLoadError] = useState<string | null>(null);
const [branches, setBranches] = useState<string[]>([]);
const [remoteBranches, setRemoteBranches] = useState<string[]>([]);
const [defaultBranch, setDefaultBranch] = useState<string>('main');

useEffect(() => {
let cancelled = false;
const service = new WorktreeService(projectPath);

const loadBranches = async () => {
const branchesEffect = includeRemoteBranches
? service.getBranchesWithRemotesEffect()
: Effect.map(service.getAllBranchesEffect(), (list: string[]) => ({
local: list,
remote: [] as string[],
}));

const workflow = Effect.all(
[service.getAllBranchesEffect(), service.getDefaultBranchEffect()],
[branchesEffect, service.getDefaultBranchEffect()],
{concurrency: 2},
);

Expand All @@ -112,9 +121,13 @@ const NewWorktree: React.FC<NewWorktreeProps> = ({
type: 'error' as const,
message: formatError(error),
}),
onSuccess: ([branchList, defaultBr]: [string[], string]) => ({
onSuccess: ([branchData, defaultBr]: [
{local: string[]; remote: string[]},
string,
]) => ({
type: 'success' as const,
branches: branchList,
local: branchData.local,
remote: branchData.remote,
defaultBranch: defaultBr,
}),
}),
Expand All @@ -125,7 +138,8 @@ const NewWorktree: React.FC<NewWorktreeProps> = ({
setBranchLoadError(result.message);
setIsLoadingBranches(false);
} else {
setBranches(result.branches);
setBranches(result.local);
setRemoteBranches(result.remote);
setDefaultBranch(result.defaultBranch);
setIsLoadingBranches(false);

Expand All @@ -149,23 +163,41 @@ const NewWorktree: React.FC<NewWorktreeProps> = ({
return () => {
cancelled = true;
};
}, [projectPath, isAutoUseDefaultBranch]);
}, [projectPath, isAutoUseDefaultBranch, includeRemoteBranches]);

const allBranchItems: BranchItem[] = useMemo(() => {
const defaultRemoteSuffix = `/${defaultBranch}`;
const defaultRemotes = remoteBranches.filter(br =>
br.endsWith(defaultRemoteSuffix),
);
const otherRemotes = remoteBranches.filter(
br => !br.endsWith(defaultRemoteSuffix),
);

const allBranchItems: BranchItem[] = useMemo(
() => [
return [
{label: `${defaultBranch} (default)`, value: defaultBranch},
...defaultRemotes.map(br => ({
label: `${br} (default remote)`,
value: br,
})),
...branches
.filter(br => br !== defaultBranch)
.map(br => ({label: br, value: br})),
],
[branches, defaultBranch],
);
...otherRemotes.map(br => ({label: br, value: br})),
];
}, [branches, remoteBranches, defaultBranch]);

const {isSearchMode, searchQuery, selectedIndex, setSearchQuery} =
useSearchMode(allBranchItems.length, {
isDisabled: step !== 'base-branch',
});

const limit = useDynamicLimit({
fixedRows: includeRemoteBranches ? 10 : 8,
isSearchMode,
hasError: !!branchLoadError,
});

const branchItems = useMemo(() => {
if (!searchQuery) return allBranchItems;
return allBranchItems.filter(item =>
Expand Down Expand Up @@ -464,6 +496,14 @@ const NewWorktree: React.FC<NewWorktreeProps> = ({
<Text dimColor>Press / to search</Text>
</Box>
)}
{includeRemoteBranches && (
<Box marginTop={1}>
<Text dimColor>
Tip: If the branch list feels slow, disable &quot;Include Remote
Branches&quot; in Configuration → Configure Worktree Settings.
</Text>
</Box>
)}
</Box>
)}

Expand Down
50 changes: 50 additions & 0 deletions src/services/worktreeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,56 @@ export class WorktreeService {
);
}

/**
* Effect-based getBranchesWithRemotes operation
* Returns local and remote branches separately so callers can distinguish them.
* Remote branches keep their `<remote>/<branch>` prefix (e.g. `origin/main`).
*
* @returns {Effect.Effect<{local: string[]; remote: string[]}, GitError, never>}
*/
getBranchesWithRemotesEffect(): Effect.Effect<
{local: string[]; remote: string[]},
GitError,
never
> {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
return Effect.catchAll(
Effect.try({
try: () => {
const output = execSync(
"git branch -a --format='%(refname:short)' | grep -v HEAD | sort -u",
{
cwd: self.rootPath,
encoding: 'utf8',
shell: '/bin/bash',
},
);

const remotes = self.getAllRemotes();
const remotePrefixes = remotes.map(r => `${r}/`);

const local: string[] = [];
const remote: string[] = [];

for (const raw of output.trim().split('\n')) {
const branch = raw.trim();
if (!branch) continue;
if (remotePrefixes.some(prefix => branch.startsWith(prefix))) {
remote.push(branch);
} else {
local.push(branch);
}
}

return {local, remote};
},
catch: (error: unknown) => error,
}),
(_error: unknown) => Effect.succeed({local: [], remote: []}),
);
}

/**
* Effect-based getCurrentBranch operation
* Returns Effect that may fail with GitError
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export interface WorktreeConfig {
copySessionData?: boolean; // Whether to copy Claude session data by default
sortByLastSession?: boolean; // Whether to sort worktrees by last opened session
autoUseDefaultBranch?: boolean; // Whether to automatically use default branch as base branch
includeRemoteBranches?: boolean; // Whether to include remote branches in base branch selection
}

export interface MergeConfig {
Expand Down
Loading