Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/agent/detail/AgentDetailPopover.tsx
iris 2eaa02e892 swarm-ui: the agents page is one column of cards that open in place
The roster loses its second column and its table view. Selecting a card opens
its details under it as four folded sections: overview, settings, accounts and
the terminal. The status, message and wanted rows, which the card header
already shows, are gone from the details. The terminal page's details dialog
keeps the flat list.
2026-11-01 01:21:43 +01:00

93 lines
3.2 KiB
TypeScript

// <AgentDetailPopover> — an agent's details from `/agents`, as in an opened card, in a popover
// under its anchor, for a page that is already showing the agent's terminal
// and should not be left, or dimmed, to read or change its details. It carries
// the same body (`AgentDetail`) and the same actions (`useAgentActions`) as the
// panel, minus the terminal preview the page itself already is.
//
// The row comes from `GET /api/agents/status` when the popover opens, found by
// name: the panel on `/agents` has the whole roster in hand, this page does
// not. A wanted-state change patches the row in place from the PUT's answer.
// The confirm and link dialogs those actions open stay modal: they hold a
// decision, and the popover ignores clicks inside them.
import { useEffect, useState } from "preact/hooks";
import type { RefObject } from "preact";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Panel } from "../../ui/panel/Panel.js";
import { Popover } from "../../ui/popover/Popover.js";
import { useAgentActions } from "../roster/AgentActions.js";
import { AgentDetail } from "./AgentDetail.js";
import type { AgentRow } from "../AgentTypes.js";
export function AgentDetailPopover({
agent,
open,
onClose,
anchorRef,
}: {
agent: string;
open: boolean;
onClose: () => void;
anchorRef: RefObject<HTMLElement>;
}) {
// `undefined` until the roster has answered; `null` when it has and the
// agent is not in it.
const [row, setRow] = useState<AgentRow | null | undefined>(undefined);
const [error, setError] = useState<ProblemDetails | null>(null);
const actions = useAgentActions((declaredRow, declared) => {
const state = declared.find((d) => d.agent === declaredRow.name)?.state;
if (state !== undefined) {
setRow((prev) => (prev ? { ...prev, wanted: state } : prev));
}
});
// Fetched on each open, so what the popover shows is current rather than
// what the page last saw.
useEffect(() => {
if (!open) return;
let cancelled = false;
(async () => {
const res = await fetch("/api/agents/status");
if (!res.ok) {
if (!cancelled) setError(await readApiError(res));
return;
}
const rows = (await res.json()) as AgentRow[];
if (cancelled) return;
setRow(rows.find((r) => r.name === agent) ?? null);
setError(null);
})().catch((e: unknown) => {
if (!cancelled) setError({ detail: String(e) });
});
return () => {
cancelled = true;
};
}, [open, agent]);
return (
<>
<Popover
open={open}
onClose={onClose}
anchorRef={anchorRef}
label={`${agent} details`}
>
<Panel title={agent} icon="🔎" onClose={onClose}>
{error ? (
<ApiErrorPanel
context="failed to load the agent's details"
problem={error}
/>
) : row ? (
<AgentDetail row={row} actions={actions} />
) : row === null ? (
<p>{agent} is not in the swarm's agent roster</p>
) : (
<p>loading…</p>
)}
</Panel>
</Popover>
{actions.dialogs}
</>
);
}