diff --git a/backend/.sqlx/query-bd57e47989627ac5e9f44e83f4bacff590d357586b149c7c17ff0e8f349e7025.json b/backend/.sqlx/query-bd57e47989627ac5e9f44e83f4bacff590d357586b149c7c17ff0e8f349e7025.json new file mode 100644 index 00000000..09e2a088 --- /dev/null +++ b/backend/.sqlx/query-bd57e47989627ac5e9f44e83f4bacff590d357586b149c7c17ff0e8f349e7025.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT \n taa.id as \"activity_id!: Uuid\",\n taa.execution_process_id as \"execution_process_id!: Uuid\",\n taa.status as \"status!: TaskAttemptStatus\",\n taa.note,\n taa.created_at as \"created_at!: DateTime\",\n es.prompt\n FROM task_attempt_activities taa\n INNER JOIN execution_processes ep ON taa.execution_process_id = ep.id\n LEFT JOIN executor_sessions es ON es.execution_process_id = ep.id\n WHERE ep.task_attempt_id = $1\n ORDER BY taa.created_at ASC", + "describe": { + "columns": [ + { + "name": "activity_id!: Uuid", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "execution_process_id!: Uuid", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "status!: TaskAttemptStatus", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "note", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at!: DateTime", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "prompt", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false, + false, + true, + false, + true + ] + }, + "hash": "bd57e47989627ac5e9f44e83f4bacff590d357586b149c7c17ff0e8f349e7025" +} diff --git a/backend/src/models/task_attempt_activity.rs b/backend/src/models/task_attempt_activity.rs index 72a91384..c6ffb3b1 100644 --- a/backend/src/models/task_attempt_activity.rs +++ b/backend/src/models/task_attempt_activity.rs @@ -24,6 +24,17 @@ pub struct CreateTaskAttemptActivity { pub note: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +pub struct TaskAttemptActivityWithPrompt { + pub id: Uuid, + pub execution_process_id: Uuid, + pub status: TaskAttemptStatus, + pub note: Option, + pub created_at: DateTime, + pub prompt: Option, // From executor_session +} + impl TaskAttemptActivity { pub async fn find_by_execution_process_id( pool: &SqlitePool, @@ -84,4 +95,40 @@ impl TaskAttemptActivity { Ok(records.into_iter().map(|r| r.id).collect()) } + + /// Find activities for all execution processes in a task attempt, with executor session prompts + pub async fn find_with_prompts_by_task_attempt_id( + pool: &SqlitePool, + task_attempt_id: Uuid, + ) -> Result, sqlx::Error> { + let records = sqlx::query!( + r#"SELECT + taa.id as "activity_id!: Uuid", + taa.execution_process_id as "execution_process_id!: Uuid", + taa.status as "status!: TaskAttemptStatus", + taa.note, + taa.created_at as "created_at!: DateTime", + es.prompt + FROM task_attempt_activities taa + INNER JOIN execution_processes ep ON taa.execution_process_id = ep.id + LEFT JOIN executor_sessions es ON es.execution_process_id = ep.id + WHERE ep.task_attempt_id = $1 + ORDER BY taa.created_at ASC"#, + task_attempt_id + ) + .fetch_all(pool) + .await?; + + Ok(records + .into_iter() + .map(|record| TaskAttemptActivityWithPrompt { + id: record.activity_id, + execution_process_id: record.execution_process_id, + status: record.status, + note: record.note, + created_at: record.created_at, + prompt: record.prompt, + }) + .collect()) + } } diff --git a/backend/src/routes/task_attempts.rs b/backend/src/routes/task_attempts.rs index 22c6df20..5a7e38d5 100644 --- a/backend/src/routes/task_attempts.rs +++ b/backend/src/routes/task_attempts.rs @@ -17,7 +17,7 @@ use crate::models::{ BranchStatus, CreateFollowUpAttempt, CreateTaskAttempt, TaskAttempt, TaskAttemptStatus, WorktreeDiff, }, - task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity}, + task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity, TaskAttemptActivityWithPrompt}, ApiResponse, }; @@ -51,7 +51,7 @@ pub async fn get_task_attempts( pub async fn get_task_attempt_activities( Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Extension(pool): Extension, -) -> Result>>, StatusCode> { +) -> Result>>, StatusCode> { // Verify task attempt exists and belongs to the correct task match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { Ok(false) => return Err(StatusCode::NOT_FOUND), @@ -62,40 +62,8 @@ pub async fn get_task_attempt_activities( Ok(true) => {} } - // Get all execution processes for the task attempt - let execution_processes = - match ExecutionProcess::find_by_task_attempt_id(&pool, attempt_id).await { - Ok(processes) => processes, - Err(e) => { - tracing::error!( - "Failed to fetch execution processes for attempt {}: {}", - attempt_id, - e - ); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - }; - - // Get activities for all execution processes - let mut all_activities = Vec::new(); - for process in execution_processes { - match TaskAttemptActivity::find_by_execution_process_id(&pool, process.id).await { - Ok(mut activities) => all_activities.append(&mut activities), - Err(e) => { - tracing::error!( - "Failed to fetch activities for execution process {}: {}", - process.id, - e - ); - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } - } - } - - // Sort activities by created_at - all_activities.sort_by(|a, b| a.created_at.cmp(&b.created_at)); - - match Ok::, sqlx::Error>(all_activities) { + // Get activities with prompts for the task attempt + match TaskAttemptActivity::find_with_prompts_by_task_attempt_id(&pool, attempt_id).await { Ok(activities) => Ok(ResponseJson(ApiResponse { success: true, data: Some(activities), diff --git a/frontend/src/components/tasks/TaskDetailsPanel.tsx b/frontend/src/components/tasks/TaskDetailsPanel.tsx index 996067d1..768aa783 100644 --- a/frontend/src/components/tasks/TaskDetailsPanel.tsx +++ b/frontend/src/components/tasks/TaskDetailsPanel.tsx @@ -47,6 +47,7 @@ import type { TaskStatus, TaskAttempt, TaskAttemptActivity, + TaskAttemptActivityWithPrompt, TaskAttemptStatus, ApiResponse, TaskWithAttemptStatus, @@ -150,7 +151,7 @@ export function TaskDetailsPanel({ ); // Combined attempt data state const [attemptData, setAttemptData] = useState<{ - activities: TaskAttemptActivity[]; + activities: TaskAttemptActivityWithPrompt[]; processes: ExecutionProcessSummary[]; runningProcessDetails: Record; }>({ @@ -421,7 +422,7 @@ export function TaskDetailsPanel({ ]); if (activitiesResponse.ok && processesResponse.ok) { - const activitiesResult: ApiResponse = + const activitiesResult: ApiResponse = await activitiesResponse.json(); const processesResult: ApiResponse = await processesResponse.json(); @@ -1160,7 +1161,30 @@ export function TaskDetailsPanel({ - {/* Show stdio output for running processes */} + {/* Show prompt for coding agent executions */} + {activity.prompt && + (activity.status === "setuprunning" || + activity.status === "setupcomplete" || + activity.status === "setupfailed" || + activity.status === "executorrunning" || + activity.status === "executorcomplete" || + activity.status === "executorfailed") && ( +
+
+
+ + + Prompt + +
+
+                                         {activity.prompt}
+                                       
+
+
+ )} + + {/* Show stdio output for running processes */} {(activity.status === "setuprunning" || activity.status === "executorrunning") && attemptData.runningProcessDetails[ diff --git a/shared/types.ts b/shared/types.ts index 1c0f1f9c..3a037de8 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -62,6 +62,8 @@ export type TaskAttemptActivity = { id: string, execution_process_id: string, st export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, }; +export type TaskAttemptActivityWithPrompt = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, prompt: string | null, }; + export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, }; export type DiffChunkType = "Equal" | "Insert" | "Delete";