mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: implement work mode selection in feature dialogs
- Added WorkModeSelector component to allow users to choose between 'current', 'auto', and 'custom' work modes for feature management. - Updated AddFeatureDialog and EditFeatureDialog to utilize the new work mode functionality, replacing the previous branch selector logic. - Enhanced useBoardActions hook to handle branch name generation based on the selected work mode. - Adjusted settings to default to using worktrees, improving the overall feature creation experience. These changes streamline the feature management process by providing clearer options for branch handling and worktree isolation.
This commit is contained in:
@@ -615,7 +615,8 @@ export class SettingsService {
|
|||||||
appState.skipVerificationInAutoMode !== undefined
|
appState.skipVerificationInAutoMode !== undefined
|
||||||
? (appState.skipVerificationInAutoMode as boolean)
|
? (appState.skipVerificationInAutoMode as boolean)
|
||||||
: false,
|
: false,
|
||||||
useWorktrees: (appState.useWorktrees as boolean) || false,
|
useWorktrees:
|
||||||
|
appState.useWorktrees !== undefined ? (appState.useWorktrees as boolean) : true,
|
||||||
showProfilesOnly: (appState.showProfilesOnly as boolean) || false,
|
showProfilesOnly: (appState.showProfilesOnly as boolean) || false,
|
||||||
defaultPlanningMode:
|
defaultPlanningMode:
|
||||||
(appState.defaultPlanningMode as GlobalSettings['defaultPlanningMode']) || 'skip',
|
(appState.defaultPlanningMode as GlobalSettings['defaultPlanningMode']) || 'skip',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -46,11 +46,12 @@ import {
|
|||||||
import {
|
import {
|
||||||
TestingTabContent,
|
TestingTabContent,
|
||||||
PrioritySelector,
|
PrioritySelector,
|
||||||
BranchSelector,
|
WorkModeSelector,
|
||||||
PlanningModeSelect,
|
PlanningModeSelect,
|
||||||
AncestorContextSection,
|
AncestorContextSection,
|
||||||
ProfileTypeahead,
|
ProfileTypeahead,
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
|
import type { WorkMode } from '../shared';
|
||||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||||
import {
|
import {
|
||||||
@@ -84,6 +85,7 @@ type FeatureData = {
|
|||||||
planningMode: PlanningMode;
|
planningMode: PlanningMode;
|
||||||
requirePlanApproval: boolean;
|
requirePlanApproval: boolean;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
|
workMode: WorkMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AddFeatureDialogProps {
|
interface AddFeatureDialogProps {
|
||||||
@@ -123,7 +125,7 @@ export function AddFeatureDialog({
|
|||||||
}: AddFeatureDialogProps) {
|
}: AddFeatureDialogProps) {
|
||||||
const isSpawnMode = !!parentFeature;
|
const isSpawnMode = !!parentFeature;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [useCurrentBranch, setUseCurrentBranch] = useState(true);
|
const [workMode, setWorkMode] = useState<WorkMode>('current');
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
@@ -161,22 +163,27 @@ export function AddFeatureDialog({
|
|||||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Get defaults from store
|
// Get defaults from store
|
||||||
const { defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId, useWorktrees } =
|
const { defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId } = useAppStore();
|
||||||
useAppStore();
|
|
||||||
|
|
||||||
// Enhancement model override
|
// Enhancement model override
|
||||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||||
|
|
||||||
// Sync defaults when dialog opens
|
// Track previous open state to detect when dialog opens
|
||||||
|
const wasOpenRef = useRef(false);
|
||||||
|
|
||||||
|
// Sync defaults only when dialog opens (transitions from closed to open)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
const justOpened = open && !wasOpenRef.current;
|
||||||
|
wasOpenRef.current = open;
|
||||||
|
|
||||||
|
if (justOpened) {
|
||||||
const defaultProfile = defaultAIProfileId
|
const defaultProfile = defaultAIProfileId
|
||||||
? aiProfiles.find((p) => p.id === defaultAIProfileId)
|
? aiProfiles.find((p) => p.id === defaultAIProfileId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
setSkipTests(defaultSkipTests);
|
setSkipTests(defaultSkipTests);
|
||||||
setBranchName(defaultBranch || '');
|
setBranchName(defaultBranch || '');
|
||||||
setUseCurrentBranch(true);
|
setWorkMode('current');
|
||||||
setPlanningMode(defaultPlanningMode);
|
setPlanningMode(defaultPlanningMode);
|
||||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||||
|
|
||||||
@@ -248,7 +255,7 @@ export function AddFeatureDialog({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useWorktrees && !useCurrentBranch && !branchName.trim()) {
|
if (workMode === 'custom' && !branchName.trim()) {
|
||||||
toast.error('Please select a branch name');
|
toast.error('Please select a branch name');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -262,7 +269,10 @@ export function AddFeatureDialog({
|
|||||||
? modelEntry.reasoningEffort || 'none'
|
? modelEntry.reasoningEffort || 'none'
|
||||||
: 'none';
|
: 'none';
|
||||||
|
|
||||||
const finalBranchName = useCurrentBranch ? currentBranch || '' : branchName || '';
|
// For 'current' mode, use empty string (work on current branch)
|
||||||
|
// For 'auto' mode, use empty string (will be auto-generated in use-board-actions)
|
||||||
|
// For 'custom' mode, use the specified branch name
|
||||||
|
const finalBranchName = workMode === 'custom' ? branchName || '' : '';
|
||||||
|
|
||||||
// Build final description with ancestor context in spawn mode
|
// Build final description with ancestor context in spawn mode
|
||||||
let finalDescription = description;
|
let finalDescription = description;
|
||||||
@@ -303,6 +313,7 @@ export function AddFeatureDialog({
|
|||||||
planningMode,
|
planningMode,
|
||||||
requirePlanApproval,
|
requirePlanApproval,
|
||||||
dependencies: isSpawnMode && parentFeature ? [parentFeature.id] : undefined,
|
dependencies: isSpawnMode && parentFeature ? [parentFeature.id] : undefined,
|
||||||
|
workMode,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -318,7 +329,7 @@ export function AddFeatureDialog({
|
|||||||
setPriority(2);
|
setPriority(2);
|
||||||
setSelectedProfileId(undefined);
|
setSelectedProfileId(undefined);
|
||||||
setModelEntry({ model: 'opus' });
|
setModelEntry({ model: 'opus' });
|
||||||
setUseCurrentBranch(true);
|
setWorkMode('current');
|
||||||
setPlanningMode(defaultPlanningMode);
|
setPlanningMode(defaultPlanningMode);
|
||||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||||
setPreviewMap(new Map());
|
setPreviewMap(new Map());
|
||||||
@@ -643,21 +654,19 @@ export function AddFeatureDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Branch Selector */}
|
{/* Work Mode Selector */}
|
||||||
{useWorktrees && (
|
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<BranchSelector
|
<WorkModeSelector
|
||||||
useCurrentBranch={useCurrentBranch}
|
workMode={workMode}
|
||||||
onUseCurrentBranchChange={setUseCurrentBranch}
|
onWorkModeChange={setWorkMode}
|
||||||
branchName={branchName}
|
branchName={branchName}
|
||||||
onBranchNameChange={setBranchName}
|
onBranchNameChange={setBranchName}
|
||||||
branchSuggestions={branchSuggestions}
|
branchSuggestions={branchSuggestions}
|
||||||
branchCardCounts={branchCardCounts}
|
branchCardCounts={branchCardCounts}
|
||||||
currentBranch={currentBranch}
|
currentBranch={currentBranch}
|
||||||
testIdPrefix="feature"
|
testIdPrefix="feature-work-mode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -670,7 +679,7 @@ export function AddFeatureDialog({
|
|||||||
onClick={handleAddAndStart}
|
onClick={handleAddAndStart}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
data-testid="confirm-add-and-start-feature"
|
data-testid="confirm-add-and-start-feature"
|
||||||
disabled={useWorktrees && !useCurrentBranch && !branchName.trim()}
|
disabled={workMode === 'custom' && !branchName.trim()}
|
||||||
>
|
>
|
||||||
<Play className="w-4 h-4 mr-2" />
|
<Play className="w-4 h-4 mr-2" />
|
||||||
Make
|
Make
|
||||||
@@ -681,7 +690,7 @@ export function AddFeatureDialog({
|
|||||||
hotkey={{ key: 'Enter', cmdCtrl: true }}
|
hotkey={{ key: 'Enter', cmdCtrl: true }}
|
||||||
hotkeyActive={open}
|
hotkeyActive={open}
|
||||||
data-testid="confirm-add-feature"
|
data-testid="confirm-add-feature"
|
||||||
disabled={useWorktrees && !useCurrentBranch && !branchName.trim()}
|
disabled={workMode === 'custom' && !branchName.trim()}
|
||||||
>
|
>
|
||||||
{isSpawnMode ? 'Spawn Task' : 'Add Feature'}
|
{isSpawnMode ? 'Spawn Task' : 'Add Feature'}
|
||||||
</HotkeyButton>
|
</HotkeyButton>
|
||||||
|
|||||||
@@ -46,10 +46,11 @@ import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '
|
|||||||
import {
|
import {
|
||||||
TestingTabContent,
|
TestingTabContent,
|
||||||
PrioritySelector,
|
PrioritySelector,
|
||||||
BranchSelector,
|
WorkModeSelector,
|
||||||
PlanningModeSelect,
|
PlanningModeSelect,
|
||||||
ProfileTypeahead,
|
ProfileTypeahead,
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
|
import type { WorkMode } from '../shared';
|
||||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||||
import {
|
import {
|
||||||
@@ -118,9 +119,11 @@ export function EditFeatureDialog({
|
|||||||
}: EditFeatureDialogProps) {
|
}: EditFeatureDialogProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
||||||
const [useCurrentBranch, setUseCurrentBranch] = useState(() => {
|
// Derive initial workMode from feature's branchName
|
||||||
// If feature has no branchName, default to using current branch
|
const [workMode, setWorkMode] = useState<WorkMode>(() => {
|
||||||
return !feature?.branchName;
|
// If feature has a branchName, it's using 'custom' mode
|
||||||
|
// Otherwise, it's on 'current' branch (no worktree isolation)
|
||||||
|
return feature?.branchName ? 'custom' : 'current';
|
||||||
});
|
});
|
||||||
const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState<ImagePreviewMap>(
|
const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState<ImagePreviewMap>(
|
||||||
() => new Map()
|
() => new Map()
|
||||||
@@ -156,9 +159,6 @@ export function EditFeatureDialog({
|
|||||||
// Track if history dropdown is open
|
// Track if history dropdown is open
|
||||||
const [showHistory, setShowHistory] = useState(false);
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
|
|
||||||
// Get worktrees setting from store
|
|
||||||
const { useWorktrees } = useAppStore();
|
|
||||||
|
|
||||||
// Enhancement model override
|
// Enhancement model override
|
||||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||||
|
|
||||||
@@ -167,8 +167,8 @@ export function EditFeatureDialog({
|
|||||||
if (feature) {
|
if (feature) {
|
||||||
setPlanningMode(feature.planningMode ?? 'skip');
|
setPlanningMode(feature.planningMode ?? 'skip');
|
||||||
setRequirePlanApproval(feature.requirePlanApproval ?? false);
|
setRequirePlanApproval(feature.requirePlanApproval ?? false);
|
||||||
// If feature has no branchName, default to using current branch
|
// Derive workMode from feature's branchName
|
||||||
setUseCurrentBranch(!feature.branchName);
|
setWorkMode(feature.branchName ? 'custom' : 'current');
|
||||||
// Reset history tracking state
|
// Reset history tracking state
|
||||||
setOriginalDescription(feature.description ?? '');
|
setOriginalDescription(feature.description ?? '');
|
||||||
setDescriptionChangeSource(null);
|
setDescriptionChangeSource(null);
|
||||||
@@ -222,14 +222,9 @@ export function EditFeatureDialog({
|
|||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
if (!editingFeature) return;
|
if (!editingFeature) return;
|
||||||
|
|
||||||
// Validate branch selection when "other branch" is selected and branch selector is enabled
|
// Validate branch selection for custom mode
|
||||||
const isBranchSelectorEnabled = editingFeature.status === 'backlog';
|
const isBranchSelectorEnabled = editingFeature.status === 'backlog';
|
||||||
if (
|
if (isBranchSelectorEnabled && workMode === 'custom' && !editingFeature.branchName?.trim()) {
|
||||||
useWorktrees &&
|
|
||||||
isBranchSelectorEnabled &&
|
|
||||||
!useCurrentBranch &&
|
|
||||||
!editingFeature.branchName?.trim()
|
|
||||||
) {
|
|
||||||
toast.error('Please select a branch name');
|
toast.error('Please select a branch name');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -242,12 +237,10 @@ export function EditFeatureDialog({
|
|||||||
? (modelEntry.reasoningEffort ?? 'none')
|
? (modelEntry.reasoningEffort ?? 'none')
|
||||||
: 'none';
|
: 'none';
|
||||||
|
|
||||||
// Use current branch if toggle is on
|
// For 'current' mode, use empty string (work on current branch)
|
||||||
// If currentBranch is provided (non-primary worktree), use it
|
// For 'auto' mode, use empty string (will be auto-generated in use-board-actions)
|
||||||
// Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary)
|
// For 'custom' mode, use the specified branch name
|
||||||
const finalBranchName = useCurrentBranch
|
const finalBranchName = workMode === 'custom' ? editingFeature.branchName || '' : '';
|
||||||
? currentBranch || ''
|
|
||||||
: editingFeature.branchName || '';
|
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
title: editingFeature.title ?? '',
|
title: editingFeature.title ?? '',
|
||||||
@@ -263,6 +256,7 @@ export function EditFeatureDialog({
|
|||||||
priority: editingFeature.priority ?? 2,
|
priority: editingFeature.priority ?? 2,
|
||||||
planningMode,
|
planningMode,
|
||||||
requirePlanApproval,
|
requirePlanApproval,
|
||||||
|
workMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine if description changed and what source to use
|
// Determine if description changed and what source to use
|
||||||
@@ -688,12 +682,11 @@ export function EditFeatureDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Branch Selector */}
|
{/* Work Mode Selector */}
|
||||||
{useWorktrees && (
|
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<BranchSelector
|
<WorkModeSelector
|
||||||
useCurrentBranch={useCurrentBranch}
|
workMode={workMode}
|
||||||
onUseCurrentBranchChange={setUseCurrentBranch}
|
onWorkModeChange={setWorkMode}
|
||||||
branchName={editingFeature.branchName ?? ''}
|
branchName={editingFeature.branchName ?? ''}
|
||||||
onBranchNameChange={(value) =>
|
onBranchNameChange={(value) =>
|
||||||
setEditingFeature({
|
setEditingFeature({
|
||||||
@@ -705,10 +698,9 @@ export function EditFeatureDialog({
|
|||||||
branchCardCounts={branchCardCounts}
|
branchCardCounts={branchCardCounts}
|
||||||
currentBranch={currentBranch}
|
currentBranch={currentBranch}
|
||||||
disabled={editingFeature.status !== 'backlog'}
|
disabled={editingFeature.status !== 'backlog'}
|
||||||
testIdPrefix="edit-feature"
|
testIdPrefix="edit-feature-work-mode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -731,9 +723,8 @@ export function EditFeatureDialog({
|
|||||||
hotkeyActive={!!editingFeature}
|
hotkeyActive={!!editingFeature}
|
||||||
data-testid="confirm-edit-feature"
|
data-testid="confirm-edit-feature"
|
||||||
disabled={
|
disabled={
|
||||||
useWorktrees &&
|
|
||||||
editingFeature.status === 'backlog' &&
|
editingFeature.status === 'backlog' &&
|
||||||
!useCurrentBranch &&
|
workMode === 'custom' &&
|
||||||
!editingFeature.branchName?.trim()
|
!editingFeature.branchName?.trim()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -111,14 +111,32 @@ export function useBoardActions({
|
|||||||
planningMode: PlanningMode;
|
planningMode: PlanningMode;
|
||||||
requirePlanApproval: boolean;
|
requirePlanApproval: boolean;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
|
workMode?: 'current' | 'auto' | 'custom';
|
||||||
}) => {
|
}) => {
|
||||||
// Empty string means "unassigned" (show only on primary worktree) - convert to undefined
|
const workMode = featureData.workMode || 'current';
|
||||||
// Non-empty string is the actual branch name (for non-primary worktrees)
|
|
||||||
const finalBranchName = featureData.branchName || undefined;
|
|
||||||
|
|
||||||
// If worktrees enabled and a branch is specified, create the worktree now
|
// Determine final branch name based on work mode:
|
||||||
// This ensures the worktree exists before the feature starts
|
// - 'current': No branch name, work on current branch (no worktree)
|
||||||
if (useWorktrees && finalBranchName && currentProject) {
|
// - 'auto': Auto-generate branch name based on current branch
|
||||||
|
// - '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 current branch and timestamp
|
||||||
|
const baseBranch = currentWorktreeBranch || 'main';
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
||||||
|
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
|
||||||
|
} else {
|
||||||
|
// Custom mode - use provided branch name
|
||||||
|
finalBranchName = featureData.branchName || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create worktree for 'auto' or 'custom' modes when we have a branch name
|
||||||
|
if ((workMode === 'auto' || workMode === 'custom') && finalBranchName && currentProject) {
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (api?.worktree?.create) {
|
if (api?.worktree?.create) {
|
||||||
@@ -207,10 +225,10 @@ export function useBoardActions({
|
|||||||
persistFeatureUpdate,
|
persistFeatureUpdate,
|
||||||
updateFeature,
|
updateFeature,
|
||||||
saveCategory,
|
saveCategory,
|
||||||
useWorktrees,
|
|
||||||
currentProject,
|
currentProject,
|
||||||
onWorktreeCreated,
|
onWorktreeCreated,
|
||||||
onWorktreeAutoSelect,
|
onWorktreeAutoSelect,
|
||||||
|
currentWorktreeBranch,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -230,15 +248,29 @@ export function useBoardActions({
|
|||||||
priority: number;
|
priority: number;
|
||||||
planningMode?: PlanningMode;
|
planningMode?: PlanningMode;
|
||||||
requirePlanApproval?: boolean;
|
requirePlanApproval?: boolean;
|
||||||
|
workMode?: 'current' | 'auto' | 'custom';
|
||||||
},
|
},
|
||||||
descriptionHistorySource?: 'enhance' | 'edit',
|
descriptionHistorySource?: 'enhance' | 'edit',
|
||||||
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance'
|
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance'
|
||||||
) => {
|
) => {
|
||||||
const finalBranchName = updates.branchName || undefined;
|
const workMode = updates.workMode || 'current';
|
||||||
|
|
||||||
// If worktrees enabled and a branch is specified, create the worktree now
|
// Determine final branch name based on work mode
|
||||||
// This ensures the worktree exists before the feature starts
|
let finalBranchName: string | undefined;
|
||||||
if (useWorktrees && finalBranchName && currentProject) {
|
|
||||||
|
if (workMode === 'current') {
|
||||||
|
finalBranchName = undefined;
|
||||||
|
} else if (workMode === 'auto') {
|
||||||
|
const baseBranch = currentWorktreeBranch || 'main';
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomSuffix = Math.random().toString(36).substring(2, 6);
|
||||||
|
finalBranchName = `feature/${baseBranch}-${timestamp}-${randomSuffix}`;
|
||||||
|
} else {
|
||||||
|
finalBranchName = updates.branchName || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create worktree for 'auto' or 'custom' modes when we have a branch name
|
||||||
|
if ((workMode === 'auto' || workMode === 'custom') && finalBranchName && currentProject) {
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (api?.worktree?.create) {
|
if (api?.worktree?.create) {
|
||||||
@@ -287,9 +319,9 @@ export function useBoardActions({
|
|||||||
persistFeatureUpdate,
|
persistFeatureUpdate,
|
||||||
saveCategory,
|
saveCategory,
|
||||||
setEditingFeature,
|
setEditingFeature,
|
||||||
useWorktrees,
|
|
||||||
currentProject,
|
currentProject,
|
||||||
onWorktreeCreated,
|
onWorktreeCreated,
|
||||||
|
currentWorktreeBranch,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export * from './branch-selector';
|
|||||||
export * from './planning-mode-selector';
|
export * from './planning-mode-selector';
|
||||||
export * from './planning-mode-select';
|
export * from './planning-mode-select';
|
||||||
export * from './ancestor-context-section';
|
export * from './ancestor-context-section';
|
||||||
|
export * from './work-mode-selector';
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { BranchAutocomplete } from '@/components/ui/branch-autocomplete';
|
||||||
|
import { GitBranch, GitFork, Pencil } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export type WorkMode = 'current' | 'auto' | 'custom';
|
||||||
|
|
||||||
|
interface WorkModeSelectorProps {
|
||||||
|
workMode: WorkMode;
|
||||||
|
onWorkModeChange: (mode: WorkMode) => void;
|
||||||
|
branchName: string;
|
||||||
|
onBranchNameChange: (branchName: string) => void;
|
||||||
|
branchSuggestions: string[];
|
||||||
|
branchCardCounts?: Record<string, number>;
|
||||||
|
currentBranch?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
testIdPrefix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORK_MODES = [
|
||||||
|
{
|
||||||
|
value: 'current' as const,
|
||||||
|
label: 'Current Branch',
|
||||||
|
description: 'Work directly on the selected branch',
|
||||||
|
icon: GitBranch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'auto' as const,
|
||||||
|
label: 'Auto Worktree',
|
||||||
|
description: 'Create isolated worktree automatically',
|
||||||
|
icon: GitFork,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'custom' as const,
|
||||||
|
label: 'Custom Branch',
|
||||||
|
description: 'Specify a branch name',
|
||||||
|
icon: Pencil,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function WorkModeSelector({
|
||||||
|
workMode,
|
||||||
|
onWorkModeChange,
|
||||||
|
branchName,
|
||||||
|
onBranchNameChange,
|
||||||
|
branchSuggestions,
|
||||||
|
branchCardCounts,
|
||||||
|
currentBranch,
|
||||||
|
disabled = false,
|
||||||
|
testIdPrefix = 'work-mode',
|
||||||
|
}: WorkModeSelectorProps) {
|
||||||
|
const hasError = workMode === 'custom' && !branchName.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label id={`${testIdPrefix}-label`}>Work Mode</Label>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{WORK_MODES.map((mode) => {
|
||||||
|
const isSelected = workMode === mode.value;
|
||||||
|
const Icon = mode.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={mode.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => !disabled && onWorkModeChange(mode.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid={`${testIdPrefix}-${mode.value}`}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center gap-1.5 p-3 rounded-lg cursor-pointer transition-all duration-200',
|
||||||
|
'border-2 hover:border-primary/50',
|
||||||
|
isSelected
|
||||||
|
? 'border-primary bg-primary/10'
|
||||||
|
: 'border-border/50 bg-card/50 hover:bg-accent/30',
|
||||||
|
disabled && 'opacity-50 cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-8 h-8 rounded-full flex items-center justify-center transition-colors',
|
||||||
|
isSelected ? 'bg-primary/20' : 'bg-muted'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
'h-4 w-4 transition-colors',
|
||||||
|
isSelected ? 'text-primary' : 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'font-medium text-xs text-center',
|
||||||
|
isSelected ? 'text-foreground' : 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{mode.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description text based on selected mode */}
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{workMode === 'current' && (
|
||||||
|
<>
|
||||||
|
Work will be done directly on{' '}
|
||||||
|
{currentBranch ? (
|
||||||
|
<span className="font-medium">{currentBranch}</span>
|
||||||
|
) : (
|
||||||
|
'the current branch'
|
||||||
|
)}
|
||||||
|
. No isolation.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{workMode === 'auto' && (
|
||||||
|
<>
|
||||||
|
A new worktree will be created automatically based on{' '}
|
||||||
|
{currentBranch ? (
|
||||||
|
<span className="font-medium">{currentBranch}</span>
|
||||||
|
) : (
|
||||||
|
'the current branch'
|
||||||
|
)}{' '}
|
||||||
|
when this card is created.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{workMode === 'custom' && (
|
||||||
|
<>Specify a branch name below. A worktree will be created if it doesn't exist.</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Branch input for custom mode */}
|
||||||
|
{workMode === 'custom' && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<BranchAutocomplete
|
||||||
|
value={branchName}
|
||||||
|
onChange={onBranchNameChange}
|
||||||
|
branches={branchSuggestions}
|
||||||
|
branchCardCounts={branchCardCounts}
|
||||||
|
placeholder="Select or create branch..."
|
||||||
|
data-testid={`${testIdPrefix}-branch-input`}
|
||||||
|
disabled={disabled}
|
||||||
|
error={hasError}
|
||||||
|
/>
|
||||||
|
{hasError && (
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
Branch name is required for custom branch mode.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{disabled && (
|
||||||
|
<p className="text-xs text-muted-foreground italic">
|
||||||
|
Work mode cannot be changed after work has started.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -358,9 +358,6 @@ export function FeatureDefaultsSection({
|
|||||||
>
|
>
|
||||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||||
Enable Git Worktree Isolation
|
Enable Git Worktree Isolation
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded-md bg-amber-500/15 text-amber-500 border border-amber-500/20 font-medium">
|
|
||||||
experimental
|
|
||||||
</span>
|
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||||
Creates isolated git branches for each feature. When disabled, agents work directly in
|
Creates isolated git branches for each feature. When disabled, agents work directly in
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
|||||||
defaultSkipTests: settings.defaultSkipTests ?? true,
|
defaultSkipTests: settings.defaultSkipTests ?? true,
|
||||||
enableDependencyBlocking: settings.enableDependencyBlocking ?? true,
|
enableDependencyBlocking: settings.enableDependencyBlocking ?? true,
|
||||||
skipVerificationInAutoMode: settings.skipVerificationInAutoMode ?? false,
|
skipVerificationInAutoMode: settings.skipVerificationInAutoMode ?? false,
|
||||||
useWorktrees: settings.useWorktrees ?? false,
|
useWorktrees: settings.useWorktrees ?? true,
|
||||||
showProfilesOnly: settings.showProfilesOnly ?? false,
|
showProfilesOnly: settings.showProfilesOnly ?? false,
|
||||||
defaultPlanningMode: settings.defaultPlanningMode ?? 'skip',
|
defaultPlanningMode: settings.defaultPlanningMode ?? 'skip',
|
||||||
defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false,
|
defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false,
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ export interface AppState {
|
|||||||
skipVerificationInAutoMode: boolean; // When true, auto-mode grabs features even if dependencies are not verified (only checks they're not running)
|
skipVerificationInAutoMode: boolean; // When true, auto-mode grabs features even if dependencies are not verified (only checks they're not running)
|
||||||
|
|
||||||
// Worktree Settings
|
// Worktree Settings
|
||||||
useWorktrees: boolean; // Whether to use git worktree isolation for features (default: false)
|
useWorktrees: boolean; // Whether to use git worktree isolation for features (default: true)
|
||||||
|
|
||||||
// User-managed Worktrees (per-project)
|
// User-managed Worktrees (per-project)
|
||||||
// projectPath -> { path: worktreePath or null for main, branch: branch name }
|
// projectPath -> { path: worktreePath or null for main, branch: branch name }
|
||||||
@@ -1172,7 +1172,7 @@ const initialState: AppState = {
|
|||||||
defaultSkipTests: true, // Default to manual verification (tests disabled)
|
defaultSkipTests: true, // Default to manual verification (tests disabled)
|
||||||
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
|
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
|
||||||
skipVerificationInAutoMode: false, // Default to disabled (require dependencies to be verified)
|
skipVerificationInAutoMode: false, // Default to disabled (require dependencies to be verified)
|
||||||
useWorktrees: false, // Default to disabled (worktree feature is experimental)
|
useWorktrees: true, // Default to enabled (git worktree isolation)
|
||||||
currentWorktreeByProject: {},
|
currentWorktreeByProject: {},
|
||||||
worktreesByProject: {},
|
worktreesByProject: {},
|
||||||
showProfilesOnly: false, // Default to showing all options (not profiles only)
|
showProfilesOnly: false, // Default to showing all options (not profiles only)
|
||||||
|
|||||||
@@ -785,7 +785,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||||||
defaultSkipTests: true,
|
defaultSkipTests: true,
|
||||||
enableDependencyBlocking: true,
|
enableDependencyBlocking: true,
|
||||||
skipVerificationInAutoMode: false,
|
skipVerificationInAutoMode: false,
|
||||||
useWorktrees: false,
|
useWorktrees: true,
|
||||||
showProfilesOnly: false,
|
showProfilesOnly: false,
|
||||||
defaultPlanningMode: 'skip',
|
defaultPlanningMode: 'skip',
|
||||||
defaultRequirePlanApproval: false,
|
defaultRequirePlanApproval: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user