diff --git a/backend/.sqlx/query-94a21be956c9451a8b117d25ffd4e5ee75bba0aa032139572cf87651e2856f3a.json b/backend/.sqlx/query-93a279fc7df10c38bdc1b0a3d7b48006147092bfb459e12424a55d42e6390d6a.json similarity index 52% rename from backend/.sqlx/query-94a21be956c9451a8b117d25ffd4e5ee75bba0aa032139572cf87651e2856f3a.json rename to backend/.sqlx/query-93a279fc7df10c38bdc1b0a3d7b48006147092bfb459e12424a55d42e6390d6a.json index a4894947..7a3cb27b 100644 --- a/backend/.sqlx/query-94a21be956c9451a8b117d25ffd4e5ee75bba0aa032139572cf87651e2856f3a.json +++ b/backend/.sqlx/query-93a279fc7df10c38bdc1b0a3d7b48006147092bfb459e12424a55d42e6390d6a.json @@ -1,6 +1,6 @@ { "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.parent_task_attempt AS \"parent_task_attempt: Uuid\", \n t.created_at AS \"created_at!: DateTime\",\n t.updated_at AS \"updated_at!: DateTime\",\n CASE \n WHEN ip.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_in_progress_attempt!: i64\",\n CASE \n WHEN ma.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_merged_attempt!: i64\",\n CASE \n WHEN fa.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_failed_attempt!: i64\",\n latest_executor_attempts.executor AS \"latest_attempt_executor\"\n FROM tasks t\n\n -- in-progress if any running setupscript/codingagent\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 WHERE ep.status = 'running'\n AND ep.process_type IN ('setupscript','codingagent')\n ) ip \n ON t.id = ip.task_id\n\n -- merged if merge_commit not null\n LEFT JOIN (\n SELECT DISTINCT task_id\n FROM task_attempts\n WHERE merge_commit IS NOT NULL\n ) ma \n ON t.id = ma.task_id\n\n -- failed if latest attempt has a failed setupscript/codingagent\n LEFT JOIN (\n SELECT sub.task_id\n FROM (\n SELECT\n ta.task_id,\n ep.status,\n ep.process_type,\n ROW_NUMBER() OVER (\n PARTITION BY ta.task_id \n ORDER BY ta.created_at DESC\n ) AS rn\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n WHERE ep.process_type IN ('setupscript','codingagent')\n ) sub\n WHERE sub.rn = 1\n AND sub.status IN ('failed','killed')\n ) fa\n ON t.id = fa.task_id\n\n -- get the executor of the latest attempt\n LEFT JOIN (\n SELECT task_id, executor\n FROM (\n SELECT task_id, executor, created_at,\n ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at DESC) AS rn\n FROM task_attempts\n ) latest_attempts\n WHERE rn = 1\n ) latest_executor_attempts \n ON t.id = latest_executor_attempts.task_id\n\n WHERE t.project_id = $1\n ORDER BY t.created_at DESC;\n ", + "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.parent_task_attempt AS \"parent_task_attempt: Uuid\", \n t.created_at AS \"created_at!: DateTime\",\n t.updated_at AS \"updated_at!: DateTime\",\n CASE \n WHEN ip.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_in_progress_attempt!: i64\",\n CASE \n WHEN ma.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_merged_attempt!: i64\",\n CASE \n WHEN fa.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"last_attempt_failed!: i64\",\n latest_executor_attempts.executor AS \"latest_attempt_executor\"\n FROM tasks t\n\n -- in-progress if any running setupscript/codingagent\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 WHERE ep.status = 'running'\n AND ep.process_type IN ('setupscript','codingagent')\n ) ip \n ON t.id = ip.task_id\n\n -- merged if merge_commit not null\n LEFT JOIN (\n SELECT DISTINCT task_id\n FROM task_attempts\n WHERE merge_commit IS NOT NULL\n ) ma \n ON t.id = ma.task_id\n\n -- failed if latest execution process has a failed setupscript/codingagent\n LEFT JOIN (\n SELECT sub.task_id\n FROM (\n SELECT\n ta.task_id,\n ep.status,\n ep.process_type,\n ROW_NUMBER() OVER (\n PARTITION BY ta.task_id \n ORDER BY ep.created_at DESC\n ) AS rn\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n WHERE ep.process_type IN ('setupscript','codingagent')\n ) sub\n WHERE sub.rn = 1\n AND sub.status IN ('failed','killed')\n ) fa\n ON t.id = fa.task_id\n\n -- get the executor of the latest attempt\n LEFT JOIN (\n SELECT task_id, executor\n FROM (\n SELECT task_id, executor, created_at,\n ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at DESC) AS rn\n FROM task_attempts\n ) latest_attempts\n WHERE rn = 1\n ) latest_executor_attempts \n ON t.id = latest_executor_attempts.task_id\n\n WHERE t.project_id = $1\n ORDER BY t.created_at DESC;\n ", "describe": { "columns": [ { @@ -54,7 +54,7 @@ "type_info": "Integer" }, { - "name": "has_failed_attempt!: i64", + "name": "last_attempt_failed!: i64", "ordinal": 10, "type_info": "Integer" }, @@ -82,5 +82,5 @@ true ] }, - "hash": "94a21be956c9451a8b117d25ffd4e5ee75bba0aa032139572cf87651e2856f3a" + "hash": "93a279fc7df10c38bdc1b0a3d7b48006147092bfb459e12424a55d42e6390d6a" } diff --git a/backend/src/mcp/task_server.rs b/backend/src/mcp/task_server.rs index 40dbcf24..b7315f32 100644 --- a/backend/src/mcp/task_server.rs +++ b/backend/src/mcp/task_server.rs @@ -91,8 +91,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, + #[schemars(description = "Whether the last execution attempt failed")] + pub last_attempt_failed: Option, } #[derive(Debug, Serialize, schemars::JsonSchema)] @@ -449,7 +449,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), + last_attempt_failed: Some(task.last_attempt_failed), }) .collect(); @@ -597,7 +597,7 @@ impl TaskServer { updated_at: updated_task.updated_at.to_rfc3339(), has_in_progress_attempt: None, has_merged_attempt: None, - has_failed_attempt: None, + last_attempt_failed: None, }; let response = UpdateTaskResponse { @@ -768,7 +768,7 @@ impl TaskServer { updated_at: task.updated_at.to_rfc3339(), has_in_progress_attempt: None, has_merged_attempt: None, - has_failed_attempt: None, + last_attempt_failed: None, }; let response = GetTaskResponse { diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs index ed10ea16..937e5f04 100644 --- a/backend/src/models/task.rs +++ b/backend/src/models/task.rs @@ -42,7 +42,7 @@ pub struct TaskWithAttemptStatus { pub updated_at: DateTime, pub has_in_progress_attempt: bool, pub has_merged_attempt: bool, - pub has_failed_attempt: bool, + pub last_attempt_failed: bool, pub latest_attempt_executor: Option, } @@ -100,7 +100,7 @@ impl Task { CASE WHEN fa.task_id IS NOT NULL THEN true ELSE false - END AS "has_failed_attempt!: i64", + END AS "last_attempt_failed!: i64", latest_executor_attempts.executor AS "latest_attempt_executor" FROM tasks t @@ -123,7 +123,7 @@ impl Task { ) ma ON t.id = ma.task_id - -- failed if latest attempt has a failed setupscript/codingagent + -- failed if latest execution process has a failed setupscript/codingagent LEFT JOIN ( SELECT sub.task_id FROM ( @@ -133,7 +133,7 @@ impl Task { ep.process_type, ROW_NUMBER() OVER ( PARTITION BY ta.task_id - ORDER BY ta.created_at DESC + ORDER BY ep.created_at DESC ) AS rn FROM task_attempts ta JOIN execution_processes ep @@ -178,7 +178,7 @@ impl Task { updated_at: rec.updated_at, has_in_progress_attempt: rec.has_in_progress_attempt != 0, has_merged_attempt: rec.has_merged_attempt != 0, - has_failed_attempt: rec.has_failed_attempt != 0, + last_attempt_failed: rec.last_attempt_failed != 0, latest_attempt_executor: rec.latest_attempt_executor, }) .collect(); diff --git a/frontend/src/components/tasks/TaskCard.tsx b/frontend/src/components/tasks/TaskCard.tsx index 54bfa62c..ca3c2937 100644 --- a/frontend/src/components/tasks/TaskCard.tsx +++ b/frontend/src/components/tasks/TaskCard.tsx @@ -102,7 +102,7 @@ export function TaskCard({ )} {/* Failed Indicator */} - {task.has_failed_attempt && !task.has_merged_attempt && ( + {task.last_attempt_failed && !task.has_merged_attempt && ( )} {/* Actions Menu */} diff --git a/frontend/src/components/tasks/TaskDetails/LogsTab.tsx b/frontend/src/components/tasks/TaskDetails/LogsTab.tsx index 0937ae5d..e8ed9a92 100644 --- a/frontend/src/components/tasks/TaskDetails/LogsTab.tsx +++ b/frontend/src/components/tasks/TaskDetails/LogsTab.tsx @@ -78,12 +78,33 @@ function LogsTab() { // When setup failed or was stopped if (isSetupFailed || isSetupStopped) { - const setupProcess = executionState.setup_process_id + let setupProcess = executionState.setup_process_id ? attemptData.runningProcessDetails[executionState.setup_process_id] : Object.values(attemptData.runningProcessDetails).find( (process) => process.process_type === 'setupscript' ); + // If not found in runningProcessDetails, try to find in processes array + if (!setupProcess) { + const setupSummary = attemptData.processes.find( + (process) => process.process_type === 'setupscript' + ); + + if (setupSummary) { + setupProcess = Object.values(attemptData.runningProcessDetails).find( + (process) => process.id === setupSummary.id + ); + + if (!setupProcess) { + setupProcess = { + ...setupSummary, + stdout: null, + stderr: null, + } as any; + } + } + } + return (
@@ -106,38 +127,15 @@ function LogsTab() { ); } - // When coding agent failed or was stopped - if (isCodingAgentFailed || isCodingAgentStopped) { - const codingAgentProcess = executionState.coding_agent_process_id - ? attemptData.runningProcessDetails[ - executionState.coding_agent_process_id - ] - : Object.values(attemptData.runningProcessDetails).find( - (process) => process.process_type === 'codingagent' - ); - - return ( -
-
-

- {isCodingAgentFailed - ? 'Coding Agent Failed' - : 'Coding Agent Stopped'} -

- {isCodingAgentFailed && ( -

- The coding agent encountered an error. Error details below: -

- )} -
- - {codingAgentProcess && ( - - )} -
- ); + // When coding agent is in any state (running, complete, failed, stopped) + if ( + isCodingAgentRunning || + isCodingAgentComplete || + isCodingAgentFailed || + isCodingAgentStopped || + hasChanges + ) { + return ; } // When setup is complete but coding agent hasn't started, show waiting state diff --git a/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx b/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx index 7dd30e10..3ffc28a7 100644 --- a/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx +++ b/frontend/src/components/tasks/TaskDetails/LogsTab/Conversation.tsx @@ -171,6 +171,21 @@ function Conversation() { allEntries, ]); + // Check if we should show the status banner - only if the most recent process failed/stopped + const getMostRecentProcess = () => { + if (followUpLogs.length > 0) { + // Sort by creation time or use last in array as most recent + return followUpLogs[followUpLogs.length - 1]; + } + return mainCodingAgentLog; + }; + + const mostRecentProcess = getMostRecentProcess(); + const showStatusBanner = + mostRecentProcess && + (mostRecentProcess.status === 'failed' || + mostRecentProcess.status === 'killed'); + return (
)} + + {/* Status banner for failed/stopped states - shown at bottom */} + {showStatusBanner && mostRecentProcess && ( +
+

+ {mostRecentProcess.status === 'failed' + ? 'Coding Agent Failed' + : 'Coding Agent Stopped'} +

+

+ {mostRecentProcess.status === 'failed' + ? 'The coding agent encountered an error.' + : 'The coding agent was stopped.'} +

+
+ )}
); } diff --git a/frontend/src/components/tasks/TaskFollowUpSection.tsx b/frontend/src/components/tasks/TaskFollowUpSection.tsx index 1e3d3e56..02cf43dc 100644 --- a/frontend/src/components/tasks/TaskFollowUpSection.tsx +++ b/frontend/src/components/tasks/TaskFollowUpSection.tsx @@ -32,12 +32,13 @@ export function TaskFollowUpSection() { return false; } - const completedCodingAgentProcesses = attemptData.processes.filter( + const completedOrKilledCodingAgentProcesses = attemptData.processes.filter( (process) => - process.process_type === 'codingagent' && process.status === 'completed' + process.process_type === 'codingagent' && + (process.status === 'completed' || process.status === 'killed') ); - return completedCodingAgentProcesses.length > 0; + return completedOrKilledCodingAgentProcesses.length > 0; }, [ selectedAttempt, attemptData.processes, @@ -81,7 +82,7 @@ export function TaskFollowUpSection() { )}
{ setFollowUpMessage(value); diff --git a/shared/types.ts b/shared/types.ts index 582ad103..1583d79e 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, 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, has_failed_attempt: boolean, latest_attempt_executor: string | null, }; +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, latest_attempt_executor: string | null, }; export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, parent_task_attempt: string | null, };