feat(#2632): switch agent page loose-ends pill to todos; remove bash-tasks pill

- Add GET /api/todos backend endpoint (connects to HIVE_AGENT_SOCKET,
  calls ListTodos, returns { todos: [...] } — LooseEnd::Todo items).
- Register route in web_ui/mod.rs.
- index.html: replace loose-ends pill (🪢) + bash-tasks pill (⚙)
  with a single todos pill (📋, id=todos-pill).
- agent.css: add .header-pill-todos count colour (green, same as old tasks).
- app.js:
  - refreshTodos() fetches /api/todos, drives renderTodos/buildTodosList.
  - refreshLooseEnds() becomes background-only (no pill); still drives
    reconcileAskBinds for inline ask-form wiring.
  - Remove refreshBashTasks / buildBashTasksList / renderBashTasks.
  - Cold-load and turn_end both call refreshTodos; 4s interval replaces
    the old bash-tasks interval.
This commit is contained in:
iris 2026-07-22 17:30:01 +02:00 committed by mara
commit 4905b688be
5 changed files with 107 additions and 97 deletions

View file

@ -117,6 +117,7 @@ pub async fn serve(
.route("/api/logout", post(auth::post_logout))
.route("/api/loose-ends", get(stats::api_loose_ends))
.route("/api/bash-tasks", get(stats::api_bash_tasks))
.route("/api/todos", get(stats::api_todos))
.route("/api/stats", get(stats::api_stats))
.route("/screen/ws", get(screen::screen_ws))
.route("/icon", get(screen::serve_icon));

View file

@ -1,4 +1,4 @@
//! Stats + loose-ends + bash-tasks read endpoints.
//! Stats + loose-ends + bash-tasks + todos read endpoints.
use axum::extract::State;
use axum::http::StatusCode;
@ -130,3 +130,45 @@ pub(super) async fn api_bash_tasks() -> Response {
.unwrap_or_default();
axum::Json(serde_json::json!({ "tasks": tasks })).into_response()
}
/// `GET /api/todos` — snapshot of this agent's local todos (loose-ends v2).
///
/// Connects to the in-agent harness socket (`HIVE_AGENT_SOCKET`) and calls
/// `ListTodos`. Returns `{ "todos": [...] }` where each entry is a
/// `LooseEnd::Todo` (subsystem, key, summary, source, `age_seconds`). Returns
/// an empty array when the socket is unavailable — best-effort, same
/// silent-failure contract as `/api/bash-tasks`.
pub(super) async fn api_todos() -> Response {
use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _, BufReader};
use tokio::net::UnixStream;
let socket_path = match std::env::var_os("HIVE_AGENT_SOCKET") {
Some(p) => std::path::PathBuf::from(p),
None => return axum::Json(serde_json::json!({ "todos": [] })).into_response(),
};
if !socket_path.exists() {
return axum::Json(serde_json::json!({ "todos": [] })).into_response();
}
let todos = tokio::time::timeout(std::time::Duration::from_secs(3), async move {
let mut stream = UnixStream::connect(&socket_path).await?;
let req = hive_agent_sock::Request::ListTodos { subsystem: None };
let mut line = serde_json::to_string(&req)?;
line.push('\n');
stream.write_all(line.as_bytes()).await?;
stream.flush().await?;
let mut lines = BufReader::new(stream).lines();
let resp_line = lines
.next_line()
.await?
.ok_or_else(|| anyhow::anyhow!("agent socket closed without response"))?;
let resp: hive_agent_sock::Response = serde_json::from_str(&resp_line)?;
anyhow::Ok(match resp {
hive_agent_sock::Response::LooseEnds { loose_ends } => loose_ends,
_ => Vec::new(),
})
})
.await
.unwrap_or_else(|_| Err(anyhow::anyhow!("timeout")))
.unwrap_or_default();
axum::Json(serde_json::json!({ "todos": todos })).into_response()
}