Task attempt 7002fc66-7954-44a7-a023-35faabd0e93f - Final changes

This commit is contained in:
Louis Knight-Webb
2025-06-19 11:40:56 -04:00
parent 68a3fa2109
commit 94425c515e
3 changed files with 4243 additions and 14 deletions

View File

@@ -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<SqlitePool>,
) -> Result<ResponseJson<ApiResponse<()>>, 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 { pub fn tasks_router() -> Router {
use axum::routing::{delete, post, put}; 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", "/projects/:project_id/tasks/:task_id/attempts/:attempt_id/merge",
post(merge_task_attempt), post(merge_task_attempt),
) )
.route(
"/projects/:project_id/tasks/:task_id/attempts/:attempt_id/open-editor",
post(open_task_attempt_in_editor),
)
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -11,7 +11,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator"; 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 { makeRequest } from "@/lib/api";
import { TaskFormDialog } from "@/components/tasks/TaskFormDialog"; import { TaskFormDialog } from "@/components/tasks/TaskFormDialog";
import type { import type {
@@ -90,6 +90,7 @@ export function TaskDetailsPage() {
const [selectedExecutor, setSelectedExecutor] = useState<string>("claude"); const [selectedExecutor, setSelectedExecutor] = useState<string>("claude");
const [creatingAttempt, setCreatingAttempt] = useState(false); const [creatingAttempt, setCreatingAttempt] = useState(false);
const [stoppingAttempt, setStoppingAttempt] = useState(false); const [stoppingAttempt, setStoppingAttempt] = useState(false);
const [openingEditor, setOpeningEditor] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false); 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<null> = 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 = () => { const handleBackClick = () => {
navigate(`/projects/${projectId}/tasks`); navigate(`/projects/${projectId}/tasks`);
}; };
@@ -618,19 +649,31 @@ export function TaskDetailsPage() {
</Label> </Label>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{selectedAttempt && ( {selectedAttempt && (
<Button <>
onClick={() => <Button
navigate( onClick={() =>
`/projects/${projectId}/tasks/${taskId}/attempts/${selectedAttempt.id}/compare` navigate(
) `/projects/${projectId}/tasks/${taskId}/attempts/${selectedAttempt.id}/compare`
} )
size="sm" }
variant="outline" size="sm"
className="w-full" variant="outline"
> className="w-full"
<FileText className="mr-2 h-4 w-4" /> >
View Changes <FileText className="mr-2 h-4 w-4" />
</Button> View Changes
</Button>
<Button
onClick={openTaskAttemptInEditor}
disabled={openingEditor}
size="sm"
variant="outline"
className="w-full"
>
<Code className="mr-2 h-4 w-4" />
{openingEditor ? "Opening..." : "Open in Editor"}
</Button>
</>
)} )}
{isAttemptRunning && ( {isAttemptRunning && (
<Button <Button

4134
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff