Task attempt 2c218bb9-0865-4b9e-8ecb-0d60ed20ca19 - Final changes

This commit is contained in:
Louis Knight-Webb
2025-06-27 21:45:00 +01:00
parent e45374eea0
commit 598ea83313
7 changed files with 137 additions and 7 deletions

View File

@@ -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<string, string> = {
"echo": "Echo (Test Mode)",
"claude": "Claude",
"amp": "Amp",
"gemini": "Gemini"
"gemini": "Gemini",
"opencode": "OpenCode"
};
export const EDITOR_LABELS: Record<string, string> = {

View File

@@ -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),
}
}
}

View File

@@ -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;

View File

@@ -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<AsyncGroupChild, ExecutorError> {
// 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<AsyncGroupChild, ExecutorError> {
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)
}
}

View File

@@ -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<dyn crate::executor::Executor> = 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

View File

@@ -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,

View File

@@ -20,7 +20,7 @@ export type SoundConstants = { sound_files: Array<SoundFile>, 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<ExecutorConfig>, executor_labels: Array<string>, };
@@ -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<string, string> = {
"echo": "Echo (Test Mode)",
"claude": "Claude",
"amp": "Amp",
"gemini": "Gemini"
"gemini": "Gemini",
"opencode": "OpenCode"
};
export const EDITOR_LABELS: Record<string, string> = {