agent page: backend-supplied links list replaces hardcoded stats/screen/forge (closes #189)
This commit is contained in:
parent
a8ab91ecd8
commit
0884a54960
3 changed files with 58 additions and 47 deletions
|
|
@ -674,19 +674,14 @@
|
||||||
const s = await resp.json();
|
const s = await resp.json();
|
||||||
if (!headerSet) { setHeader(s.label, s.dashboard_port); headerSet = true; }
|
if (!headerSet) { setHeader(s.label, s.dashboard_port); headerSet = true; }
|
||||||
currentLabel = s.label;
|
currentLabel = s.label;
|
||||||
// Show the screen link when the weston VNC compositor is enabled.
|
// Render server-supplied navigation links (stats, screen, forge, extras).
|
||||||
const screenLink = $('screen-link');
|
const metaLinks = $('meta-links');
|
||||||
if (screenLink) screenLink.style.display = s.gui_enabled ? '' : 'none';
|
if (metaLinks && s.links) {
|
||||||
// Forge profile link — the backend hands us the full URL (or
|
metaLinks.innerHTML = (s.links || []).map((lnk, i) => {
|
||||||
// null when this agent has no forge account).
|
const margin = i === 0 ? '' : ' margin-left: 1em;';
|
||||||
const forgeLink = $('forge-link');
|
const target = lnk.url.startsWith('http') ? ' target="_blank" rel="noopener"' : '';
|
||||||
if (forgeLink) {
|
return `<a href="${lnk.url}"${target} style="color: var(--cyan); text-decoration: none;${margin}">${lnk.icon} ${lnk.label} →</a>`;
|
||||||
if (s.forge_url) {
|
}).join('');
|
||||||
forgeLink.href = s.forge_url;
|
|
||||||
forgeLink.style.display = '';
|
|
||||||
} else {
|
|
||||||
forgeLink.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
renderTermInput(s.label, s.status === 'online');
|
renderTermInput(s.label, s.status === 'online');
|
||||||
renderInbox(s.inbox || []);
|
renderInbox(s.inbox || []);
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,7 @@
|
||||||
<img class="agent-icon" src="/icon" alt="">
|
<img class="agent-icon" src="/icon" alt="">
|
||||||
<h2 id="title">◆ … ◆</h2>
|
<h2 id="title">◆ … ◆</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="meta">
|
<p class="meta" id="meta-links"></p>
|
||||||
<a href="/stats" style="color: var(--cyan); text-decoration: none;">📊 stats →</a>
|
|
||||||
<a id="screen-link" href="/screen" target="_blank" rel="noopener"
|
|
||||||
style="display:none; color: var(--cyan); text-decoration: none; margin-left: 1em;">🖥 screen →</a>
|
|
||||||
<a id="forge-link" href="#" target="_blank" rel="noopener"
|
|
||||||
title="this agent's profile on the hive forge"
|
|
||||||
style="display:none; color: var(--cyan); text-decoration: none; margin-left: 1em;">⬡ forge ↗</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="status">
|
<div id="status">
|
||||||
<p class="meta">loading…</p>
|
<p class="meta">loading…</p>
|
||||||
|
|
|
||||||
|
|
@ -379,15 +379,21 @@ struct StateSnapshot {
|
||||||
/// Cumulative token usage across the most recent turn's inferences
|
/// Cumulative token usage across the most recent turn's inferences
|
||||||
/// (cost signal). `null` until the first turn finishes.
|
/// (cost signal). `null` until the first turn finishes.
|
||||||
cost_usage: Option<crate::events::TokenUsage>,
|
cost_usage: Option<crate::events::TokenUsage>,
|
||||||
/// Whether the weston VNC compositor is configured for this agent
|
/// Navigation links for this agent page. Always includes at least
|
||||||
/// (i.e. `/etc/hyperhive/gui.json` was present at harness startup).
|
/// the stats link; conditionally includes screen (gui enabled) and
|
||||||
/// When true, the UI may render a `🖥 screen` link to `/screen`.
|
/// forge (agent has account). Extra agent-specific links land here
|
||||||
gui_enabled: bool,
|
/// once the declarative `hyperhive.dashboardLinks` option (#191)
|
||||||
/// Full URL of this agent's Forgejo profile, or `null` when the
|
/// ships. Frontend renders only the entries present — no client-side
|
||||||
/// agent has no forge account (`forge-token` absent). Assembled
|
/// URL construction or icon assignment.
|
||||||
/// server-side from the request `Host` header so the UI just
|
links: Vec<AgentLink>,
|
||||||
/// links it — no client-side URL construction.
|
}
|
||||||
forge_url: Option<String>,
|
|
||||||
|
/// One navigation link in the agent page header row.
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AgentLink {
|
||||||
|
url: String,
|
||||||
|
icon: String,
|
||||||
|
label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -489,27 +495,44 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
||||||
context_window_tokens,
|
context_window_tokens,
|
||||||
ctx_usage,
|
ctx_usage,
|
||||||
cost_usage,
|
cost_usage,
|
||||||
gui_enabled: state.gui_vnc_port.is_some(),
|
links: agent_links(&headers, &state.label, state.gui_vnc_port.is_some()),
|
||||||
forge_url: forge_profile_url(&headers, &state.label),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This agent's Forgejo profile URL — `Some` only when the agent has a
|
/// Build the ordered list of navigation links for the agent page header.
|
||||||
/// forge account (its `forge-token` file exists). The host is taken
|
/// Stats is always present. Screen is included when the VNC compositor
|
||||||
/// from the request `Host` header so the link resolves against
|
/// is enabled. Forge is included when the agent has a forge account.
|
||||||
/// whatever host the operator reached the page on; the forge always
|
fn agent_links(headers: &HeaderMap, label: &str, gui_enabled: bool) -> Vec<AgentLink> {
|
||||||
/// listens on `:3000`, and the per-agent forge user is named after
|
let mut links = Vec::new();
|
||||||
/// the agent (`label`).
|
|
||||||
fn forge_profile_url(headers: &HeaderMap, label: &str) -> Option<String> {
|
links.push(AgentLink {
|
||||||
if !crate::paths::state_dir().join("forge-token").is_file() {
|
url: "/stats".to_owned(),
|
||||||
return None;
|
icon: "📊".to_owned(),
|
||||||
|
label: "stats".to_owned(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if gui_enabled {
|
||||||
|
links.push(AgentLink {
|
||||||
|
url: "/screen".to_owned(),
|
||||||
|
icon: "🖥".to_owned(),
|
||||||
|
label: "screen".to_owned(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if crate::paths::state_dir().join("forge-token").is_file() {
|
||||||
let host = headers
|
let host = headers
|
||||||
.get("host")
|
.get("host")
|
||||||
.and_then(|h| h.to_str().ok())
|
.and_then(|h| h.to_str().ok())
|
||||||
.unwrap_or("localhost");
|
.unwrap_or("localhost");
|
||||||
let hostname = host.split(':').next().unwrap_or(host);
|
let hostname = host.split(':').next().unwrap_or(host);
|
||||||
Some(format!("http://{hostname}:3000/{label}"))
|
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
|
/// Best-effort: pull the last 30 messages addressed to us via the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue