Gemini support

This commit is contained in:
Louis Knight-Webb
2025-06-25 18:23:50 +01:00
parent 88e52ce501
commit ab55dd2796
10 changed files with 127 additions and 12 deletions

View File

@@ -7,7 +7,8 @@ fn generate_constants() -> String {
export const EXECUTOR_TYPES: string[] = [
"echo",
"claude",
"amp"
"amp",
"gemini"
];
export const EDITOR_TYPES: EditorType[] = [
@@ -22,7 +23,8 @@ export const EDITOR_TYPES: EditorType[] = [
export const EXECUTOR_LABELS: Record<string, string> = {
"echo": "Echo (Test Mode)",
"claude": "Claude",
"amp": "Amp"
"amp": "Amp",
"gemini": "Gemini"
};
export const EDITOR_LABELS: Record<string, string> = {
@@ -97,6 +99,7 @@ fn main() {
vibe_kanban::models::task_attempt::UpdateTaskAttempt::decl(),
vibe_kanban::models::task_attempt::CreateFollowUpAttempt::decl(),
vibe_kanban::models::task_attempt_activity::TaskAttemptActivity::decl(),
vibe_kanban::models::task_attempt_activity::TaskAttemptActivityWithPrompt::decl(),
vibe_kanban::models::task_attempt_activity::CreateTaskAttemptActivity::decl(),
vibe_kanban::routes::filesystem::DirectoryEntry::decl(),
vibe_kanban::models::task_attempt::DiffChunkType::decl(),

View File

@@ -7,7 +7,7 @@ use tokio::{
use ts_rs::TS;
use uuid::Uuid;
use crate::executors::{AmpExecutor, ClaudeExecutor, EchoExecutor};
use crate::executors::{AmpExecutor, ClaudeExecutor, EchoExecutor, GeminiExecutor};
#[derive(Debug)]
pub enum ExecutorError {
@@ -110,6 +110,7 @@ pub enum ExecutorConfig {
Echo,
Claude,
Amp,
Gemini,
// Future executors can be added here
// Shell { command: String },
// Docker { image: String, command: String },
@@ -130,11 +131,13 @@ impl ExecutorConstants {
ExecutorConfig::Echo,
ExecutorConfig::Claude,
ExecutorConfig::Amp,
ExecutorConfig::Gemini,
],
executor_labels: vec![
"Echo (Test Mode)".to_string(),
"Claude".to_string(),
"Amp".to_string(),
"Gemini".to_string(),
],
}
}
@@ -146,6 +149,7 @@ impl ExecutorConfig {
ExecutorConfig::Echo => Box::new(EchoExecutor),
ExecutorConfig::Claude => Box::new(ClaudeExecutor),
ExecutorConfig::Amp => Box::new(AmpExecutor),
ExecutorConfig::Gemini => Box::new(GeminiExecutor),
}
}
}

View File

@@ -0,0 +1,84 @@
use async_trait::async_trait;
use tokio::process::{Child, Command};
use uuid::Uuid;
use crate::{
executor::{Executor, ExecutorError},
models::task::Task,
};
/// An executor that uses Gemini CLI to process tasks
pub struct GeminiExecutor;
/// An executor that resumes a Gemini session
pub struct GeminiFollowupExecutor {
pub session_id: String,
pub prompt: String,
}
#[async_trait]
impl Executor for GeminiExecutor {
async fn spawn(
&self,
pool: &sqlx::SqlitePool,
task_id: Uuid,
worktree_path: &str,
) -> Result<Child, ExecutorError> {
// Get the task to fetch its description
let task = Task::find_by_id(pool, task_id)
.await?
.ok_or(ExecutorError::TaskNotFound)?;
let prompt = format!(
"Task title: {}
Task description: {}",
task.title,
task.description
.as_deref()
.unwrap_or("No description provided")
);
// Use Gemini CLI to process the task
let child = Command::new("npx")
.kill_on_drop(true)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.current_dir(worktree_path)
.arg("@bloopai/gemini-cli-interactive")
.arg("-p")
.arg(&prompt)
.process_group(0) // Create new process group so we can kill entire tree
.spawn()
.map_err(ExecutorError::SpawnFailed)?;
Ok(child)
}
}
#[async_trait]
impl Executor for GeminiFollowupExecutor {
async fn spawn(
&self,
pool: &sqlx::SqlitePool,
task_id: Uuid,
worktree_path: &str,
) -> Result<Child, ExecutorError> {
// Use Gemini CLI with session resumption (if supported)
let child = Command::new("npx")
.kill_on_drop(true)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.current_dir(worktree_path)
.arg("@bloopai/gemini-cli-interactive")
.arg("-p")
.arg(&self.prompt)
.arg(format!("--resume={}", self.session_id))
.process_group(0) // Create new process group so we can kill entire tree
.spawn()
.map_err(ExecutorError::SpawnFailed)?;
Ok(child)
}
}

View File

@@ -2,10 +2,12 @@ pub mod amp;
pub mod claude;
pub mod dev_server;
pub mod echo;
pub mod gemini;
pub mod setup_script;
pub use amp::{AmpExecutor, AmpFollowupExecutor};
pub use claude::{ClaudeExecutor, ClaudeFollowupExecutor};
pub use dev_server::DevServerExecutor;
pub use echo::EchoExecutor;
pub use gemini::{GeminiExecutor, GeminiFollowupExecutor};
pub use setup_script::SetupScriptExecutor;

View File

@@ -583,6 +583,7 @@ impl TaskAttempt {
let executor_config = match most_recent_coding_agent.executor_type.as_deref() {
Some("claude") => crate::executor::ExecutorConfig::Claude,
Some("amp") => crate::executor::ExecutorConfig::Amp,
Some("gemini") => crate::executor::ExecutorConfig::Gemini,
Some("echo") => crate::executor::ExecutorConfig::Echo,
_ => return Err(TaskAttemptError::TaskNotFound), // Invalid executor type
};
@@ -613,6 +614,7 @@ impl TaskAttempt {
match executor_name.as_ref().map(|s| s.as_str()) {
Some("claude") => crate::executor::ExecutorConfig::Claude,
Some("amp") => crate::executor::ExecutorConfig::Amp,
Some("gemini") => crate::executor::ExecutorConfig::Gemini,
_ => crate::executor::ExecutorConfig::Echo, // Default for "echo" or None
}
}
@@ -725,6 +727,7 @@ impl TaskAttempt {
crate::executor::ExecutorConfig::Echo => "echo",
crate::executor::ExecutorConfig::Claude => "claude",
crate::executor::ExecutorConfig::Amp => "amp",
crate::executor::ExecutorConfig::Gemini => "gemini",
};
(
"executor".to_string(),
@@ -737,6 +740,7 @@ impl TaskAttempt {
crate::executor::ExecutorConfig::Echo => "echo",
crate::executor::ExecutorConfig::Claude => "claude",
crate::executor::ExecutorConfig::Amp => "amp",
crate::executor::ExecutorConfig::Gemini => "gemini",
};
(
"followup_executor".to_string(),
@@ -856,7 +860,9 @@ impl TaskAttempt {
session_id,
prompt,
} => {
use crate::executors::{AmpFollowupExecutor, ClaudeFollowupExecutor};
use crate::executors::{
AmpFollowupExecutor, ClaudeFollowupExecutor, GeminiFollowupExecutor,
};
let executor: Box<dyn crate::executor::Executor> = match config {
crate::executor::ExecutorConfig::Claude => {
@@ -879,6 +885,16 @@ impl TaskAttempt {
return Err(TaskAttemptError::TaskNotFound); // No thread ID for followup
}
}
crate::executor::ExecutorConfig::Gemini => {
if let Some(sid) = session_id {
Box::new(GeminiFollowupExecutor {
session_id: sid.clone(),
prompt: prompt.clone(),
})
} else {
return Err(TaskAttemptError::TaskNotFound); // No session ID for followup
}
}
crate::executor::ExecutorConfig::Echo => {
// Echo doesn't support followup, use regular echo
config.create_executor()

View File

@@ -141,6 +141,7 @@ pub async fn create_task_and_start(
crate::executor::ExecutorConfig::Echo => "echo".to_string(),
crate::executor::ExecutorConfig::Claude => "claude".to_string(),
crate::executor::ExecutorConfig::Amp => "amp".to_string(),
crate::executor::ExecutorConfig::Gemini => "gemini".to_string(),
});
let attempt_payload = CreateTaskAttempt {
executor: executor_string,

View File

@@ -99,6 +99,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
<p className="text-sm text-muted-foreground">
{executor.type === 'claude' && 'Claude Code from Anthropic'}
{executor.type === 'amp' && 'From Sourcegraph'}
{executor.type === 'gemini' && 'Google Gemini from Bloop'}
{executor.type === 'echo' &&
'This is just for debugging vibe-kanban itself'}
</p>

View File

@@ -36,12 +36,13 @@ export function ExecutionOutputViewer({
const isAmpExecutor = executor === 'amp';
const isClaudeExecutor = executor === 'claude';
const isGeminiExecutor = executor === 'gemini';
const hasStdout = !!executionProcess.stdout;
const hasStderr = !!executionProcess.stderr;
// Check if stdout looks like JSONL (for Amp or Claude executor)
// Check if stdout looks like JSONL (for Amp, Claude, or Gemini executor)
const { isValidJsonl, jsonlFormat } = useMemo(() => {
if ((!isAmpExecutor && !isClaudeExecutor) || !executionProcess.stdout) {
if ((!isAmpExecutor && !isClaudeExecutor && !isGeminiExecutor) || !executionProcess.stdout) {
return { isValidJsonl: false, jsonlFormat: null };
}
@@ -98,7 +99,7 @@ export function ExecutionOutputViewer({
} catch {
return { isValidJsonl: false, jsonlFormat: null };
}
}, [isAmpExecutor, isClaudeExecutor, executionProcess.stdout]);
}, [isAmpExecutor, isClaudeExecutor, isGeminiExecutor, executionProcess.stdout]);
// Set initial view mode based on JSONL detection
useEffect(() => {

View File

@@ -56,6 +56,7 @@ const availableExecutors = [
{ id: 'echo', name: 'Echo' },
{ id: 'claude', name: 'Claude' },
{ id: 'amp', name: 'Amp' },
{ id: 'gemini', name: 'Gemini' },
];
export function TaskDetailsToolbar({

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" };
export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" };
export type ExecutorConstants = { executor_types: Array<ExecutorConfig>, executor_labels: Array<string>, };
@@ -60,10 +60,10 @@ export type CreateFollowUpAttempt = { prompt: string, };
export type TaskAttemptActivity = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, };
export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, };
export type TaskAttemptActivityWithPrompt = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, prompt: string | null, };
export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, };
export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, };
export type DiffChunkType = "Equal" | "Insert" | "Delete";
@@ -98,7 +98,8 @@ export type UpdateExecutorSession = { session_id: string | null, prompt: string
export const EXECUTOR_TYPES: string[] = [
"echo",
"claude",
"amp"
"amp",
"gemini"
];
export const EDITOR_TYPES: EditorType[] = [
@@ -113,7 +114,8 @@ export const EDITOR_TYPES: EditorType[] = [
export const EXECUTOR_LABELS: Record<string, string> = {
"echo": "Echo (Test Mode)",
"claude": "Claude",
"amp": "Amp"
"amp": "Amp",
"gemini": "Gemini"
};
export const EDITOR_LABELS: Record<string, string> = {