diff --git a/backend/src/bin/generate_types.rs b/backend/src/bin/generate_types.rs index 66fb9c87..979d5d7a 100644 --- a/backend/src/bin/generate_types.rs +++ b/backend/src/bin/generate_types.rs @@ -1,17 +1,17 @@ use std::{env, fs, path::Path}; -use ts_rs::TS; // make sure this is in [build-dependencies] +use ts_rs::TS; // in [build-dependencies] fn main() { - // Where the combined types.ts will live + // 1. Make sure ../shared exists let shared_path = Path::new("../shared"); fs::create_dir_all(shared_path).expect("cannot create ../shared"); println!("Generating TypeScript types…"); - // Tell ts-rs where to drop its per-type files (we’ll still roll our own big one) + // 2. Let ts-rs write its per-type files here (handy for debugging) env::set_var("TS_RS_EXPORT_DIR", shared_path.to_str().unwrap()); - // Collect every declaration at *runtime* (so no const-eval issues) + // 3. Grab every Rust type you want on the TS side let decls = [ vibe_kanban::models::ApiResponse::<()>::decl(), vibe_kanban::models::config::Config::decl(), @@ -43,16 +43,29 @@ fn main() { vibe_kanban::models::task_attempt::BranchStatus::decl(), ]; - // Header banner + // 4. Friendly banner const HEADER: &str = "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs).\n\ // Do not edit this file manually.\n\ // Auto-generated from Rust backend types using ts-rs\n\n"; - // Smash it all together - let consolidated = format!("{HEADER}{}", decls.join("\n\n")); + // 5. Add `export` if it’s missing, then join + let body = decls + .into_iter() + .map(|d| { + let trimmed = d.trim_start(); + if trimmed.starts_with("export") { + d + } else { + format!("export {trimmed}") + } + }) + .collect::>() + .join("\n\n"); - fs::write(shared_path.join("types.ts"), consolidated).expect("unable to write types.ts"); + // 6. Write the consolidated types.ts + fs::write(shared_path.join("types.ts"), format!("{HEADER}{body}")) + .expect("unable to write types.ts"); println!("✅ TypeScript types generated in ../shared/"); } 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({