Task attempt 55cc3eff-6099-484a-b9c1-899d2a3528ce - Final changes

This commit is contained in:
Louis Knight-Webb
2025-06-20 19:25:18 +01:00
parent 5a9583b6b9
commit 99ab67cb65
5 changed files with 173 additions and 37 deletions

View File

@@ -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. 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 // 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::ApiResponse::<()>::decl(),
vibe_kanban::models::config::Config::decl(), vibe_kanban::models::config::Config::decl(),
vibe_kanban::models::config::ThemeMode::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::executor::ExecutorConfig::decl(),
vibe_kanban::models::project::CreateProject::decl(), vibe_kanban::models::project::CreateProject::decl(),
vibe_kanban::models::project::Project::decl(), vibe_kanban::models::project::Project::decl(),

View File

@@ -10,6 +10,7 @@ pub struct Config {
pub executor: ExecutorConfig, pub executor: ExecutorConfig,
pub disclaimer_acknowledged: bool, pub disclaimer_acknowledged: bool,
pub sound_alerts: bool, pub sound_alerts: bool,
pub editor: EditorConfig,
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
@@ -21,6 +22,25 @@ pub enum ThemeMode {
System, System,
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct EditorConfig {
pub editor_type: EditorType,
pub custom_command: Option<String>,
}
#[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 { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -28,6 +48,35 @@ impl Default for Config {
executor: ExecutorConfig::Claude, executor: ExecutorConfig::Claude,
disclaimer_acknowledged: false, disclaimer_acknowledged: false,
sound_alerts: true, 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<String> {
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
}
}
} }
} }
} }

View File

@@ -6,6 +6,7 @@ use axum::{
Json, Router, Json, Router,
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::sync::{Arc, Mutex};
use uuid::Uuid; use uuid::Uuid;
use crate::models::{ use crate::models::{
@@ -452,6 +453,7 @@ pub async fn merge_task_attempt(
pub async fn open_task_attempt_in_editor( pub async fn open_task_attempt_in_editor(
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
Extension(pool): Extension<SqlitePool>, Extension(pool): Extension<SqlitePool>,
Extension(config): Extension<Arc<Mutex<crate::models::config::Config>>>,
) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> { ) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
// Verify task attempt exists and belongs to the correct task // Verify task attempt exists and belongs to the correct task
match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { 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 // Get editor command from config
match std::process::Command::new("code") let editor_command = {
.arg(&attempt.worktree_path) let config_guard = config.lock().unwrap();
.spawn() 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(_) => { Ok(_) => {
tracing::info!( tracing::info!(
"Opened VSCode for task attempt {} at path: {}", "Opened editor ({}) for task attempt {} at path: {}",
editor_command.join(" "),
attempt_id, attempt_id,
attempt.worktree_path attempt.worktree_path
); );
Ok(ResponseJson(ApiResponse { Ok(ResponseJson(ApiResponse {
success: true, success: true,
data: None, data: None,
message: Some("VSCode opened successfully".to_string()), message: Some("Editor opened successfully".to_string()),
})) }))
} }
Err(e) => { 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 { Ok(ResponseJson(ApiResponse {
success: false, success: false,
data: None, data: None,
message: Some(format!("Failed to open VSCode: {}", e)), message: Some(format!("Failed to open editor: {}", e)),
})) }))
} }
} }

View File

@@ -5,8 +5,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Loader2 } from "lucide-react"; 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"; import { useTheme } from "@/components/theme-provider";
export function Settings() { export function Settings() {
@@ -191,6 +192,65 @@ export function Settings() {
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Editor</CardTitle>
<CardDescription>
Configure which editor to open when viewing task attempts.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="editor">Preferred Editor</Label>
<Select
value={config.editor.editor_type}
onValueChange={(value: EditorType) => updateConfig({
editor: {
...config.editor,
editor_type: value,
custom_command: value === "custom" ? config.editor.custom_command : null
}
})}
>
<SelectTrigger id="editor">
<SelectValue placeholder="Select editor" />
</SelectTrigger>
<SelectContent>
<SelectItem value="vscode">VS Code</SelectItem>
<SelectItem value="cursor">Cursor</SelectItem>
<SelectItem value="windsurf">Windsurf</SelectItem>
<SelectItem value="intellij">IntelliJ IDEA</SelectItem>
<SelectItem value="zed">Zed</SelectItem>
<SelectItem value="custom">Custom Command</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose your preferred code editor for opening task attempts.
</p>
</div>
{config.editor.editor_type === "custom" && (
<div className="space-y-2">
<Label htmlFor="custom-command">Custom Command</Label>
<Input
id="custom-command"
placeholder="e.g., code, subl, vim"
value={config.editor.custom_command || ""}
onChange={(e) => updateConfig({
editor: {
...config.editor,
custom_command: e.target.value || null
}
})}
/>
<p className="text-sm text-muted-foreground">
Enter the command to run your custom editor. Use spaces for arguments (e.g., "code --wait").
</p>
</div>
)}
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Notifications</CardTitle> <CardTitle>Notifications</CardTitle>

View File

@@ -3,10 +3,14 @@
export type ApiResponse<T> = { success: boolean, data: T | null, message: string | null, }; export type ApiResponse<T> = { 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 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 ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" };
export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, }; export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, };