Merge task attempt 55cc3eff-6099-484a-b9c1-899d2a3528ce into main
This commit is contained in:
@@ -1,79 +1,23 @@
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use ts_rs::TS;
|
||||
|
||||
// Import all the types we want to export using the library crate
|
||||
use std::{env, fs, path::Path};
|
||||
use ts_rs::TS; // make sure this is in [build-dependencies]
|
||||
|
||||
fn main() {
|
||||
// Where the combined types.ts will live
|
||||
let shared_path = Path::new("../shared");
|
||||
fs::create_dir_all(shared_path).expect("cannot create ../shared");
|
||||
|
||||
// Create the shared directory if it doesn't exist
|
||||
std::fs::create_dir_all(shared_path).unwrap();
|
||||
println!("Generating TypeScript types…");
|
||||
|
||||
println!("Generating TypeScript types...");
|
||||
|
||||
// Set environment variable to configure ts-rs output directory
|
||||
// Tell ts-rs where to drop its per-type files (we’ll still roll our own big one)
|
||||
env::set_var("TS_RS_EXPORT_DIR", shared_path.to_str().unwrap());
|
||||
|
||||
// Generate consolidated types.ts file
|
||||
let consolidated_content = format!(
|
||||
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 {}"#,
|
||||
// Collect every declaration at *runtime* (so no const-eval issues)
|
||||
let decls = [
|
||||
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(),
|
||||
@@ -97,9 +41,18 @@ export {}"#,
|
||||
vibe_kanban::models::task_attempt::FileDiff::decl(),
|
||||
vibe_kanban::models::task_attempt::WorktreeDiff::decl(),
|
||||
vibe_kanban::models::task_attempt::BranchStatus::decl(),
|
||||
);
|
||||
];
|
||||
|
||||
std::fs::write(shared_path.join("types.ts"), consolidated_content).unwrap();
|
||||
// Header banner
|
||||
const HEADER: &str =
|
||||
"// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs).\n\
|
||||
// Do not edit this file manually.\n\
|
||||
// Auto-generated from Rust backend types using ts-rs\n\n";
|
||||
|
||||
println!("TypeScript types generated successfully in ../shared/");
|
||||
// Smash it all together
|
||||
let consolidated = format!("{HEADER}{}", decls.join("\n\n"));
|
||||
|
||||
fs::write(shared_path.join("types.ts"), consolidated).expect("unable to write types.ts");
|
||||
|
||||
println!("✅ TypeScript types generated in ../shared/");
|
||||
}
|
||||
|
||||
@@ -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<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 {
|
||||
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<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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,8 +369,7 @@ impl TaskAttempt {
|
||||
let main_commit = main_repo.find_commit(main_branch_commit.id())?;
|
||||
let worktree_commit = main_repo.find_commit(worktree_branch_commit.id())?;
|
||||
|
||||
let merge_commit_message =
|
||||
format!("Merge task: {} into {}", task_title, main_branch);
|
||||
let merge_commit_message = format!("Merge task: {} into {}", task_title, main_branch);
|
||||
let merge_commit_id = main_repo.commit(
|
||||
Some(&main_branch_ref),
|
||||
&signature,
|
||||
@@ -784,15 +783,20 @@ impl TaskAttempt {
|
||||
// Look for the next sequence of matching lines (at least 2 consecutive matches)
|
||||
for search_old in (old_idx + 1)..old_lines.len() {
|
||||
for search_new in (new_idx + 1)..new_lines.len() {
|
||||
if search_old < old_lines.len() - 1 && search_new < new_lines.len() - 1 {
|
||||
if search_old < old_lines.len() - 1
|
||||
&& search_new < new_lines.len() - 1
|
||||
{
|
||||
// Check for at least 2 consecutive matching lines to confirm this is not a coincidental match
|
||||
if old_lines[search_old] == new_lines[search_new]
|
||||
&& old_lines[search_old + 1] == new_lines[search_new + 1] {
|
||||
if old_lines[search_old] == new_lines[search_new]
|
||||
&& old_lines[search_old + 1] == new_lines[search_new + 1]
|
||||
{
|
||||
next_match_old = search_old;
|
||||
next_match_new = search_new;
|
||||
break;
|
||||
}
|
||||
} else if search_old == old_lines.len() - 1 && search_new == new_lines.len() - 1 {
|
||||
} else if search_old == old_lines.len() - 1
|
||||
&& search_new == new_lines.len() - 1
|
||||
{
|
||||
// Last lines match
|
||||
if old_lines[search_old] == new_lines[search_new] {
|
||||
next_match_old = search_old;
|
||||
|
||||
@@ -6,6 +6,8 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{
|
||||
@@ -452,6 +454,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<SqlitePool>,
|
||||
Extension(config): Extension<Arc<RwLock<crate::models::config::Config>>>,
|
||||
) -> Result<ResponseJson<ApiResponse<()>>, 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 +476,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.read().await;
|
||||
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)),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -582,7 +600,12 @@ pub async fn delete_task_attempt_file(
|
||||
message: Some(format!("File '{}' deleted successfully", query.file_path)),
|
||||
})),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to delete file '{}' from task attempt {}: {}", query.file_path, attempt_id, e);
|
||||
tracing::error!(
|
||||
"Failed to delete file '{}' from task attempt {}: {}",
|
||||
query.file_path,
|
||||
attempt_id,
|
||||
e
|
||||
);
|
||||
Ok(ResponseJson(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
|
||||
@@ -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() {
|
||||
</CardContent>
|
||||
</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>
|
||||
<CardHeader>
|
||||
<CardTitle>Notifications</CardTitle>
|
||||
|
||||
@@ -1,54 +1,59 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
// 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 type ApiResponse<T> = { success: boolean, data: T | null, message: string | null, };
|
||||
type ApiResponse<T> = { success: boolean, data: T | null, message: string | null, };
|
||||
|
||||
export type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, sound_alerts: boolean, };
|
||||
type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, sound_alerts: boolean, editor: EditorConfig, };
|
||||
|
||||
export type ThemeMode = "light" | "dark" | "system";
|
||||
type ThemeMode = "light" | "dark" | "system";
|
||||
|
||||
export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" };
|
||||
type EditorConfig = { editor_type: EditorType, custom_command: string | null, };
|
||||
|
||||
export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, };
|
||||
type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom";
|
||||
|
||||
export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, created_at: Date, updated_at: Date, };
|
||||
type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" };
|
||||
|
||||
export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, };
|
||||
type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, };
|
||||
|
||||
export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, };
|
||||
type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, created_at: Date, updated_at: Date, };
|
||||
|
||||
export type SearchMatchType = "FileName" | "DirectoryName" | "FullPath";
|
||||
type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, };
|
||||
|
||||
export type CreateTask = { project_id: string, title: string, description: string | null, };
|
||||
type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, };
|
||||
|
||||
export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelled";
|
||||
type SearchMatchType = "FileName" | "DirectoryName" | "FullPath";
|
||||
|
||||
export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, };
|
||||
type CreateTask = { project_id: string, title: string, description: string | null, };
|
||||
|
||||
export type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, has_in_progress_attempt: boolean, has_merged_attempt: boolean, };
|
||||
type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelled";
|
||||
|
||||
export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, };
|
||||
type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, };
|
||||
|
||||
export type TaskAttemptStatus = "init" | "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed" | "paused";
|
||||
type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, has_in_progress_attempt: boolean, has_merged_attempt: boolean, };
|
||||
|
||||
export type TaskAttempt = { id: string, task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, stdout: string | null, stderr: string | null, created_at: string, updated_at: string, };
|
||||
type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, };
|
||||
|
||||
export type CreateTaskAttempt = { task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, };
|
||||
type TaskAttemptStatus = "init" | "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed" | "paused";
|
||||
|
||||
export type UpdateTaskAttempt = { worktree_path: string | null, merge_commit: string | null, };
|
||||
type TaskAttempt = { id: string, task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, stdout: string | null, stderr: string | null, created_at: string, updated_at: string, };
|
||||
|
||||
export type TaskAttemptActivity = { id: string, task_attempt_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, };
|
||||
type CreateTaskAttempt = { task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, };
|
||||
|
||||
export type CreateTaskAttemptActivity = { task_attempt_id: string, status: TaskAttemptStatus | null, note: string | null, };
|
||||
type UpdateTaskAttempt = { worktree_path: string | null, merge_commit: string | null, };
|
||||
|
||||
export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, };
|
||||
type TaskAttemptActivity = { id: string, task_attempt_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, };
|
||||
|
||||
export type DiffChunkType = "Equal" | "Insert" | "Delete";
|
||||
type CreateTaskAttemptActivity = { task_attempt_id: string, status: TaskAttemptStatus | null, note: string | null, };
|
||||
|
||||
export type DiffChunk = { chunk_type: DiffChunkType, content: string, };
|
||||
type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, };
|
||||
|
||||
export type FileDiff = { path: string, chunks: Array<DiffChunk>, };
|
||||
type DiffChunkType = "Equal" | "Insert" | "Delete";
|
||||
|
||||
export type WorktreeDiff = { files: Array<FileDiff>, };
|
||||
type DiffChunk = { chunk_type: DiffChunkType, content: string, };
|
||||
|
||||
export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, };
|
||||
type FileDiff = { path: string, chunks: Array<DiffChunk>, };
|
||||
|
||||
type WorktreeDiff = { files: Array<FileDiff>, };
|
||||
|
||||
type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, };
|
||||
Reference in New Issue
Block a user