use std::collections::HashMap;
use std::sync::Arc;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use crate::cmd;
use crate::store::Store;
use crate::task_actions;
use crate::task_view;
use crate::types::{Task, TaskFilter};
pub(crate) use super::api_types::{
AgentUsageResponse, DiffResponse, MessageRequest, ResultResponse,
RetryRequest, TaskEventResponse, TaskListParams, TaskOutputResponse, TaskResponse,
UsageResponse,
};
use super::actions;
use super::api_types::TaskEnrichment;
use super::diff::has_non_empty_diff;
pub async fn list_tasks(
Query(params): Query<TaskListParams>,
State(store): State<Arc<Store>>,
) -> Result<Json<Vec<TaskResponse>>, StatusCode> {
let filter = parse_filter(params.filter.as_deref()).ok_or(StatusCode::BAD_REQUEST)?;
let tasks = store.list_tasks(filter).map_err(internal_error)?;
Ok(Json(enrich_tasks(&store, tasks).map_err(internal_error)?))
}
pub async fn get_task(
Path(id): Path<String>,
State(store): State<Arc<Store>>,
) -> Result<Json<TaskResponse>, StatusCode> {
let task = store.get_task(&id).map_err(internal_error)?.ok_or(StatusCode::NOT_FOUND)?;
let response = enrich_tasks(&store, vec![task])
.map_err(internal_error)?
.into_iter()
.next()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(response))
}
pub async fn get_task_events(
Path(id): Path<String>,
State(store): State<Arc<Store>>,
) -> Result<Json<Vec<TaskEventResponse>>, StatusCode> {
ensure_task_exists(&store, &id)?;
let events = store.get_events(&id).map_err(internal_error)?;
Ok(Json(events.into_iter().map(TaskEventResponse::from).collect()))
}
pub async fn get_task_output(
Path(id): Path<String>,
State(store): State<Arc<Store>>,
) -> Result<Json<TaskOutputResponse>, StatusCode> {
let task = store.get_task(&id).map_err(internal_error)?.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(TaskOutputResponse { output: task_view::read_output(&task) }))
}
pub async fn get_task_result(
Path(id): Path<String>,
State(store): State<Arc<Store>>,
) -> Result<Json<ResultResponse>, StatusCode> {
ensure_task_exists(&store, &id)?;
let path = crate::paths::task_dir(&id).join("result.md");
let result = std::fs::read_to_string(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
})?;
Ok(Json(ResultResponse { result }))
}
pub async fn get_usage(State(store): State<Arc<Store>>) -> Result<Json<UsageResponse>, StatusCode> {
let success_rates = store.agent_success_rates().map_err(internal_error)?;
let avg_costs: HashMap<_, _> = store.agent_avg_costs().map_err(internal_error)?.into_iter().collect();
let agents = success_rates
.into_iter()
.map(|(agent, success_rate, task_count)| AgentUsageResponse {
agent: agent.as_str().to_string(),
success_rate: Some(success_rate),
task_count,
avg_cost: avg_costs.get(&agent).copied(),
})
.collect();
Ok(Json(UsageResponse { agents }))
}
pub async fn stop_task(Path(id): Path<String>, State(store): State<Arc<Store>>) -> impl IntoResponse {
match store.get_task(&id) {
Ok(Some(task)) if task.status.is_terminal() => actions::action_ok(None),
Ok(Some(_)) => actions::action_result(task_actions::stop(&store, &id).map_err(actions::internal)),
Ok(None) => actions::action_error(actions::not_found(format!("Task {id} not found"))),
Err(error) => actions::action_error(actions::internal(error)),
}
}
pub async fn retry_task(
Path(id): Path<String>,
State(store): State<Arc<Store>>,
Json(request): Json<RetryRequest>,
) -> impl IntoResponse {
if let Err(error) = actions::ensure_exists(&store, &id) {
return actions::action_error(error);
}
let args = task_actions::RetryArgs {
task_id: id,
feedback: request.feedback,
feedback_file: None,
agent: None,
model: None,
idle_timeout_secs: None,
dir: None,
reset: false,
bg: false,
};
match task_actions::retry(store, args).await {
Ok(new_task_id) => actions::action_ok(Some(new_task_id.to_string())),
Err(error) => actions::action_error(actions::internal(error)),
}
}
pub async fn merge_task(Path(id): Path<String>, State(store): State<Arc<Store>>) -> impl IntoResponse {
if let Err(error) = actions::ensure_merge_allowed(&store, &id) {
return actions::action_error(error);
}
actions::action_result(task_actions::merge(
store,
task_actions::MergeArgs {
task_id: Some(&id),
group: None,
approve: true,
check: false,
force: false,
target: None,
lanes: false,
},
).map_err(actions::internal))
}
pub async fn steer_task(
Path(id): Path<String>,
State(store): State<Arc<Store>>,
Json(request): Json<MessageRequest>,
) -> impl IntoResponse {
if let Err(error) = actions::ensure_replyable(&store, &id) {
return actions::action_error(error);
}
actions::action_result(cmd::steer::run(&store, &id, &request.message).map_err(actions::internal))
}
pub async fn respond_task(
Path(id): Path<String>,
State(store): State<Arc<Store>>,
Json(request): Json<MessageRequest>,
) -> impl IntoResponse {
if let Err(error) = actions::ensure_replyable(&store, &id) {
return actions::action_error(error);
}
actions::action_result(cmd::respond::run(&store, &id, Some(&request.message), None).map_err(actions::internal))
}
pub async fn accept_task(Path(id): Path<String>, State(store): State<Arc<Store>>) -> impl IntoResponse {
if let Err(error) = actions::ensure_terminal(&store, &id) {
return actions::action_error(error);
}
actions::action_result(crate::artifact_custody::accept(&store, &id, &local_principal()).map_err(actions::internal))
}
pub async fn reject_task(Path(id): Path<String>, State(store): State<Arc<Store>>) -> impl IntoResponse {
if let Err(error) = actions::ensure_terminal(&store, &id) {
return actions::action_error(error);
}
actions::action_result(crate::artifact_custody::reject(&store, &id, &local_principal()).map_err(actions::internal))
}
pub async fn get_task_diff(Path(id): Path<String>, State(store): State<Arc<Store>>) -> impl IntoResponse {
match task_view::diff_text(&store, &id) {
Ok(diff) if diff_unavailable(&diff) => StatusCode::NOT_FOUND.into_response(),
Ok(diff) => (StatusCode::OK, Json(DiffResponse { diff })).into_response(),
Err(error) => internal_error(error).into_response(),
}
}
pub(crate) fn enrich_tasks(store: &Store, tasks: Vec<Task>) -> anyhow::Result<Vec<TaskResponse>> {
let ids: Vec<&str> = tasks.iter().map(|task| task.id.as_str()).collect();
let memory_mb: HashMap<_, _> = tasks
.iter()
.map(|task| (task.id.as_str().to_string(), task_memory_mb(task)))
.collect();
let started_at = store.started_at_batch(&ids)?;
let profiles = store.get_task_profiles_batch(&ids)?;
let milestones = store.latest_milestones_batch(&ids)?;
let mut errors = store.latest_errors_batch(&ids)?;
for (id, error) in store.latest_errors_batch_unfiltered(&ids)? {
errors.entry(id).or_insert(error);
}
let awaiting_ids: Vec<&str> = tasks
.iter()
.filter(|task| task.status == crate::types::TaskStatus::AwaitingInput)
.map(|task| task.id.as_str())
.collect();
let awaiting = store.latest_awaiting_reasons_batch(&awaiting_ids)?;
let events = store.latest_events_three_batch(&ids)?;
Ok(tasks
.into_iter()
.map(|task| {
let id = task.id.as_str().to_string();
let has_diff = has_non_empty_diff(&task);
TaskResponse::from_task(task, TaskEnrichment {
started_at: started_at.get(&id).cloned(),
memory_mb: memory_mb.get(&id).copied().flatten(),
profile: profiles.get(&id).copied().unwrap_or_default(),
latest_milestone: milestones.get(&id).cloned(),
latest_error: errors.get(&id).cloned(),
awaiting_reason: awaiting.get(&id).cloned(),
latest_events: events
.get(&id)
.cloned()
.unwrap_or_default()
.into_iter()
.map(TaskEventResponse::from)
.collect(),
has_diff,
})
})
.collect())
}
pub(crate) fn task_memory_mb(task: &Task) -> Option<i64> {
if !matches!(task.status, crate::types::TaskStatus::Running | crate::types::TaskStatus::AwaitingInput) {
return None;
}
let worker_pid = crate::background::load_worker_pid(task.id.as_str()).ok().flatten()?;
crate::tui::metrics::get_process_metrics(worker_pid)
.map(|metrics| metrics.memory_mb.round() as i64)
}
fn parse_filter(filter: Option<&str>) -> Option<TaskFilter> {
match filter.unwrap_or("today") {
"all" => Some(TaskFilter::All),
"running" => Some(TaskFilter::Running),
"today" => Some(TaskFilter::Today),
_ => None,
}
}
fn ensure_task_exists(store: &Store, id: &str) -> Result<(), StatusCode> {
store.get_task(id).map_err(internal_error)?.ok_or(StatusCode::NOT_FOUND).map(|_| ())
}
fn local_principal() -> String {
std::env::var("USER")
.ok()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "local-principal".to_string())
}
pub(crate) fn internal_error(_: anyhow::Error) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
fn diff_unavailable(diff: &str) -> bool {
diff.contains("(worktree removed or diff unavailable)")
|| diff.contains("(no worktree diff or output file available)")
|| diff.contains("(in-place edit — no uncommitted changes detected, may already be committed)")
}