From 0884a5496015472afad97972a7c12fc1cd06a59e Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 20:49:34 +0200 Subject: [PATCH] agent page: backend-supplied links list replaces hardcoded stats/screen/forge (closes #189) --- hive-ag3nt/assets/app.js | 21 ++++------ hive-ag3nt/assets/index.html | 9 +---- hive-ag3nt/src/web_ui.rs | 75 +++++++++++++++++++++++------------- 3 files changed, 58 insertions(+), 47 deletions(-) diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index 4bc8d3e..2ff419a 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -674,19 +674,14 @@ const s = await resp.json(); if (!headerSet) { setHeader(s.label, s.dashboard_port); headerSet = true; } currentLabel = s.label; - // Show the screen link when the weston VNC compositor is enabled. - const screenLink = $('screen-link'); - if (screenLink) screenLink.style.display = s.gui_enabled ? '' : 'none'; - // Forge profile link — the backend hands us the full URL (or - // null when this agent has no forge account). - const forgeLink = $('forge-link'); - if (forgeLink) { - if (s.forge_url) { - forgeLink.href = s.forge_url; - forgeLink.style.display = ''; - } else { - forgeLink.style.display = 'none'; - } + // Render server-supplied navigation links (stats, screen, forge, extras). + const metaLinks = $('meta-links'); + if (metaLinks && s.links) { + metaLinks.innerHTML = (s.links || []).map((lnk, i) => { + const margin = i === 0 ? '' : ' margin-left: 1em;'; + const target = lnk.url.startsWith('http') ? ' target="_blank" rel="noopener"' : ''; + return `${lnk.icon} ${lnk.label} →`; + }).join(''); } renderTermInput(s.label, s.status === 'online'); renderInbox(s.inbox || []); diff --git a/hive-ag3nt/assets/index.html b/hive-ag3nt/assets/index.html index c38ef60..8721ef9 100644 --- a/hive-ag3nt/assets/index.html +++ b/hive-ag3nt/assets/index.html @@ -12,14 +12,7 @@

◆ … ◆

-

- 📊 stats → - - -

+

loading…

diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index e49ab10..53e3148 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -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, - /// 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, + /// 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, +} + +/// 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) -> 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 { - 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 { + 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