harness+dashboard: declarative dashboardLinks option (closes #191)
This commit is contained in:
parent
f510a321df
commit
66c481a07a
3 changed files with 109 additions and 1 deletions
|
|
@ -723,6 +723,21 @@
|
|||
}
|
||||
body.append(drill);
|
||||
|
||||
// ── extra agent-declared links ───────────────────────────────
|
||||
if (c.extra_links && c.extra_links.length > 0) {
|
||||
const extras = el('div', { class: 'drill-ins drill-ins-extra' });
|
||||
for (const lnk of c.extra_links) {
|
||||
extras.append(el('a', {
|
||||
class: 'panel-trigger',
|
||||
href: lnk.url,
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
title: lnk.url,
|
||||
}, (lnk.icon ? lnk.icon + ' ' : '') + lnk.label + ' ↗'));
|
||||
}
|
||||
body.append(extras);
|
||||
}
|
||||
|
||||
li.append(icon, body);
|
||||
ul.append(li);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")`.
|
||||
|
|
|
|||
|
|
@ -173,6 +173,39 @@
|
|||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.dashboardLinks = lib.mkOption {
|
||||
type = lib.types.listOf (lib.types.submodule {
|
||||
options = {
|
||||
label = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Display label for the link.";
|
||||
};
|
||||
icon = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
description = "Optional icon emoji or short glyph.";
|
||||
};
|
||||
url = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Full URL (may include a different port, e.g. http://localhost:9001/stats).";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = [ ];
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
{ label = "Stats"; icon = "📊"; url = "http://localhost:9001/stats"; }
|
||||
]
|
||||
'';
|
||||
description = ''
|
||||
Extra navigation links surfaced on the hive-c0re dashboard card for
|
||||
this agent. Declare any additional web UI pages the agent exposes —
|
||||
stats pages, custom UIs, etc. hive-c0re reads the JSON file this
|
||||
option produces at each container-view snapshot and attaches the
|
||||
links to the agent card without any code changes.
|
||||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.claudeMarketplaces = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "anthropics/claude-plugins-official" ];
|
||||
|
|
@ -480,6 +513,34 @@
|
|||
'';
|
||||
};
|
||||
|
||||
# Write declared dashboardLinks to the state dir so hive-c0re can read
|
||||
# them without accessing the container's /etc/ from the host.
|
||||
# Runs every boot; idempotent (overwrite). Always exits 0.
|
||||
systemd.services.hive-dashboard-links = lib.mkIf (config.hyperhive.dashboardLinks != [ ]) {
|
||||
description = "write declarative dashboardLinks to agent state dir";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
environment.LINKS_JSON = builtins.toJSON config.hyperhive.dashboardLinks;
|
||||
script = ''
|
||||
STATE_DIR=""
|
||||
for d in /state /agents/*/state; do
|
||||
if [ -d "$d" ]; then
|
||||
STATE_DIR="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$STATE_DIR" ]; then
|
||||
echo "hive-dashboard-links: no state dir found; skipping"
|
||||
exit 0
|
||||
fi
|
||||
printf '%s' "$LINKS_JSON" > "$STATE_DIR/hyperhive-dashboard-links.json"
|
||||
echo "hive-dashboard-links: wrote $(printf '%s' "$LINKS_JSON" | wc -c) bytes to $STATE_DIR/hyperhive-dashboard-links.json"
|
||||
'';
|
||||
};
|
||||
|
||||
# Git is needed by claude's Bash tool (for the agent <-> manager config
|
||||
# request flow) and by hive-c0re's own setup_applied / setup_proposed.
|
||||
# The per-agent `applied/<name>/flake.nix` overrides `user.name` and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue