diff --git a/backend/src/routes/task_attempts.rs b/backend/src/routes/task_attempts.rs index 02b3cb24..a6e73c99 100644 --- a/backend/src/routes/task_attempts.rs +++ b/backend/src/routes/task_attempts.rs @@ -279,10 +279,16 @@ pub async fn merge_task_attempt( } } +#[derive(serde::Deserialize)] +pub struct OpenEditorRequest { + editor_type: Option, +} + pub async fn open_task_attempt_in_editor( Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Extension(pool): Extension, Extension(config): Extension>>, + Json(payload): Json>, ) -> 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 { @@ -304,10 +310,33 @@ pub async fn open_task_attempt_in_editor( } }; - // Get editor command from config + // Get editor command from config or override let editor_command = { let config_guard = config.read().await; - config_guard.editor.get_command() + if let Some(ref request) = payload { + if let Some(ref editor_type) = request.editor_type { + // Create a temporary editor config with the override + use crate::models::config::{EditorConfig, EditorType}; + let override_editor_type = match editor_type.as_str() { + "vscode" => EditorType::VSCode, + "cursor" => EditorType::Cursor, + "windsurf" => EditorType::Windsurf, + "intellij" => EditorType::IntelliJ, + "zed" => EditorType::Zed, + "custom" => EditorType::Custom, + _ => config_guard.editor.editor_type.clone(), + }; + let temp_config = EditorConfig { + editor_type: override_editor_type, + custom_command: config_guard.editor.custom_command.clone(), + }; + temp_config.get_command() + } else { + config_guard.editor.get_command() + } + } else { + config_guard.editor.get_command() + } }; // Open editor in the worktree directory @@ -338,11 +367,7 @@ pub async fn open_task_attempt_in_editor( attempt_id, e ); - Ok(ResponseJson(ApiResponse { - success: false, - data: None, - message: Some(format!("Failed to open editor: {}", e)), - })) + Err(StatusCode::INTERNAL_SERVER_ERROR) } } } diff --git a/frontend/src/components/tasks/EditorSelectionDialog.tsx b/frontend/src/components/tasks/EditorSelectionDialog.tsx new file mode 100644 index 00000000..ec488838 --- /dev/null +++ b/frontend/src/components/tasks/EditorSelectionDialog.tsx @@ -0,0 +1,115 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { EditorType } from "shared/types"; + +interface EditorSelectionDialogProps { + isOpen: boolean; + onClose: () => void; + onSelectEditor: (editorType: EditorType) => void; +} + +const editorOptions: { value: EditorType; label: string; description: string }[] = [ + { + value: "vscode", + label: "Visual Studio Code", + description: "Microsoft's popular code editor", + }, + { + value: "cursor", + label: "Cursor", + description: "AI-powered code editor", + }, + { + value: "windsurf", + label: "Windsurf", + description: "Modern code editor", + }, + { + value: "intellij", + label: "IntelliJ IDEA", + description: "JetBrains IDE", + }, + { + value: "zed", + label: "Zed", + description: "High-performance code editor", + }, + { + value: "custom", + label: "Custom Editor", + description: "Use your configured custom editor", + }, +]; + +export function EditorSelectionDialog({ + isOpen, + onClose, + onSelectEditor, +}: EditorSelectionDialogProps) { + const [selectedEditor, setSelectedEditor] = useState("vscode"); + + const handleConfirm = () => { + onSelectEditor(selectedEditor); + onClose(); + }; + + return ( + + + + Choose Editor + + The default editor failed to open. Please select an alternative + editor to open the task worktree. + + +
+
+ + +
+
+ + + + +
+
+ ); +} diff --git a/frontend/src/components/tasks/TaskDetailsPanel.tsx b/frontend/src/components/tasks/TaskDetailsPanel.tsx index b84e3fc2..4520a4dc 100644 --- a/frontend/src/components/tasks/TaskDetailsPanel.tsx +++ b/frontend/src/components/tasks/TaskDetailsPanel.tsx @@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Chip } from "@/components/ui/chip"; import { ExecutionOutputViewer } from "./ExecutionOutputViewer"; +import { EditorSelectionDialog } from "./EditorSelectionDialog"; import { DropdownMenu, @@ -40,6 +41,7 @@ import type { ApiResponse, TaskWithAttemptStatus, ExecutionProcess, + EditorType, } from "shared/types"; interface TaskDetailsPanelProps { @@ -143,6 +145,7 @@ export function TaskDetailsPanel({ const [expandedOutputs, setExpandedOutputs] = useState>( new Set() ); + const [showEditorDialog, setShowEditorDialog] = useState(false); const { config } = useConfig(); // Available executors @@ -310,21 +313,30 @@ export function TaskDetailsPanel({ } }; - const openInEditor = async () => { + const openInEditor = async (editorType?: EditorType) => { if (!task || !selectedAttempt) return; try { - await makeRequest( + const response = await makeRequest( `/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/open-editor`, { method: "POST", headers: { "Content-Type": "application/json", }, + body: JSON.stringify(editorType ? { editor_type: editorType } : null), } ); + + if (!response.ok) { + throw new Error("Failed to open editor"); + } } catch (err) { console.error("Failed to open editor:", err); + // Show editor selection dialog if editor failed to open + if (!editorType) { + setShowEditorDialog(true); + } } }; @@ -616,7 +628,7 @@ export function TaskDetailsPanel({