The terminal page gets an agent badge (icon and name) that opens the agent's detail panel in a dialog over the terminal, without the terminal preview. The panel body and the actions on an agent (declare a wanted state, link an account, with their confirm and form dialogs) move out of the agent roster page into AgentDetail and useAgentActions, so the roster and the dialog share them. AgentIcon moves to its own file for the same reason.
85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
// <AgentDetailDialog> — the agent detail panel from `/agents`, in a dialog, for
|
|
// a page that is already showing the agent's terminal and should not be left
|
|
// 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 dialog 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.
|
|
import { useEffect, useState } from "preact/hooks";
|
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|
import { Dialog } from "../../ui/dialog/Dialog.js";
|
|
import { Panel } from "../../ui/panel/Panel.js";
|
|
import { useAgentActions } from "./AgentActions.js";
|
|
import { AgentDetail } from "./AgentDetail.js";
|
|
import type { AgentRow } from "./AgentTypes.js";
|
|
|
|
export function AgentDetailDialog({
|
|
agent,
|
|
open,
|
|
onClose,
|
|
}: {
|
|
agent: string;
|
|
open: boolean;
|
|
onClose: () => void;
|
|
}) {
|
|
// `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 dialog 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 (
|
|
<>
|
|
<Dialog open={open} onClose={onClose} label={`${agent} details`} plain>
|
|
{open ? (
|
|
<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>
|
|
) : null}
|
|
</Dialog>
|
|
{actions.dialogs}
|
|
</>
|
|
);
|
|
}
|