Show branch selector in create new task (vibe-kanban) (#637)
* Perfect! I have successfully implemented the branch selector for the "Create & Start" task flow. Let me provide a summary of what was implemented: 1. **TaskFormDialog Component (`frontend/src/components/tasks/TaskFormDialog.tsx`)**: - Added imports for `projectsApi` and `GitBranch` type - Added state management for branches and selected branch - Enhanced the template/branches fetch logic to also load project branches - Added a branch selector dropdown that appears only for "Create & Start" mode - Updated the `onCreateAndStartTask` interface to accept optional `baseBranch` parameter - Added branch reset logic when form is cleared 2. **TaskFormDialogContainer (`frontend/src/components/tasks/TaskFormDialogContainer.tsx`)**: - Updated `handleCreateAndStartTask` to accept and forward the `baseBranch` parameter - Modified the mutation data to include `base_branch` field 1. **CreateTask Model (`crates/db/src/models/task.rs`)**: - Added `base_branch: Option<String>` field to the `CreateTask` struct 2. **Create-and-Start Endpoint (`crates/server/src/routes/tasks.rs`)**: - Modified `create_task_and_start` function to use the provided `base_branch` from payload - Added fallback logic to current branch when no base_branch is specified - Updated the TaskAttempt creation to use the selected base branch 3. **MCP Task Server (`crates/server/src/mcp/task_server.rs`)**: - Updated CreateTask construction to include `base_branch: None` field 4. **Type Generation**: - Regenerated TypeScript types to include the new `base_branch` field in `CreateTask` - **Branch selector appears only for "Create & Start"** - keeps UI clean and contextual - **Smart defaults** - automatically selects current branch, then falls back to main/master - **Visual indicators** - shows "(current)" next to the current branch in the dropdown - **Proper state management** - branch selection is reset when form is cleared - **Backward compatibility** - existing functionality works unchanged The implementation follows the existing patterns in the codebase and provides a seamless way for users to select which branch they want to base their task work on when using the "Create & Start" workflow. Perfect! I can see your approach now. You've created a much cleaner separation of concerns by: 1. **Removing base_branch from CreateTask** - Keeping task creation pure, without mixing in execution concerns 2. **Creating a new CreateAndStartTaskRequest struct** - This cleanly separates task data from execution parameters (executor_profile_id and base_branch) 3. **Modified the create-and-start endpoint** - To use the new request structure This is a much better design because it maintains clear boundaries between task creation and task execution. Let me examine what still needs to be completed: Perfect! Everything compiles successfully. Let me do a final check of the implementation by reviewing what we've accomplished: Using your excellent design approach, I've successfully completed the branch selector implementation: 1. **`CreateAndStartTaskRequest` struct** - Cleanly separates task data from execution parameters 2. **Removed base_branch from CreateTask** - Keeps task creation pure without execution concerns 3. **Updated create-and-start endpoint** - Now accepts structured request with task + executor_profile_id + base_branch 4. **TypeScript types generated** - All new types properly exported to frontend 1. **Updated `tasksApi.createAndStart`** - Now accepts `CreateAndStartTaskRequest` format 2. **Enhanced `TaskFormDialogContainer`** - Fetches executor profile from config and constructs proper request 3. **Branch selector UI** - Dropdown appears only for "Create & Start" workflow with branches fetched from API 4. **Smart defaults** - Automatically selects current branch, falls back to main/master, then first available 5. **State management** - Proper branch state handling and form reset logic 6. **Visual indicators** - Shows "(current)" next to the current branch in dropdown - **Contextual UI**: Branch selector only appears for "Create & Start" - keeps interface clean - **Smart defaults**: Automatically selects the most appropriate branch - **Validation**: Won't submit without a selected branch and executor profile - **Error handling**: Graceful handling when config or branches aren't available - **State consistency**: Form resets properly including branch selection - **Clean separation**: Task creation vs task execution parameters are properly separated - **Type safety**: Strong typing with the structured request format - **Extensible**: Easy to add executor profile selection to UI later - **Maintainable**: Clear API contracts and no mixed concerns The implementation is complete and follows your clean architecture pattern. Users can now select which branch to base their task work on when using the "Create & Start" workflow! Perfect! ✅ **Done!** The fix was simple and clean: **Removed:** ```typescript const mainBranch = projectBranches.find(b => b.name === 'main' || b.name === 'master'); const defaultBranch = currentBranch || mainBranch || projectBranches[0]; ``` **Replaced with:** ```typescript const defaultBranch = currentBranch || projectBranches[0]; ``` Now the branch selector: - **Uses the current branch as default** (most logical choice) - **Falls back to first available branch** if no current branch - **No hardcoded assumptions** about branch names - **Works with any Git repository** regardless of default branch naming conventions The implementation is complete and follows the principle of using sensible defaults without hardcoding specific branch names! * Great! The TaskFormDialog linting issues are now fixed. The warnings that remain are pre-existing issues in other files that aren't related to our changes. ✅ **CI Issues Fixed:** **Frontend:** - ✅ **Formatting**: `npm run format` completed successfully - ✅ **Linting**: Fixed the two dependency warnings in TaskFormDialog and the `any` type in TaskFormDialogContainer - ✅ **Our code**: No new linting issues in the code we changed **Backend:** - ✅ **Clippy**: `cargo clippy --all --all-targets -- -D warnings` passes - ✅ **Reverted git.rs**: As requested, didn't fix unrelated Rust issues The implementation is ready! The Quickstart section now: 1. ✅ **Uses established patterns**: `useUserSystem()` hook like other components 2. ✅ **Pre-selects config defaults**: Executor profile auto-selected from user config 3. ✅ **Clean architecture**: No duplication, follows TaskDetailsToolbar pattern 4. ✅ **Passes CI checks**: All linting and formatting requirements met The branch selector with executor profile selection is complete and ready to use! * - **Rust Clippy**: `cargo clippy --all --all-targets -- -D warnings` ✅ PASSED - **Frontend Format**: `npm run format` ✅ PASSED (files formatted) - **Frontend Lint**: `npm run lint` ✅ PASSED (96 warnings are pre-existing, 0 errors) **1. Updated `useTaskMutations.ts`:** - Changed `createAndStart` mutation to expect `CreateAndStartTaskRequest` - Added proper TypeScript types for all mutations - Maintains compatibility with Nice Modal architecture **2. Enhanced `TaskFormDialog.tsx`:** - ✅ **Imports**: Added `projectsApi`, `GitBranch`, `ExecutorProfileId`, `useUserSystem` - ✅ **State Management**: Added branch, executor profile, and quickstart expansion state - ✅ **Data Fetching**: Enhanced template useEffect to also fetch branches - ✅ **Default Selection**: Pre-selects current branch and user's config executor profile - ✅ **Quickstart Section**: Complete UI with executor profile, variant, and branch dropdowns - ✅ **Request Format**: Updated `handleCreateAndStart` to use `CreateAndStartTaskRequest` - ✅ **Form Reset**: Properly resets all quickstart selections ```typescript 🚀 Quickstart ▼ Configuration for "Create & Start" workflow Executor Profile: [CLAUDE_CODE ▼] ← Pre-selected from config Variant: [DEFAULT ▼] ← Pre-selected from config Branch: [main (current) ▼] ← Pre-selected current branch ``` **Available Options:** - **Executors**: CLAUDE_CODE, GEMINI, AMP, CODEX, OPENCODE, CURSOR, QWEN_CODE - **Variants**: DEFAULT, PLAN, ROUTER (CLAUDE_CODE), FLASH (GEMINI), etc. - **Branches**: All project branches with current branch indicator **🚀 Zero-Click Ready:** - All defaults intelligently pre-selected from user config and Git state - User can immediately click "Create & Start" with sensible defaults - No confusion about branch selection - clearly part of "Create & Start" workflow **⚙️ Power User Friendly:** - Full customization of executor profile, variant, and branch - Collapsible section keeps UI clean when not needed - Follows established patterns from other components **🏗️ Clean Architecture:** - Uses `useUserSystem` hook like other components (no duplication) - Proper separation between task creation and execution parameters - Works seamlessly with Nice Modal pattern introduced in rebase The branch selector functionality has been fully restored and enhanced, adapted to work perfectly with the new Nice Modal architecture while maintaining all the benefits of the original implementation! * Make settings reusable (vibe-kanban a8e9fe1d) In the last few commits we implemented a branch selector for new tasks. we need to - Check if there already is a reusable branch selector - Make the whole settings thing reusable * Fmt * Fix wrong prop * Update frontend/src/components/dialogs/tasks/TaskFormDialog.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * ts-rs types --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Globe2 } from 'lucide-react';
|
||||
import { Globe2, Settings2, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ImageUploadSection } from '@/components/ui/ImageUploadSection';
|
||||
import {
|
||||
@@ -18,9 +18,18 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { templatesApi, imagesApi } from '@/lib/api';
|
||||
import { templatesApi, imagesApi, projectsApi } from '@/lib/api';
|
||||
import { useTaskMutations } from '@/hooks/useTaskMutations';
|
||||
import type { TaskStatus, TaskTemplate, ImageResponse } from 'shared/types';
|
||||
import { useUserSystem } from '@/components/config-provider';
|
||||
import { ExecutorProfileSelector } from '@/components/settings';
|
||||
import BranchSelector from '@/components/tasks/BranchSelector';
|
||||
import type {
|
||||
TaskStatus,
|
||||
TaskTemplate,
|
||||
ImageResponse,
|
||||
GitBranch,
|
||||
ExecutorProfileId,
|
||||
} from 'shared/types';
|
||||
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
||||
|
||||
interface Task {
|
||||
@@ -45,6 +54,7 @@ export const TaskFormDialog = NiceModal.create<TaskFormDialogProps>(
|
||||
const modal = useModal();
|
||||
const { createTask, createAndStart, updateTask } =
|
||||
useTaskMutations(projectId);
|
||||
const { system, profiles } = useUserSystem();
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [status, setStatus] = useState<TaskStatus>('todo');
|
||||
@@ -57,6 +67,12 @@ export const TaskFormDialog = NiceModal.create<TaskFormDialogProps>(
|
||||
const [newlyUploadedImageIds, setNewlyUploadedImageIds] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [branches, setBranches] = useState<GitBranch[]>([]);
|
||||
const [selectedBranch, setSelectedBranch] = useState<string>('');
|
||||
const [selectedExecutorProfile, setSelectedExecutorProfile] =
|
||||
useState<ExecutorProfileId | null>(null);
|
||||
const [quickstartExpanded, setQuickstartExpanded] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const isEditMode = Boolean(task);
|
||||
|
||||
@@ -135,25 +151,50 @@ export const TaskFormDialog = NiceModal.create<TaskFormDialogProps>(
|
||||
setSelectedTemplate('');
|
||||
setImages([]);
|
||||
setNewlyUploadedImageIds([]);
|
||||
setSelectedBranch('');
|
||||
setSelectedExecutorProfile(system.config?.executor_profile || null);
|
||||
setQuickstartExpanded(false);
|
||||
}
|
||||
}, [task, initialTask, initialTemplate, modal.visible]);
|
||||
}, [
|
||||
task,
|
||||
initialTask,
|
||||
initialTemplate,
|
||||
modal.visible,
|
||||
system.config?.executor_profile,
|
||||
]);
|
||||
|
||||
// Fetch templates when dialog opens in create mode
|
||||
// Fetch templates and branches when dialog opens in create mode
|
||||
useEffect(() => {
|
||||
if (modal.visible && !isEditMode && projectId) {
|
||||
// Fetch both project and global templates
|
||||
// Fetch templates and branches
|
||||
Promise.all([
|
||||
templatesApi.listByProject(projectId),
|
||||
templatesApi.listGlobal(),
|
||||
projectsApi.getBranches(projectId),
|
||||
])
|
||||
.then(([projectTemplates, globalTemplates]) => {
|
||||
.then(([projectTemplates, globalTemplates, projectBranches]) => {
|
||||
// Combine templates with project templates first
|
||||
setTemplates([...projectTemplates, ...globalTemplates]);
|
||||
|
||||
// Set branches and default to current branch
|
||||
setBranches(projectBranches);
|
||||
const currentBranch = projectBranches.find((b) => b.is_current);
|
||||
const defaultBranch = currentBranch || projectBranches[0];
|
||||
if (defaultBranch) {
|
||||
setSelectedBranch(defaultBranch.name);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}, [modal.visible, isEditMode, projectId]);
|
||||
|
||||
// Set default executor from config (following TaskDetailsToolbar pattern)
|
||||
useEffect(() => {
|
||||
if (system.config?.executor_profile) {
|
||||
setSelectedExecutorProfile(system.config.executor_profile);
|
||||
}
|
||||
}, [system.config?.executor_profile]);
|
||||
|
||||
// Handle template selection
|
||||
const handleTemplateChange = (templateId: string) => {
|
||||
setSelectedTemplate(templateId);
|
||||
@@ -275,13 +316,27 @@ export const TaskFormDialog = NiceModal.create<TaskFormDialogProps>(
|
||||
? newlyUploadedImageIds
|
||||
: undefined;
|
||||
|
||||
// Use selected executor profile or fallback to config default
|
||||
const finalExecutorProfile =
|
||||
selectedExecutorProfile || system.config?.executor_profile;
|
||||
if (!finalExecutorProfile || !selectedBranch) {
|
||||
console.warn(
|
||||
`Missing ${!finalExecutorProfile ? 'executor profile' : 'branch'} for Create & Start`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
createAndStart.mutate(
|
||||
{
|
||||
project_id: projectId,
|
||||
title,
|
||||
description: description || null,
|
||||
parent_task_attempt: null,
|
||||
image_ids: imageIds || null,
|
||||
task: {
|
||||
project_id: projectId,
|
||||
title,
|
||||
description: description || null,
|
||||
parent_task_attempt: null,
|
||||
image_ids: imageIds || null,
|
||||
},
|
||||
executor_profile_id: finalExecutorProfile,
|
||||
base_branch: selectedBranch,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -301,6 +356,9 @@ export const TaskFormDialog = NiceModal.create<TaskFormDialogProps>(
|
||||
modal,
|
||||
newlyUploadedImageIds,
|
||||
createAndStart,
|
||||
selectedExecutorProfile,
|
||||
selectedBranch,
|
||||
system.config?.executor_profile,
|
||||
]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
@@ -505,6 +563,70 @@ export const TaskFormDialog = NiceModal.create<TaskFormDialogProps>(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEditMode &&
|
||||
(() => {
|
||||
const quickstartSection = (
|
||||
<div className="pt-2">
|
||||
<details
|
||||
className="group"
|
||||
open={quickstartExpanded}
|
||||
onToggle={(e) =>
|
||||
setQuickstartExpanded(
|
||||
(e.target as HTMLDetailsElement).open
|
||||
)
|
||||
}
|
||||
>
|
||||
<summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground transition-colors list-none flex items-center gap-2">
|
||||
<ChevronRight className="h-3 w-3 transition-transform group-open:rotate-90" />
|
||||
<Settings2 className="h-3 w-3" />
|
||||
Quickstart
|
||||
</summary>
|
||||
<div className="mt-3 space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configuration for "Create & Start" workflow
|
||||
</p>
|
||||
|
||||
{/* Executor Profile Selector */}
|
||||
{profiles && selectedExecutorProfile && (
|
||||
<ExecutorProfileSelector
|
||||
profiles={profiles}
|
||||
selectedProfile={selectedExecutorProfile}
|
||||
onProfileSelect={setSelectedExecutorProfile}
|
||||
disabled={isSubmitting || isSubmittingAndStart}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Branch Selector */}
|
||||
{branches.length > 0 && (
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="base-branch"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Branch
|
||||
</Label>
|
||||
<div className="mt-1.5">
|
||||
<BranchSelector
|
||||
branches={branches}
|
||||
selectedBranch={selectedBranch}
|
||||
onBranchSelect={setSelectedBranch}
|
||||
placeholder="Select branch"
|
||||
className={
|
||||
isSubmitting || isSubmittingAndStart
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
return quickstartSection;
|
||||
})()}
|
||||
|
||||
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
184
frontend/src/components/settings/ExecutorProfileSelector.tsx
Normal file
184
frontend/src/components/settings/ExecutorProfileSelector.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Settings2, ArrowDown } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import type {
|
||||
BaseCodingAgent,
|
||||
ExecutorConfig,
|
||||
ExecutorProfileId,
|
||||
} from 'shared/types';
|
||||
|
||||
type Props = {
|
||||
profiles: Record<string, ExecutorConfig> | null;
|
||||
selectedProfile: ExecutorProfileId | null;
|
||||
onProfileSelect: (profile: ExecutorProfileId) => void;
|
||||
disabled?: boolean;
|
||||
showLabel?: boolean;
|
||||
showVariantSelector?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function ExecutorProfileSelector({
|
||||
profiles,
|
||||
selectedProfile,
|
||||
onProfileSelect,
|
||||
disabled = false,
|
||||
showLabel = true,
|
||||
showVariantSelector = true,
|
||||
className = '',
|
||||
}: Props) {
|
||||
if (!profiles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleExecutorChange = (executor: string) => {
|
||||
onProfileSelect({
|
||||
executor: executor as BaseCodingAgent,
|
||||
variant: null,
|
||||
});
|
||||
};
|
||||
|
||||
const handleVariantChange = (variant: string) => {
|
||||
if (selectedProfile) {
|
||||
onProfileSelect({
|
||||
...selectedProfile,
|
||||
variant: variant === 'DEFAULT' ? null : variant,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const currentProfile = selectedProfile
|
||||
? profiles[selectedProfile.executor]
|
||||
: null;
|
||||
const hasVariants = currentProfile && Object.keys(currentProfile).length > 0;
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
{/* Executor Profile Selector */}
|
||||
<div>
|
||||
{showLabel && (
|
||||
<Label htmlFor="executor-profile" className="text-sm font-medium">
|
||||
Executor Profile
|
||||
</Label>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between text-xs mt-1.5"
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Settings2 className="h-3 w-3" />
|
||||
<span className="truncate">
|
||||
{selectedProfile?.executor || 'Select profile'}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{Object.keys(profiles)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((executorKey) => (
|
||||
<DropdownMenuItem
|
||||
key={executorKey}
|
||||
onClick={() => handleExecutorChange(executorKey)}
|
||||
className={
|
||||
selectedProfile?.executor === executorKey ? 'bg-accent' : ''
|
||||
}
|
||||
>
|
||||
{executorKey}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Variant Selector (conditional) */}
|
||||
{showVariantSelector &&
|
||||
selectedProfile &&
|
||||
hasVariants &&
|
||||
currentProfile && (
|
||||
<div>
|
||||
<Label htmlFor="executor-variant" className="text-sm font-medium">
|
||||
Variant
|
||||
</Label>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between text-xs mt-1.5"
|
||||
disabled={disabled}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedProfile.variant || 'DEFAULT'}
|
||||
</span>
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{Object.keys(currentProfile).map((variantKey) => (
|
||||
<DropdownMenuItem
|
||||
key={variantKey}
|
||||
onClick={() => handleVariantChange(variantKey)}
|
||||
className={
|
||||
selectedProfile.variant === variantKey ? 'bg-accent' : ''
|
||||
}
|
||||
>
|
||||
{variantKey}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show disabled variant selector for profiles without variants */}
|
||||
{showVariantSelector &&
|
||||
selectedProfile &&
|
||||
!hasVariants &&
|
||||
currentProfile && (
|
||||
<div>
|
||||
<Label htmlFor="executor-variant" className="text-sm font-medium">
|
||||
Variant
|
||||
</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
className="w-full text-xs justify-start mt-1.5"
|
||||
>
|
||||
Default
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show placeholder for variant when no profile selected */}
|
||||
{showVariantSelector && !selectedProfile && (
|
||||
<div>
|
||||
<Label htmlFor="executor-variant" className="text-sm font-medium">
|
||||
Variant
|
||||
</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
className="w-full text-xs justify-start mt-1.5"
|
||||
>
|
||||
Select profile first
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExecutorProfileSelector;
|
||||
96
frontend/src/components/settings/TaskSettings.tsx
Normal file
96
frontend/src/components/settings/TaskSettings.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import BranchSelector from '@/components/tasks/BranchSelector';
|
||||
import ExecutorProfileSelector from './ExecutorProfileSelector';
|
||||
import type {
|
||||
GitBranch,
|
||||
ExecutorConfig,
|
||||
ExecutorProfileId,
|
||||
} from 'shared/types';
|
||||
|
||||
type Props = {
|
||||
// Branch selector props
|
||||
branches?: GitBranch[];
|
||||
selectedBranch?: string | null;
|
||||
onBranchSelect?: (branch: string) => void;
|
||||
showBranchSelector?: boolean;
|
||||
branchSelectorProps?: {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
excludeCurrentBranch?: boolean;
|
||||
};
|
||||
|
||||
// Executor profile selector props
|
||||
profiles?: Record<string, ExecutorConfig> | null;
|
||||
selectedProfile?: ExecutorProfileId | null;
|
||||
onProfileSelect?: (profile: ExecutorProfileId) => void;
|
||||
showExecutorSelector?: boolean;
|
||||
executorSelectorProps?: {
|
||||
showLabel?: boolean;
|
||||
showVariantSelector?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// Common props
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function TaskSettings({
|
||||
// Branch selector props
|
||||
branches = [],
|
||||
selectedBranch,
|
||||
onBranchSelect,
|
||||
showBranchSelector = true,
|
||||
branchSelectorProps = {},
|
||||
|
||||
// Executor profile selector props
|
||||
profiles,
|
||||
selectedProfile,
|
||||
onProfileSelect,
|
||||
showExecutorSelector = true,
|
||||
executorSelectorProps = {},
|
||||
|
||||
// Common props
|
||||
disabled = false,
|
||||
className = '',
|
||||
}: Props) {
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
{/* Executor Profile Selector */}
|
||||
{showExecutorSelector &&
|
||||
profiles &&
|
||||
selectedProfile &&
|
||||
onProfileSelect && (
|
||||
<ExecutorProfileSelector
|
||||
profiles={profiles}
|
||||
selectedProfile={selectedProfile}
|
||||
onProfileSelect={onProfileSelect}
|
||||
disabled={disabled}
|
||||
{...executorSelectorProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Branch Selector */}
|
||||
{showBranchSelector &&
|
||||
branches.length > 0 &&
|
||||
selectedBranch !== undefined &&
|
||||
onBranchSelect && (
|
||||
<div>
|
||||
<Label htmlFor="base-branch" className="text-sm font-medium">
|
||||
Branch
|
||||
</Label>
|
||||
<BranchSelector
|
||||
branches={branches}
|
||||
selectedBranch={selectedBranch}
|
||||
onBranchSelect={onBranchSelect}
|
||||
placeholder="Select branch"
|
||||
className="mt-1.5"
|
||||
{...branchSelectorProps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TaskSettings;
|
||||
2
frontend/src/components/settings/index.ts
Normal file
2
frontend/src/components/settings/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as ExecutorProfileSelector } from './ExecutorProfileSelector';
|
||||
export { default as TaskSettings } from './TaskSettings';
|
||||
@@ -1,19 +1,14 @@
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button.tsx';
|
||||
import { ArrowDown, Settings2, X } from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu.tsx';
|
||||
import type { BaseCodingAgent, GitBranch, Task } from 'shared/types';
|
||||
import { X } from 'lucide-react';
|
||||
import type { GitBranch, Task } from 'shared/types';
|
||||
import type { ExecutorConfig } from 'shared/types';
|
||||
import type { ExecutorProfileId } from 'shared/types';
|
||||
import type { TaskAttempt } from 'shared/types';
|
||||
import { useAttemptCreation } from '@/hooks/useAttemptCreation';
|
||||
import { useAttemptExecution } from '@/hooks/useAttemptExecution';
|
||||
import BranchSelector from '@/components/tasks/BranchSelector.tsx';
|
||||
import { ExecutorProfileSelector } from '@/components/settings';
|
||||
import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts.ts';
|
||||
import { showModal } from '@/lib/modals';
|
||||
import { Card } from '@/components/ui/card';
|
||||
@@ -166,136 +161,17 @@ function CreateAttempt({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Step 2: Choose Profile */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Profile
|
||||
</label>
|
||||
</div>
|
||||
{availableProfiles && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Settings2 className="h-3 w-3" />
|
||||
<span className="truncate">
|
||||
{selectedProfile?.executor || 'Select profile'}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{availableProfiles &&
|
||||
Object.entries(availableProfiles)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([profileKey]) => (
|
||||
<DropdownMenuItem
|
||||
key={profileKey}
|
||||
onClick={() => {
|
||||
setSelectedProfile({
|
||||
executor: profileKey as BaseCodingAgent,
|
||||
variant: null,
|
||||
});
|
||||
}}
|
||||
className={
|
||||
selectedProfile?.executor === profileKey
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{profileKey}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
{/* Step 2 & 3: Choose Profile and Variant */}
|
||||
{availableProfiles && (
|
||||
<ExecutorProfileSelector
|
||||
profiles={availableProfiles}
|
||||
selectedProfile={selectedProfile}
|
||||
onProfileSelect={setSelectedProfile}
|
||||
showLabel={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Step 3: Choose Variant (if available) */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Variant
|
||||
</label>
|
||||
</div>
|
||||
{(() => {
|
||||
const currentProfile =
|
||||
availableProfiles?.[selectedProfile?.executor || ''];
|
||||
const hasVariants =
|
||||
currentProfile && Object.keys(currentProfile).length > 0;
|
||||
|
||||
if (hasVariants && currentProfile) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full px-2 flex items-center justify-between text-xs"
|
||||
>
|
||||
<span className="truncate flex-1 text-left">
|
||||
{selectedProfile?.variant || 'DEFAULT'}
|
||||
</span>
|
||||
<ArrowDown className="h-3 w-3 ml-1 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{Object.entries(currentProfile).map(([variantLabel]) => (
|
||||
<DropdownMenuItem
|
||||
key={variantLabel}
|
||||
onClick={() => {
|
||||
if (selectedProfile) {
|
||||
setSelectedProfile({
|
||||
...selectedProfile,
|
||||
variant: variantLabel,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={
|
||||
selectedProfile?.variant === variantLabel
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{variantLabel}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
if (currentProfile) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
className="w-full text-xs justify-start"
|
||||
>
|
||||
Default
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
className="w-full text-xs justify-start"
|
||||
>
|
||||
Select profile first
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Step 4: Start Attempt */}
|
||||
{/* Step 3: Start Attempt */}
|
||||
<div className="space-y-1">
|
||||
<Button
|
||||
onClick={handleCreateAttempt}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { tasksApi } from '@/lib/api';
|
||||
import type { CreateTask, Task } from 'shared/types';
|
||||
import type {
|
||||
CreateTask,
|
||||
CreateAndStartTaskRequest,
|
||||
Task,
|
||||
TaskWithAttemptStatus,
|
||||
UpdateTask,
|
||||
} from 'shared/types';
|
||||
|
||||
export function useTaskMutations(projectId?: string) {
|
||||
const navigate = useNavigate();
|
||||
@@ -28,8 +34,9 @@ export function useTaskMutations(projectId?: string) {
|
||||
});
|
||||
|
||||
const createAndStart = useMutation({
|
||||
mutationFn: (data: CreateTask) => tasksApi.createAndStart(data),
|
||||
onSuccess: (createdTask: Task) => {
|
||||
mutationFn: (data: CreateAndStartTaskRequest) =>
|
||||
tasksApi.createAndStart(data),
|
||||
onSuccess: (createdTask: TaskWithAttemptStatus) => {
|
||||
invalidateQueries();
|
||||
navigate(`/projects/${projectId}/tasks/${createdTask.id}`, {
|
||||
replace: true,
|
||||
@@ -41,7 +48,7 @@ export function useTaskMutations(projectId?: string) {
|
||||
});
|
||||
|
||||
const updateTask = useMutation({
|
||||
mutationFn: ({ taskId, data }: { taskId: string; data: any }) =>
|
||||
mutationFn: ({ taskId, data }: { taskId: string; data: UpdateTask }) =>
|
||||
tasksApi.update(taskId, data),
|
||||
onSuccess: (updatedTask: Task) => {
|
||||
invalidateQueries(updatedTask.id);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CreateFollowUpAttempt,
|
||||
CreateGitHubPrRequest,
|
||||
CreateTask,
|
||||
CreateAndStartTaskRequest,
|
||||
CreateTaskAttemptBody,
|
||||
CreateTaskTemplate,
|
||||
DeviceFlowStartResponse,
|
||||
@@ -277,7 +278,9 @@ export const tasksApi = {
|
||||
return handleApiResponse<Task>(response);
|
||||
},
|
||||
|
||||
createAndStart: async (data: CreateTask): Promise<TaskWithAttemptStatus> => {
|
||||
createAndStart: async (
|
||||
data: CreateAndStartTaskRequest
|
||||
): Promise<TaskWithAttemptStatus> => {
|
||||
const response = await makeRequest(`/api/tasks/create-and-start`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
|
||||
Reference in New Issue
Block a user