mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +00:00
merge: sync with latest v0.9.0rc changes
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">
|
<WorkModeSelector
|
||||||
<BranchSelector
|
workMode={workMode}
|
||||||
useCurrentBranch={useCurrentBranch}
|
onWorkModeChange={setWorkMode}
|
||||||
onUseCurrentBranchChange={setUseCurrentBranch}
|
branchName={branchName}
|
||||||
branchName={branchName}
|
onBranchNameChange={setBranchName}
|
||||||
onBranchNameChange={setBranchName}
|
branchSuggestions={branchSuggestions}
|
||||||
branchSuggestions={branchSuggestions}
|
branchCardCounts={branchCardCounts}
|
||||||
branchCardCounts={branchCardCounts}
|
currentBranch={currentBranch}
|
||||||
currentBranch={currentBranch}
|
testIdPrefix="feature-work-mode"
|
||||||
testIdPrefix="feature"
|
/>
|
||||||
/>
|
</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,27 +682,25 @@ export function EditFeatureDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Branch Selector */}
|
{/* Work Mode Selector */}
|
||||||
{useWorktrees && (
|
<div className="pt-2">
|
||||||
<div className="pt-2">
|
<WorkModeSelector
|
||||||
<BranchSelector
|
workMode={workMode}
|
||||||
useCurrentBranch={useCurrentBranch}
|
onWorkModeChange={setWorkMode}
|
||||||
onUseCurrentBranchChange={setUseCurrentBranch}
|
branchName={editingFeature.branchName ?? ''}
|
||||||
branchName={editingFeature.branchName ?? ''}
|
onBranchNameChange={(value) =>
|
||||||
onBranchNameChange={(value) =>
|
setEditingFeature({
|
||||||
setEditingFeature({
|
...editingFeature,
|
||||||
...editingFeature,
|
branchName: value,
|
||||||
branchName: value,
|
})
|
||||||
})
|
}
|
||||||
}
|
branchSuggestions={branchSuggestions}
|
||||||
branchSuggestions={branchSuggestions}
|
branchCardCounts={branchCardCounts}
|
||||||
branchCardCounts={branchCardCounts}
|
currentBranch={currentBranch}
|
||||||
currentBranch={currentBranch}
|
disabled={editingFeature.status !== 'backlog'}
|
||||||
disabled={editingFeature.status !== 'backlog'}
|
testIdPrefix="edit-feature-work-mode"
|
||||||
testIdPrefix="edit-feature"
|
/>
|
||||||
/>
|
</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