Merge task attempt f0c071f3-47ae-453e-b230-276ddf8ac3e6 into main
This commit is contained in:
@@ -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 (we’ll 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 it’s 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/");
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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, };
|
||||
Reference in New Issue
Block a user