feat: implement worktree session creation and management
- Added `createWorktreeSession` utility to handle the creation of new sessions with auto-generated worktrees. - Introduced branch name generation utilities for creating unique and friendly branch names. - Updated `CommandPalette` to trigger worktree session creation via a command. - Refactored keyboard shortcuts to support new worktree session creation with Shift + Cmd/Ctrl + N. - Enhanced project settings to include worktree defaults (branch prefix, base branch, auto-create option). - Updated persistence logic to preserve worktree defaults when saving project settings. - Modified menu actions to initiate new worktree sessions. - Ensured proper handling of existing branches to avoid conflicts during worktree creation.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Utility for creating a new session with an auto-generated worktree.
|
||||
* This is a standalone function that can be called from keyboard shortcuts,
|
||||
* menu actions, or other non-hook contexts.
|
||||
*/
|
||||
|
||||
import { toast } from 'sonner';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { checkIsGitRepository } from '@/lib/gitApi';
|
||||
import { generateUniqueBranchName } from '@/lib/git/branchNameGenerator';
|
||||
import {
|
||||
createWorktree,
|
||||
getWorktreeStatus,
|
||||
removeWorktree,
|
||||
runWorktreeSetupCommands,
|
||||
} from '@/lib/git/worktreeService';
|
||||
import { getWorktreeSetupCommands } from '@/lib/openchamberConfig';
|
||||
|
||||
const sanitizeWorktreeSlug = (value: string): string => {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
||||
.replace(/^[-_]+|[-_]+$/g, '')
|
||||
.slice(0, 120);
|
||||
};
|
||||
|
||||
// Track if we're currently creating a worktree session
|
||||
let isCreatingWorktreeSession = false;
|
||||
|
||||
/**
|
||||
* Create a new session with an auto-generated worktree.
|
||||
* Uses project's worktree defaults (branch prefix, base branch) from settings.
|
||||
*
|
||||
* @returns The created session, or null if creation failed
|
||||
*/
|
||||
export async function createWorktreeSession(): Promise<{ id: string } | null> {
|
||||
if (isCreatingWorktreeSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeProject = useProjectsStore.getState().getActiveProject();
|
||||
if (!activeProject?.path) {
|
||||
toast.error('No active project', {
|
||||
description: 'Please select a project first.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const projectDirectory = activeProject.path;
|
||||
|
||||
// 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;
|
||||
|
||||
try {
|
||||
// Get worktree defaults from project settings
|
||||
const worktreeDefaults = activeProject.worktreeDefaults;
|
||||
const branchPrefix = worktreeDefaults?.branchPrefix;
|
||||
const baseBranch = worktreeDefaults?.baseBranch;
|
||||
|
||||
// Generate a unique branch name
|
||||
const branchName = await generateUniqueBranchName(projectDirectory, branchPrefix);
|
||||
if (!branchName) {
|
||||
toast.error('Failed to generate branch name', {
|
||||
description: 'Could not generate a unique branch name. Please try again.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const worktreeSlug = sanitizeWorktreeSlug(branchName);
|
||||
|
||||
// Determine start point (base branch)
|
||||
const startPoint = baseBranch && baseBranch !== 'HEAD' ? baseBranch : undefined;
|
||||
|
||||
// Create the worktree
|
||||
const metadata = await createWorktree({
|
||||
projectDirectory,
|
||||
worktreeSlug,
|
||||
branch: branchName,
|
||||
createBranch: true,
|
||||
startPoint,
|
||||
});
|
||||
|
||||
// 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 agents = useConfigStore.getState().agents;
|
||||
sessionStore.initializeNewOpenChamberSession(session.id, agents);
|
||||
sessionStore.setSessionDirectory(session.id, metadata.path);
|
||||
sessionStore.setWorktreeMetadata(session.id, createdMetadata);
|
||||
|
||||
// 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 {
|
||||
isCreatingWorktreeSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a worktree session is currently being created.
|
||||
*/
|
||||
export function isCreatingWorktree(): boolean {
|
||||
return isCreatingWorktreeSession;
|
||||
}
|
||||
Reference in New Issue
Block a user