swarm-ui: a terminal tab takes its agent's icon as the browser-tab icon
The agent terminal page swaps the page's favicon for the agent's icon while it is mounted, so tabs of different agents tell apart. A subagent terminal shows its agent's icon. An agent with no icon, or an unreachable controller, keeps the hyperhive mark; leaving the page restores the page's own icon.
This commit is contained in:
parent
e2ea842a93
commit
9761ffa24a
3 changed files with 45 additions and 0 deletions
|
|
@ -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
|
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.
|
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
|
### Scheduled prompts
|
||||||
|
|
||||||
`/schedules` has one card per scheduled prompt. The **name** is the card's
|
`/schedules` has one card per scheduled prompt. The **name** is the card's
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import type { RouteComponentProps } from "wouter-preact";
|
import type { RouteComponentProps } from "wouter-preact";
|
||||||
import { AgentTermPreview, termPaths } from "./AgentTermPreview.js";
|
import { AgentTermPreview, termPaths } from "./AgentTermPreview.js";
|
||||||
|
import { useAgentFavicon } from "../../shell/useAgentFavicon.js";
|
||||||
import { useDynamicTabs } from "../../shell/useDynamicTabs.js";
|
import { useDynamicTabs } from "../../shell/useDynamicTabs.js";
|
||||||
import "./AgentTerminalPage.css";
|
import "./AgentTerminalPage.css";
|
||||||
|
|
||||||
|
|
@ -27,6 +28,8 @@ export function AgentTerminalPage({
|
||||||
? undefined
|
? undefined
|
||||||
: decodeURIComponent(params.subagent);
|
: decodeURIComponent(params.subagent);
|
||||||
const { page, label } = termPaths(name, 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
|
// Runs once per mounted terminal — re-registering on every render
|
||||||
// would just be redundant work against an already-deduped list, but
|
// would just be redundant work against an already-deduped list, but
|
||||||
|
|
|
||||||
38
frontend/packages/swarm-ui/src/shell/useAgentFavicon.ts
Normal file
38
frontend/packages/swarm-ui/src/shell/useAgentFavicon.ts
Normal file
|
|
@ -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 `<link>` 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<HTMLLinkElement>('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]);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue