diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 15eee72b..11dc54fe 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -34,6 +34,10 @@ An agent's terminal opens with its last 100 rows from the past 24 hours, then follows new ones; a reconnect picks up after the last row it showed. A subagent terminal shows rows published while it's open, and has no turn-state badges. +A terminal's own tab takes the agent's icon as its browser-tab icon, so tabs of +different agents tell apart. A subagent's tab shows its agent's icon. An agent +with no icon keeps the hyperhive mark. + ### Scheduled prompts `/schedules` has one card per scheduled prompt. The **name** is the card's diff --git a/frontend/packages/swarm-ui/src/pages/agents/AgentTerminalPage.tsx b/frontend/packages/swarm-ui/src/pages/agents/AgentTerminalPage.tsx index cebaaad6..8fe9989d 100644 --- a/frontend/packages/swarm-ui/src/pages/agents/AgentTerminalPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/agents/AgentTerminalPage.tsx @@ -12,6 +12,7 @@ import { useEffect } from "preact/hooks"; import type { RouteComponentProps } from "wouter-preact"; import { AgentTermPreview, termPaths } from "./AgentTermPreview.js"; +import { useAgentFavicon } from "../../shell/useAgentFavicon.js"; import { useDynamicTabs } from "../../shell/useDynamicTabs.js"; import "./AgentTerminalPage.css"; @@ -27,6 +28,8 @@ export function AgentTerminalPage({ ? undefined : decodeURIComponent(params.subagent); const { page, label } = termPaths(name, subagent); + // A subagent has no icon of its own: its tab wears its agent's. + useAgentFavicon(name); // Runs once per mounted terminal — re-registering on every render // would just be redundant work against an already-deduped list, but diff --git a/frontend/packages/swarm-ui/src/shell/useAgentFavicon.ts b/frontend/packages/swarm-ui/src/shell/useAgentFavicon.ts new file mode 100644 index 00000000..c5cba8e8 --- /dev/null +++ b/frontend/packages/swarm-ui/src/shell/useAgentFavicon.ts @@ -0,0 +1,38 @@ +// useAgentFavicon — while the calling page is mounted, the browser tab's icon +// is this agent's icon, so tabs of different agents tell apart at a glance. +// +// The icon comes from swarm-controller's `GET /api/agents/{name}/icon`, an +// SVG that is a 404 for an agent with none. A `` can't report a failed +// load, so an off-screen image loads it first and the tab icon switches only +// once that succeeds: an agent with no icon, or an unreachable controller, +// keeps the hyperhive mark. Leaving the page puts the page's own icon back. +import { useEffect } from "preact/hooks"; + +export function useAgentFavicon(agent: string): void { + useEffect(() => { + const link = document.querySelector('link[rel="icon"]'); + if (link === null) return; + const original = { + href: link.getAttribute("href"), + type: link.getAttribute("type"), + }; + const url = `/api/agents/${encodeURIComponent(agent)}/icon`; + let cancelled = false; + + const probe = new Image(); + probe.onload = () => { + if (cancelled) return; + link.setAttribute("type", "image/svg+xml"); + link.setAttribute("href", url); + }; + probe.src = url; + + return () => { + cancelled = true; + for (const [attr, value] of Object.entries(original)) { + if (value === null) link.removeAttribute(attr); + else link.setAttribute(attr, value); + } + }; + }, [agent]); +}