Files
openchamber/packages/ui/src/lib/worktreeSessionCreator.ts
T

184 lines
6.0 KiB
TypeScript
Raw Normal View History

/**
* 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;
}