story3.1 - add execution mode tabs to task creation modal

Implement Full Auto / Semi-Auto execution mode selection for task creation:
- Add ExecutionModeTabs component with Radix UI Tabs
- Full Auto: autonomous execution without interruption
- Semi-Auto: review at 3 checkpoints (planning, coding, validation)
- Integrate into TaskCreationWizard with state management
- Add ExecutionMode type to task.ts, update TaskDraft and TaskMetadata
- Add i18n translations (EN/FR)
- Add comprehensive test suite (8 tests)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
Test User
2026-01-14 17:56:10 +01:00
co-authored by Claude Opus 4.5
parent 2551ed8f27
commit 990d30cb22
6 changed files with 281 additions and 1 deletions
@@ -24,6 +24,7 @@ import {
} from './ui/select';
import { TaskModalLayout } from './task-form/TaskModalLayout';
import { TaskFormFields } from './task-form/TaskFormFields';
import { ExecutionModeTabs, type ExecutionMode } from './task-form/ExecutionModeTabs';
import { TaskFileExplorerDrawer } from './TaskFileExplorerDrawer';
import { FileAutocomplete } from './FileAutocomplete';
import { createTask, saveDraft, loadDraft, clearDraft, isDraftEmpty } from '../stores/task-store';
@@ -106,6 +107,9 @@ export function TaskCreationWizard({
// Review setting
const [requireReviewBeforeCoding, setRequireReviewBeforeCoding] = useState(false);
// Execution mode - default to full_auto
const [executionMode, setExecutionMode] = useState<ExecutionMode>('full_auto');
// Draft state
const [isDraftRestored, setIsDraftRestored] = useState(false);
@@ -137,6 +141,7 @@ export function TaskCreationWizard({
setImages(draft.images);
setReferencedFiles(draft.referencedFiles ?? []);
setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false);
setExecutionMode(draft.executionMode ?? 'full_auto');
setIsDraftRestored(true);
if (draft.category || draft.priority || draft.complexity || draft.impact) {
@@ -218,8 +223,9 @@ export function TaskCreationWizard({
images,
referencedFiles,
requireReviewBeforeCoding,
executionMode,
savedAt: new Date()
}), [projectId, title, description, category, priority, complexity, impact, profileId, model, thinkingLevel, phaseModels, phaseThinking, images, referencedFiles, requireReviewBeforeCoding]);
}), [projectId, title, description, category, priority, complexity, impact, profileId, model, thinkingLevel, phaseModels, phaseThinking, images, referencedFiles, requireReviewBeforeCoding, executionMode]);
/**
* Detect @ mention being typed and show autocomplete
@@ -348,6 +354,8 @@ export function TaskCreationWizard({
if (images.length > 0) metadata.attachedImages = images;
if (allReferencedFiles.length > 0) metadata.referencedFiles = allReferencedFiles;
if (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true;
// Always include execution mode - it determines checkpoint behavior
metadata.executionMode = executionMode;
// Always include baseBranch - resolve PROJECT_DEFAULT_BRANCH to actual branch name
// This ensures the backend always knows which branch to use for worktree creation
if (baseBranch === PROJECT_DEFAULT_BRANCH) {
@@ -389,6 +397,7 @@ export function TaskCreationWizard({
setImages([]);
setReferencedFiles([]);
setRequireReviewBeforeCoding(false);
setExecutionMode('full_auto');
setBaseBranch(PROJECT_DEFAULT_BRANCH);
setUseWorktree(true);
setError(null);
@@ -521,6 +530,13 @@ export function TaskCreationWizard({
}
>
<div className="space-y-6">
{/* Execution Mode Tabs - Full Auto vs Semi-Auto */}
<ExecutionModeTabs
value={executionMode}
onChange={setExecutionMode}
disabled={isCreating}
/>
{/* Worktree isolation info banner */}
<div className="flex items-start gap-3 p-4 bg-info/10 border border-info/30 rounded-lg">
<Info className="h-5 w-5 text-info flex-shrink-0 mt-0.5" />
@@ -0,0 +1,115 @@
/**
* @vitest-environment jsdom
*/
/**
* Tests for ExecutionModeTabs component
*/
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { describe, it, expect, vi } from 'vitest';
import { ExecutionModeTabs } from './ExecutionModeTabs';
// Mock i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'tasks:executionMode.fullAuto.title': 'Full Auto',
'tasks:executionMode.fullAuto.description': 'Tasks run autonomously without interruption. The AI handles planning, coding, and validation automatically.',
'tasks:executionMode.semiAuto.title': 'Semi-Auto',
'tasks:executionMode.semiAuto.description': 'Review and approve at key checkpoints.',
'tasks:executionMode.semiAuto.checkpoints.planning': 'After planning - Review the implementation plan',
'tasks:executionMode.semiAuto.checkpoints.coding': 'After coding - Review the implemented code',
'tasks:executionMode.semiAuto.checkpoints.validation': 'After validation - Review QA results'
};
return translations[key] || key;
}
})
}));
describe('ExecutionModeTabs', () => {
it('renders both Full Auto and Semi-Auto tabs', () => {
const onChange = vi.fn();
render(<ExecutionModeTabs value="full_auto" onChange={onChange} />);
expect(screen.getByRole('tab', { name: /full auto/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /semi-auto/i })).toBeInTheDocument();
});
it('defaults to Full Auto tab selected', () => {
const onChange = vi.fn();
render(<ExecutionModeTabs value="full_auto" onChange={onChange} />);
const fullAutoTab = screen.getByRole('tab', { name: /full auto/i });
expect(fullAutoTab).toHaveAttribute('data-state', 'active');
});
it('shows Full Auto description when Full Auto is selected', () => {
const onChange = vi.fn();
render(<ExecutionModeTabs value="full_auto" onChange={onChange} />);
expect(screen.getByText(/tasks run autonomously/i)).toBeInTheDocument();
});
it('calls onChange when Semi-Auto tab is selected via keyboard', () => {
const onChange = vi.fn();
render(<ExecutionModeTabs value="full_auto" onChange={onChange} />);
const semiAutoTab = screen.getByRole('tab', { name: /semi-auto/i });
// Focus the tab first, then trigger keyboard event (Radix pattern)
semiAutoTab.focus();
expect(semiAutoTab).toHaveFocus();
fireEvent.keyDown(semiAutoTab, { key: 'Enter' });
expect(onChange).toHaveBeenCalledWith('semi_auto');
});
it('tabs are clickable and not disabled by default', () => {
const onChange = vi.fn();
render(<ExecutionModeTabs value="full_auto" onChange={onChange} />);
const semiAutoTab = screen.getByRole('tab', { name: /semi-auto/i });
const fullAutoTab = screen.getByRole('tab', { name: /full auto/i });
// Verify tabs are not disabled and are interactive
expect(semiAutoTab).not.toBeDisabled();
expect(fullAutoTab).not.toBeDisabled();
expect(semiAutoTab).toHaveAttribute('data-state', 'inactive');
expect(fullAutoTab).toHaveAttribute('data-state', 'active');
});
it('shows Semi-Auto description with checkpoints when Semi-Auto is selected', () => {
const onChange = vi.fn();
render(<ExecutionModeTabs value="semi_auto" onChange={onChange} />);
expect(screen.getByText(/review and approve at key checkpoints/i)).toBeInTheDocument();
expect(screen.getByText(/after planning/i)).toBeInTheDocument();
expect(screen.getByText(/after coding/i)).toBeInTheDocument();
expect(screen.getByText(/after validation/i)).toBeInTheDocument();
});
it('updates form state when tab changes', () => {
const onChange = vi.fn();
const { rerender } = render(<ExecutionModeTabs value="full_auto" onChange={onChange} />);
// Initially Full Auto is selected
expect(screen.getByRole('tab', { name: /full auto/i })).toHaveAttribute('data-state', 'active');
// Simulate parent state update
rerender(<ExecutionModeTabs value="semi_auto" onChange={onChange} />);
// Now Semi-Auto should be active
expect(screen.getByRole('tab', { name: /semi-auto/i })).toHaveAttribute('data-state', 'active');
});
it('respects disabled state', () => {
const onChange = vi.fn();
render(<ExecutionModeTabs value="full_auto" onChange={onChange} disabled />);
const fullAutoTab = screen.getByRole('tab', { name: /full auto/i });
const semiAutoTab = screen.getByRole('tab', { name: /semi-auto/i });
expect(fullAutoTab).toBeDisabled();
expect(semiAutoTab).toBeDisabled();
});
});
@@ -0,0 +1,112 @@
/**
* ExecutionModeTabs - Tab selector for Full Auto vs Semi-Auto execution modes
*
* Allows users to choose between:
* - Full Auto: Tasks run autonomously without interruption
* - Semi-Auto: Review and approve at key checkpoints (planning, coding, validation)
*/
import { useTranslation } from 'react-i18next';
import { Zap, UserCheck } from 'lucide-react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
import { cn } from '../../lib/utils';
import type { ExecutionMode } from '../../../shared/types/task';
// Re-export for convenience
export type { ExecutionMode };
interface ExecutionModeTabsProps {
/** Currently selected execution mode */
value: ExecutionMode;
/** Callback when execution mode changes */
onChange: (value: ExecutionMode) => void;
/** Whether the tabs are disabled */
disabled?: boolean;
}
export function ExecutionModeTabs({ value, onChange, disabled = false }: ExecutionModeTabsProps) {
const { t } = useTranslation(['tasks']);
return (
<div className="space-y-3">
<Tabs
value={value}
onValueChange={(v) => onChange(v as ExecutionMode)}
>
<TabsList className="grid w-full grid-cols-2 h-11">
<TabsTrigger
value="full_auto"
disabled={disabled}
className="gap-2 data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
>
<Zap className="h-4 w-4" />
{t('tasks:executionMode.fullAuto.title')}
</TabsTrigger>
<TabsTrigger
value="semi_auto"
disabled={disabled}
className="gap-2 data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
>
<UserCheck className="h-4 w-4" />
{t('tasks:executionMode.semiAuto.title')}
</TabsTrigger>
</TabsList>
<TabsContent
value="full_auto"
className="mt-4"
aria-label={t('tasks:executionMode.fullAuto.title')}
>
<div className={cn(
'p-4 rounded-lg border border-border bg-muted/30',
'space-y-2'
)}>
<p className="text-sm text-muted-foreground">
{t('tasks:executionMode.fullAuto.description')}
</p>
</div>
</TabsContent>
<TabsContent
value="semi_auto"
className="mt-4"
aria-label={t('tasks:executionMode.semiAuto.title')}
>
<div className={cn(
'p-4 rounded-lg border border-border bg-muted/30',
'space-y-3'
)}>
<p className="text-sm text-muted-foreground">
{t('tasks:executionMode.semiAuto.description')}
</p>
<ul className="space-y-2 text-sm" aria-label="Checkpoints">
<li className="flex items-start gap-2">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium" aria-hidden="true">
1
</span>
<span className="text-muted-foreground">
{t('tasks:executionMode.semiAuto.checkpoints.planning')}
</span>
</li>
<li className="flex items-start gap-2">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium" aria-hidden="true">
2
</span>
<span className="text-muted-foreground">
{t('tasks:executionMode.semiAuto.checkpoints.coding')}
</span>
</li>
<li className="flex items-start gap-2">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium" aria-hidden="true">
3
</span>
<span className="text-muted-foreground">
{t('tasks:executionMode.semiAuto.checkpoints.validation')}
</span>
</li>
</ul>
</div>
</TabsContent>
</Tabs>
</div>
);
}
@@ -240,5 +240,20 @@
},
"subtasks": {
"untitled": "Untitled subtask"
},
"executionMode": {
"fullAuto": {
"title": "Full Auto",
"description": "Tasks run autonomously without interruption. The AI handles planning, coding, and validation automatically."
},
"semiAuto": {
"title": "Semi-Auto",
"description": "Review and approve at key checkpoints.",
"checkpoints": {
"planning": "After planning - Review the implementation plan",
"coding": "After coding - Review the implemented code",
"validation": "After validation - Review QA results"
}
}
}
}
@@ -240,5 +240,20 @@
},
"subtasks": {
"untitled": "Sous-tâche sans titre"
},
"executionMode": {
"fullAuto": {
"title": "Auto complet",
"description": "Les tâches s'exécutent de manière autonome sans interruption. L'IA gère automatiquement la planification, le codage et la validation."
},
"semiAuto": {
"title": "Semi-Auto",
"description": "Révisez et approuvez aux points de contrôle clés.",
"checkpoints": {
"planning": "Après la planification - Révisez le plan d'implémentation",
"coding": "Après le codage - Révisez le code implémenté",
"validation": "Après la validation - Révisez les résultats QA"
}
}
}
}
+7
View File
@@ -151,9 +151,13 @@ export interface TaskDraft {
images: ImageAttachment[];
referencedFiles: ReferencedFile[];
requireReviewBeforeCoding?: boolean;
executionMode?: ExecutionMode;
savedAt: Date;
}
// Execution mode for task execution
export type ExecutionMode = 'full_auto' | 'semi_auto';
// Task metadata from ideation or manual entry
export type TaskComplexity = 'trivial' | 'small' | 'medium' | 'large' | 'complex';
export type TaskImpact = 'low' | 'medium' | 'high' | 'critical';
@@ -222,6 +226,9 @@ export interface TaskMetadata {
// Review settings
requireReviewBeforeCoding?: boolean; // Require human review of spec/plan before coding starts
// Execution mode
executionMode?: ExecutionMode; // Full auto (no interruption) or semi-auto (checkpoints)
// Agent configuration (from agent profile or manual selection)
model?: ModelType; // Claude model to use (haiku, sonnet, opus) - used when not auto profile
thinkingLevel?: ThinkingLevel; // Thinking budget level (none, low, medium, high, ultrathink)