Gemini support
This commit is contained in:
@@ -7,7 +7,8 @@ fn generate_constants() -> String {
|
|||||||
export const EXECUTOR_TYPES: string[] = [
|
export const EXECUTOR_TYPES: string[] = [
|
||||||
"echo",
|
"echo",
|
||||||
"claude",
|
"claude",
|
||||||
"amp"
|
"amp",
|
||||||
|
"gemini"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EDITOR_TYPES: EditorType[] = [
|
export const EDITOR_TYPES: EditorType[] = [
|
||||||
@@ -22,7 +23,8 @@ export const EDITOR_TYPES: EditorType[] = [
|
|||||||
export const EXECUTOR_LABELS: Record<string, string> = {
|
export const EXECUTOR_LABELS: Record<string, string> = {
|
||||||
"echo": "Echo (Test Mode)",
|
"echo": "Echo (Test Mode)",
|
||||||
"claude": "Claude",
|
"claude": "Claude",
|
||||||
"amp": "Amp"
|
"amp": "Amp",
|
||||||
|
"gemini": "Gemini"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EDITOR_LABELS: Record<string, string> = {
|
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::UpdateTaskAttempt::decl(),
|
||||||
vibe_kanban::models::task_attempt::CreateFollowUpAttempt::decl(),
|
vibe_kanban::models::task_attempt::CreateFollowUpAttempt::decl(),
|
||||||
vibe_kanban::models::task_attempt_activity::TaskAttemptActivity::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::models::task_attempt_activity::CreateTaskAttemptActivity::decl(),
|
||||||
vibe_kanban::routes::filesystem::DirectoryEntry::decl(),
|
vibe_kanban::routes::filesystem::DirectoryEntry::decl(),
|
||||||
vibe_kanban::models::task_attempt::DiffChunkType::decl(),
|
vibe_kanban::models::task_attempt::DiffChunkType::decl(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tokio::{
|
|||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::executors::{AmpExecutor, ClaudeExecutor, EchoExecutor};
|
use crate::executors::{AmpExecutor, ClaudeExecutor, EchoExecutor, GeminiExecutor};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ExecutorError {
|
pub enum ExecutorError {
|
||||||
@@ -110,6 +110,7 @@ pub enum ExecutorConfig {
|
|||||||
Echo,
|
Echo,
|
||||||
Claude,
|
Claude,
|
||||||
Amp,
|
Amp,
|
||||||
|
Gemini,
|
||||||
// Future executors can be added here
|
// Future executors can be added here
|
||||||
// Shell { command: String },
|
// Shell { command: String },
|
||||||
// Docker { image: String, command: String },
|
// Docker { image: String, command: String },
|
||||||
@@ -130,11 +131,13 @@ impl ExecutorConstants {
|
|||||||
ExecutorConfig::Echo,
|
ExecutorConfig::Echo,
|
||||||
ExecutorConfig::Claude,
|
ExecutorConfig::Claude,
|
||||||
ExecutorConfig::Amp,
|
ExecutorConfig::Amp,
|
||||||
|
ExecutorConfig::Gemini,
|
||||||
],
|
],
|
||||||
executor_labels: vec![
|
executor_labels: vec![
|
||||||
"Echo (Test Mode)".to_string(),
|
"Echo (Test Mode)".to_string(),
|
||||||
"Claude".to_string(),
|
"Claude".to_string(),
|
||||||
"Amp".to_string(),
|
"Amp".to_string(),
|
||||||
|
"Gemini".to_string(),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,6 +149,7 @@ impl ExecutorConfig {
|
|||||||
ExecutorConfig::Echo => Box::new(EchoExecutor),
|
ExecutorConfig::Echo => Box::new(EchoExecutor),
|
||||||
ExecutorConfig::Claude => Box::new(ClaudeExecutor),
|
ExecutorConfig::Claude => Box::new(ClaudeExecutor),
|
||||||
ExecutorConfig::Amp => Box::new(AmpExecutor),
|
ExecutorConfig::Amp => Box::new(AmpExecutor),
|
||||||
|
ExecutorConfig::Gemini => Box::new(GeminiExecutor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
84
backend/src/executors/gemini.rs
Normal file
84
backend/src/executors/gemini.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ pub mod amp;
|
|||||||
pub mod claude;
|
pub mod claude;
|
||||||
pub mod dev_server;
|
pub mod dev_server;
|
||||||
pub mod echo;
|
pub mod echo;
|
||||||
|
pub mod gemini;
|
||||||
pub mod setup_script;
|
pub mod setup_script;
|
||||||
|
|
||||||
pub use amp::{AmpExecutor, AmpFollowupExecutor};
|
pub use amp::{AmpExecutor, AmpFollowupExecutor};
|
||||||
pub use claude::{ClaudeExecutor, ClaudeFollowupExecutor};
|
pub use claude::{ClaudeExecutor, ClaudeFollowupExecutor};
|
||||||
pub use dev_server::DevServerExecutor;
|
pub use dev_server::DevServerExecutor;
|
||||||
pub use echo::EchoExecutor;
|
pub use echo::EchoExecutor;
|
||||||
|
pub use gemini::{GeminiExecutor, GeminiFollowupExecutor};
|
||||||
pub use setup_script::SetupScriptExecutor;
|
pub use setup_script::SetupScriptExecutor;
|
||||||
|
|||||||
@@ -583,6 +583,7 @@ impl TaskAttempt {
|
|||||||
let executor_config = match most_recent_coding_agent.executor_type.as_deref() {
|
let executor_config = match most_recent_coding_agent.executor_type.as_deref() {
|
||||||
Some("claude") => crate::executor::ExecutorConfig::Claude,
|
Some("claude") => crate::executor::ExecutorConfig::Claude,
|
||||||
Some("amp") => crate::executor::ExecutorConfig::Amp,
|
Some("amp") => crate::executor::ExecutorConfig::Amp,
|
||||||
|
Some("gemini") => crate::executor::ExecutorConfig::Gemini,
|
||||||
Some("echo") => crate::executor::ExecutorConfig::Echo,
|
Some("echo") => crate::executor::ExecutorConfig::Echo,
|
||||||
_ => return Err(TaskAttemptError::TaskNotFound), // Invalid executor type
|
_ => return Err(TaskAttemptError::TaskNotFound), // Invalid executor type
|
||||||
};
|
};
|
||||||
@@ -613,6 +614,7 @@ impl TaskAttempt {
|
|||||||
match executor_name.as_ref().map(|s| s.as_str()) {
|
match executor_name.as_ref().map(|s| s.as_str()) {
|
||||||
Some("claude") => crate::executor::ExecutorConfig::Claude,
|
Some("claude") => crate::executor::ExecutorConfig::Claude,
|
||||||
Some("amp") => crate::executor::ExecutorConfig::Amp,
|
Some("amp") => crate::executor::ExecutorConfig::Amp,
|
||||||
|
Some("gemini") => crate::executor::ExecutorConfig::Gemini,
|
||||||
_ => crate::executor::ExecutorConfig::Echo, // Default for "echo" or None
|
_ => crate::executor::ExecutorConfig::Echo, // Default for "echo" or None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,6 +727,7 @@ impl TaskAttempt {
|
|||||||
crate::executor::ExecutorConfig::Echo => "echo",
|
crate::executor::ExecutorConfig::Echo => "echo",
|
||||||
crate::executor::ExecutorConfig::Claude => "claude",
|
crate::executor::ExecutorConfig::Claude => "claude",
|
||||||
crate::executor::ExecutorConfig::Amp => "amp",
|
crate::executor::ExecutorConfig::Amp => "amp",
|
||||||
|
crate::executor::ExecutorConfig::Gemini => "gemini",
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
"executor".to_string(),
|
"executor".to_string(),
|
||||||
@@ -737,6 +740,7 @@ impl TaskAttempt {
|
|||||||
crate::executor::ExecutorConfig::Echo => "echo",
|
crate::executor::ExecutorConfig::Echo => "echo",
|
||||||
crate::executor::ExecutorConfig::Claude => "claude",
|
crate::executor::ExecutorConfig::Claude => "claude",
|
||||||
crate::executor::ExecutorConfig::Amp => "amp",
|
crate::executor::ExecutorConfig::Amp => "amp",
|
||||||
|
crate::executor::ExecutorConfig::Gemini => "gemini",
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
"followup_executor".to_string(),
|
"followup_executor".to_string(),
|
||||||
@@ -856,7 +860,9 @@ impl TaskAttempt {
|
|||||||
session_id,
|
session_id,
|
||||||
prompt,
|
prompt,
|
||||||
} => {
|
} => {
|
||||||
use crate::executors::{AmpFollowupExecutor, ClaudeFollowupExecutor};
|
use crate::executors::{
|
||||||
|
AmpFollowupExecutor, ClaudeFollowupExecutor, GeminiFollowupExecutor,
|
||||||
|
};
|
||||||
|
|
||||||
let executor: Box<dyn crate::executor::Executor> = match config {
|
let executor: Box<dyn crate::executor::Executor> = match config {
|
||||||
crate::executor::ExecutorConfig::Claude => {
|
crate::executor::ExecutorConfig::Claude => {
|
||||||
@@ -879,6 +885,16 @@ impl TaskAttempt {
|
|||||||
return Err(TaskAttemptError::TaskNotFound); // No thread ID for followup
|
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 => {
|
crate::executor::ExecutorConfig::Echo => {
|
||||||
// Echo doesn't support followup, use regular echo
|
// Echo doesn't support followup, use regular echo
|
||||||
config.create_executor()
|
config.create_executor()
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ pub async fn create_task_and_start(
|
|||||||
crate::executor::ExecutorConfig::Echo => "echo".to_string(),
|
crate::executor::ExecutorConfig::Echo => "echo".to_string(),
|
||||||
crate::executor::ExecutorConfig::Claude => "claude".to_string(),
|
crate::executor::ExecutorConfig::Claude => "claude".to_string(),
|
||||||
crate::executor::ExecutorConfig::Amp => "amp".to_string(),
|
crate::executor::ExecutorConfig::Amp => "amp".to_string(),
|
||||||
|
crate::executor::ExecutorConfig::Gemini => "gemini".to_string(),
|
||||||
});
|
});
|
||||||
let attempt_payload = CreateTaskAttempt {
|
let attempt_payload = CreateTaskAttempt {
|
||||||
executor: executor_string,
|
executor: executor_string,
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{executor.type === 'claude' && 'Claude Code from Anthropic'}
|
{executor.type === 'claude' && 'Claude Code from Anthropic'}
|
||||||
{executor.type === 'amp' && 'From Sourcegraph'}
|
{executor.type === 'amp' && 'From Sourcegraph'}
|
||||||
|
{executor.type === 'gemini' && 'Google Gemini from Bloop'}
|
||||||
{executor.type === 'echo' &&
|
{executor.type === 'echo' &&
|
||||||
'This is just for debugging vibe-kanban itself'}
|
'This is just for debugging vibe-kanban itself'}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -36,12 +36,13 @@ export function ExecutionOutputViewer({
|
|||||||
|
|
||||||
const isAmpExecutor = executor === 'amp';
|
const isAmpExecutor = executor === 'amp';
|
||||||
const isClaudeExecutor = executor === 'claude';
|
const isClaudeExecutor = executor === 'claude';
|
||||||
|
const isGeminiExecutor = executor === 'gemini';
|
||||||
const hasStdout = !!executionProcess.stdout;
|
const hasStdout = !!executionProcess.stdout;
|
||||||
const hasStderr = !!executionProcess.stderr;
|
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(() => {
|
const { isValidJsonl, jsonlFormat } = useMemo(() => {
|
||||||
if ((!isAmpExecutor && !isClaudeExecutor) || !executionProcess.stdout) {
|
if ((!isAmpExecutor && !isClaudeExecutor && !isGeminiExecutor) || !executionProcess.stdout) {
|
||||||
return { isValidJsonl: false, jsonlFormat: null };
|
return { isValidJsonl: false, jsonlFormat: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ export function ExecutionOutputViewer({
|
|||||||
} catch {
|
} catch {
|
||||||
return { isValidJsonl: false, jsonlFormat: null };
|
return { isValidJsonl: false, jsonlFormat: null };
|
||||||
}
|
}
|
||||||
}, [isAmpExecutor, isClaudeExecutor, executionProcess.stdout]);
|
}, [isAmpExecutor, isClaudeExecutor, isGeminiExecutor, executionProcess.stdout]);
|
||||||
|
|
||||||
// Set initial view mode based on JSONL detection
|
// Set initial view mode based on JSONL detection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const availableExecutors = [
|
|||||||
{ id: 'echo', name: 'Echo' },
|
{ id: 'echo', name: 'Echo' },
|
||||||
{ id: 'claude', name: 'Claude' },
|
{ id: 'claude', name: 'Claude' },
|
||||||
{ id: 'amp', name: 'Amp' },
|
{ id: 'amp', name: 'Amp' },
|
||||||
|
{ id: 'gemini', name: 'Gemini' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function TaskDetailsToolbar({
|
export function TaskDetailsToolbar({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export type SoundConstants = { sound_files: Array<SoundFile>, sound_labels: Arra
|
|||||||
|
|
||||||
export type ConfigConstants = { editor: EditorConstants, sound: SoundConstants, };
|
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>, };
|
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 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 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 DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, };
|
||||||
|
|
||||||
export type DiffChunkType = "Equal" | "Insert" | "Delete";
|
export type DiffChunkType = "Equal" | "Insert" | "Delete";
|
||||||
@@ -98,7 +98,8 @@ export type UpdateExecutorSession = { session_id: string | null, prompt: string
|
|||||||
export const EXECUTOR_TYPES: string[] = [
|
export const EXECUTOR_TYPES: string[] = [
|
||||||
"echo",
|
"echo",
|
||||||
"claude",
|
"claude",
|
||||||
"amp"
|
"amp",
|
||||||
|
"gemini"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EDITOR_TYPES: EditorType[] = [
|
export const EDITOR_TYPES: EditorType[] = [
|
||||||
@@ -113,7 +114,8 @@ export const EDITOR_TYPES: EditorType[] = [
|
|||||||
export const EXECUTOR_LABELS: Record<string, string> = {
|
export const EXECUTOR_LABELS: Record<string, string> = {
|
||||||
"echo": "Echo (Test Mode)",
|
"echo": "Echo (Test Mode)",
|
||||||
"claude": "Claude",
|
"claude": "Claude",
|
||||||
"amp": "Amp"
|
"amp": "Amp",
|
||||||
|
"gemini": "Gemini"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EDITOR_LABELS: Record<string, string> = {
|
export const EDITOR_LABELS: Record<string, string> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user