diff --git a/backend/src/executor.rs b/backend/src/executor.rs index f784d589..80d443db 100644 --- a/backend/src/executor.rs +++ b/backend/src/executor.rs @@ -5,7 +5,7 @@ use tokio::process::Child; use ts_rs::TS; use uuid::Uuid; -use crate::executors::{ClaudeExecutor, EchoExecutor}; +use crate::executors::{AmpExecutor, ClaudeExecutor, EchoExecutor}; #[derive(Debug)] pub enum ExecutorError { @@ -146,6 +146,7 @@ pub trait Executor: Send + Sync { pub enum ExecutorConfig { Echo, Claude, + Amp, // Future executors can be added here // Shell { command: String }, // Docker { image: String, command: String }, @@ -156,13 +157,7 @@ impl ExecutorConfig { match self { ExecutorConfig::Echo => Box::new(EchoExecutor), ExecutorConfig::Claude => Box::new(ClaudeExecutor), - } - } - - pub fn executor_type(&self) -> &'static str { - match self { - ExecutorConfig::Echo => "echo", - ExecutorConfig::Claude => "claude", + ExecutorConfig::Amp => Box::new(AmpExecutor), } } } diff --git a/backend/src/executors/amp.rs b/backend/src/executors/amp.rs new file mode 100644 index 00000000..53bad420 --- /dev/null +++ b/backend/src/executors/amp.rs @@ -0,0 +1,62 @@ +use async_trait::async_trait; +use tokio::process::{Child, Command}; +use uuid::Uuid; + +use crate::executor::{Executor, ExecutorError}; +use crate::models::task::Task; + +/// An executor that uses Claude CLI to process tasks +pub struct AmpExecutor; + +#[async_trait] +impl Executor for AmpExecutor { + fn executor_type(&self) -> &'static str { + "amp" + } + + async fn spawn( + &self, + pool: &sqlx::SqlitePool, + task_id: Uuid, + worktree_path: &str, + ) -> Result { + // Get the task to fetch its description + let task = Task::find_by_id(pool, task_id) + .await? + .ok_or(ExecutorError::TaskNotFound)?; + + use std::process::Stdio; + use tokio::{io::AsyncWriteExt, process::Command}; + + let prompt = format!( + "Task title: {}\nTask description: {}", + task.title, + task.description + .as_deref() + .unwrap_or("No description provided") + ); + + let mut child = Command::new("npx") + .kill_on_drop(true) + .stdin(Stdio::piped()) // <-- open a pipe + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(worktree_path) + .arg("@sourcegraph/amp") + .arg("--format=jsonl") + .spawn() + .map_err(ExecutorError::SpawnFailed)?; + + // feed the prompt in, then close the pipe so `amp` sees EOF + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(prompt.as_bytes()).await.unwrap(); + stdin.shutdown().await.unwrap(); // or `drop(stdin);` + } + + Ok(child) + } + + fn description(&self) -> &'static str { + "Executes tasks using Claude CLI for AI-powered code assistance" + } +} diff --git a/backend/src/executors/claude.rs b/backend/src/executors/claude.rs index ae424747..a65e0d98 100644 --- a/backend/src/executors/claude.rs +++ b/backend/src/executors/claude.rs @@ -27,7 +27,7 @@ impl Executor for ClaudeExecutor { let prompt = format!( "Task title: {} - Task description:{}", + Task description: {}", task.title, task.description .as_deref() diff --git a/backend/src/executors/mod.rs b/backend/src/executors/mod.rs index 1f4e0a2a..aa3b1183 100644 --- a/backend/src/executors/mod.rs +++ b/backend/src/executors/mod.rs @@ -1,5 +1,7 @@ +pub mod amp; pub mod claude; pub mod echo; +pub use amp::AmpExecutor; pub use claude::ClaudeExecutor; pub use echo::EchoExecutor; diff --git a/backend/src/models/task_attempt.rs b/backend/src/models/task_attempt.rs index eddc18d7..9056f2ab 100644 --- a/backend/src/models/task_attempt.rs +++ b/backend/src/models/task_attempt.rs @@ -225,6 +225,7 @@ impl TaskAttempt { match executor_name.as_str() { "echo" => ExecutorConfig::Echo.create_executor(), "claude" => ExecutorConfig::Claude.create_executor(), + "amp" => ExecutorConfig::Amp.create_executor(), _ => ExecutorConfig::Echo.create_executor(), // Default fallback } } else { diff --git a/frontend/src/components/tasks/TaskDetailsDialog.tsx b/frontend/src/components/tasks/TaskDetailsDialog.tsx index a71c58f5..d0dd9729 100644 --- a/frontend/src/components/tasks/TaskDetailsDialog.tsx +++ b/frontend/src/components/tasks/TaskDetailsDialog.tsx @@ -85,7 +85,9 @@ export function TaskDetailsDialog({ const [savingTask, setSavingTask] = useState(false); // Check if the selected attempt is currently running (latest activity is "inprogress") - const isAttemptRunning = selectedAttempt && attemptActivities.length > 0 && + const isAttemptRunning = + selectedAttempt && + attemptActivities.length > 0 && attemptActivities[0].status === "inprogress"; useEffect(() => { @@ -94,7 +96,7 @@ export function TaskDetailsDialog({ setSelectedAttempt(null); setAttemptActivities([]); setActivitiesLoading(false); - + fetchTaskAttempts(task.id); // Initialize edit state with current task values setEditedTitle(task.title); @@ -572,15 +574,18 @@ export function TaskDetailsDialog({