diff --git a/backend/src/bin/generate_types.rs b/backend/src/bin/generate_types.rs index ecafa4ea..dd359e8e 100644 --- a/backend/src/bin/generate_types.rs +++ b/backend/src/bin/generate_types.rs @@ -20,60 +20,66 @@ fn main() { r#"// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // Auto-generated from Rust backend types using ts-rs -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {} +{} -export {}"#, +{} + +{} + +{}"#, vibe_kanban::models::ApiResponse::<()>::decl(), vibe_kanban::models::config::Config::decl(), vibe_kanban::models::config::ThemeMode::decl(), + vibe_kanban::models::config::EditorConfig::decl(), + vibe_kanban::models::config::EditorType::decl(), vibe_kanban::executor::ExecutorConfig::decl(), vibe_kanban::models::project::CreateProject::decl(), vibe_kanban::models::project::Project::decl(), diff --git a/backend/src/models/config.rs b/backend/src/models/config.rs index d421d57a..daf35449 100644 --- a/backend/src/models/config.rs +++ b/backend/src/models/config.rs @@ -10,6 +10,7 @@ pub struct Config { pub executor: ExecutorConfig, pub disclaimer_acknowledged: bool, pub sound_alerts: bool, + pub editor: EditorConfig, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] @@ -21,6 +22,25 @@ pub enum ThemeMode { System, } +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct EditorConfig { + pub editor_type: EditorType, + pub custom_command: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "lowercase")] +pub enum EditorType { + VSCode, + Cursor, + Windsurf, + IntelliJ, + Zed, + Custom, +} + impl Default for Config { fn default() -> Self { Self { @@ -28,6 +48,35 @@ impl Default for Config { executor: ExecutorConfig::Claude, disclaimer_acknowledged: false, sound_alerts: true, + editor: EditorConfig::default(), + } + } +} + +impl Default for EditorConfig { + fn default() -> Self { + Self { + editor_type: EditorType::VSCode, + custom_command: None, + } + } +} + +impl EditorConfig { + pub fn get_command(&self) -> Vec { + match &self.editor_type { + EditorType::VSCode => vec!["code".to_string()], + EditorType::Cursor => vec!["cursor".to_string()], + EditorType::Windsurf => vec!["windsurf".to_string()], + EditorType::IntelliJ => vec!["idea".to_string()], + EditorType::Zed => vec!["zed".to_string()], + EditorType::Custom => { + if let Some(custom) = &self.custom_command { + custom.split_whitespace().map(|s| s.to_string()).collect() + } else { + vec!["code".to_string()] // fallback to VSCode + } + } } } } diff --git a/backend/src/routes/tasks.rs b/backend/src/routes/tasks.rs index e0e95f31..f592fb92 100644 --- a/backend/src/routes/tasks.rs +++ b/backend/src/routes/tasks.rs @@ -6,6 +6,7 @@ use axum::{ Json, Router, }; use sqlx::SqlitePool; +use std::sync::{Arc, Mutex}; use uuid::Uuid; use crate::models::{ @@ -452,6 +453,7 @@ pub async fn merge_task_attempt( pub async fn open_task_attempt_in_editor( Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Extension(pool): Extension, + Extension(config): Extension>>, ) -> Result>, StatusCode> { // Verify task attempt exists and belongs to the correct task match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { @@ -473,29 +475,44 @@ pub async fn open_task_attempt_in_editor( } }; - // Open VSCode in the worktree directory - match std::process::Command::new("code") - .arg(&attempt.worktree_path) - .spawn() - { + // Get editor command from config + let editor_command = { + let config_guard = config.lock().unwrap(); + config_guard.editor.get_command() + }; + + // Open editor in the worktree directory + let mut cmd = std::process::Command::new(&editor_command[0]); + for arg in &editor_command[1..] { + cmd.arg(arg); + } + cmd.arg(&attempt.worktree_path); + + match cmd.spawn() { Ok(_) => { tracing::info!( - "Opened VSCode for task attempt {} at path: {}", + "Opened editor ({}) for task attempt {} at path: {}", + editor_command.join(" "), attempt_id, attempt.worktree_path ); Ok(ResponseJson(ApiResponse { success: true, data: None, - message: Some("VSCode opened successfully".to_string()), + message: Some("Editor opened successfully".to_string()), })) } Err(e) => { - tracing::error!("Failed to open VSCode for attempt {}: {}", attempt_id, e); + tracing::error!( + "Failed to open editor ({}) for attempt {}: {}", + editor_command.join(" "), + attempt_id, + e + ); Ok(ResponseJson(ApiResponse { success: false, data: None, - message: Some(format!("Failed to open VSCode: {}", e)), + message: Some(format!("Failed to open editor: {}", e)), })) } } diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 7c6d9a71..65e326c8 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -5,8 +5,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Label } from "@/components/ui/label"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; import { Loader2 } from "lucide-react"; -import type { Config, ThemeMode, ApiResponse } from "shared/types"; +import type { Config, ThemeMode, EditorType, ApiResponse } from "shared/types"; import { useTheme } from "@/components/theme-provider"; export function Settings() { @@ -191,6 +192,65 @@ export function Settings() { + + + Editor + + Configure which editor to open when viewing task attempts. + + + +
+ + +

+ Choose your preferred code editor for opening task attempts. +

+
+ + {config.editor.editor_type === "custom" && ( +
+ + updateConfig({ + editor: { + ...config.editor, + custom_command: e.target.value || null + } + })} + /> +

+ Enter the command to run your custom editor. Use spaces for arguments (e.g., "code --wait"). +

+
+ )} +
+
+ Notifications diff --git a/shared/types.ts b/shared/types.ts index 3de50ba7..506f6123 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -3,10 +3,14 @@ export type ApiResponse = { success: boolean, data: T | null, message: string | null, }; -export type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, sound_alerts: boolean, }; +export type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, sound_alerts: boolean, editor: EditorConfig, }; export type ThemeMode = "light" | "dark" | "system"; +export type EditorConfig = { editor_type: EditorType, custom_command: string | null, }; + +export type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom"; + export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" }; export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, };