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);
|
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);
|
li.append(icon, body);
|
||||||
ul.append(li);
|
ul.append(li);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,22 @@ use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME};
|
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)]
|
#[derive(Serialize, Clone, PartialEq, Eq, Debug)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct ContainerView {
|
pub struct ContainerView {
|
||||||
|
|
@ -50,6 +61,13 @@ pub struct ContainerView {
|
||||||
/// removes when it resumes. Stale by up to one crash-watch cycle.
|
/// removes when it resumes. Stale by up to one crash-watch cycle.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub rate_limited: bool,
|
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
|
/// 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);
|
.unwrap_or(0);
|
||||||
let ctx_tokens = read_last_ctx_tokens(&logical);
|
let ctx_tokens = read_last_ctx_tokens(&logical);
|
||||||
let rate_limited = is_rate_limited(&logical);
|
let rate_limited = is_rate_limited(&logical);
|
||||||
|
let extra_links = read_dashboard_links(&logical);
|
||||||
out.push(ContainerView {
|
out.push(ContainerView {
|
||||||
port: lifecycle::agent_web_port(&logical),
|
port: lifecycle::agent_web_port(&logical),
|
||||||
running: lifecycle::is_running(&logical).await,
|
running: lifecycle::is_running(&logical).await,
|
||||||
|
|
@ -99,6 +118,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
||||||
pending_reminders,
|
pending_reminders,
|
||||||
ctx_tokens,
|
ctx_tokens,
|
||||||
rate_limited,
|
rate_limited,
|
||||||
|
extra_links,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
out
|
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()))
|
.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
|
/// Returns true if the agent's harness is currently parked after an API
|
||||||
/// rate-limit response. Detected via the sentinel file written by
|
/// rate-limit response. Detected via the sentinel file written by
|
||||||
/// `hive_ag3nt::events::Bus::emit_status("rate_limited")`.
|
/// `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 {
|
options.hyperhive.claudeMarketplaces = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ "anthropics/claude-plugins-official" ];
|
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
|
# 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.
|
# request flow) and by hive-c0re's own setup_applied / setup_proposed.
|
||||||
# The per-agent `applied/<name>/flake.nix` overrides `user.name` and
|
# The per-agent `applied/<name>/flake.nix` overrides `user.name` and
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue