diff --git a/backend/.sqlx/query-5303664388a91c378ade88cad68fcb1bce32a4be1269820bf2031dd6b7e13be8.json b/backend/.sqlx/query-5303664388a91c378ade88cad68fcb1bce32a4be1269820bf2031dd6b7e13be8.json deleted file mode 100644 index 41dc7992..00000000 --- a/backend/.sqlx/query-5303664388a91c378ade88cad68fcb1bce32a4be1269820bf2031dd6b7e13be8.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT \n t.id AS \"id!: Uuid\", \n t.project_id AS \"project_id!: Uuid\", \n t.title, \n t.description, \n t.status AS \"status!: TaskStatus\", \n t.created_at AS \"created_at!: DateTime\", \n t.updated_at AS \"updated_at!: DateTime\",\n CASE \n WHEN in_progress_attempts.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_in_progress_attempt!: i64\",\n CASE \n WHEN merged_attempts.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_merged_attempt!\"\n FROM tasks t\n LEFT JOIN (\n SELECT DISTINCT ta.task_id\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n JOIN (\n -- pick exactly one “latest” activity per process,\n -- tiebreaking so that running‐states are lower priority\n SELECT execution_process_id, status\n FROM (\n SELECT\n execution_process_id,\n status,\n ROW_NUMBER() OVER (\n PARTITION BY execution_process_id\n ORDER BY\n created_at DESC,\n CASE \n WHEN status IN ('setuprunning','executorrunning') THEN 1 \n ELSE 0 \n END\n ) AS rn\n FROM task_attempt_activities\n ) sub\n WHERE rn = 1\n ) latest_act \n ON ep.id = latest_act.execution_process_id\n WHERE latest_act.status IN ('setuprunning','executorrunning')\n ) in_progress_attempts \n ON t.id = in_progress_attempts.task_id\n LEFT JOIN (\n SELECT DISTINCT ta.task_id\n FROM task_attempts ta\n WHERE ta.merge_commit IS NOT NULL\n ) merged_attempts \n ON t.id = merged_attempts.task_id\n WHERE t.project_id = $1\n ORDER BY t.created_at DESC;\n ", - "describe": { - "columns": [ - { - "name": "id!: Uuid", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "project_id!: Uuid", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "description", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "status!: TaskStatus", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "created_at!: DateTime", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "updated_at!: DateTime", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "has_in_progress_attempt!: i64", - "ordinal": 7, - "type_info": "Integer" - }, - { - "name": "has_merged_attempt!", - "ordinal": 8, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true, - false, - false, - true, - false, - false, - false, - false, - false - ] - }, - "hash": "5303664388a91c378ade88cad68fcb1bce32a4be1269820bf2031dd6b7e13be8" -} diff --git a/backend/.sqlx/query-b7d9fc7198a30ad4e88d7cdd7b59a1a73018de16506003622cd7f779028c0fa8.json b/backend/.sqlx/query-b7d9fc7198a30ad4e88d7cdd7b59a1a73018de16506003622cd7f779028c0fa8.json new file mode 100644 index 00000000..6c779920 --- /dev/null +++ b/backend/.sqlx/query-b7d9fc7198a30ad4e88d7cdd7b59a1a73018de16506003622cd7f779028c0fa8.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "SELECT \n t.id AS \"id!: Uuid\", \n t.project_id AS \"project_id!: Uuid\", \n t.title, \n t.description, \n t.status AS \"status!: TaskStatus\", \n t.created_at AS \"created_at!: DateTime\", \n t.updated_at AS \"updated_at!: DateTime\",\n CASE \n WHEN in_progress_attempts.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_in_progress_attempt!: i64\",\n CASE \n WHEN merged_attempts.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_merged_attempt!\",\n CASE \n WHEN failed_attempts.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_failed_attempt!\"\n FROM tasks t\n LEFT JOIN (\n SELECT DISTINCT ta.task_id\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n JOIN (\n -- pick exactly one “latest” activity per process,\n -- tiebreaking so that running‐states are lower priority\n SELECT execution_process_id, status\n FROM (\n SELECT\n execution_process_id,\n status,\n ROW_NUMBER() OVER (\n PARTITION BY execution_process_id\n ORDER BY\n created_at DESC,\n CASE \n WHEN status IN ('setuprunning','executorrunning') THEN 1 \n ELSE 0 \n END\n ) AS rn\n FROM task_attempt_activities\n ) sub\n WHERE rn = 1\n ) latest_act \n ON ep.id = latest_act.execution_process_id\n WHERE latest_act.status IN ('setuprunning','executorrunning')\n ) in_progress_attempts \n ON t.id = in_progress_attempts.task_id\n LEFT JOIN (\n SELECT DISTINCT ta.task_id\n FROM task_attempts ta\n WHERE ta.merge_commit IS NOT NULL\n ) merged_attempts \n ON t.id = merged_attempts.task_id\n LEFT JOIN (\n SELECT DISTINCT ta.task_id\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n JOIN (\n -- pick exactly one \"latest\" activity per process,\n -- tiebreaking so that running‐states are lower priority\n SELECT execution_process_id, status\n FROM (\n SELECT\n execution_process_id,\n status,\n ROW_NUMBER() OVER (\n PARTITION BY execution_process_id\n ORDER BY\n created_at DESC,\n CASE \n WHEN status IN ('setuprunning','executorrunning') THEN 1 \n ELSE 0 \n END\n ) AS rn\n FROM task_attempt_activities\n ) sub\n WHERE rn = 1\n ) latest_act \n ON ep.id = latest_act.execution_process_id\n WHERE latest_act.status IN ('setupfailed','executorfailed')\n AND ta.merge_commit IS NULL -- Don't show as failed if already merged\n ) failed_attempts \n ON t.id = failed_attempts.task_id\n WHERE t.project_id = $1\n ORDER BY t.created_at DESC;\n ", + "describe": { + "columns": [ + { + "name": "id!: Uuid", + "ordinal": 0, + "type_info": "Blob" + }, + { + "name": "project_id!: Uuid", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "status!: TaskStatus", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "created_at!: DateTime", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "updated_at!: DateTime", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "has_in_progress_attempt!: i64", + "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "has_merged_attempt!", + "ordinal": 8, + "type_info": "Integer" + }, + { + "name": "has_failed_attempt!", + "ordinal": 9, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false, + false, + true, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "b7d9fc7198a30ad4e88d7cdd7b59a1a73018de16506003622cd7f779028c0fa8" +} diff --git a/backend/src/mcp/task_server.rs b/backend/src/mcp/task_server.rs index de9f1659..a99a0f25 100644 --- a/backend/src/mcp/task_server.rs +++ b/backend/src/mcp/task_server.rs @@ -93,6 +93,8 @@ pub struct TaskSummary { pub has_in_progress_attempt: Option, #[schemars(description = "Whether the task has a merged execution attempt")] pub has_merged_attempt: Option, + #[schemars(description = "Whether the task has a failed execution attempt")] + pub has_failed_attempt: Option, } #[derive(Debug, Serialize, schemars::JsonSchema)] @@ -513,6 +515,7 @@ impl TaskServer { updated_at: task.updated_at.to_rfc3339(), has_in_progress_attempt: Some(task.has_in_progress_attempt), has_merged_attempt: Some(task.has_merged_attempt), + has_failed_attempt: Some(task.has_failed_attempt), }) .collect(); @@ -656,6 +659,7 @@ impl TaskServer { updated_at: updated_task.updated_at.to_rfc3339(), has_in_progress_attempt: None, has_merged_attempt: None, + has_failed_attempt: None, }; let response = UpdateTaskResponse { @@ -1220,6 +1224,7 @@ impl TaskServer { updated_at: task.updated_at.to_rfc3339(), has_in_progress_attempt: None, has_merged_attempt: None, + has_failed_attempt: None, }; let response = GetTaskResponse { diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs index 82925419..7f98fb3a 100644 --- a/backend/src/models/task.rs +++ b/backend/src/models/task.rs @@ -40,6 +40,7 @@ pub struct TaskWithAttemptStatus { pub updated_at: DateTime, pub has_in_progress_attempt: bool, pub has_merged_attempt: bool, + pub has_failed_attempt: bool, } #[derive(Debug, Deserialize, TS)] @@ -88,7 +89,11 @@ impl Task { CASE WHEN merged_attempts.task_id IS NOT NULL THEN true ELSE false - END AS "has_merged_attempt!" + END AS "has_merged_attempt!", + CASE + WHEN failed_attempts.task_id IS NOT NULL THEN true + ELSE false + END AS "has_failed_attempt!" FROM tasks t LEFT JOIN ( SELECT DISTINCT ta.task_id @@ -126,6 +131,37 @@ impl Task { WHERE ta.merge_commit IS NOT NULL ) merged_attempts ON t.id = merged_attempts.task_id + LEFT JOIN ( + SELECT DISTINCT ta.task_id + FROM task_attempts ta + JOIN execution_processes ep + ON ta.id = ep.task_attempt_id + JOIN ( + -- pick exactly one "latest" activity per process, + -- tiebreaking so that running‐states are lower priority + SELECT execution_process_id, status + FROM ( + SELECT + execution_process_id, + status, + ROW_NUMBER() OVER ( + PARTITION BY execution_process_id + ORDER BY + created_at DESC, + CASE + WHEN status IN ('setuprunning','executorrunning') THEN 1 + ELSE 0 + END + ) AS rn + FROM task_attempt_activities + ) sub + WHERE rn = 1 + ) latest_act + ON ep.id = latest_act.execution_process_id + WHERE latest_act.status IN ('setupfailed','executorfailed') + AND ta.merge_commit IS NULL -- Don't show as failed if already merged + ) failed_attempts + ON t.id = failed_attempts.task_id WHERE t.project_id = $1 ORDER BY t.created_at DESC; "#, @@ -146,6 +182,7 @@ impl Task { updated_at: record.updated_at, has_in_progress_attempt: record.has_in_progress_attempt != 0, has_merged_attempt: record.has_merged_attempt != 0, + has_failed_attempt: record.has_failed_attempt != 0, }) .collect(); diff --git a/frontend/src/components/tasks/TaskCard.tsx b/frontend/src/components/tasks/TaskCard.tsx index d8356295..4b0f1496 100644 --- a/frontend/src/components/tasks/TaskCard.tsx +++ b/frontend/src/components/tasks/TaskCard.tsx @@ -12,6 +12,7 @@ import { Edit, Loader2, CheckCircle, + XCircle, } from 'lucide-react'; import type { TaskWithAttemptStatus } from 'shared/types'; @@ -57,6 +58,10 @@ export function TaskCard({ {task.has_merged_attempt && ( )} + {/* Failed Indicator */} + {task.has_failed_attempt && !task.has_merged_attempt && ( + + )} {/* Actions Menu */}
e.stopPropagation()} diff --git a/shared/types.ts b/shared/types.ts index bf9d70b2..1fd97575 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -50,7 +50,7 @@ export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelle export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, }; -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, }; +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, has_failed_attempt: boolean, }; export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, };