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.
93 lines
3.2 KiB
TypeScript
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}
|
|
</>
|
|
);
|
|
}
|