Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 74 additions & 19 deletions apps/ui/src/components/views/board-view/hooks/use-board-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,23 +117,51 @@ 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': No branch name, work on current branch (no worktree)
// - '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;

if (workMode === 'current') {
// No worktree isolation - work directly on current branch
finalBranchName = 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';
// 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
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
.substring(0, 50) || 'untitled'; // Fallback if slug is empty (e.g., title was only special chars)
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(2, 6);
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
finalBranchName = `feature/${titleSlug}-${timestamp}`;
Comment on lines 125 to 164
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for generating a branch name in 'auto' mode, including title generation and slugification, is duplicated between handleAddFeature and handleUpdateFeature (lines 314-348). To improve maintainability and adhere to the DRY principle, consider extracting this logic into a shared helper function.

} else {
// Custom mode - use provided branch name
finalBranchName = featureData.branchName || undefined;
Expand Down Expand Up @@ -176,12 +204,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,
Expand Down Expand Up @@ -247,7 +276,6 @@ export function useBoardActions({
currentProject,
onWorktreeCreated,
onWorktreeAutoSelect,
getPrimaryWorktreeBranch,
features,
]
);
Expand Down Expand Up @@ -278,19 +306,47 @@ 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;

if (workMode === 'current') {
finalBranchName = 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';
// 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
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
.substring(0, 50) || 'untitled'; // Fallback if slug is empty (e.g., title was only special chars)
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(2, 6);
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
finalBranchName = `feature/${titleSlug}-${timestamp}`;
} else {
finalBranchName = updates.branchName || undefined;
}
Expand Down Expand Up @@ -332,7 +388,7 @@ export function useBoardActions({

const finalUpdates = {
...restUpdates,
title: updates.title,
title: titleWasGenerated ? titleForBranch : updates.title,
branchName: finalBranchName,
};
Comment on lines 389 to 393
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's an inconsistency here compared to handleAddFeature. In handleAddFeature, if a title isn't generated synchronously for the branch name, needsTitleGeneration is set to true, and a background task is kicked off to generate the title for the feature. This logic seems to be missing in handleUpdateFeature.

As a result, if a user updates a feature to use 'auto' worktree mode without a title, and the synchronous title generation fails, the feature's title will remain empty, even though a branch is created.

For consistency, you should consider adding the background title generation logic here as well, similar to what's done in handleAddFeature.


Expand Down Expand Up @@ -395,7 +451,6 @@ export function useBoardActions({
setEditingFeature,
currentProject,
onWorktreeCreated,
getPrimaryWorktreeBranch,
features,
]
);
Expand Down