Task attempt 015264cd-8abd-49bc-aa95-5672dbab0759 - Final changes

This commit is contained in:
Louis Knight-Webb
2025-06-22 22:41:16 +01:00
parent dfe768767e
commit f5ef6a79de
3 changed files with 169 additions and 10 deletions

View File

@@ -279,10 +279,16 @@ pub async fn merge_task_attempt(
} }
} }
#[derive(serde::Deserialize)]
pub struct OpenEditorRequest {
editor_type: Option<String>,
}
pub async fn open_task_attempt_in_editor( pub async fn open_task_attempt_in_editor(
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
Extension(pool): Extension<SqlitePool>, Extension(pool): Extension<SqlitePool>,
Extension(config): Extension<Arc<RwLock<crate::models::config::Config>>>, Extension(config): Extension<Arc<RwLock<crate::models::config::Config>>>,
Json(payload): Json<Option<OpenEditorRequest>>,
) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> { ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
// Verify task attempt exists and belongs to the correct task // Verify task attempt exists and belongs to the correct task
match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { 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 editor_command = {
let config_guard = config.read().await; 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 // Open editor in the worktree directory
@@ -338,11 +367,7 @@ pub async fn open_task_attempt_in_editor(
attempt_id, attempt_id,
e e
); );
Ok(ResponseJson(ApiResponse { Err(StatusCode::INTERNAL_SERVER_ERROR)
success: false,
data: None,
message: Some(format!("Failed to open editor: {}", e)),
}))
} }
} }
} }

View File

@@ -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<EditorType>("vscode");
const handleConfirm = () => {
onSelectEditor(selectedEditor);
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Choose Editor</DialogTitle>
<DialogDescription>
The default editor failed to open. Please select an alternative
editor to open the task worktree.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Editor</label>
<Select
value={selectedEditor}
onValueChange={(value) => setSelectedEditor(value as EditorType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{editorOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">
{option.description}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleConfirm}>Open Editor</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -18,6 +18,7 @@ import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Chip } from "@/components/ui/chip"; import { Chip } from "@/components/ui/chip";
import { ExecutionOutputViewer } from "./ExecutionOutputViewer"; import { ExecutionOutputViewer } from "./ExecutionOutputViewer";
import { EditorSelectionDialog } from "./EditorSelectionDialog";
import { import {
DropdownMenu, DropdownMenu,
@@ -40,6 +41,7 @@ import type {
ApiResponse, ApiResponse,
TaskWithAttemptStatus, TaskWithAttemptStatus,
ExecutionProcess, ExecutionProcess,
EditorType,
} from "shared/types"; } from "shared/types";
interface TaskDetailsPanelProps { interface TaskDetailsPanelProps {
@@ -143,6 +145,7 @@ export function TaskDetailsPanel({
const [expandedOutputs, setExpandedOutputs] = useState<Set<string>>( const [expandedOutputs, setExpandedOutputs] = useState<Set<string>>(
new Set() new Set()
); );
const [showEditorDialog, setShowEditorDialog] = useState(false);
const { config } = useConfig(); const { config } = useConfig();
// Available executors // Available executors
@@ -310,21 +313,30 @@ export function TaskDetailsPanel({
} }
}; };
const openInEditor = async () => { const openInEditor = async (editorType?: EditorType) => {
if (!task || !selectedAttempt) return; if (!task || !selectedAttempt) return;
try { try {
await makeRequest( const response = await makeRequest(
`/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/open-editor`, `/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/open-editor`,
{ {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(editorType ? { editor_type: editorType } : null),
} }
); );
if (!response.ok) {
throw new Error("Failed to open editor");
}
} catch (err) { } catch (err) {
console.error("Failed to open editor:", 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({
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={openInEditor} onClick={() => openInEditor()}
> >
<Code className="h-4 w-4 mr-1" /> <Code className="h-4 w-4 mr-1" />
Editor Editor
@@ -802,6 +814,13 @@ export function TaskDetailsPanel({
</div> */} </div> */}
</div> </div>
</div> </div>
{/* Editor Selection Dialog */}
<EditorSelectionDialog
isOpen={showEditorDialog}
onClose={() => setShowEditorDialog(false)}
onSelectEditor={(editorType) => openInEditor(editorType)}
/>
</> </>
)} )}
</> </>