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:
Bohdan Triapitsyn
2026-01-07 22:06:28 +02:00
parent 4f6eea9ea5
commit d92a1ab297
14 changed files with 1004 additions and 883 deletions
+57 -1
View File
@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { opencodeClient } from '@/lib/opencode/client';
import type { ProjectEntry } from '@/lib/api/types';
import type { ProjectEntry, WorktreeDefaults } from '@/lib/api/types';
import type { DesktopSettings } from '@/lib/desktop';
import { updateDesktopSettings } from '@/lib/persistence';
import { getSafeStorage } from './utils/safeStorage';
@@ -27,6 +27,7 @@ interface ProjectsStore {
validateProjectPath: (path: string) => ProjectPathValidationResult;
synchronizeFromSettings: (settings: DesktopSettings) => void;
getActiveProject: () => ProjectEntry | null;
updateWorktreeDefaults: (projectId: string, defaults: Partial<WorktreeDefaults>) => void;
}
const safeStorage = getSafeStorage();
@@ -120,6 +121,22 @@ const sanitizeProjects = (value: unknown): ProjectEntry[] => {
if (typeof candidate.lastOpenedAt === 'number' && Number.isFinite(candidate.lastOpenedAt) && candidate.lastOpenedAt >= 0) {
project.lastOpenedAt = candidate.lastOpenedAt;
}
if (candidate.worktreeDefaults && typeof candidate.worktreeDefaults === 'object') {
const wt = candidate.worktreeDefaults as Record<string, unknown>;
const defaults: WorktreeDefaults = {};
if (typeof wt.branchPrefix === 'string') {
defaults.branchPrefix = wt.branchPrefix;
}
if (typeof wt.baseBranch === 'string') {
defaults.baseBranch = wt.baseBranch;
}
if (typeof wt.autoCreateWorktree === 'boolean') {
defaults.autoCreateWorktree = wt.autoCreateWorktree;
}
if (Object.keys(defaults).length > 0) {
project.worktreeDefaults = defaults;
}
}
result.push(project);
}
@@ -434,6 +451,45 @@ export const useProjectsStore = create<ProjectsStore>()(
}
return projects.find((project) => project.id === activeProjectId) ?? null;
},
updateWorktreeDefaults: (projectId: string, defaults: Partial<WorktreeDefaults>) => {
if (vscodeWorkspace) {
return;
}
const { projects, activeProjectId } = get();
const target = projects.find((project) => project.id === projectId);
if (!target) {
return;
}
const merged: WorktreeDefaults = { ...target.worktreeDefaults };
if (defaults.branchPrefix !== undefined) {
if (defaults.branchPrefix.trim()) {
merged.branchPrefix = defaults.branchPrefix.trim();
} else {
delete merged.branchPrefix;
}
}
if (defaults.baseBranch !== undefined) {
if (defaults.baseBranch.trim()) {
merged.baseBranch = defaults.baseBranch.trim();
} else {
delete merged.baseBranch;
}
}
if (defaults.autoCreateWorktree !== undefined) {
merged.autoCreateWorktree = defaults.autoCreateWorktree;
}
const nextProjects = projects.map((project) =>
project.id === projectId
? { ...project, worktreeDefaults: Object.keys(merged).length > 0 ? merged : undefined }
: project
);
set({ projects: nextProjects });
persistProjects(nextProjects, activeProjectId);
},
}), { name: 'projects-store' })
);