Task attempt f0c071f3-47ae-453e-b230-276ddf8ac3e6 - Final changes
This commit is contained in:
@@ -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(
|
pub async fn update_task(
|
||||||
Path((project_id, task_id)): Path<(Uuid, Uuid)>,
|
Path((project_id, task_id)): Path<(Uuid, Uuid)>,
|
||||||
Extension(pool): Extension<SqlitePool>,
|
Extension(pool): Extension<SqlitePool>,
|
||||||
@@ -623,6 +711,10 @@ pub fn tasks_router() -> Router {
|
|||||||
"/projects/:project_id/tasks",
|
"/projects/:project_id/tasks",
|
||||||
get(get_project_tasks).post(create_task),
|
get(get_project_tasks).post(create_task),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/projects/:project_id/tasks/create-and-start",
|
||||||
|
post(create_task_and_start),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/projects/:project_id/tasks/:task_id",
|
"/projects/:project_id/tasks/:task_id",
|
||||||
get(get_task).put(update_task).delete(delete_task),
|
get(get_task).put(update_task).delete(delete_task),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface TaskFormDialogProps {
|
|||||||
task?: Task | null // Optional for create mode
|
task?: Task | null // Optional for create mode
|
||||||
projectId?: string // For file search functionality
|
projectId?: string // For file search functionality
|
||||||
onCreateTask?: (title: string, description: string) => Promise<void>
|
onCreateTask?: (title: string, description: string) => Promise<void>
|
||||||
|
onCreateAndStartTask?: (title: string, description: string) => Promise<void>
|
||||||
onUpdateTask?: (title: string, description: string, status: TaskStatus) => Promise<void>
|
onUpdateTask?: (title: string, description: string, status: TaskStatus) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,12 +44,14 @@ export function TaskFormDialog({
|
|||||||
task,
|
task,
|
||||||
projectId,
|
projectId,
|
||||||
onCreateTask,
|
onCreateTask,
|
||||||
|
onCreateAndStartTask,
|
||||||
onUpdateTask
|
onUpdateTask
|
||||||
}: TaskFormDialogProps) {
|
}: TaskFormDialogProps) {
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [status, setStatus] = useState<TaskStatus>('todo')
|
const [status, setStatus] = useState<TaskStatus>('todo')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [isSubmittingAndStart, setIsSubmittingAndStart] = useState(false)
|
||||||
|
|
||||||
const isEditMode = Boolean(task)
|
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 = () => {
|
const handleCancel = () => {
|
||||||
// Reset form state when canceling
|
// Reset form state when canceling
|
||||||
if (task) {
|
if (task) {
|
||||||
@@ -118,7 +141,7 @@ export function TaskFormDialog({
|
|||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="Enter task title"
|
placeholder="Enter task title"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isSubmittingAndStart}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -129,7 +152,7 @@ export function TaskFormDialog({
|
|||||||
onChange={setDescription}
|
onChange={setDescription}
|
||||||
placeholder="Enter task description (optional). Type @ to search files."
|
placeholder="Enter task description (optional). Type @ to search files."
|
||||||
rows={3}
|
rows={3}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isSubmittingAndStart}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +163,7 @@ export function TaskFormDialog({
|
|||||||
<Select
|
<Select
|
||||||
value={status}
|
value={status}
|
||||||
onValueChange={(value) => setStatus(value as TaskStatus)}
|
onValueChange={(value) => setStatus(value as TaskStatus)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isSubmittingAndStart}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -160,19 +183,36 @@ export function TaskFormDialog({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting || isSubmittingAndStart}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{isEditMode ? (
|
||||||
onClick={handleSubmit}
|
<Button
|
||||||
disabled={isSubmitting || !title.trim()}
|
onClick={handleSubmit}
|
||||||
>
|
disabled={isSubmitting || !title.trim()}
|
||||||
{isSubmitting
|
>
|
||||||
? (isEditMode ? 'Updating...' : 'Creating...')
|
{isSubmitting ? 'Updating...' : 'Update Task'}
|
||||||
: (isEditMode ? 'Update Task' : 'Create Task')
|
</Button>
|
||||||
}
|
) : (
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</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 (
|
const handleUpdateTask = async (
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
@@ -292,6 +313,7 @@ export function ProjectTasks() {
|
|||||||
task={editingTask}
|
task={editingTask}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
onCreateTask={handleCreateTask}
|
onCreateTask={handleCreateTask}
|
||||||
|
onCreateAndStartTask={handleCreateAndStartTask}
|
||||||
onUpdateTask={handleUpdateTask}
|
onUpdateTask={handleUpdateTask}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -2,58 +2,58 @@
|
|||||||
// Do not edit this file manually.
|
// Do not edit this file manually.
|
||||||
// Auto-generated from Rust backend types using ts-rs
|
// 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