feat: add branch picker dialog for creating worktree sessions (#160)
- Add BranchPickerDialog component to browse branches across all projects - Filter out branches that already have worktrees - Show commit hash and ahead/behind indicators for each branch - Add createWorktreeSessionForBranch() for specific project/branch - Add worktree and branch picker buttons in sidebar project header Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
033e370c55
commit
7aa4d79b1d
@@ -249,3 +249,192 @@ export async function createWorktreeSession(): Promise<{ id: string } | null> {
|
||||
export function isCreatingWorktree(): boolean {
|
||||
return isCreatingWorktreeSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session with a worktree for a specific branch.
|
||||
* Unlike createWorktreeSession(), this allows specifying the project and branch explicitly.
|
||||
*
|
||||
* @param projectDirectory - The root directory of the git repository
|
||||
* @param branchName - The name of the branch to create a worktree for
|
||||
* @returns The created session, or null if creation failed
|
||||
*/
|
||||
export async function createWorktreeSessionForBranch(
|
||||
projectDirectory: string,
|
||||
branchName: string
|
||||
): Promise<{ id: string } | null> {
|
||||
if (isCreatingWorktreeSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if it's a git repo
|
||||
let isGitRepo = false;
|
||||
try {
|
||||
isGitRepo = await checkIsGitRepository(projectDirectory);
|
||||
} catch {
|
||||
// Ignore errors, treat as not a git repo
|
||||
}
|
||||
|
||||
if (!isGitRepo) {
|
||||
toast.error('Not a Git repository', {
|
||||
description: 'Worktrees can only be created in Git repositories.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
isCreatingWorktreeSession = true;
|
||||
startConfigUpdate("Creating worktree session...");
|
||||
|
||||
try {
|
||||
// Use the branch name as the worktree slug (sanitized)
|
||||
const worktreeSlug = sanitizeWorktreeSlug(branchName);
|
||||
|
||||
// Create the worktree - don't create a new branch, use existing one
|
||||
const metadata = await createWorktree({
|
||||
projectDirectory,
|
||||
worktreeSlug,
|
||||
branch: branchName,
|
||||
createBranch: false, // Use existing branch
|
||||
});
|
||||
|
||||
// Get worktree status
|
||||
const status = await getWorktreeStatus(metadata.path).catch(() => undefined);
|
||||
const createdMetadata = status ? { ...metadata, status } : metadata;
|
||||
|
||||
// Create the session
|
||||
const sessionStore = useSessionStore.getState();
|
||||
const session = await sessionStore.createSession(undefined, metadata.path);
|
||||
if (!session) {
|
||||
// Clean up the worktree if session creation failed
|
||||
await removeWorktree({ projectDirectory, path: metadata.path, force: true }).catch(() => undefined);
|
||||
toast.error('Failed to create session', {
|
||||
description: 'Could not create a session for the worktree.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Initialize the session
|
||||
const configState = useConfigStore.getState();
|
||||
const agents = configState.agents;
|
||||
sessionStore.initializeNewOpenChamberSession(session.id, agents);
|
||||
sessionStore.setSessionDirectory(session.id, metadata.path);
|
||||
sessionStore.setWorktreeMetadata(session.id, createdMetadata);
|
||||
|
||||
// Apply default agent and model settings
|
||||
try {
|
||||
const visibleAgents = configState.getVisibleAgents();
|
||||
let agentName: string | undefined;
|
||||
|
||||
// Priority: settingsDefaultAgent → build → first visible
|
||||
if (configState.settingsDefaultAgent) {
|
||||
const settingsAgent = visibleAgents.find((a) => a.name === configState.settingsDefaultAgent);
|
||||
if (settingsAgent) {
|
||||
agentName = settingsAgent.name;
|
||||
}
|
||||
}
|
||||
if (!agentName) {
|
||||
agentName =
|
||||
visibleAgents.find((agent) => agent.name === 'build')?.name ||
|
||||
visibleAgents[0]?.name;
|
||||
}
|
||||
|
||||
if (agentName) {
|
||||
// 1. Update global UI state
|
||||
configState.setAgent(agentName);
|
||||
|
||||
// 2. Persist to session context so it sticks after reload/switch
|
||||
useContextStore.getState().saveSessionAgentSelection(session.id, agentName);
|
||||
|
||||
// 3. Handle default model for the agent if set in global settings
|
||||
const settingsDefaultModel = configState.settingsDefaultModel;
|
||||
if (settingsDefaultModel) {
|
||||
const parts = settingsDefaultModel.split('/');
|
||||
if (parts.length === 2) {
|
||||
const [providerId, modelId] = parts;
|
||||
// Validate model exists (optional, but good practice)
|
||||
const modelMetadata = configState.getModelMetadata(providerId, modelId);
|
||||
if (modelMetadata) {
|
||||
useContextStore.getState().saveSessionModelSelection(session.id, providerId, modelId);
|
||||
// Also save the specific agent's model preference for this session
|
||||
useContextStore.getState().saveAgentModelForSession(session.id, agentName, providerId, modelId);
|
||||
|
||||
// Seed default variant into session context so ModelControls restore logic
|
||||
// doesn't wipe it on first switch to the new session.
|
||||
const settingsDefaultVariant = configState.settingsDefaultVariant;
|
||||
if (settingsDefaultVariant) {
|
||||
const provider = configState.providers.find((p) => p.id === providerId);
|
||||
const model = provider?.models.find((m: Record<string, unknown>) => (m as { id?: string }).id === modelId) as
|
||||
| { variants?: Record<string, unknown> }
|
||||
| undefined;
|
||||
const variants = model?.variants;
|
||||
|
||||
if (variants && Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) {
|
||||
configState.setCurrentVariant(settingsDefaultVariant);
|
||||
useContextStore
|
||||
.getState()
|
||||
.saveAgentModelVariantForSession(session.id, agentName, providerId, modelId, settingsDefaultVariant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors setting default agent
|
||||
}
|
||||
|
||||
// Update directory
|
||||
useDirectoryStore.getState().setDirectory(metadata.path, { showOverlay: false });
|
||||
|
||||
// Refresh sessions list
|
||||
try {
|
||||
await sessionStore.loadSessions();
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Get and run setup commands
|
||||
const setupCommands = await getWorktreeSetupCommands(projectDirectory);
|
||||
const commandsToRun = setupCommands.filter(cmd => cmd.trim().length > 0);
|
||||
|
||||
if (commandsToRun.length > 0) {
|
||||
toast.success('Worktree created', {
|
||||
description: `Branch: ${branchName}. Running ${commandsToRun.length} setup command${commandsToRun.length === 1 ? '' : 's'}...`,
|
||||
});
|
||||
|
||||
// Run setup commands in background
|
||||
runWorktreeSetupCommands(metadata.path, projectDirectory, commandsToRun).then((result) => {
|
||||
if (result.success) {
|
||||
toast.success('Setup commands completed', {
|
||||
description: `All ${result.results.length} command${result.results.length === 1 ? '' : 's'} succeeded.`,
|
||||
});
|
||||
} else {
|
||||
const failed = result.results.filter(r => !r.success);
|
||||
const succeeded = result.results.filter(r => r.success);
|
||||
toast.error('Setup commands failed', {
|
||||
description: `${failed.length} of ${result.results.length} command${result.results.length === 1 ? '' : 's'} failed.` +
|
||||
(succeeded.length > 0 ? ` ${succeeded.length} succeeded.` : ''),
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
toast.error('Setup commands failed', {
|
||||
description: 'Could not execute setup commands.',
|
||||
});
|
||||
});
|
||||
} else {
|
||||
toast.success('Worktree created', {
|
||||
description: `Branch: ${branchName}`,
|
||||
});
|
||||
}
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create worktree session';
|
||||
toast.error('Failed to create worktree', {
|
||||
description: message,
|
||||
});
|
||||
return null;
|
||||
} finally {
|
||||
finishConfigUpdate();
|
||||
isCreatingWorktreeSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user