agent web UI: bulk mark-done for the todos flyout

The per-agent web UI todos flyout (loose-ends v2) had no mark-done
affordance at all — dismissing a todo was only possible via the
cancel_loose_end MCP tool, one id at a time. Add a checkbox per row,
a select-all/select-none/mark-done bulk row, and a new
POST /api/todos/mark-done handler that loops the existing single-id
MarkTodoDone request over the in-agent socket (no new wire request
type needed — the todos list is small, so N same-host round-trips is
cheap).

Fixes #2917
This commit is contained in:
iris 2026-08-01 19:11:46 +02:00 committed by mara
commit 41c1b1a3fb
4 changed files with 119 additions and 7 deletions

View file

@ -1,4 +1,5 @@
//! Operator action POST handlers (send, cancel, compact, model, effort, reset).
//! Operator action POST handlers (send, cancel, compact, model, effort,
//! reset, todos mark-done).
use axum::{
Form,
@ -162,3 +163,41 @@ pub(super) async fn post_set_effort(
tracing::info!(%level, "operator set effort");
(axum::http::StatusCode::OK, "ok").into_response()
}
#[derive(Deserialize)]
pub(super) struct MarkTodosDoneForm {
/// Comma-separated todo ids. Same "one field, JS joins the checked
/// boxes" shape as `hive-c0re`'s `meta_inputs::MetaUpdateForm` — axum's
/// `Form` extractor doesn't natively decode repeated same-name keys.
ids: String,
}
/// `POST /api/todos/mark-done` — dismiss one or more of this agent's own
/// todos (loose-ends v2) from the todos flyout. Loops a `MarkTodoDone` call
/// per id over the in-agent socket rather than adding a new bulk request to
/// `hive-agent-sock`: the todos list is small (single-digit rows most of the
/// time), so N same-host socket round-trips isn't a real cost, and it keeps
/// the wire protocol's `Request` enum — already used by the `cancel_loose_end`
/// MCP tool — unchanged. Unknown/already-acked ids just don't add to the
/// `acked` count (same "acking twice is not a new action" semantics as the
/// single-id path); a request with no ids or where every id fails to parse
/// is rejected as a client error rather than silently acking nothing.
pub(super) async fn post_mark_todos_done(Form(form): Form<MarkTodosDoneForm>) -> Response {
let ids: Vec<i64> = form
.ids
.split(',')
.filter_map(|s| s.trim().parse::<i64>().ok())
.collect();
if ids.is_empty() {
return error_response(StatusCode::BAD_REQUEST, "mark-done: no todo ids selected");
}
let mut acked = 0u64;
for id in ids {
if let Some(hive_agent_sock::Response::Acked { count }) =
crate::todo_server::dial(&hive_agent_sock::Request::MarkTodoDone { id }).await
{
acked += count;
}
}
axum::Json(serde_json::json!({ "acked": acked })).into_response()
}

View file

@ -116,6 +116,7 @@ pub async fn serve(
.route("/api/new-session", post(actions::post_new_session))
.route("/api/logout", post(auth::post_logout))
.route("/api/todos", get(stats::api_todos))
.route("/api/todos/mark-done", post(actions::post_mark_todos_done))
.route("/api/stats", get(stats::api_stats))
.route("/screen/ws", get(screen::screen_ws))
.route("/icon", get(screen::serve_icon));