From 598ea83313778bf4a4392ca9acaefd548ff9571e Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Fri, 27 Jun 2025 21:45:00 +0100 Subject: [PATCH] Task attempt 2c218bb9-0865-4b9e-8ecb-0d60ed20ca19 - Final changes --- backend/src/bin/generate_types.rs | 6 +- backend/src/executor.rs | 4 +- backend/src/executors/mod.rs | 2 + backend/src/executors/opencode.rs | 109 +++++++++++++++++++++++++++++ backend/src/models/task_attempt.rs | 14 +++- backend/src/routes/tasks.rs | 1 + shared/types.ts | 8 ++- 7 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 backend/src/executors/opencode.rs diff --git a/backend/src/bin/generate_types.rs b/backend/src/bin/generate_types.rs index 233c310f..a3928799 100644 --- a/backend/src/bin/generate_types.rs +++ b/backend/src/bin/generate_types.rs @@ -8,7 +8,8 @@ export const EXECUTOR_TYPES: string[] = [ "echo", "claude", "amp", - "gemini" + "gemini", + "opencode" ]; export const EDITOR_TYPES: EditorType[] = [ @@ -24,7 +25,8 @@ export const EXECUTOR_LABELS: Record = { "echo": "Echo (Test Mode)", "claude": "Claude", "amp": "Amp", - "gemini": "Gemini" + "gemini": "Gemini", + "opencode": "OpenCode" }; export const EDITOR_LABELS: Record = { diff --git a/backend/src/executor.rs b/backend/src/executor.rs index ecd910fe..ba2f0fb8 100644 --- a/backend/src/executor.rs +++ b/backend/src/executor.rs @@ -4,7 +4,7 @@ use tokio::io::{AsyncBufReadExt, BufReader}; use ts_rs::TS; use uuid::Uuid; -use crate::executors::{AmpExecutor, ClaudeExecutor, EchoExecutor, GeminiExecutor}; +use crate::executors::{AmpExecutor, ClaudeExecutor, EchoExecutor, GeminiExecutor, OpencodeExecutor}; /// Context information for spawn failures to provide comprehensive error details #[derive(Debug, Clone)] @@ -231,6 +231,7 @@ pub enum ExecutorConfig { Claude, Amp, Gemini, + Opencode, // Future executors can be added here // Shell { command: String }, // Docker { image: String, command: String }, @@ -251,6 +252,7 @@ impl ExecutorConfig { ExecutorConfig::Claude => Box::new(ClaudeExecutor), ExecutorConfig::Amp => Box::new(AmpExecutor), ExecutorConfig::Gemini => Box::new(GeminiExecutor), + ExecutorConfig::Opencode => Box::new(OpencodeExecutor), } } } diff --git a/backend/src/executors/mod.rs b/backend/src/executors/mod.rs index fc7a245a..602a9c96 100644 --- a/backend/src/executors/mod.rs +++ b/backend/src/executors/mod.rs @@ -3,6 +3,7 @@ pub mod claude; pub mod dev_server; pub mod echo; pub mod gemini; +pub mod opencode; pub mod setup_script; pub use amp::{AmpExecutor, AmpFollowupExecutor}; @@ -10,4 +11,5 @@ pub use claude::{ClaudeExecutor, ClaudeFollowupExecutor}; pub use dev_server::DevServerExecutor; pub use echo::EchoExecutor; pub use gemini::{GeminiExecutor, GeminiFollowupExecutor}; +pub use opencode::{OpencodeExecutor, OpencodeFollowupExecutor}; pub use setup_script::SetupScriptExecutor; diff --git a/backend/src/executors/opencode.rs b/backend/src/executors/opencode.rs new file mode 100644 index 00000000..dea666a6 --- /dev/null +++ b/backend/src/executors/opencode.rs @@ -0,0 +1,109 @@ +use async_trait::async_trait; +use command_group::{AsyncCommandGroup, AsyncGroupChild}; +use uuid::Uuid; + +use crate::{ + executor::{Executor, ExecutorError}, + models::task::Task, + utils::shell::get_shell_command, +}; + +/// An executor that uses OpenCode to process tasks +pub struct OpencodeExecutor; + +/// An executor that continues an OpenCode thread +pub struct OpencodeFollowupExecutor { + pub session_id: String, + pub prompt: String, +} + +#[async_trait] +impl Executor for OpencodeExecutor { + 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::process::Command; + + let prompt = format!( + "Task title: {}\nTask description: {}", + task.title, + task.description + .as_deref() + .unwrap_or("No description provided") + ); + + // Use shell command for cross-platform compatibility + let (shell_cmd, shell_arg) = get_shell_command(); + let opencode_command = format!("opencode -p \"{}\"", prompt.replace('"', "\\\"")); + + let mut command = Command::new(shell_cmd); + command + .kill_on_drop(true) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(worktree_path) + .arg(shell_arg) + .arg(opencode_command); + + let child = command + .group_spawn() // Create new process group so we can kill entire tree + .map_err(|e| { + crate::executor::SpawnContext::from_command(&command, "OpenCode") + .with_task(task_id, Some(task.title.clone())) + .with_context("OpenCode CLI execution for new task") + .spawn_error(e) + })?; + + Ok(child) + } +} + +#[async_trait] +impl Executor for OpencodeFollowupExecutor { + async fn spawn( + &self, + _pool: &sqlx::SqlitePool, + _task_id: Uuid, + worktree_path: &str, + ) -> Result { + use std::process::Stdio; + + use tokio::process::Command; + + // Use shell command for cross-platform compatibility + let (shell_cmd, shell_arg) = get_shell_command(); + let opencode_command = format!("opencode -p \"{}\"", self.prompt.replace('"', "\\\"")); + + let mut command = Command::new(shell_cmd); + command + .kill_on_drop(true) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .current_dir(worktree_path) + .arg(shell_arg) + .arg(&opencode_command); + + let child = command + .group_spawn() // Create new process group so we can kill entire tree + .map_err(|e| { + crate::executor::SpawnContext::from_command(&command, "OpenCode") + .with_context(format!( + "OpenCode CLI followup execution for session {}", + self.session_id + )) + .spawn_error(e) + })?; + + Ok(child) + } +} diff --git a/backend/src/models/task_attempt.rs b/backend/src/models/task_attempt.rs index d7b31557..26959415 100644 --- a/backend/src/models/task_attempt.rs +++ b/backend/src/models/task_attempt.rs @@ -731,6 +731,7 @@ impl TaskAttempt { crate::executor::ExecutorConfig::Claude => "claude", crate::executor::ExecutorConfig::Amp => "amp", crate::executor::ExecutorConfig::Gemini => "gemini", + crate::executor::ExecutorConfig::Opencode => "opencode", }; ( "executor".to_string(), @@ -744,6 +745,7 @@ impl TaskAttempt { crate::executor::ExecutorConfig::Claude => "claude", crate::executor::ExecutorConfig::Amp => "amp", crate::executor::ExecutorConfig::Gemini => "gemini", + crate::executor::ExecutorConfig::Opencode => "opencode", }; ( "followup_executor".to_string(), @@ -864,7 +866,7 @@ impl TaskAttempt { prompt, } => { use crate::executors::{ - AmpFollowupExecutor, ClaudeFollowupExecutor, GeminiFollowupExecutor, + AmpFollowupExecutor, ClaudeFollowupExecutor, GeminiFollowupExecutor, OpencodeFollowupExecutor, }; let executor: Box = match config { @@ -902,6 +904,16 @@ impl TaskAttempt { // Echo doesn't support followup, use regular echo config.create_executor() } + crate::executor::ExecutorConfig::Opencode => { + if let Some(sid) = session_id { + Box::new(OpencodeFollowupExecutor { + session_id: sid.clone(), + prompt: prompt.clone(), + }) + } else { + return Err(TaskAttemptError::TaskNotFound); // No session ID for followup + } + } }; executor diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index 814ec995..dc2db7b5 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -142,6 +142,7 @@ pub async fn create_task_and_start( crate::executor::ExecutorConfig::Claude => "claude".to_string(), crate::executor::ExecutorConfig::Amp => "amp".to_string(), crate::executor::ExecutorConfig::Gemini => "gemini".to_string(), + crate::executor::ExecutorConfig::Opencode => "opencode".to_string(), }); let attempt_payload = CreateTaskAttempt { executor: executor_string, diff --git a/shared/types.ts b/shared/types.ts index 509686c4..e7cb5ecf 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -20,7 +20,7 @@ export type SoundConstants = { sound_files: Array, sound_labels: Arra export type ConfigConstants = { editor: EditorConstants, sound: SoundConstants, }; -export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" }; +export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" } | { "type": "opencode" }; export type ExecutorConstants = { executor_types: Array, executor_labels: Array, }; @@ -99,7 +99,8 @@ export const EXECUTOR_TYPES: string[] = [ "echo", "claude", "amp", - "gemini" + "gemini", + "opencode" ]; export const EDITOR_TYPES: EditorType[] = [ @@ -115,7 +116,8 @@ export const EXECUTOR_LABELS: Record = { "echo": "Echo (Test Mode)", "claude": "Claude", "amp": "Amp", - "gemini": "Gemini" + "gemini": "Gemini", + "opencode": "OpenCode" }; export const EDITOR_LABELS: Record = {