merge: sync with latest v0.9.0rc changes

This commit is contained in:
Kacper
2026-01-09 22:27:06 +01:00
10 changed files with 287 additions and 93 deletions

View File

@@ -615,7 +615,8 @@ export class SettingsService {
appState.skipVerificationInAutoMode !== undefined
? (appState.skipVerificationInAutoMode as boolean)
: false,
useWorktrees: (appState.useWorktrees as boolean) || false,
useWorktrees:
appState.useWorktrees !== undefined ? (appState.useWorktrees as boolean) : true,
showProfilesOnly: (appState.showProfilesOnly as boolean) || false,
defaultPlanningMode:
(appState.defaultPlanningMode as GlobalSettings['defaultPlanningMode']) || 'skip',

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { createLogger } from '@automaker/utils/logger';
import {
Dialog,
@@ -46,11 +46,12 @@ import {
import {
TestingTabContent,
PrioritySelector,
BranchSelector,
WorkModeSelector,
PlanningModeSelect,
AncestorContextSection,
ProfileTypeahead,
} from '../shared';
import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
import {
@@ -84,6 +85,7 @@ type FeatureData = {
planningMode: PlanningMode;
requirePlanApproval: boolean;
dependencies?: string[];
workMode: WorkMode;
};
interface AddFeatureDialogProps {
@@ -123,7 +125,7 @@ export function AddFeatureDialog({
}: AddFeatureDialogProps) {
const isSpawnMode = !!parentFeature;
const navigate = useNavigate();
const [useCurrentBranch, setUseCurrentBranch] = useState(true);
const [workMode, setWorkMode] = useState<WorkMode>('current');
// Form state
const [title, setTitle] = useState('');
@@ -161,22 +163,27 @@ export function AddFeatureDialog({
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
// Get defaults from store
const { defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId, useWorktrees } =
useAppStore();
const { defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId } = useAppStore();
// Enhancement model override
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(() => {
if (open) {
const justOpened = open && !wasOpenRef.current;
wasOpenRef.current = open;
if (justOpened) {
const defaultProfile = defaultAIProfileId
? aiProfiles.find((p) => p.id === defaultAIProfileId)
: null;
setSkipTests(defaultSkipTests);
setBranchName(defaultBranch || '');
setUseCurrentBranch(true);
setWorkMode('current');
setPlanningMode(defaultPlanningMode);
setRequirePlanApproval(defaultRequirePlanApproval);
@@ -248,7 +255,7 @@ export function AddFeatureDialog({
return null;
}
if (useWorktrees && !useCurrentBranch && !branchName.trim()) {
if (workMode === 'custom' && !branchName.trim()) {
toast.error('Please select a branch name');
return null;
}
@@ -262,7 +269,10 @@ export function AddFeatureDialog({
? modelEntry.reasoningEffort || '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
let finalDescription = description;
@@ -303,6 +313,7 @@ export function AddFeatureDialog({
planningMode,
requirePlanApproval,
dependencies: isSpawnMode && parentFeature ? [parentFeature.id] : undefined,
workMode,
};
};
@@ -318,7 +329,7 @@ export function AddFeatureDialog({
setPriority(2);
setSelectedProfileId(undefined);
setModelEntry({ model: 'opus' });
setUseCurrentBranch(true);
setWorkMode('current');
setPlanningMode(defaultPlanningMode);
setRequirePlanApproval(defaultRequirePlanApproval);
setPreviewMap(new Map());
@@ -643,21 +654,19 @@ export function AddFeatureDialog({
</div>
</div>
{/* Branch Selector */}
{useWorktrees && (
<div className="pt-2">
<BranchSelector
useCurrentBranch={useCurrentBranch}
onUseCurrentBranchChange={setUseCurrentBranch}
branchName={branchName}
onBranchNameChange={setBranchName}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch}
testIdPrefix="feature"
/>
</div>
)}
{/* Work Mode Selector */}
<div className="pt-2">
<WorkModeSelector
workMode={workMode}
onWorkModeChange={setWorkMode}
branchName={branchName}
onBranchNameChange={setBranchName}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch}
testIdPrefix="feature-work-mode"
/>
</div>
</div>
</div>
@@ -670,7 +679,7 @@ export function AddFeatureDialog({
onClick={handleAddAndStart}
variant="secondary"
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" />
Make
@@ -681,7 +690,7 @@ export function AddFeatureDialog({
hotkey={{ key: 'Enter', cmdCtrl: true }}
hotkeyActive={open}
data-testid="confirm-add-feature"
disabled={useWorktrees && !useCurrentBranch && !branchName.trim()}
disabled={workMode === 'custom' && !branchName.trim()}
>
{isSpawnMode ? 'Spawn Task' : 'Add Feature'}
</HotkeyButton>

View File

@@ -46,10 +46,11 @@ import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '
import {
TestingTabContent,
PrioritySelector,
BranchSelector,
WorkModeSelector,
PlanningModeSelect,
ProfileTypeahead,
} from '../shared';
import type { WorkMode } from '../shared';
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
import {
@@ -118,9 +119,11 @@ export function EditFeatureDialog({
}: EditFeatureDialogProps) {
const navigate = useNavigate();
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
const [useCurrentBranch, setUseCurrentBranch] = useState(() => {
// If feature has no branchName, default to using current branch
return !feature?.branchName;
// Derive initial workMode from feature's branchName
const [workMode, setWorkMode] = useState<WorkMode>(() => {
// 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>(
() => new Map()
@@ -156,9 +159,6 @@ export function EditFeatureDialog({
// Track if history dropdown is open
const [showHistory, setShowHistory] = useState(false);
// Get worktrees setting from store
const { useWorktrees } = useAppStore();
// Enhancement model override
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
@@ -167,8 +167,8 @@ export function EditFeatureDialog({
if (feature) {
setPlanningMode(feature.planningMode ?? 'skip');
setRequirePlanApproval(feature.requirePlanApproval ?? false);
// If feature has no branchName, default to using current branch
setUseCurrentBranch(!feature.branchName);
// Derive workMode from feature's branchName
setWorkMode(feature.branchName ? 'custom' : 'current');
// Reset history tracking state
setOriginalDescription(feature.description ?? '');
setDescriptionChangeSource(null);
@@ -222,14 +222,9 @@ export function EditFeatureDialog({
const handleUpdate = () => {
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';
if (
useWorktrees &&
isBranchSelectorEnabled &&
!useCurrentBranch &&
!editingFeature.branchName?.trim()
) {
if (isBranchSelectorEnabled && workMode === 'custom' && !editingFeature.branchName?.trim()) {
toast.error('Please select a branch name');
return;
}
@@ -242,12 +237,10 @@ export function EditFeatureDialog({
? (modelEntry.reasoningEffort ?? 'none')
: 'none';
// Use current branch if toggle is on
// If currentBranch is provided (non-primary worktree), use it
// Otherwise (primary worktree), use empty string which means "unassigned" (show only on primary)
const finalBranchName = useCurrentBranch
? currentBranch || ''
: editingFeature.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' ? editingFeature.branchName || '' : '';
const updates = {
title: editingFeature.title ?? '',
@@ -263,6 +256,7 @@ export function EditFeatureDialog({
priority: editingFeature.priority ?? 2,
planningMode,
requirePlanApproval,
workMode,
};
// Determine if description changed and what source to use
@@ -688,27 +682,25 @@ export function EditFeatureDialog({
</div>
</div>
{/* Branch Selector */}
{useWorktrees && (
<div className="pt-2">
<BranchSelector
useCurrentBranch={useCurrentBranch}
onUseCurrentBranchChange={setUseCurrentBranch}
branchName={editingFeature.branchName ?? ''}
onBranchNameChange={(value) =>
setEditingFeature({
...editingFeature,
branchName: value,
})
}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch}
disabled={editingFeature.status !== 'backlog'}
testIdPrefix="edit-feature"
/>
</div>
)}
{/* Work Mode Selector */}
<div className="pt-2">
<WorkModeSelector
workMode={workMode}
onWorkModeChange={setWorkMode}
branchName={editingFeature.branchName ?? ''}
onBranchNameChange={(value) =>
setEditingFeature({
...editingFeature,
branchName: value,
})
}
branchSuggestions={branchSuggestions}
branchCardCounts={branchCardCounts}
currentBranch={currentBranch}
disabled={editingFeature.status !== 'backlog'}
testIdPrefix="edit-feature-work-mode"
/>
</div>
</div>
</div>
@@ -731,9 +723,8 @@ export function EditFeatureDialog({
hotkeyActive={!!editingFeature}
data-testid="confirm-edit-feature"
disabled={
useWorktrees &&
editingFeature.status === 'backlog' &&
!useCurrentBranch &&
workMode === 'custom' &&
!editingFeature.branchName?.trim()
}
>

View File

@@ -111,14 +111,32 @@ export function useBoardActions({
planningMode: PlanningMode;
requirePlanApproval: boolean;
dependencies?: string[];
workMode?: 'current' | 'auto' | 'custom';
}) => {
// Empty string means "unassigned" (show only on primary worktree) - convert to undefined
// Non-empty string is the actual branch name (for non-primary worktrees)
const finalBranchName = featureData.branchName || undefined;
const workMode = featureData.workMode || 'current';
// If worktrees enabled and a branch is specified, create the worktree now
// This ensures the worktree exists before the feature starts
if (useWorktrees && finalBranchName && currentProject) {
// 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
// - '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 {
const api = getElectronAPI();
if (api?.worktree?.create) {
@@ -207,10 +225,10 @@ export function useBoardActions({
persistFeatureUpdate,
updateFeature,
saveCategory,
useWorktrees,
currentProject,
onWorktreeCreated,
onWorktreeAutoSelect,
currentWorktreeBranch,
]
);
@@ -230,15 +248,29 @@ export function useBoardActions({
priority: number;
planningMode?: PlanningMode;
requirePlanApproval?: boolean;
workMode?: 'current' | 'auto' | 'custom';
},
descriptionHistorySource?: 'enhance' | 'edit',
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
// This ensures the worktree exists before the feature starts
if (useWorktrees && finalBranchName && currentProject) {
// Determine final branch name based on work mode
let finalBranchName: string | undefined;
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 {
const api = getElectronAPI();
if (api?.worktree?.create) {
@@ -287,9 +319,9 @@ export function useBoardActions({
persistFeatureUpdate,
saveCategory,
setEditingFeature,
useWorktrees,
currentProject,
onWorktreeCreated,
currentWorktreeBranch,
]
);

View File

@@ -12,3 +12,4 @@ export * from './branch-selector';
export * from './planning-mode-selector';
export * from './planning-mode-select';
export * from './ancestor-context-section';
export * from './work-mode-selector';

View File

@@ -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>
);
}

View File

@@ -358,9 +358,6 @@ export function FeatureDefaultsSection({
>
<GitBranch className="w-4 h-4 text-brand-500" />
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>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Creates isolated git branches for each feature. When disabled, agents work directly in

View File

@@ -560,7 +560,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
defaultSkipTests: settings.defaultSkipTests ?? true,
enableDependencyBlocking: settings.enableDependencyBlocking ?? true,
skipVerificationInAutoMode: settings.skipVerificationInAutoMode ?? false,
useWorktrees: settings.useWorktrees ?? false,
useWorktrees: settings.useWorktrees ?? true,
showProfilesOnly: settings.showProfilesOnly ?? false,
defaultPlanningMode: settings.defaultPlanningMode ?? 'skip',
defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false,

View File

@@ -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)
// 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)
// 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)
enableDependencyBlocking: true, // Default to enabled (show dependency blocking UI)
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: {},
worktreesByProject: {},
showProfilesOnly: false, // Default to showing all options (not profiles only)

View File

@@ -785,7 +785,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
defaultSkipTests: true,
enableDependencyBlocking: true,
skipVerificationInAutoMode: false,
useWorktrees: false,
useWorktrees: true,
showProfilesOnly: false,
defaultPlanningMode: 'skip',
defaultRequirePlanApproval: false,