From ceb852e5b4afda3d753ad5cf70e162512efbf147 Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 19 Jun 2026 12:38:59 +0200 Subject: [PATCH] hive-ag3nt: add GET /api/bash-tasks endpoint for the agent page Snapshot of the agent's in-flight bash tasks: reads the in-container bash-tasks/ dir (the co-located hive-bash-mcp daemon writes one TaskFile JSON per task), deserializes the canonical hive_sh4re::TaskFile, filters to Pending/Running, and returns them running-first then oldest-first. Skips unreadable/malformed files (and the daemon's .json.tmp scratch writes) so a stray file can't fail the list. Snapshot-only for v1; the page polls it like /api/loose-ends, SSE live-push is a possible follow-up. --- hive-ag3nt/src/web_ui.rs | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index f52c2dae..fe68ea69 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -123,6 +123,7 @@ pub async fn serve( .route("/api/new-session", post(post_new_session)) .route("/api/logout", post(post_logout)) .route("/api/loose-ends", get(api_loose_ends)) + .route("/api/bash-tasks", get(api_bash_tasks)) .route("/api/stats", get(api_stats)) .route("/screen/ws", get(screen_ws)) .route("/icon", get(serve_icon)) @@ -525,6 +526,53 @@ async fn api_loose_ends(State(state): State) -> Response { axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response() } +/// `GET /api/bash-tasks` — snapshot of this agent's in-flight bash tasks. +/// +/// The `hive-bash-mcp` daemon runs in this same container and writes one +/// `.json` ([`hive_sh4re::TaskFile`]) per task under the harness +/// `bash-tasks/` dir. This reads that dir and returns the tasks still +/// `Pending` or `Running`, so the agent page can show what's running without +/// going through the broker. Snapshot only — the page polls/refreshes it like +/// `/api/loose-ends`; there's no live SSE push for task state yet. Unreadable +/// or malformed files (incl. the daemon's `.json.tmp` scratch writes, which +/// don't match the `.json` extension) are skipped so one stray file can't +/// fail the whole list. +async fn api_bash_tasks() -> Response { + let dir = crate::paths::harness_dir().join("bash-tasks"); + let mut tasks: Vec = Vec::new(); + if let Ok(rd) = std::fs::read_dir(&dir) { + for entry in rd.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("json") { + continue; + } + let Ok(text) = std::fs::read_to_string(&path) else { + continue; + }; + let Ok(task) = serde_json::from_str::(&text) else { + continue; + }; + if matches!( + task.status, + hive_sh4re::TaskStatus::Pending | hive_sh4re::TaskStatus::Running + ) { + tasks.push(task); + } + } + } + // Running before Pending, then oldest-first so a long-runner sits on top. + tasks.sort_by(|a, b| { + let rank = |s: &hive_sh4re::TaskStatus| match s { + hive_sh4re::TaskStatus::Running => 0, + _ => 1, + }; + rank(&a.status) + .cmp(&rank(&b.status)) + .then(a.created_at.cmp(&b.created_at)) + }); + axum::Json(serde_json::json!({ "tasks": tasks })).into_response() +} + async fn api_state(State(state): State) -> axum::Json { // Capture seq *before* any reads so the dedupe contract is // "events with seq > snapshot.seq are post-snapshot, never missed."