harness+dashboard: declarative dashboardLinks option (closes #191)

This commit is contained in:
damocles 2026-05-21 23:06:44 +02:00 committed by Mara
parent f510a321df
commit 66c481a07a
3 changed files with 109 additions and 1 deletions

View file

@ -9,11 +9,22 @@ use std::collections::HashMap;
use std::path::Path;
use rusqlite::Connection;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::coordinator::Coordinator;
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME};
/// An agent-declared extra navigation link surfaced on the dashboard card.
/// Written by the `hive-dashboard-links` NixOS oneshot into
/// `{state_dir}/hyperhive-dashboard-links.json` and read by `build_all`.
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Default)]
pub struct DashboardLink {
pub label: String,
#[serde(default)]
pub icon: String,
pub url: String,
}
#[derive(Serialize, Clone, PartialEq, Eq, Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct ContainerView {
@ -50,6 +61,13 @@ pub struct ContainerView {
/// removes when it resumes. Stale by up to one crash-watch cycle.
#[serde(default)]
pub rate_limited: bool,
/// Extra navigation links declared by the agent via
/// `hyperhive.dashboardLinks` in `agent.nix`. Written to
/// `{state_dir}/hyperhive-dashboard-links.json` by the
/// `hive-dashboard-links` oneshot at container boot. Empty when
/// the file is absent or the agent declares no links.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extra_links: Vec<DashboardLink>,
}
/// Build the full container list. Wraps `lifecycle::list()` and
@ -87,6 +105,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
.unwrap_or(0);
let ctx_tokens = read_last_ctx_tokens(&logical);
let rate_limited = is_rate_limited(&logical);
let extra_links = read_dashboard_links(&logical);
out.push(ContainerView {
port: lifecycle::agent_web_port(&logical),
running: lifecycle::is_running(&logical).await,
@ -99,6 +118,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
pending_reminders,
ctx_tokens,
rate_limited,
extra_links,
});
}
out
@ -117,6 +137,18 @@ pub fn claude_has_session(dir: &Path) -> bool {
.any(|e| e.file_type().is_ok_and(|t| t.is_file()))
}
/// Read agent-declared extra dashboard links from
/// `{state_dir}/hyperhive-dashboard-links.json`. Returns an empty vec when
/// the file is absent, empty, or unparseable — best-effort, never panics.
fn read_dashboard_links(name: &str) -> Vec<DashboardLink> {
let path = Coordinator::agent_notes_dir(name).join("hyperhive-dashboard-links.json");
let text = match std::fs::read_to_string(&path) {
Ok(t) if !t.trim().is_empty() => t,
_ => return Vec::new(),
};
serde_json::from_str::<Vec<DashboardLink>>(&text).unwrap_or_default()
}
/// Returns true if the agent's harness is currently parked after an API
/// rate-limit response. Detected via the sentinel file written by
/// `hive_ag3nt::events::Bus::emit_status("rate_limited")`.