feat: instant draft-first worktree creation and multi-run launcher redesign (#741)

## Summary

- **Instant worktree creation from chat draft**: selecting "+ New worktree" in the draft branch selector immediately creates a session draft and bootstraps the worktree in the background — no modal interruption
- **Redesigned multi-run launcher**: compact 2-column grid layout in a right-sized dialog with scroll shadow, sticky footer, tooltips replacing verbose descriptions, and project icons in the selector
- **Branch selector aligned across surfaces**: multi-run and agent manager branch pickers now use the shared git store and match NewWorktreeDialog behavior (same default resolution cascade, no synthetic HEAD option, all branches shown)
- **Opaque model multi-select dropdown**: fixes text bleed-through on translucent backgrounds by compositing `--surface-elevated` over `--surface-background`
- **"+ New" inline button in sidebar worktree headers** for faster worktree creation

## Why

Worktree creation was behind modal flow that interrupted the user's train of thought. The draft-first approach lets users start typing immediately while the worktree bootstraps. The multi-run launcher had an oversized form layout with redundant explanations, and its branch picker behaved differently from the main worktree dialog - causing confusion about which branches were available and what the default was.
This commit is contained in:
Bohdan Triapitsyn
2026-03-22 22:31:29 +02:00
committed by GitHub
parent c66d480782
commit 53c2a0d919
40 changed files with 1850 additions and 909 deletions
@@ -66,6 +66,9 @@ export const SessionDialogs: React.FC = () => {
archiveSessions,
loadSessions,
getWorktreeMetadata,
newSessionDraft,
setNewSessionDraftTarget,
setDraftBootstrapPendingDirectory,
} = useSessionStore();
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
@@ -381,12 +384,34 @@ export const SessionDialogs: React.FC = () => {
deleteLocalBranch: boolean
): Promise<boolean> => {
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
const projectRef = getProjectRefForWorktree(worktree);
const normalizedWorktreePath = normalizeProjectDirectory(worktree.path);
const normalizedProjectPath = normalizeProjectDirectory(projectRef.path);
try {
await removeProjectWorktree(
getProjectRefForWorktree(worktree),
projectRef,
worktree,
{ deleteRemoteBranch: shouldRemoveRemote, deleteLocalBranch }
);
const draftDirectory = normalizeProjectDirectory(newSessionDraft?.directoryOverride);
if (
newSessionDraft?.open
&& normalizedWorktreePath
&& draftDirectory === normalizedWorktreePath
&& normalizedProjectPath
) {
setDraftBootstrapPendingDirectory(null);
setNewSessionDraftTarget({
projectId: projectRef.id,
directoryOverride: normalizedProjectPath,
}, { force: true });
}
if (normalizeProjectDirectory(currentDirectory) === normalizedWorktreePath && normalizedProjectPath) {
useDirectoryStore.getState().setDirectory(normalizedProjectPath, { showOverlay: false });
}
return true;
} catch (error) {
toast.error('Failed to remove worktree', {
@@ -394,7 +419,7 @@ export const SessionDialogs: React.FC = () => {
});
return false;
}
}, [canRemoveRemoteBranches, deleteDialogShouldRemoveRemote, getProjectRefForWorktree]);
}, [canRemoveRemoteBranches, currentDirectory, deleteDialogShouldRemoveRemote, getProjectRefForWorktree, newSessionDraft?.directoryOverride, newSessionDraft?.open, setDraftBootstrapPendingDirectory, setNewSessionDraftTarget]);
const handleConfirmDelete = React.useCallback(async () => {
if (!deleteDialog) {