agent page: backend-supplied links list replaces hardcoded stats/screen/forge (closes #189)

This commit is contained in:
damocles 2026-05-21 20:49:34 +02:00
parent a8ab91ecd8
commit 0884a54960
3 changed files with 58 additions and 47 deletions

View file

@ -379,15 +379,21 @@ struct StateSnapshot {
/// Cumulative token usage across the most recent turn's inferences
/// (cost signal). `null` until the first turn finishes.
cost_usage: Option<crate::events::TokenUsage>,
/// Whether the weston VNC compositor is configured for this agent
/// (i.e. `/etc/hyperhive/gui.json` was present at harness startup).
/// When true, the UI may render a `🖥 screen` link to `/screen`.
gui_enabled: bool,
/// Full URL of this agent's Forgejo profile, or `null` when the
/// agent has no forge account (`forge-token` absent). Assembled
/// server-side from the request `Host` header so the UI just
/// links it — no client-side URL construction.
forge_url: Option<String>,
/// Navigation links for this agent page. Always includes at least
/// the stats link; conditionally includes screen (gui enabled) and
/// forge (agent has account). Extra agent-specific links land here
/// once the declarative `hyperhive.dashboardLinks` option (#191)
/// ships. Frontend renders only the entries present — no client-side
/// URL construction or icon assignment.
links: Vec<AgentLink>,
}
/// One navigation link in the agent page header row.
#[derive(Serialize)]
struct AgentLink {
url: String,
icon: String,
label: String,
}
#[derive(Serialize)]
@ -489,27 +495,44 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
context_window_tokens,
ctx_usage,
cost_usage,
gui_enabled: state.gui_vnc_port.is_some(),
forge_url: forge_profile_url(&headers, &state.label),
links: agent_links(&headers, &state.label, state.gui_vnc_port.is_some()),
})
}
/// This agent's Forgejo profile URL — `Some` only when the agent has a
/// forge account (its `forge-token` file exists). The host is taken
/// from the request `Host` header so the link resolves against
/// whatever host the operator reached the page on; the forge always
/// listens on `:3000`, and the per-agent forge user is named after
/// the agent (`label`).
fn forge_profile_url(headers: &HeaderMap, label: &str) -> Option<String> {
if !crate::paths::state_dir().join("forge-token").is_file() {
return None;
/// Build the ordered list of navigation links for the agent page header.
/// Stats is always present. Screen is included when the VNC compositor
/// is enabled. Forge is included when the agent has a forge account.
fn agent_links(headers: &HeaderMap, label: &str, gui_enabled: bool) -> Vec<AgentLink> {
let mut links = Vec::new();
links.push(AgentLink {
url: "/stats".to_owned(),
icon: "📊".to_owned(),
label: "stats".to_owned(),
});
if gui_enabled {
links.push(AgentLink {
url: "/screen".to_owned(),
icon: "🖥".to_owned(),
label: "screen".to_owned(),
});
}
let host = headers
.get("host")
.and_then(|h| h.to_str().ok())
.unwrap_or("localhost");
let hostname = host.split(':').next().unwrap_or(host);
Some(format!("http://{hostname}:3000/{label}"))
if crate::paths::state_dir().join("forge-token").is_file() {
let host = headers
.get("host")
.and_then(|h| h.to_str().ok())
.unwrap_or("localhost");
let hostname = host.split(':').next().unwrap_or(host);
links.push(AgentLink {
url: format!("http://{hostname}:3000/{label}"),
icon: "".to_owned(),
label: "forge".to_owned(),
});
}
links
}
/// Best-effort: pull the last 30 messages addressed to us via the