4dbb7ee4ee
* auto-claude: subtask-1-1 - Add i18n translation keys for worktree notice banner and merge tooltip - Added wizard.worktreeNotice.title and wizard.worktreeNotice.description for task creation banner - Added review.mergeTooltip for merge button explanation - Translations added to both en/tasks.json and fr/tasks.json * auto-claude: subtask-1-2 - Add visible info banner to TaskCreationWizard expl * auto-claude: subtask-1-3 - Add tooltip to 'Merge with AI' button in WorkspaceStatus - Import Tooltip components from ui/tooltip - Wrap merge button with Tooltip, TooltipTrigger, TooltipContent - Add contextual tooltip text explaining merge operation: * With AI: explains worktree merge, removal, and AI conflict resolution * Without AI: explains worktree merge and removal - Follows Radix UI tooltip pattern from reference file * fix: use i18n key for merge button tooltip in WorkspaceStatus * fix: clarify merge tooltip - worktree removal is optional (qa-requested) Fixes misleading tooltip text that implied worktree is automatically removed during merge. In reality, after merge, users are shown a dialog where they can choose to keep or remove the worktree. Updated tooltip to reflect this flow. Changes: - Updated en/tasks.json: Changed tooltip to clarify worktree removal is optional - Updated fr/tasks.json: Updated French translation to match QA Feedback: "Its currently saying on the tooltip that it will 'remove the worktree' Please validate if this is the actual logic. As per my understanding, there will be an extra button afterwards that will make sure that the user has access to the work tree if they want to revert anything. The user has to manually accept to remove the work tree." Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: use theme-aware colors for worktree info banner Replace hardcoded blue colors with semantic theme classes to support dark mode properly. Uses the same pattern as other info banners in the codebase (bg-info/10, border-info/30, text-info). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
661 lines
24 KiB
TypeScript
661 lines
24 KiB
TypeScript
/**
|
|
* TaskCreationWizard - Dialog for creating new tasks
|
|
*
|
|
* Now uses the shared TaskModalLayout for consistent styling with other task modals,
|
|
* and TaskFormFields for the form content.
|
|
*
|
|
* Features unique to creation (not in TaskEditDialog):
|
|
* - Draft persistence (auto-save to localStorage)
|
|
* - @ mention autocomplete for file references
|
|
* - File explorer drawer sidebar
|
|
* - Git branch selection options
|
|
*/
|
|
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Loader2, ChevronDown, ChevronUp, RotateCcw, FolderTree, GitBranch, Info } from 'lucide-react';
|
|
import { Button } from './ui/button';
|
|
import { Label } from './ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue
|
|
} from './ui/select';
|
|
import { TaskModalLayout } from './task-form/TaskModalLayout';
|
|
import { TaskFormFields } from './task-form/TaskFormFields';
|
|
import { TaskFileExplorerDrawer } from './TaskFileExplorerDrawer';
|
|
import { FileAutocomplete } from './FileAutocomplete';
|
|
import { createTask, saveDraft, loadDraft, clearDraft, isDraftEmpty } from '../stores/task-store';
|
|
import { useProjectStore } from '../stores/project-store';
|
|
import { cn } from '../lib/utils';
|
|
import type { TaskCategory, TaskPriority, TaskComplexity, TaskImpact, TaskMetadata, ImageAttachment, TaskDraft, ModelType, ThinkingLevel, ReferencedFile } from '../../shared/types';
|
|
import type { PhaseModelConfig, PhaseThinkingConfig } from '../../shared/types/settings';
|
|
import {
|
|
DEFAULT_AGENT_PROFILES,
|
|
DEFAULT_PHASE_MODELS,
|
|
DEFAULT_PHASE_THINKING
|
|
} from '../../shared/constants';
|
|
import { useSettingsStore } from '../stores/settings-store';
|
|
|
|
interface TaskCreationWizardProps {
|
|
projectId: string;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
// Special value for "use project default" branch
|
|
const PROJECT_DEFAULT_BRANCH = '__project_default__';
|
|
|
|
export function TaskCreationWizard({
|
|
projectId,
|
|
open,
|
|
onOpenChange
|
|
}: TaskCreationWizardProps) {
|
|
const { t } = useTranslation(['tasks', 'common']);
|
|
const { settings } = useSettingsStore();
|
|
const selectedProfile = DEFAULT_AGENT_PROFILES.find(
|
|
p => p.id === settings.selectedAgentProfile
|
|
) || DEFAULT_AGENT_PROFILES.find(p => p.id === 'auto')!;
|
|
|
|
// Form state
|
|
const [title, setTitle] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [showClassification, setShowClassification] = useState(false);
|
|
const [showFileExplorer, setShowFileExplorer] = useState(false);
|
|
const [showGitOptions, setShowGitOptions] = useState(false);
|
|
|
|
// Git options state
|
|
const [branches, setBranches] = useState<string[]>([]);
|
|
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
|
const [baseBranch, setBaseBranch] = useState<string>(PROJECT_DEFAULT_BRANCH);
|
|
const [projectDefaultBranch, setProjectDefaultBranch] = useState<string>('');
|
|
// Worktree isolation - default to true for safety
|
|
const [useWorktree, setUseWorktree] = useState(true);
|
|
|
|
// Get project path from project store
|
|
const projects = useProjectStore((state) => state.projects);
|
|
const projectPath = useMemo(() => {
|
|
const project = projects.find((p) => p.id === projectId);
|
|
return project?.path ?? null;
|
|
}, [projects, projectId]);
|
|
|
|
// Classification fields
|
|
const [category, setCategory] = useState<TaskCategory | ''>('');
|
|
const [priority, setPriority] = useState<TaskPriority | ''>('');
|
|
const [complexity, setComplexity] = useState<TaskComplexity | ''>('');
|
|
const [impact, setImpact] = useState<TaskImpact | ''>('');
|
|
|
|
// Model configuration
|
|
const [profileId, setProfileId] = useState<string>(settings.selectedAgentProfile || 'auto');
|
|
const [model, setModel] = useState<ModelType | ''>(selectedProfile.model);
|
|
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel | ''>(selectedProfile.thinkingLevel);
|
|
const [phaseModels, setPhaseModels] = useState<PhaseModelConfig | undefined>(
|
|
settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS
|
|
);
|
|
const [phaseThinking, setPhaseThinking] = useState<PhaseThinkingConfig | undefined>(
|
|
settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING
|
|
);
|
|
|
|
// Images and files
|
|
const [images, setImages] = useState<ImageAttachment[]>([]);
|
|
const [referencedFiles, setReferencedFiles] = useState<ReferencedFile[]>([]);
|
|
|
|
// Review setting
|
|
const [requireReviewBeforeCoding, setRequireReviewBeforeCoding] = useState(false);
|
|
|
|
// Draft state
|
|
const [isDraftRestored, setIsDraftRestored] = useState(false);
|
|
|
|
// @ autocomplete state
|
|
const descriptionRef = useRef<HTMLTextAreaElement>(null);
|
|
const [autocomplete, setAutocomplete] = useState<{
|
|
show: boolean;
|
|
query: string;
|
|
startPos: number;
|
|
position: { top: number; left: number };
|
|
} | null>(null);
|
|
|
|
// Load draft when dialog opens
|
|
useEffect(() => {
|
|
if (open && projectId) {
|
|
const draft = loadDraft(projectId);
|
|
if (draft && !isDraftEmpty(draft)) {
|
|
setTitle(draft.title);
|
|
setDescription(draft.description);
|
|
setCategory(draft.category);
|
|
setPriority(draft.priority);
|
|
setComplexity(draft.complexity);
|
|
setImpact(draft.impact);
|
|
setProfileId(draft.profileId || settings.selectedAgentProfile || 'auto');
|
|
setModel(draft.model || selectedProfile.model);
|
|
setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel);
|
|
setPhaseModels(draft.phaseModels || settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
|
setPhaseThinking(draft.phaseThinking || settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
|
setImages(draft.images);
|
|
setReferencedFiles(draft.referencedFiles ?? []);
|
|
setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false);
|
|
setIsDraftRestored(true);
|
|
|
|
if (draft.category || draft.priority || draft.complexity || draft.impact) {
|
|
setShowClassification(true);
|
|
}
|
|
} else {
|
|
// No draft - initialize from selected profile
|
|
setProfileId(settings.selectedAgentProfile || 'auto');
|
|
setModel(selectedProfile.model);
|
|
setThinkingLevel(selectedProfile.thinkingLevel);
|
|
setPhaseModels(settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
|
setPhaseThinking(settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
|
}
|
|
}
|
|
}, [open, projectId, settings.selectedAgentProfile, settings.customPhaseModels, settings.customPhaseThinking, selectedProfile.model, selectedProfile.thinkingLevel, selectedProfile.phaseModels, selectedProfile.phaseThinking]);
|
|
|
|
// Fetch branches when dialog opens
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const fetchBranches = async () => {
|
|
if (!projectPath) return;
|
|
if (isMounted) setIsLoadingBranches(true);
|
|
try {
|
|
const result = await window.electronAPI.getGitBranches(projectPath);
|
|
if (isMounted && result.success && result.data) {
|
|
setBranches(result.data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch branches:', err);
|
|
} finally {
|
|
if (isMounted) setIsLoadingBranches(false);
|
|
}
|
|
};
|
|
|
|
const fetchProjectDefaultBranch = async () => {
|
|
if (!projectId) return;
|
|
try {
|
|
const result = await window.electronAPI.getProjectEnv(projectId);
|
|
if (isMounted && result.success && result.data?.defaultBranch) {
|
|
setProjectDefaultBranch(result.data.defaultBranch);
|
|
} else if (projectPath) {
|
|
const detectResult = await window.electronAPI.detectMainBranch(projectPath);
|
|
if (isMounted && detectResult.success && detectResult.data) {
|
|
setProjectDefaultBranch(detectResult.data);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch project default branch:', err);
|
|
}
|
|
};
|
|
|
|
if (open && projectPath) {
|
|
fetchBranches();
|
|
fetchProjectDefaultBranch();
|
|
}
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [open, projectPath, projectId]);
|
|
|
|
/**
|
|
* Get current form state as a draft
|
|
*/
|
|
const getCurrentDraft = useCallback((): TaskDraft => ({
|
|
projectId,
|
|
title,
|
|
description,
|
|
category,
|
|
priority,
|
|
complexity,
|
|
impact,
|
|
profileId,
|
|
model,
|
|
thinkingLevel,
|
|
phaseModels,
|
|
phaseThinking,
|
|
images,
|
|
referencedFiles,
|
|
requireReviewBeforeCoding,
|
|
savedAt: new Date()
|
|
}), [projectId, title, description, category, priority, complexity, impact, profileId, model, thinkingLevel, phaseModels, phaseThinking, images, referencedFiles, requireReviewBeforeCoding]);
|
|
|
|
/**
|
|
* Detect @ mention being typed and show autocomplete
|
|
*/
|
|
const detectAtMention = useCallback((text: string, cursorPos: number) => {
|
|
const beforeCursor = text.slice(0, cursorPos);
|
|
const match = beforeCursor.match(/@([\w\-./\\]*)$/);
|
|
if (match) {
|
|
return { query: match[1], startPos: cursorPos - match[0].length };
|
|
}
|
|
return null;
|
|
}, []);
|
|
|
|
/**
|
|
* Handle description change and check for @ mentions
|
|
*/
|
|
const handleDescriptionChange = useCallback((newValue: string) => {
|
|
const textarea = descriptionRef.current;
|
|
const cursorPos = textarea?.selectionStart || 0;
|
|
|
|
setDescription(newValue);
|
|
|
|
const mention = detectAtMention(newValue, cursorPos);
|
|
if (mention && textarea) {
|
|
const rect = textarea.getBoundingClientRect();
|
|
const textareaStyle = window.getComputedStyle(textarea);
|
|
const lineHeight = parseFloat(textareaStyle.lineHeight) || 20;
|
|
const paddingTop = parseFloat(textareaStyle.paddingTop) || 8;
|
|
const paddingLeft = parseFloat(textareaStyle.paddingLeft) || 12;
|
|
|
|
const textBeforeCursor = newValue.slice(0, cursorPos);
|
|
const lines = textBeforeCursor.split('\n');
|
|
const currentLineIndex = lines.length - 1;
|
|
const currentLineLength = lines[currentLineIndex].length;
|
|
|
|
const charWidth = 8;
|
|
const top = paddingTop + (currentLineIndex + 1) * lineHeight + 4;
|
|
const left = paddingLeft + Math.min(currentLineLength * charWidth, rect.width - 300);
|
|
|
|
setAutocomplete({
|
|
show: true,
|
|
query: mention.query,
|
|
startPos: mention.startPos,
|
|
position: { top, left: Math.max(0, left) }
|
|
});
|
|
} else if (autocomplete?.show) {
|
|
setAutocomplete(null);
|
|
}
|
|
}, [detectAtMention, autocomplete?.show]);
|
|
|
|
/**
|
|
* Handle autocomplete selection
|
|
*/
|
|
const handleAutocompleteSelect = useCallback((filename: string, _fullPath?: string) => {
|
|
if (!autocomplete) return;
|
|
const textarea = descriptionRef.current;
|
|
if (!textarea) return;
|
|
|
|
const beforeMention = description.slice(0, autocomplete.startPos);
|
|
const afterMention = description.slice(autocomplete.startPos + 1 + autocomplete.query.length);
|
|
const newDescription = beforeMention + '@' + filename + afterMention;
|
|
|
|
setDescription(newDescription);
|
|
setAutocomplete(null);
|
|
|
|
// Use queueMicrotask instead of setTimeout - doesn't need cleanup on unmount
|
|
queueMicrotask(() => {
|
|
const newCursorPos = autocomplete.startPos + 1 + filename.length;
|
|
textarea.focus();
|
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
});
|
|
}, [autocomplete, description]);
|
|
|
|
/**
|
|
* Parse @mentions from description
|
|
*/
|
|
const parseFileMentions = useCallback((text: string, existingFiles: ReferencedFile[]): ReferencedFile[] => {
|
|
const mentionRegex = /@([\w\-./\\]+\.\w+)/g;
|
|
const matches = Array.from(text.matchAll(mentionRegex));
|
|
if (matches.length === 0) return existingFiles;
|
|
|
|
const existingNames = new Set(existingFiles.map(f => f.name));
|
|
const newFiles: ReferencedFile[] = [];
|
|
|
|
matches.forEach(match => {
|
|
const fileName = match[1];
|
|
if (!existingNames.has(fileName)) {
|
|
newFiles.push({
|
|
id: crypto.randomUUID(),
|
|
path: fileName,
|
|
name: fileName,
|
|
isDirectory: false,
|
|
addedAt: new Date()
|
|
});
|
|
existingNames.add(fileName);
|
|
}
|
|
});
|
|
|
|
return [...existingFiles, ...newFiles];
|
|
}, []);
|
|
|
|
const handleCreate = async () => {
|
|
if (!description.trim()) {
|
|
setError(t('tasks:form.errors.descriptionRequired'));
|
|
return;
|
|
}
|
|
|
|
setIsCreating(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const allReferencedFiles = parseFileMentions(description, referencedFiles);
|
|
|
|
const metadata: TaskMetadata = { sourceType: 'manual' };
|
|
if (category) metadata.category = category;
|
|
if (priority) metadata.priority = priority;
|
|
if (complexity) metadata.complexity = complexity;
|
|
if (impact) metadata.impact = impact;
|
|
if (model) metadata.model = model;
|
|
if (thinkingLevel) metadata.thinkingLevel = thinkingLevel;
|
|
if (phaseModels && phaseThinking) {
|
|
metadata.isAutoProfile = profileId === 'auto';
|
|
metadata.phaseModels = phaseModels;
|
|
metadata.phaseThinking = phaseThinking;
|
|
}
|
|
if (images.length > 0) metadata.attachedImages = images;
|
|
if (allReferencedFiles.length > 0) metadata.referencedFiles = allReferencedFiles;
|
|
if (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true;
|
|
// 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) {
|
|
// Use the resolved project default branch
|
|
if (projectDefaultBranch) metadata.baseBranch = projectDefaultBranch;
|
|
} else if (baseBranch) {
|
|
metadata.baseBranch = baseBranch;
|
|
}
|
|
// Pass worktree preference - false means use --direct mode
|
|
if (!useWorktree) metadata.useWorktree = false;
|
|
|
|
const task = await createTask(projectId, title.trim(), description.trim(), metadata);
|
|
if (task) {
|
|
clearDraft(projectId);
|
|
resetForm();
|
|
onOpenChange(false);
|
|
} else {
|
|
setError(t('tasks:wizard.errors.createFailed'));
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : t('common:errors.unknownError'));
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setTitle('');
|
|
setDescription('');
|
|
setCategory('');
|
|
setPriority('');
|
|
setComplexity('');
|
|
setImpact('');
|
|
setProfileId(settings.selectedAgentProfile || 'auto');
|
|
setModel(selectedProfile.model);
|
|
setThinkingLevel(selectedProfile.thinkingLevel);
|
|
setPhaseModels(settings.customPhaseModels || selectedProfile.phaseModels || DEFAULT_PHASE_MODELS);
|
|
setPhaseThinking(settings.customPhaseThinking || selectedProfile.phaseThinking || DEFAULT_PHASE_THINKING);
|
|
setImages([]);
|
|
setReferencedFiles([]);
|
|
setRequireReviewBeforeCoding(false);
|
|
setBaseBranch(PROJECT_DEFAULT_BRANCH);
|
|
setUseWorktree(true);
|
|
setError(null);
|
|
setShowClassification(false);
|
|
setShowFileExplorer(false);
|
|
setShowGitOptions(false);
|
|
setIsDraftRestored(false);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
if (isCreating) return;
|
|
|
|
const draft = getCurrentDraft();
|
|
if (!isDraftEmpty(draft)) {
|
|
saveDraft(draft);
|
|
} else {
|
|
clearDraft(projectId);
|
|
}
|
|
|
|
resetForm();
|
|
onOpenChange(false);
|
|
};
|
|
|
|
const handleDiscardDraft = () => {
|
|
clearDraft(projectId);
|
|
resetForm();
|
|
setError(null);
|
|
};
|
|
|
|
// Render @ mention highlight overlay for the description textarea
|
|
const descriptionOverlay = (
|
|
<div
|
|
className="absolute inset-0 pointer-events-none overflow-hidden rounded-md border border-transparent"
|
|
style={{
|
|
padding: '0.5rem 0.75rem',
|
|
font: 'inherit',
|
|
lineHeight: '1.5',
|
|
wordWrap: 'break-word',
|
|
whiteSpace: 'pre-wrap',
|
|
color: 'transparent'
|
|
}}
|
|
>
|
|
{description.split(/(@[\w\-./\\]+\.\w+)/g).map((part, i) => {
|
|
if (part.match(/^@[\w\-./\\]+\.\w+$/)) {
|
|
return (
|
|
<span
|
|
key={i}
|
|
className="bg-info/20 text-info-foreground rounded px-0.5"
|
|
style={{ color: 'hsl(var(--info))' }}
|
|
>
|
|
{part}
|
|
</span>
|
|
);
|
|
}
|
|
return <span key={i}>{part}</span>;
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<TaskModalLayout
|
|
open={open}
|
|
onOpenChange={handleClose}
|
|
title={t('tasks:wizard.createTitle')}
|
|
description={t('tasks:wizard.createDescription')}
|
|
disabled={isCreating}
|
|
sidebar={
|
|
projectPath && (
|
|
<TaskFileExplorerDrawer
|
|
isOpen={showFileExplorer}
|
|
onClose={() => setShowFileExplorer(false)}
|
|
projectPath={projectPath}
|
|
/>
|
|
)
|
|
}
|
|
sidebarOpen={showFileExplorer}
|
|
footer={
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{/* Draft restored indicator */}
|
|
{isDraftRestored && (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs bg-info/10 text-info px-2 py-1 rounded-md">
|
|
{t('tasks:wizard.draftRestored')}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground"
|
|
onClick={handleDiscardDraft}
|
|
>
|
|
<RotateCcw className="h-3 w-3 mr-1" />
|
|
{t('tasks:wizard.startFresh')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* File Explorer Toggle */}
|
|
{projectPath && (
|
|
<Button
|
|
type="button"
|
|
variant={showFileExplorer ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => setShowFileExplorer(!showFileExplorer)}
|
|
disabled={isCreating}
|
|
className="gap-1.5"
|
|
>
|
|
<FolderTree className="h-4 w-4" />
|
|
{showFileExplorer ? t('tasks:wizard.hideFiles') : t('tasks:wizard.browseFiles')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<Button variant="outline" onClick={handleClose} disabled={isCreating}>
|
|
{t('common:buttons.cancel')}
|
|
</Button>
|
|
<Button onClick={handleCreate} disabled={isCreating || !description.trim()}>
|
|
{isCreating ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
{t('tasks:wizard.creating')}
|
|
</>
|
|
) : (
|
|
t('tasks:wizard.createTask')
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="space-y-6">
|
|
{/* 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" />
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="text-sm font-medium text-foreground mb-1">
|
|
{t('tasks:wizard.worktreeNotice.title')}
|
|
</h4>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('tasks:wizard.worktreeNotice.description')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main form fields */}
|
|
<TaskFormFields
|
|
description={description}
|
|
onDescriptionChange={handleDescriptionChange}
|
|
descriptionPlaceholder={t('tasks:wizard.descriptionPlaceholder')}
|
|
descriptionOverlay={descriptionOverlay}
|
|
descriptionRef={descriptionRef}
|
|
title={title}
|
|
onTitleChange={setTitle}
|
|
profileId={profileId}
|
|
model={model}
|
|
thinkingLevel={thinkingLevel}
|
|
phaseModels={phaseModels}
|
|
phaseThinking={phaseThinking}
|
|
onProfileChange={(newProfileId, newModel, newThinkingLevel) => {
|
|
setProfileId(newProfileId);
|
|
setModel(newModel);
|
|
setThinkingLevel(newThinkingLevel);
|
|
}}
|
|
onModelChange={setModel}
|
|
onThinkingLevelChange={setThinkingLevel}
|
|
onPhaseModelsChange={setPhaseModels}
|
|
onPhaseThinkingChange={setPhaseThinking}
|
|
category={category}
|
|
priority={priority}
|
|
complexity={complexity}
|
|
impact={impact}
|
|
onCategoryChange={setCategory}
|
|
onPriorityChange={setPriority}
|
|
onComplexityChange={setComplexity}
|
|
onImpactChange={setImpact}
|
|
showClassification={showClassification}
|
|
onShowClassificationChange={setShowClassification}
|
|
images={images}
|
|
onImagesChange={setImages}
|
|
requireReviewBeforeCoding={requireReviewBeforeCoding}
|
|
onRequireReviewChange={setRequireReviewBeforeCoding}
|
|
disabled={isCreating}
|
|
error={error}
|
|
onError={setError}
|
|
idPrefix="create"
|
|
>
|
|
{/* File autocomplete popup - positioned relative to TaskFormFields */}
|
|
{autocomplete?.show && projectPath && (
|
|
<FileAutocomplete
|
|
query={autocomplete.query}
|
|
projectPath={projectPath}
|
|
position={autocomplete.position}
|
|
onSelect={handleAutocompleteSelect}
|
|
onClose={() => setAutocomplete(null)}
|
|
/>
|
|
)}
|
|
</TaskFormFields>
|
|
|
|
{/* Git Options Toggle - unique to creation */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowGitOptions(!showGitOptions)}
|
|
className={cn(
|
|
'flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors',
|
|
'w-full justify-between py-2 px-3 rounded-md hover:bg-muted/50'
|
|
)}
|
|
disabled={isCreating}
|
|
aria-expanded={showGitOptions}
|
|
aria-controls="git-options-section"
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<GitBranch className="h-4 w-4" />
|
|
{t('tasks:wizard.gitOptions.title')}
|
|
{baseBranch && baseBranch !== PROJECT_DEFAULT_BRANCH && (
|
|
<span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">
|
|
{baseBranch}
|
|
</span>
|
|
)}
|
|
</span>
|
|
{showGitOptions ? (
|
|
<ChevronUp className="h-4 w-4" />
|
|
) : (
|
|
<ChevronDown className="h-4 w-4" />
|
|
)}
|
|
</button>
|
|
|
|
{/* Git Options */}
|
|
{showGitOptions && (
|
|
<div id="git-options-section" className="space-y-4 p-4 rounded-lg border border-border bg-muted/30">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="base-branch" className="text-sm font-medium text-foreground">
|
|
{t('tasks:wizard.gitOptions.baseBranchLabel')}
|
|
</Label>
|
|
<Select
|
|
value={baseBranch}
|
|
onValueChange={setBaseBranch}
|
|
disabled={isCreating || isLoadingBranches}
|
|
>
|
|
<SelectTrigger id="base-branch" className="h-9">
|
|
<SelectValue placeholder={projectDefaultBranch
|
|
? t('tasks:wizard.gitOptions.useProjectDefaultWithBranch', { branch: projectDefaultBranch })
|
|
: t('tasks:wizard.gitOptions.useProjectDefault')
|
|
} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value={PROJECT_DEFAULT_BRANCH}>
|
|
{projectDefaultBranch
|
|
? t('tasks:wizard.gitOptions.useProjectDefaultWithBranch', { branch: projectDefaultBranch })
|
|
: t('tasks:wizard.gitOptions.useProjectDefault')
|
|
}
|
|
</SelectItem>
|
|
{branches.map((branch) => (
|
|
<SelectItem key={branch} value={branch}>
|
|
{branch}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-xs text-muted-foreground">
|
|
{t('tasks:wizard.gitOptions.helpText')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TaskModalLayout>
|
|
);
|
|
}
|