Merge task attempt f0c071f3-47ae-453e-b230-276ddf8ac3e6 into main

This commit is contained in:
Louis Knight-Webb
2025-06-20 20:03:12 +01:00
5 changed files with 216 additions and 49 deletions

View File

@@ -1,17 +1,17 @@
use std::{env, fs, path::Path};
use ts_rs::TS; // make sure this is in [build-dependencies]
use ts_rs::TS; // in [build-dependencies]
fn main() {
// Where the combined types.ts will live
// 1. Make sure ../shared exists
let shared_path = Path::new("../shared");
fs::create_dir_all(shared_path).expect("cannot create ../shared");
println!("Generating TypeScript types…");
// Tell ts-rs where to drop its per-type files (well still roll our own big one)
// 2. Let ts-rs write its per-type files here (handy for debugging)
env::set_var("TS_RS_EXPORT_DIR", shared_path.to_str().unwrap());
// Collect every declaration at *runtime* (so no const-eval issues)
// 3. Grab every Rust type you want on the TS side
let decls = [
vibe_kanban::models::ApiResponse::<()>::decl(),
vibe_kanban::models::config::Config::decl(),
@@ -43,16 +43,29 @@ fn main() {
vibe_kanban::models::task_attempt::BranchStatus::decl(),
];
// Header banner
// 4. Friendly 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";
// Smash it all together
let consolidated = format!("{HEADER}{}", decls.join("\n\n"));
// 5. Add `export` if its missing, then join
let body = decls
.into_iter()
.map(|d| {
let trimmed = d.trim_start();
if trimmed.starts_with("export") {
d
} else {
format!("export {trimmed}")
}
})
.collect::<Vec<_>>()
.join("\n\n");
fs::write(shared_path.join("types.ts"), consolidated).expect("unable to write types.ts");
// 6. Write the consolidated types.ts
fs::write(shared_path.join("types.ts"), format!("{HEADER}{body}"))
.expect("unable to write types.ts");
println!("✅ TypeScript types generated in ../shared/");
}

View File

@@ -97,6 +97,94 @@ pub async fn create_task(
}
}
pub async fn create_task_and_start(
Path(project_id): Path<Uuid>,
Extension(pool): Extension<SqlitePool>,
Extension(app_state): Extension<crate::execution_monitor::AppState>,
Json(mut payload): Json<CreateTask>,
) -> Result<ResponseJson<ApiResponse<Task>>, StatusCode> {
let task_id = Uuid::new_v4();
// Ensure the project_id in the payload matches the path parameter
payload.project_id = project_id;
// Verify project exists first
match Project::exists(&pool, project_id).await {
Ok(false) => return Err(StatusCode::NOT_FOUND),
Err(e) => {
tracing::error!("Failed to check project existence: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
Ok(true) => {}
}
tracing::debug!(
"Creating and starting task '{}' in project {}",
payload.title,
project_id
);
// Create the task first
let task = match Task::create(&pool, &payload, task_id).await {
Ok(task) => task,
Err(e) => {
tracing::error!("Failed to create task: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
// Create task attempt
let attempt_id = Uuid::new_v4();
let worktree_path = format!("/tmp/task-{}-attempt-{}", task_id, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis());
let attempt_payload = CreateTaskAttempt {
task_id,
worktree_path,
merge_commit: None,
executor: Some("claude".to_string()), // Default executor
};
match TaskAttempt::create(&pool, &attempt_payload, attempt_id).await {
Ok(attempt) => {
// Create initial activity record
let activity_id = Uuid::new_v4();
let _ = TaskAttemptActivity::create_initial(&pool, attempt.id, activity_id).await;
// Start execution asynchronously (don't block the response)
let pool_clone = pool.clone();
let app_state_clone = app_state.clone();
let attempt_id = attempt.id;
tokio::spawn(async move {
if let Err(e) = TaskAttempt::start_execution(
&pool_clone,
&app_state_clone,
attempt_id,
task_id,
project_id,
)
.await
{
tracing::error!(
"Failed to start execution for task attempt {}: {}",
attempt_id,
e
);
}
});
Ok(ResponseJson(ApiResponse {
success: true,
data: Some(task),
message: Some("Task created and started successfully".to_string()),
}))
}
Err(e) => {
tracing::error!("Failed to create task attempt: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
pub async fn update_task(
Path((project_id, task_id)): Path<(Uuid, Uuid)>,
Extension(pool): Extension<SqlitePool>,
@@ -623,6 +711,10 @@ pub fn tasks_router() -> Router {
"/projects/:project_id/tasks",
get(get_project_tasks).post(create_task),
)
.route(
"/projects/:project_id/tasks/create-and-start",
post(create_task_and_start),
)
.route(
"/projects/:project_id/tasks/:task_id",
get(get_task).put(update_task).delete(delete_task),

View File

@@ -34,6 +34,7 @@ interface TaskFormDialogProps {
task?: Task | null // Optional for create mode
projectId?: string // For file search functionality
onCreateTask?: (title: string, description: string) => Promise<void>
onCreateAndStartTask?: (title: string, description: string) => Promise<void>
onUpdateTask?: (title: string, description: string, status: TaskStatus) => Promise<void>
}
@@ -43,12 +44,14 @@ export function TaskFormDialog({
task,
projectId,
onCreateTask,
onCreateAndStartTask,
onUpdateTask
}: TaskFormDialogProps) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [status, setStatus] = useState<TaskStatus>('todo')
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSubmittingAndStart, setIsSubmittingAndStart] = useState(false)
const isEditMode = Boolean(task)
@@ -90,6 +93,26 @@ export function TaskFormDialog({
}
}
const handleCreateAndStart = async () => {
if (!title.trim()) return
setIsSubmittingAndStart(true)
try {
if (!isEditMode && onCreateAndStartTask) {
await onCreateAndStartTask(title, description)
}
// Reset form on successful creation
setTitle('')
setDescription('')
setStatus('todo')
onOpenChange(false)
} finally {
setIsSubmittingAndStart(false)
}
}
const handleCancel = () => {
// Reset form state when canceling
if (task) {
@@ -118,7 +141,7 @@ export function TaskFormDialog({
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter task title"
disabled={isSubmitting}
disabled={isSubmitting || isSubmittingAndStart}
/>
</div>
@@ -129,7 +152,7 @@ export function TaskFormDialog({
onChange={setDescription}
placeholder="Enter task description (optional). Type @ to search files."
rows={3}
disabled={isSubmitting}
disabled={isSubmitting || isSubmittingAndStart}
projectId={projectId}
/>
</div>
@@ -140,7 +163,7 @@ export function TaskFormDialog({
<Select
value={status}
onValueChange={(value) => setStatus(value as TaskStatus)}
disabled={isSubmitting}
disabled={isSubmitting || isSubmittingAndStart}
>
<SelectTrigger>
<SelectValue />
@@ -160,19 +183,36 @@ export function TaskFormDialog({
<Button
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
disabled={isSubmitting || isSubmittingAndStart}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting || !title.trim()}
>
{isSubmitting
? (isEditMode ? 'Updating...' : 'Creating...')
: (isEditMode ? 'Update Task' : 'Create Task')
}
</Button>
{isEditMode ? (
<Button
onClick={handleSubmit}
disabled={isSubmitting || !title.trim()}
>
{isSubmitting ? 'Updating...' : 'Update Task'}
</Button>
) : (
<>
<Button
variant="outline"
onClick={handleSubmit}
disabled={isSubmitting || isSubmittingAndStart || !title.trim()}
>
{isSubmitting ? 'Creating...' : 'Create Task'}
</Button>
{onCreateAndStartTask && (
<Button
onClick={handleCreateAndStart}
disabled={isSubmitting || isSubmittingAndStart || !title.trim()}
>
{isSubmittingAndStart ? 'Creating & Starting...' : 'Create & Start'}
</Button>
)}
</>
)}
</div>
</div>
</DialogContent>

View File

@@ -137,6 +137,27 @@ export function ProjectTasks() {
}
};
const handleCreateAndStartTask = async (title: string, description: string) => {
try {
const response = await makeRequest(`/api/projects/${projectId}/tasks/create-and-start`, {
method: "POST",
body: JSON.stringify({
project_id: projectId,
title,
description: description || null,
}),
});
if (response.ok) {
await fetchTasks();
} else {
setError("Failed to create and start task");
}
} catch (err) {
setError("Failed to create and start task");
}
};
const handleUpdateTask = async (
title: string,
description: string,
@@ -292,6 +313,7 @@ export function ProjectTasks() {
task={editingTask}
projectId={projectId}
onCreateTask={handleCreateTask}
onCreateAndStartTask={handleCreateAndStartTask}
onUpdateTask={handleUpdateTask}
/>

View File

@@ -2,58 +2,58 @@
// Do not edit this file manually.
// Auto-generated from Rust backend types using ts-rs
type ApiResponse<T> = { success: boolean, data: T | null, message: string | null, };
export type ApiResponse<T> = { success: boolean, data: T | null, message: string | null, };
type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, sound_alerts: boolean, editor: EditorConfig, };
export type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, sound_alerts: boolean, editor: EditorConfig, };
type ThemeMode = "light" | "dark" | "system";
export type ThemeMode = "light" | "dark" | "system";
type EditorConfig = { editor_type: EditorType, custom_command: string | null, };
export type EditorConfig = { editor_type: EditorType, custom_command: string | null, };
type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom";
export type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom";
type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" };
export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" };
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, };
type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, created_at: Date, updated_at: Date, };
export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, created_at: Date, updated_at: Date, };
type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, };
export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, };
type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, };
export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, };
type SearchMatchType = "FileName" | "DirectoryName" | "FullPath";
export type SearchMatchType = "FileName" | "DirectoryName" | "FullPath";
type CreateTask = { project_id: string, title: string, description: string | null, };
export type CreateTask = { project_id: string, title: string, description: string | null, };
type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelled";
export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelled";
type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, };
export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, };
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 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 UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, };
export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, };
type TaskAttemptStatus = "init" | "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed" | "paused";
export type TaskAttemptStatus = "init" | "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed" | "paused";
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 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 CreateTaskAttempt = { task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, };
export type CreateTaskAttempt = { task_id: string, worktree_path: string, merge_commit: string | null, executor: string | null, };
type UpdateTaskAttempt = { worktree_path: string | null, merge_commit: string | null, };
export type UpdateTaskAttempt = { worktree_path: string | null, merge_commit: string | null, };
type TaskAttemptActivity = { id: string, task_attempt_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, };
export type TaskAttemptActivity = { id: string, task_attempt_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, };
type CreateTaskAttemptActivity = { task_attempt_id: string, status: TaskAttemptStatus | null, note: string | null, };
export type CreateTaskAttemptActivity = { task_attempt_id: string, status: TaskAttemptStatus | null, note: string | null, };
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, };
type DiffChunkType = "Equal" | "Insert" | "Delete";
export type DiffChunkType = "Equal" | "Insert" | "Delete";
type DiffChunk = { chunk_type: DiffChunkType, content: string, };
export type DiffChunk = { chunk_type: DiffChunkType, content: string, };
type FileDiff = { path: string, chunks: Array<DiffChunk>, };
export type FileDiff = { path: string, chunks: Array<DiffChunk>, };
type WorktreeDiff = { files: Array<FileDiff>, };
export type WorktreeDiff = { files: Array<FileDiff>, };
type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, };
export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, };