diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 866ce2eb..4c6f5972 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -449,6 +449,54 @@ pub async fn merge_task_attempt( } } +pub async fn open_task_attempt_in_editor( + Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, + Extension(pool): Extension, +) -> Result>, StatusCode> { + // Verify task attempt exists and belongs to the correct task + match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { + Ok(false) => return Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("Failed to check task attempt existence: {}", e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + Ok(true) => {} + } + + // Get the task attempt to access the worktree path + let attempt = match TaskAttempt::find_by_id(&pool, attempt_id).await { + Ok(Some(attempt)) => attempt, + Ok(None) => return Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("Failed to fetch task attempt {}: {}", attempt_id, e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + // Open VSCode in the worktree directory + match std::process::Command::new("code") + .arg(&attempt.worktree_path) + .spawn() + { + Ok(_) => { + tracing::info!("Opened VSCode for task attempt {} at path: {}", attempt_id, attempt.worktree_path); + Ok(ResponseJson(ApiResponse { + success: true, + data: None, + message: Some("VSCode opened successfully".to_string()), + })) + } + Err(e) => { + tracing::error!("Failed to open VSCode for attempt {}: {}", attempt_id, e); + Ok(ResponseJson(ApiResponse { + success: false, + data: None, + message: Some(format!("Failed to open VSCode: {}", e)), + })) + } + } +} + pub fn tasks_router() -> Router { use axum::routing::{delete, post, put}; @@ -481,6 +529,10 @@ pub fn tasks_router() -> Router { "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/merge", post(merge_task_attempt), ) + .route( + "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/open-editor", + post(open_task_attempt_in_editor), + ) } #[cfg(test)] diff --git a/frontend/src/pages/task-details.tsx b/frontend/src/pages/task-details.tsx index fc70f4fc..630728ea 100644 --- a/frontend/src/pages/task-details.tsx +++ b/frontend/src/pages/task-details.tsx @@ -11,7 +11,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; -import { ArrowLeft, FileText } from "lucide-react"; +import { ArrowLeft, FileText, Code } from "lucide-react"; import { makeRequest } from "@/lib/api"; import { TaskFormDialog } from "@/components/tasks/TaskFormDialog"; import type { @@ -90,6 +90,7 @@ export function TaskDetailsPage() { const [selectedExecutor, setSelectedExecutor] = useState("claude"); const [creatingAttempt, setCreatingAttempt] = useState(false); const [stoppingAttempt, setStoppingAttempt] = useState(false); + const [openingEditor, setOpeningEditor] = useState(false); const [error, setError] = useState(null); const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false); @@ -354,6 +355,36 @@ export function TaskDetailsPage() { } }; + const openTaskAttemptInEditor = async () => { + if (!task || !selectedAttempt || !projectId) return; + + try { + setOpeningEditor(true); + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/open-editor`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (response.ok) { + const result: ApiResponse = await response.json(); + if (!result.success && result.message) { + setError(result.message); + } + } else { + setError("Failed to open editor"); + } + } catch (err) { + setError("Failed to open editor"); + } finally { + setOpeningEditor(false); + } + }; + const handleBackClick = () => { navigate(`/projects/${projectId}/tasks`); }; @@ -618,19 +649,31 @@ export function TaskDetailsPage() {
{selectedAttempt && ( - + <> + + + )} {isAttemptRunning && (