diff --git a/crates/db/src/models/task.rs b/crates/db/src/models/task.rs index 3eefd8f3..1f4309d9 100644 --- a/crates/db/src/models/task.rs +++ b/crates/db/src/models/task.rs @@ -31,20 +31,28 @@ pub struct Task { #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct TaskWithAttemptStatus { - pub id: Uuid, - pub project_id: Uuid, - pub title: String, - pub description: Option, - pub status: TaskStatus, - pub parent_task_attempt: Option, - pub created_at: DateTime, - pub updated_at: DateTime, + #[serde(flatten)] + #[ts(flatten)] + pub task: Task, pub has_in_progress_attempt: bool, pub has_merged_attempt: bool, pub last_attempt_failed: bool, pub executor: String, } +impl std::ops::Deref for TaskWithAttemptStatus { + type Target = Task; + fn deref(&self) -> &Self::Target { + &self.task + } +} + +impl std::ops::DerefMut for TaskWithAttemptStatus { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.task + } +} + #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct TaskRelationships { pub parent_task: Option, // The task that owns this attempt @@ -139,14 +147,16 @@ ORDER BY t.created_at DESC"#, let tasks = records .into_iter() .map(|rec| TaskWithAttemptStatus { - id: rec.id, - project_id: rec.project_id, - title: rec.title, - description: rec.description, - status: rec.status, - parent_task_attempt: rec.parent_task_attempt, - created_at: rec.created_at, - updated_at: rec.updated_at, + task: Task { + id: rec.id, + project_id: rec.project_id, + title: rec.title, + description: rec.description, + status: rec.status, + parent_task_attempt: rec.parent_task_attempt, + created_at: rec.created_at, + updated_at: rec.updated_at, + }, has_in_progress_attempt: rec.has_in_progress_attempt != 0, has_merged_attempt: false, // TODO use merges table last_attempt_failed: rec.last_attempt_failed != 0, diff --git a/crates/server/src/mcp/task_server.rs b/crates/server/src/mcp/task_server.rs index d69a865a..f239a33a 100644 --- a/crates/server/src/mcp/task_server.rs +++ b/crates/server/src/mcp/task_server.rs @@ -440,8 +440,8 @@ impl TaskServer { .into_iter() .map(|task| TaskSummary { id: task.id.to_string(), - title: task.title, - description: task.description, + title: task.title.clone(), + description: task.description.clone(), status: task_status_to_string(&task.status), created_at: task.created_at.to_rfc3339(), updated_at: task.updated_at.to_rfc3339(), diff --git a/crates/server/src/routes/tasks.rs b/crates/server/src/routes/tasks.rs index bf87e0f1..4d1795b7 100644 --- a/crates/server/src/routes/tasks.rs +++ b/crates/server/src/routes/tasks.rs @@ -195,14 +195,7 @@ pub async fn create_task_and_start( tracing::info!("Started execution process {}", execution_process.id); Ok(ResponseJson(ApiResponse::success(TaskWithAttemptStatus { - id: task.id, - title: task.title, - description: task.description, - project_id: task.project_id, - status: task.status, - parent_task_attempt: task.parent_task_attempt, - created_at: task.created_at, - updated_at: task.updated_at, + task, has_in_progress_attempt: true, has_merged_attempt: false, last_attempt_failed: false, diff --git a/crates/services/src/services/events.rs b/crates/services/src/services/events.rs index 6d14326f..bbc3e57a 100644 --- a/crates/services/src/services/events.rs +++ b/crates/services/src/services/events.rs @@ -14,7 +14,7 @@ use futures::{StreamExt, TryStreamExt}; use json_patch::{AddOperation, Patch, PatchOperation, RemoveOperation, ReplaceOperation}; use serde::{Deserialize, Serialize}; use serde_json::json; -use sqlx::{Error as SqlxError, sqlite::SqliteOperation}; +use sqlx::{Error as SqlxError, SqlitePool, sqlite::SqliteOperation}; use strum_macros::{Display, EnumString}; use thiserror::Error; use tokio::sync::RwLock; @@ -198,6 +198,37 @@ impl EventService { } } + async fn push_task_update_for_task( + pool: &SqlitePool, + msg_store: Arc, + task_id: Uuid, + ) -> Result<(), SqlxError> { + if let Some(task) = Task::find_by_id(pool, task_id).await? { + let tasks = Task::find_by_project_id_with_attempt_status(pool, task.project_id).await?; + + if let Some(task_with_status) = tasks + .into_iter() + .find(|task_with_status| task_with_status.id == task_id) + { + msg_store.push_patch(task_patch::replace(&task_with_status)); + } + } + + Ok(()) + } + + async fn push_task_update_for_attempt( + pool: &SqlitePool, + msg_store: Arc, + attempt_id: Uuid, + ) -> Result<(), SqlxError> { + if let Some(attempt) = TaskAttempt::find_by_id(pool, attempt_id).await? { + Self::push_task_update_for_task(pool, msg_store, attempt.task_id).await?; + } + + Ok(()) + } + /// Creates the hook function that should be used with DBService::new_with_after_connect pub fn create_hook( msg_store: Arc, @@ -440,14 +471,45 @@ impl EventService { _ => execution_process_patch::replace(process), // fallback }; msg_store_for_hook.push_patch(patch); + + if let Err(err) = EventService::push_task_update_for_attempt( + &db.pool, + msg_store_for_hook.clone(), + process.task_attempt_id, + ) + .await + { + tracing::error!( + "Failed to push task update after execution process change: {:?}", + err + ); + } + return; } RecordTypes::DeletedExecutionProcess { process_id: Some(process_id), + task_attempt_id, .. } => { let patch = execution_process_patch::remove(*process_id); msg_store_for_hook.push_patch(patch); + + if let Some(task_attempt_id) = task_attempt_id + && let Err(err) = + EventService::push_task_update_for_attempt( + &db.pool, + msg_store_for_hook.clone(), + *task_attempt_id, + ) + .await + { + tracing::error!( + "Failed to push task update after execution process removal: {:?}", + err + ); + } + return; } _ => {} diff --git a/shared/types.ts b/shared/types.ts index e7ce7112..a9bf5be3 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -44,7 +44,7 @@ export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelle export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, parent_task_attempt: string | null, created_at: string, updated_at: string, }; -export type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, parent_task_attempt: string | null, created_at: string, updated_at: string, has_in_progress_attempt: boolean, has_merged_attempt: boolean, last_attempt_failed: boolean, executor: string, }; +export type TaskWithAttemptStatus = { has_in_progress_attempt: boolean, has_merged_attempt: boolean, last_attempt_failed: boolean, executor: string, id: string, project_id: string, title: string, description: string | null, status: TaskStatus, parent_task_attempt: string | null, created_at: string, updated_at: string, }; export type TaskRelationships = { parent_task: Task | null, current_attempt: TaskAttempt, children: Array, };