Skip to content
Open
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
10 changes: 8 additions & 2 deletions electron/ipc/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export async function createWorktree(
branchName: string,
symlinkDirs: string[],
forceClean = false,
startPoint?: string,
): Promise<{ path: string; branch: string }> {
const worktreePath = `${repoRoot}/.worktrees/${branchName}`;

Expand All @@ -362,7 +363,11 @@ export async function createWorktree(
}

// Create fresh worktree with new branch
await exec('git', ['worktree', 'add', '-b', branchName, worktreePath], { cwd: repoRoot });
await exec(
'git',
['worktree', 'add', '-b', branchName, worktreePath, ...(startPoint ? [startPoint] : [])],
{ cwd: repoRoot },
);

// Symlink selected directories
for (const name of symlinkDirs) {
Expand Down Expand Up @@ -863,11 +868,12 @@ export async function mergeTask(
squash: boolean,
message: string | null,
cleanup: boolean,
targetBranch?: string,
): Promise<{ main_branch: string; lines_added: number; lines_removed: number }> {
const lockKey = await detectRepoLockKey(projectRoot).catch(() => projectRoot);

return withWorktreeLock(lockKey, async () => {
const mainBranch = await detectMainBranch(projectRoot);
const mainBranch = targetBranch ?? (await detectMainBranch(projectRoot));
const { linesAdded, linesRemoved } = await computeBranchDiffStats(
projectRoot,
mainBranch,
Expand Down
19 changes: 17 additions & 2 deletions electron/ipc/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,14 @@ export function registerAllHandlers(win: BrowserWindow): void {
validatePath(args.projectRoot, 'projectRoot');
assertStringArray(args.symlinkDirs, 'symlinkDirs');
assertOptionalString(args.branchPrefix, 'branchPrefix');
const result = createTask(args.name, args.projectRoot, args.symlinkDirs, args.branchPrefix);
assertOptionalString(args.baseBranch, 'baseBranch');
const result = createTask(
args.name,
args.projectRoot,
args.symlinkDirs,
args.branchPrefix,
args.baseBranch,
);
result.then((r: { id: string }) => taskNames.set(r.id, args.name)).catch(() => {});
return result;
});
Expand Down Expand Up @@ -200,7 +207,15 @@ export function registerAllHandlers(win: BrowserWindow): void {
assertBoolean(args.squash, 'squash');
assertOptionalString(args.message, 'message');
assertOptionalBoolean(args.cleanup, 'cleanup');
return mergeTask(args.projectRoot, args.branchName, args.squash, args.message, args.cleanup);
assertOptionalString(args.targetBranch, 'targetBranch');
return mergeTask(
args.projectRoot,
args.branchName,
args.squash,
args.message,
args.cleanup,
args.targetBranch,
);
});
ipcMain.handle(IPC.GetBranchLog, (_e, args) => {
validatePath(args.worktreePath, 'worktreePath');
Expand Down
3 changes: 2 additions & 1 deletion electron/ipc/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ export async function createTask(
projectRoot: string,
symlinkDirs: string[],
branchPrefix: string,
baseBranch?: string,
): Promise<{ id: string; branch_name: string; worktree_path: string }> {
const prefix = sanitizeBranchPrefix(branchPrefix);
const branchName = `${prefix}/${slug(name)}`;
const worktree = await createWorktree(projectRoot, branchName, symlinkDirs);
const worktree = await createWorktree(projectRoot, branchName, symlinkDirs, false, baseBranch);
return {
id: randomUUID(),
branch_name: worktree.branch,
Expand Down
69 changes: 69 additions & 0 deletions src/components/NewTaskDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
const [directMode, setDirectMode] = createSignal(false);
const [skipPermissions, setSkipPermissions] = createSignal(false);
const [branchPrefix, setBranchPrefix] = createSignal('');
const [baseBranch, setBaseBranch] = createSignal('');
let promptRef!: HTMLTextAreaElement;
let formRef!: HTMLFormElement;

Expand Down Expand Up @@ -105,6 +106,7 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
setLoading(false);
setDirectMode(false);
setSkipPermissions(false);
setBaseBranch('');

void (async () => {
if (store.availableAgents.length === 0) {
Expand Down Expand Up @@ -194,6 +196,32 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
setBranchPrefix(pid ? getProjectBranchPrefix(pid) : 'task');
});

// Auto-detect base branch when project changes
createEffect(() => {
const pid = selectedProjectId();
const path = pid ? getProjectPath(pid) : undefined;
let cancelled = false;

if (!path) {
setBaseBranch('');
return;
}

void (async () => {
try {
const detected = await invoke<string>(IPC.GetMainBranch, { projectRoot: path });
if (cancelled) return;
setBaseBranch((prev) => (prev === '' ? detected : prev));
} catch {
/* ignore */
}
})();

onCleanup(() => {
cancelled = true;
});
});

// Pre-check direct mode based on project setting
createEffect(() => {
const pid = selectedProjectId();
Expand Down Expand Up @@ -298,6 +326,7 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
skipPermissions: agentSupportsSkipPermissions() && skipPermissions(),
});
} else {
const bb = baseBranch().trim() || undefined;
taskId = await createTask({
name: n,
agentDef: agent,
Expand All @@ -307,6 +336,7 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
branchPrefixOverride: prefix,
githubUrl: ghUrl,
skipPermissions: agentSupportsSkipPermissions() && skipPermissions(),
baseBranch: bb,
});
}
// Drop flow: prefill prompt without auto-sending
Expand Down Expand Up @@ -495,6 +525,45 @@ export function NewTaskDialog(props: NewTaskDialogProps) {
/>
</Show>

<Show when={!directMode()}>
<div
data-nav-field="base-branch"
style={{ display: 'flex', 'flex-direction': 'column', gap: '8px' }}
>
<label
style={{
'font-size': '11px',
color: theme.fgMuted,
'text-transform': 'uppercase',
'letter-spacing': '0.05em',
}}
>
Base branch{' '}
<span style={{ opacity: '0.5', 'text-transform': 'none' }}>(merge target)</span>
</label>
<input
class="input-field"
type="text"
value={baseBranch()}
onInput={(e) => setBaseBranch(e.currentTarget.value)}
placeholder="main"
style={{
background: theme.bgInput,
border: `1px solid ${theme.border}`,
'border-radius': '8px',
padding: '10px 14px',
color: theme.fg,
'font-size': '13px',
'font-family': "'JetBrains Mono', monospace",
outline: 'none',
}}
/>
<span style={{ 'font-size': '11px', color: theme.fgSubtle, padding: '0 2px' }}>
Worktree branches from and merges back into this branch.
</span>
</div>
</Show>

<AgentSelector
agents={store.availableAgents}
selectedAgent={selectedAgent()}
Expand Down
4 changes: 4 additions & 0 deletions src/store/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export async function saveState(): Promise<void> {
githubUrl: task.githubUrl,
savedInitialPrompt: task.savedInitialPrompt,
planFileName: task.planFileName,
baseBranch: task.baseBranch,
};
}

Expand All @@ -91,6 +92,7 @@ export async function saveState(): Promise<void> {
savedInitialPrompt: task.savedInitialPrompt,
planFileName: task.planFileName,
collapsed: true,
baseBranch: task.baseBranch,
};
}

Expand Down Expand Up @@ -337,6 +339,7 @@ export async function loadState(): Promise<void> {
githubUrl: pt.githubUrl,
savedInitialPrompt: pt.savedInitialPrompt,
planFileName: pt.planFileName,
baseBranch: pt.baseBranch,
};

s.tasks[taskId] = task;
Expand Down Expand Up @@ -404,6 +407,7 @@ export async function loadState(): Promise<void> {
planFileName: pt.planFileName,
collapsed: true,
savedAgentDef: agentDef ?? undefined,
baseBranch: pt.baseBranch,
};

s.tasks[taskId] = task;
Expand Down
5 changes: 5 additions & 0 deletions src/store/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface CreateTaskOptions {
branchPrefixOverride?: string;
githubUrl?: string;
skipPermissions?: boolean;
baseBranch?: string;
}

export async function createTask(opts: CreateTaskOptions): Promise<string> {
Expand All @@ -68,6 +69,7 @@ export async function createTask(opts: CreateTaskOptions): Promise<string> {
initialPrompt,
githubUrl,
skipPermissions,
baseBranch,
} = opts;
const projectRoot = getProjectPath(projectId);
if (!projectRoot) throw new Error('Project not found');
Expand All @@ -79,6 +81,7 @@ export async function createTask(opts: CreateTaskOptions): Promise<string> {
projectRoot,
symlinkDirs,
branchPrefix,
baseBranch,
});

const agentId = crypto.randomUUID();
Expand All @@ -96,6 +99,7 @@ export async function createTask(opts: CreateTaskOptions): Promise<string> {
skipPermissions: skipPermissions || undefined,
githubUrl,
savedInitialPrompt: initialPrompt || undefined,
baseBranch: baseBranch || undefined,
};

const agent: Agent = {
Expand Down Expand Up @@ -327,6 +331,7 @@ export async function mergeTask(
squash: options?.squash ?? false,
message: options?.message,
cleanup,
targetBranch: task.baseBranch,
});
recordMergedLines(mergeResult.lines_added, mergeResult.lines_removed);

Expand Down
2 changes: 2 additions & 0 deletions src/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface Task {
savedAgentDef?: AgentDef;
planContent?: string;
planFileName?: string;
baseBranch?: string;
}

export interface Terminal {
Expand All @@ -77,6 +78,7 @@ export interface PersistedTask {
savedInitialPrompt?: string;
collapsed?: boolean;
planFileName?: string;
baseBranch?: string;
}

export interface PersistedTerminal {
Expand Down