From 708bb9ad5bfcc59188b0ca1f4ea380b0a724dbf5 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Fri, 20 Jun 2025 19:57:38 +0100 Subject: [PATCH] Task attempt f0c071f3-47ae-453e-b230-276ddf8ac3e6 - Final changes --- backend/src/routes/tasks.rs | 92 +++++++++++++++++++ .../src/components/tasks/TaskFormDialog.tsx | 66 ++++++++++--- frontend/src/pages/project-tasks.tsx | 22 +++++ shared/types.ts | 56 +++++------ 4 files changed, 195 insertions(+), 41 deletions(-) diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 6c68afb3..2a4fe087 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -97,6 +97,94 @@ pub async fn create_task( } } +pub async fn create_task_and_start( + Path(project_id): Path, + Extension(pool): Extension, + Extension(app_state): Extension, + Json(mut payload): Json, +) -> Result>, StatusCode> { + let task_id = Uuid::new_v4(); + + // Ensure the project_id in the payload matches the path parameter + payload.project_id = project_id; + + // Verify project exists first + match Project::exists(&pool, project_id).await { + Ok(false) => return Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("Failed to check project existence: {}", e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + Ok(true) => {} + } + + tracing::debug!( + "Creating and starting task '{}' in project {}", + payload.title, + project_id + ); + + // Create the task first + let task = match Task::create(&pool, &payload, task_id).await { + Ok(task) => task, + Err(e) => { + tracing::error!("Failed to create task: {}", e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + // Create task attempt + let attempt_id = Uuid::new_v4(); + let worktree_path = format!("/tmp/task-{}-attempt-{}", task_id, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis()); + + let attempt_payload = CreateTaskAttempt { + task_id, + worktree_path, + merge_commit: None, + executor: Some("claude".to_string()), // Default executor + }; + + match TaskAttempt::create(&pool, &attempt_payload, attempt_id).await { + Ok(attempt) => { + // Create initial activity record + let activity_id = Uuid::new_v4(); + let _ = TaskAttemptActivity::create_initial(&pool, attempt.id, activity_id).await; + + // Start execution asynchronously (don't block the response) + let pool_clone = pool.clone(); + let app_state_clone = app_state.clone(); + let attempt_id = attempt.id; + tokio::spawn(async move { + if let Err(e) = TaskAttempt::start_execution( + &pool_clone, + &app_state_clone, + attempt_id, + task_id, + project_id, + ) + .await + { + tracing::error!( + "Failed to start execution for task attempt {}: {}", + attempt_id, + e + ); + } + }); + + Ok(ResponseJson(ApiResponse { + success: true, + data: Some(task), + message: Some("Task created and started successfully".to_string()), + })) + } + Err(e) => { + tracing::error!("Failed to create task attempt: {}", e); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } +} + pub async fn update_task( Path((project_id, task_id)): Path<(Uuid, Uuid)>, Extension(pool): Extension, @@ -623,6 +711,10 @@ pub fn tasks_router() -> Router { "/projects/:project_id/tasks", get(get_project_tasks).post(create_task), ) + .route( + "/projects/:project_id/tasks/create-and-start", + post(create_task_and_start), + ) .route( "/projects/:project_id/tasks/:task_id", get(get_task).put(update_task).delete(delete_task), diff --git a/frontend/src/components/tasks/TaskFormDialog.tsx b/frontend/src/components/tasks/TaskFormDialog.tsx index 54185b8c..fc0c14c0 100644 --- a/frontend/src/components/tasks/TaskFormDialog.tsx +++ b/frontend/src/components/tasks/TaskFormDialog.tsx @@ -34,6 +34,7 @@ interface TaskFormDialogProps { task?: Task | null // Optional for create mode projectId?: string // For file search functionality onCreateTask?: (title: string, description: string) => Promise + onCreateAndStartTask?: (title: string, description: string) => Promise onUpdateTask?: (title: string, description: string, status: TaskStatus) => Promise } @@ -43,12 +44,14 @@ export function TaskFormDialog({ task, projectId, onCreateTask, + onCreateAndStartTask, onUpdateTask }: TaskFormDialogProps) { const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [status, setStatus] = useState('todo') const [isSubmitting, setIsSubmitting] = useState(false) + const [isSubmittingAndStart, setIsSubmittingAndStart] = useState(false) const isEditMode = Boolean(task) @@ -90,6 +93,26 @@ export function TaskFormDialog({ } } + const handleCreateAndStart = async () => { + if (!title.trim()) return + + setIsSubmittingAndStart(true) + try { + if (!isEditMode && onCreateAndStartTask) { + await onCreateAndStartTask(title, description) + } + + // Reset form on successful creation + setTitle('') + setDescription('') + setStatus('todo') + + onOpenChange(false) + } finally { + setIsSubmittingAndStart(false) + } + } + const handleCancel = () => { // Reset form state when canceling if (task) { @@ -118,7 +141,7 @@ export function TaskFormDialog({ value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Enter task title" - disabled={isSubmitting} + disabled={isSubmitting || isSubmittingAndStart} /> @@ -129,7 +152,7 @@ export function TaskFormDialog({ onChange={setDescription} placeholder="Enter task description (optional). Type @ to search files." rows={3} - disabled={isSubmitting} + disabled={isSubmitting || isSubmittingAndStart} projectId={projectId} /> @@ -140,7 +163,7 @@ export function TaskFormDialog({