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:
co-authored by
Claude Opus 4.5
parent
2551ed8f27
commit
990d30cb22
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user