diff --git a/docs/web-ui/agent.md b/docs/web-ui/agent.md index e69c76a1..e681625e 100644 --- a/docs/web-ui/agent.md +++ b/docs/web-ui/agent.md @@ -383,10 +383,10 @@ shaped). earliest recorded turn with an adaptive bucket width. - `GET /icon` — agent's icon as `image/svg+xml`. Returns `/etc/hyperhive/icon.svg` (set via `hyperhive.icon` in `agent.nix`) - when present, otherwise the bundled default hyperhive logo. Always - returns an image — consumers (dashboard container row, per-agent - favicon) can hit `/icon` unconditionally without probing for a - custom config. + when present, otherwise **404** — there is no server-side default. + Consumers (dashboard container row, this page's own header icon) + hit `/icon` optimistically and fall back client-side on load failure + to the frontend-bundled `/favicon.svg` rather than probing first. - `GET /events/history` — replay buffer for the terminal. - `GET /screen` — VNC viewer page (minimal RFB-over-WebSocket renderer — deliberately thin, just enough to display the diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 6a66e6bf..c064a404 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -38,6 +38,22 @@ window.marked = marked; // native confirm()/alert() dialogs that broke out of the page theme. bindAsyncForms(() => refreshState()); + // ─── header icon fallback ─────────────────────────────────────────────── + // `/icon` 404s when this agent has no `hyperhive.icon` override (see + // hive-agent::web_ui::screen::serve_icon — no bundled server-side + // default any more). Mirrors the dashboard's `swarm.js` `/favicon.svg` + // fallback: fire-and-forget load, swap on failure, guarded so a 404 on + // the fallback itself can't loop. + (function bindHeaderIconFallback() { + const iconImg = document.querySelector('.agent-icon'); + if (!iconImg) return; + iconImg.addEventListener('error', () => { + if (iconImg.dataset.fallback) return; + iconImg.dataset.fallback = '1'; + iconImg.src = '/favicon.svg'; + }); + })(); + // ─── side panel (singleton drawer for inbox + loose-ends flyouts) ────── // The shared `` element (see @hive/shared/side-panel.js // for the chrome/behavior it owns), created once, eagerly, when this diff --git a/hive-agent/src/web_ui/screen.rs b/hive-agent/src/web_ui/screen.rs index e8a9babc..bb989566 100644 --- a/hive-agent/src/web_ui/screen.rs +++ b/hive-agent/src/web_ui/screen.rs @@ -11,20 +11,18 @@ use super::AppState; /// This agent's icon. Serves the operator-configured SVG from /// `/etc/hyperhive/icon.svg` (set via the `hyperhive.icon` agent.nix -/// option) when present, otherwise the bundled default hyperhive logo. -/// Always returns an image, so consumers (dashboard, favicon) can hit -/// `/icon` unconditionally without probing whether one is configured. -pub(super) async fn serve_icon() -> impl IntoResponse { - // Per-agent icon overrides go through `/etc/hyperhive/icon.svg` - // (set via the `hyperhive.icon` agent.nix option); the bundled - // default is resolved at runtime from - // `$HIVE_ASSETS_DIR/branding/hyperhive.svg`. If neither file can - // be read we serve an empty body — keeps the response a valid SVG - // content-type without a panic on a misconfigured container. - let body = std::fs::read_to_string("/etc/hyperhive/icon.svg").unwrap_or_else(|_| { - std::fs::read_to_string(hive_sh4re::assets::branding_svg()).unwrap_or_default() - }); - ([("content-type", "image/svg+xml")], body) +/// option) when present, otherwise **404** — there is no bundled +/// server-side default any more (that was `hive_sh4re::assets:: +/// branding_svg`, now removed). Consumers fall back client-side: the +/// dashboard's `swarm.js` and this agent's own `app.js` both swap an +/// `/icon` load failure to the frontend-bundled `/favicon.svg` rather +/// than probing first, so a 404 here is the expected "unconfigured" +/// signal, not an error case to work around. +pub(super) async fn serve_icon() -> Response { + match std::fs::read_to_string("/etc/hyperhive/icon.svg") { + Ok(body) => ([("content-type", "image/svg+xml")], body).into_response(), + Err(_) => (StatusCode::NOT_FOUND, "no icon configured").into_response(), + } } /// WebSocket handler: upgrade then pump bytes between the WS client and diff --git a/hive-c0re/src/forge/users.rs b/hive-c0re/src/forge/users.rs index 23ce88ac..cbd8ea20 100644 --- a/hive-c0re/src/forge/users.rs +++ b/hive-c0re/src/forge/users.rs @@ -27,7 +27,9 @@ use crate::paths::FORGE_CORE_TOKEN as CORE_TOKEN_PATH; // Avatar PNG paths are resolved by `core_avatar_png_path` / // `config_org_avatar_png_path` below — only-used-here, so they live // in this module rather than the shared `hive_sh4re::assets` helpers -// (which stay for paths every crate needs, e.g. `branding_svg`). +// (which now hold only `prompt_template`, the one asset path every +// crate needs — the agent icon, the last other one, moved off the +// shared-asset model entirely; see `hive-agent::web_ui::screen`). /// `$HIVE_ASSETS_DIR/branding/hyperhive.png` — the core mark, /// rasterised. Not independently configurable (unlike the org avatar diff --git a/hive-sh4re/src/assets.rs b/hive-sh4re/src/assets.rs index 7db91b07..9802a2de 100644 --- a/hive-sh4re/src/assets.rs +++ b/hive-sh4re/src/assets.rs @@ -16,10 +16,13 @@ //! layout. //! //! Each crate that wants a specific asset goes through one of the -//! typed helpers (`branding_svg()`, `prompt_template()`, …) so the -//! lookup contract is centralised. Missing files panic at first -//! call with a clear "set `HIVE_ASSETS_DIR` + put the file at …" -//! message. +//! typed helpers (currently just `prompt_template()` — the branding +//! SVG used to live here too; the per-agent icon now 404s server-side +//! when unconfigured and falls back to a frontend-bundled default +//! client-side instead of a server-resolved runtime path, see +//! `hive-agent::web_ui::screen::serve_icon`) so the lookup contract is +//! centralised. Missing files panic at first call with a clear "set +//! `HIVE_ASSETS_DIR` + put the file at …" message. use std::path::PathBuf; @@ -41,15 +44,6 @@ fn dir() -> PathBuf { } } -/// `$HIVE_ASSETS_DIR/branding/hyperhive.svg` — the project's primary -/// mark. Loaded by the per-agent web UI as its default icon -/// (`/agents//icon.svg` falls through here when the agent didn't -/// override `hyperhive.icon` in its `agent.nix`). -#[must_use] -pub fn branding_svg() -> PathBuf { - dir().join("branding/hyperhive.svg") -} - /// `$HIVE_ASSETS_DIR/prompts/system.md` — the claude system prompt /// template. `hive-agent::prompt::render` filters the role markers /// inside it per agent / manager flavor. diff --git a/nix/agent-modules/default.nix b/nix/agent-modules/default.nix index 653fa20d..fb49e100 100644 --- a/nix/agent-modules/default.nix +++ b/nix/agent-modules/default.nix @@ -55,9 +55,10 @@ the SVG into the agent's config repo next to `agent.nix` and reference it as a relative path (`./icon.svg`). - When null (the default) the agent falls back to the shared - hyperhive logo. The harness serves the icon (configured or - default) at `GET /icon` on the per-agent web port. + When null (the default), `GET /icon` on the per-agent web port + 404s and consumers (dashboard, this agent's own page header) + fall back client-side to the frontend-bundled `/favicon.svg` + rather than a server-resolved default. ''; }; @@ -113,9 +114,9 @@ # Operator-set per-agent icon (hyperhive.icon). When configured, the # SVG lands at /etc/hyperhive/icon.svg; the harness serves it at - # GET /icon, falling back to the bundled hyperhive logo when absent. - # Consumed by forge-avatar-sync (./forge.nix) and the matrix avatar - # sync (./matrix.nix) too. + # GET /icon, 404ing when absent (client-side fallback, no + # server-side default). Consumed by forge-avatar-sync (./forge.nix) + # and the matrix avatar sync (./matrix.nix) too. environment.etc."hyperhive/icon.svg" = lib.mkIf (config.hyperhive.icon != null) { source = config.hyperhive.icon; };