From c6db94e9125d86702c70abc6598146488bf5bb33 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 14:53:43 +0100 Subject: [PATCH 1/2] feat(ui): generate meaningful worktree branch names from feature titles Instead of generating random branch names like `feature/main-1737547200000-tt2v`, this change creates human-readable branch names based on the feature title: `feature/add-user-authentication-a3b2` Changes: - Generate branch name slug from feature title (lowercase, alphanumeric, hyphens) - Use 4-character random suffix for uniqueness instead of timestamp - If no title provided, generate one from description first (for auto worktree mode) - Fall back to 'untitled' if both title and description are empty - Fix: Apply substring limit before removing trailing hyphens to prevent malformed branch names when truncation occurs at a hyphen position This makes it much easier to identify which worktree corresponds to which feature when working with multiple features simultaneously. Closes #604 --- .../board-view/hooks/use-board-actions.ts | 93 +++++++++++++++---- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts index 3fbdfd5db..9f6fc7313 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts @@ -123,9 +123,34 @@ export function useBoardActions({ }) => { const workMode = featureData.workMode || 'current'; + // For auto worktree mode, we need a title for the branch name. + // If no title provided, generate one from the description first. + let titleForBranch = featureData.title; + let titleWasGenerated = false; + + if (workMode === 'auto' && !featureData.title.trim() && featureData.description.trim()) { + // Generate title first so we can use it for the branch name + const api = getElectronAPI(); + if (api?.features?.generateTitle) { + try { + const result = await api.features.generateTitle(featureData.description); + if (result.success && result.title) { + titleForBranch = result.title; + titleWasGenerated = true; + } + } catch (error) { + logger.error('Error generating title for branch name:', error); + } + } + // If title generation failed, fall back to first part of description + if (!titleForBranch.trim()) { + titleForBranch = featureData.description.substring(0, 60); + } + } + // Determine final branch name based on work mode: // - 'current': Use current worktree's branch (or undefined if on main) - // - 'auto': Auto-generate branch name based on current branch + // - 'auto': Auto-generate branch name based on feature title // - 'custom': Use the provided branch name let finalBranchName: string | undefined; @@ -134,13 +159,16 @@ export function useBoardActions({ // This ensures features created on a non-main worktree are associated with that worktree finalBranchName = currentWorktreeBranch || undefined; } else if (workMode === 'auto') { - // Auto-generate a branch name based on primary branch (main/master) and timestamp - // Always use primary branch to avoid nested feature/feature/... paths - const baseBranch = - (currentProject?.path ? getPrimaryWorktreeBranch(currentProject.path) : null) || 'main'; - const timestamp = Date.now(); + // Auto-generate a branch name based on feature title and timestamp + // Create a slug from the title: lowercase, replace non-alphanumeric with hyphens + const titleSlug = + titleForBranch + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric sequences with hyphens + .substring(0, 50) // Limit length first + .replace(/^-|-$/g, '') || 'untitled'; // Then remove leading/trailing hyphens, with fallback const randomSuffix = Math.random().toString(36).substring(2, 6); - finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`; + finalBranchName = `feature/${titleSlug}-${randomSuffix}`; } else { // Custom mode - use provided branch name finalBranchName = featureData.branchName || undefined; @@ -183,12 +211,13 @@ export function useBoardActions({ } } - // Check if we need to generate a title - const needsTitleGeneration = !featureData.title.trim() && featureData.description.trim(); + // Check if we need to generate a title (only if we didn't already generate it for the branch name) + const needsTitleGeneration = + !titleWasGenerated && !featureData.title.trim() && featureData.description.trim(); const newFeatureData = { ...featureData, - title: featureData.title, + title: titleWasGenerated ? titleForBranch : featureData.title, titleGenerating: needsTitleGeneration, status: 'backlog' as const, branchName: finalBranchName, @@ -255,7 +284,6 @@ export function useBoardActions({ projectPath, onWorktreeCreated, onWorktreeAutoSelect, - getPrimaryWorktreeBranch, features, currentWorktreeBranch, ] @@ -287,6 +315,31 @@ export function useBoardActions({ ) => { const workMode = updates.workMode || 'current'; + // For auto worktree mode, we need a title for the branch name. + // If no title provided, generate one from the description first. + let titleForBranch = updates.title; + let titleWasGenerated = false; + + if (workMode === 'auto' && !updates.title.trim() && updates.description.trim()) { + // Generate title first so we can use it for the branch name + const api = getElectronAPI(); + if (api?.features?.generateTitle) { + try { + const result = await api.features.generateTitle(updates.description); + if (result.success && result.title) { + titleForBranch = result.title; + titleWasGenerated = true; + } + } catch (error) { + logger.error('Error generating title for branch name:', error); + } + } + // If title generation failed, fall back to first part of description + if (!titleForBranch.trim()) { + titleForBranch = updates.description.substring(0, 60); + } + } + // Determine final branch name based on work mode let finalBranchName: string | undefined; @@ -295,13 +348,16 @@ export function useBoardActions({ // This ensures features updated on a non-main worktree are associated with that worktree finalBranchName = currentWorktreeBranch || undefined; } else if (workMode === 'auto') { - // Auto-generate a branch name based on primary branch (main/master) and timestamp - // Always use primary branch to avoid nested feature/feature/... paths - const baseBranch = - (currentProject?.path ? getPrimaryWorktreeBranch(currentProject.path) : null) || 'main'; - const timestamp = Date.now(); + // Auto-generate a branch name based on feature title and timestamp + // Create a slug from the title: lowercase, replace non-alphanumeric with hyphens + const titleSlug = + titleForBranch + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric sequences with hyphens + .substring(0, 50) // Limit length first + .replace(/^-|-$/g, '') || 'untitled'; // Then remove leading/trailing hyphens, with fallback const randomSuffix = Math.random().toString(36).substring(2, 6); - finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`; + finalBranchName = `feature/${titleSlug}-${randomSuffix}`; } else { finalBranchName = updates.branchName || undefined; } @@ -343,7 +399,7 @@ export function useBoardActions({ const finalUpdates = { ...restUpdates, - title: updates.title, + title: titleWasGenerated ? titleForBranch : updates.title, branchName: finalBranchName, }; @@ -406,7 +462,6 @@ export function useBoardActions({ setEditingFeature, currentProject, onWorktreeCreated, - getPrimaryWorktreeBranch, features, currentWorktreeBranch, ] From 84fc650db991b44987856905f8a456d98df667ea Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Thu, 22 Jan 2026 15:10:20 +0100 Subject: [PATCH 2/2] fix(ui): preserve existing branch name in auto mode when editing features When editing a feature that already has a branch name assigned, preserve it instead of generating a new one. This prevents orphaning existing worktrees when users edit features in auto worktree mode. --- .../board-view/hooks/use-board-actions.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts index 9f6fc7313..526779585 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-actions.ts @@ -348,16 +348,21 @@ export function useBoardActions({ // This ensures features updated on a non-main worktree are associated with that worktree finalBranchName = currentWorktreeBranch || undefined; } else if (workMode === 'auto') { - // Auto-generate a branch name based on feature title and timestamp - // Create a slug from the title: lowercase, replace non-alphanumeric with hyphens - const titleSlug = - titleForBranch - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric sequences with hyphens - .substring(0, 50) // Limit length first - .replace(/^-|-$/g, '') || 'untitled'; // Then remove leading/trailing hyphens, with fallback - const randomSuffix = Math.random().toString(36).substring(2, 6); - finalBranchName = `feature/${titleSlug}-${randomSuffix}`; + // Preserve existing branch name if one exists (avoid orphaning worktrees on edit) + if (updates.branchName?.trim()) { + finalBranchName = updates.branchName; + } else { + // Auto-generate a branch name based on feature title + // Create a slug from the title: lowercase, replace non-alphanumeric with hyphens + const titleSlug = + titleForBranch + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric sequences with hyphens + .substring(0, 50) // Limit length first + .replace(/^-|-$/g, '') || 'untitled'; // Then remove leading/trailing hyphens, with fallback + const randomSuffix = Math.random().toString(36).substring(2, 6); + finalBranchName = `feature/${titleSlug}-${randomSuffix}`; + } } else { finalBranchName = updates.branchName || undefined; }