feat(board-view): enhance feature and plan dialogs with worktree branch settings

- Added WorktreeSettingsDialog and PlanSettingsDialog components to manage worktree branch settings.
- Integrated new settings into BoardHeader for toggling worktree branch usage in feature creation.
- Updated AddFeatureDialog to utilize selected worktree branch for custom mode.
- Introduced new state management in app-store for handling worktree branch preferences.

These changes improve user control over feature creation workflows by allowing branch selection based on the current worktree context.
This commit is contained in:
Kacper
2026-01-11 23:05:32 +01:00
parent 53f5c2b2bb
commit a0669d4262
6 changed files with 273 additions and 15 deletions

View File

@@ -101,6 +101,8 @@ export function BoardView() {
useWorktrees,
enableDependencyBlocking,
skipVerificationInAutoMode,
planUseSelectedWorktreeBranch,
addFeatureUseSelectedWorktreeBranch,
isPrimaryWorktreeBranch,
getPrimaryWorktreeBranch,
setPipelineConfig,
@@ -1370,6 +1372,14 @@ export function BoardView() {
isMaximized={isMaximized}
parentFeature={spawnParentFeature}
allFeatures={hookFeatures}
// When setting is enabled and a non-main worktree is selected, pass its branch to default to 'custom' work mode
selectedNonMainWorktreeBranch={
addFeatureUseSelectedWorktreeBranch && currentWorktreePath !== null
? currentWorktreeBranch || undefined
: undefined
}
// When the worktree setting is disabled, force 'current' branch mode
forceCurrentBranchMode={!addFeatureUseSelectedWorktreeBranch}
/>
{/* Edit Feature Dialog */}
@@ -1449,7 +1459,7 @@ export function BoardView() {
setPendingPlanResult={setPendingBacklogPlan}
isGeneratingPlan={isGeneratingPlan}
setIsGeneratingPlan={setIsGeneratingPlan}
currentBranch={selectedWorktreeBranch}
currentBranch={planUseSelectedWorktreeBranch ? selectedWorktreeBranch : undefined}
/>
{/* Plan Approval Dialog */}

View File

@@ -9,6 +9,8 @@ import { UsagePopover } from '@/components/usage-popover';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { AutoModeSettingsDialog } from './dialogs/auto-mode-settings-dialog';
import { WorktreeSettingsDialog } from './dialogs/worktree-settings-dialog';
import { PlanSettingsDialog } from './dialogs/plan-settings-dialog';
import { getHttpApiClient } from '@/lib/http-api-client';
import { BoardSearchBar } from './board-search-bar';
import { BoardControls } from './board-controls';
@@ -55,10 +57,22 @@ export function BoardHeader({
completedCount,
}: BoardHeaderProps) {
const [showAutoModeSettings, setShowAutoModeSettings] = useState(false);
const [showWorktreeSettings, setShowWorktreeSettings] = useState(false);
const [showPlanSettings, setShowPlanSettings] = useState(false);
const apiKeys = useAppStore((state) => state.apiKeys);
const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus);
const skipVerificationInAutoMode = useAppStore((state) => state.skipVerificationInAutoMode);
const setSkipVerificationInAutoMode = useAppStore((state) => state.setSkipVerificationInAutoMode);
const planUseSelectedWorktreeBranch = useAppStore((state) => state.planUseSelectedWorktreeBranch);
const setPlanUseSelectedWorktreeBranch = useAppStore(
(state) => state.setPlanUseSelectedWorktreeBranch
);
const addFeatureUseSelectedWorktreeBranch = useAppStore(
(state) => state.addFeatureUseSelectedWorktreeBranch
);
const setAddFeatureUseSelectedWorktreeBranch = useAppStore(
(state) => state.setAddFeatureUseSelectedWorktreeBranch
);
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
// Worktree panel visibility (per-project)
@@ -132,9 +146,25 @@ export function BoardHeader({
onCheckedChange={handleWorktreePanelToggle}
data-testid="worktrees-toggle"
/>
<button
onClick={() => setShowWorktreeSettings(true)}
className="p-1 rounded hover:bg-accent/50 transition-colors"
title="Worktree Settings"
data-testid="worktree-settings-button"
>
<Settings2 className="w-4 h-4 text-muted-foreground" />
</button>
</div>
)}
{/* Worktree Settings Dialog */}
<WorktreeSettingsDialog
open={showWorktreeSettings}
onOpenChange={setShowWorktreeSettings}
addFeatureUseSelectedWorktreeBranch={addFeatureUseSelectedWorktreeBranch}
onAddFeatureUseSelectedWorktreeBranchChange={setAddFeatureUseSelectedWorktreeBranch}
/>
{/* Concurrency Control - only show after mount to prevent hydration issues */}
{isMounted && (
<Popover>
@@ -209,15 +239,33 @@ export function BoardHeader({
onSkipVerificationChange={setSkipVerificationInAutoMode}
/>
<Button
size="sm"
variant="outline"
onClick={onOpenPlanDialog}
data-testid="plan-backlog-button"
>
<Wand2 className="w-4 h-4 mr-2" />
Plan
</Button>
{/* Plan Button with Settings */}
<div className={controlContainerClass} data-testid="plan-button-container">
<button
onClick={onOpenPlanDialog}
className="flex items-center gap-1.5 hover:text-foreground transition-colors"
data-testid="plan-backlog-button"
>
<Wand2 className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">Plan</span>
</button>
<button
onClick={() => setShowPlanSettings(true)}
className="p-1 rounded hover:bg-accent/50 transition-colors"
title="Plan Settings"
data-testid="plan-settings-button"
>
<Settings2 className="w-4 h-4 text-muted-foreground" />
</button>
</div>
{/* Plan Settings Dialog */}
<PlanSettingsDialog
open={showPlanSettings}
onOpenChange={setShowPlanSettings}
planUseSelectedWorktreeBranch={planUseSelectedWorktreeBranch}
onPlanUseSelectedWorktreeBranchChange={setPlanUseSelectedWorktreeBranch}
/>
</div>
</div>
);

View File

@@ -56,6 +56,32 @@ import {
const logger = createLogger('AddFeatureDialog');
/**
* Determines the default work mode based on global settings and current worktree selection.
*
* Priority:
* 1. If forceCurrentBranchMode is true, always defaults to 'current' (work on current branch)
* 2. If a non-main worktree is selected in the board header, defaults to 'custom' (use that branch)
* 3. If useWorktrees global setting is enabled, defaults to 'auto' (automatic worktree creation)
* 4. Otherwise, defaults to 'current' (work on current branch without isolation)
*/
const getDefaultWorkMode = (
useWorktrees: boolean,
selectedNonMainWorktreeBranch?: string,
forceCurrentBranchMode?: boolean
): WorkMode => {
// If force current branch mode is enabled (worktree setting is off), always use 'current'
if (forceCurrentBranchMode) {
return 'current';
}
// If a non-main worktree is selected, default to 'custom' mode with that branch
if (selectedNonMainWorktreeBranch) {
return 'custom';
}
// Otherwise, respect the global worktree setting
return useWorktrees ? 'auto' : 'current';
};
type FeatureData = {
title: string;
category: string;
@@ -89,6 +115,16 @@ interface AddFeatureDialogProps {
isMaximized: boolean;
parentFeature?: Feature | null;
allFeatures?: Feature[];
/**
* When a non-main worktree is selected in the board header, this will be set to that worktree's branch.
* When set, the dialog will default to 'custom' work mode with this branch pre-filled.
*/
selectedNonMainWorktreeBranch?: string;
/**
* When true, forces the dialog to default to 'current' work mode (work on current branch).
* This is used when the "Use selected worktree branch" setting is disabled.
*/
forceCurrentBranchMode?: boolean;
}
/**
@@ -112,6 +148,8 @@ export function AddFeatureDialog({
isMaximized,
parentFeature = null,
allFeatures = [],
selectedNonMainWorktreeBranch,
forceCurrentBranchMode,
}: AddFeatureDialogProps) {
const isSpawnMode = !!parentFeature;
const [workMode, setWorkMode] = useState<WorkMode>('current');
@@ -149,7 +187,7 @@ export function AddFeatureDialog({
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
// Get defaults from store
const { defaultPlanningMode, defaultRequirePlanApproval } = useAppStore();
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees } = useAppStore();
// Track previous open state to detect when dialog opens
const wasOpenRef = useRef(false);
@@ -161,8 +199,12 @@ export function AddFeatureDialog({
if (justOpened) {
setSkipTests(defaultSkipTests);
setBranchName(defaultBranch || '');
setWorkMode('current');
// When a non-main worktree is selected, use its branch name for custom mode
// Otherwise, use the default branch
setBranchName(selectedNonMainWorktreeBranch || defaultBranch || '');
setWorkMode(
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
);
setPlanningMode(defaultPlanningMode);
setRequirePlanApproval(defaultRequirePlanApproval);
setModelEntry({ model: 'opus' });
@@ -186,6 +228,9 @@ export function AddFeatureDialog({
defaultBranch,
defaultPlanningMode,
defaultRequirePlanApproval,
useWorktrees,
selectedNonMainWorktreeBranch,
forceCurrentBranchMode,
parentFeature,
allFeatures,
]);
@@ -270,10 +315,13 @@ export function AddFeatureDialog({
setImagePaths([]);
setTextFilePaths([]);
setSkipTests(defaultSkipTests);
setBranchName('');
// When a non-main worktree is selected, use its branch name for custom mode
setBranchName(selectedNonMainWorktreeBranch || '');
setPriority(2);
setModelEntry({ model: 'opus' });
setWorkMode('current');
setWorkMode(
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
);
setPlanningMode(defaultPlanningMode);
setRequirePlanApproval(defaultRequirePlanApproval);
setPreviewMap(new Map());

View File

@@ -0,0 +1,67 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { GitBranch, Settings2 } from 'lucide-react';
interface PlanSettingsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
planUseSelectedWorktreeBranch: boolean;
onPlanUseSelectedWorktreeBranchChange: (value: boolean) => void;
}
export function PlanSettingsDialog({
open,
onOpenChange,
planUseSelectedWorktreeBranch,
onPlanUseSelectedWorktreeBranchChange,
}: PlanSettingsDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md" data-testid="plan-settings-dialog">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Settings2 className="w-5 h-5" />
Plan Settings
</DialogTitle>
<DialogDescription>
Configure how the Plan feature creates and organizes new features.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Use Selected Worktree Branch Setting */}
<div className="flex items-start space-x-3 p-3 rounded-lg bg-secondary/50">
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<Label
htmlFor="plan-worktree-branch-toggle"
className="text-sm font-medium cursor-pointer flex items-center gap-2"
>
<GitBranch className="w-4 h-4 text-brand-500" />
Use selected worktree branch
</Label>
<Switch
id="plan-worktree-branch-toggle"
checked={planUseSelectedWorktreeBranch}
onCheckedChange={onPlanUseSelectedWorktreeBranchChange}
data-testid="plan-worktree-branch-toggle"
/>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
When enabled, features created via the Plan dialog will be assigned to the currently
selected worktree branch. When disabled, features will be added to the main branch.
</p>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,67 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { GitBranch, Settings2 } from 'lucide-react';
interface WorktreeSettingsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
addFeatureUseSelectedWorktreeBranch: boolean;
onAddFeatureUseSelectedWorktreeBranchChange: (value: boolean) => void;
}
export function WorktreeSettingsDialog({
open,
onOpenChange,
addFeatureUseSelectedWorktreeBranch,
onAddFeatureUseSelectedWorktreeBranchChange,
}: WorktreeSettingsDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md" data-testid="worktree-settings-dialog">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Settings2 className="w-5 h-5" />
Worktree Settings
</DialogTitle>
<DialogDescription>
Configure how worktrees affect feature creation and organization.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Use Selected Worktree Branch Setting */}
<div className="flex items-start space-x-3 p-3 rounded-lg bg-secondary/50">
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<Label
htmlFor="worktree-branch-toggle"
className="text-sm font-medium cursor-pointer flex items-center gap-2"
>
<GitBranch className="w-4 h-4 text-brand-500" />
Use selected worktree branch
</Label>
<Switch
id="worktree-branch-toggle"
checked={addFeatureUseSelectedWorktreeBranch}
onCheckedChange={onAddFeatureUseSelectedWorktreeBranchChange}
data-testid="worktree-branch-toggle"
/>
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
When enabled, the Add Feature dialog will default to custom branch mode with the
currently selected worktree branch pre-filled.
</p>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -530,6 +530,8 @@ export interface AppState {
defaultSkipTests: boolean; // Default value for skip tests when creating new features
enableDependencyBlocking: boolean; // When true, show blocked badges and warnings for features with incomplete dependencies (default: true)
skipVerificationInAutoMode: boolean; // When true, auto-mode grabs features even if dependencies are not verified (only checks they're not running)
planUseSelectedWorktreeBranch: boolean; // When true, Plan dialog creates features on the currently selected worktree branch
addFeatureUseSelectedWorktreeBranch: boolean; // When true, Add Feature dialog defaults to custom mode with selected worktree branch
// Worktree Settings
useWorktrees: boolean; // Whether to use git worktree isolation for features (default: true)
@@ -913,6 +915,8 @@ export interface AppActions {
setDefaultSkipTests: (skip: boolean) => void;
setEnableDependencyBlocking: (enabled: boolean) => void;
setSkipVerificationInAutoMode: (enabled: boolean) => Promise<void>;
setPlanUseSelectedWorktreeBranch: (enabled: boolean) => Promise<void>;
setAddFeatureUseSelectedWorktreeBranch: (enabled: boolean) => Promise<void>;
// Worktree Settings actions
setUseWorktrees: (enabled: boolean) => void;
@@ -1191,6 +1195,8 @@ const initialState: AppState = {
defaultSkipTests: true, // Default to manual verification (tests disabled)
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
skipVerificationInAutoMode: false, // Default to disabled (require dependencies to be verified)
planUseSelectedWorktreeBranch: true, // Default to enabled (Plan creates features on selected worktree branch)
addFeatureUseSelectedWorktreeBranch: false, // Default to disabled (Add Feature uses normal defaults)
useWorktrees: true, // Default to enabled (git worktree isolation)
currentWorktreeByProject: {},
worktreesByProject: {},
@@ -1816,6 +1822,18 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
setPlanUseSelectedWorktreeBranch: async (enabled) => {
set({ planUseSelectedWorktreeBranch: enabled });
// Sync to server settings file
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
setAddFeatureUseSelectedWorktreeBranch: async (enabled) => {
set({ addFeatureUseSelectedWorktreeBranch: enabled });
// Sync to server settings file
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
// Worktree Settings actions
setUseWorktrees: (enabled) => set({ useWorktrees: enabled }),