Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: show an agent's details in a popover, not a modal dialog

The details open under the agent badge without dimming or blocking the page; a click outside or Escape closes it. New ui/popover primitive (anchored, portal, no backdrop) with a sample on the components page. A confirm or link dialog opened from inside it stays modal, and the popover ignores clicks and Escape that belong to that dialog. AgentDetailDialog becomes AgentDetailPopover.
This commit is contained in:
iris 2026-10-12 00:16:25 +02:00
commit f957e19d0d
7 changed files with 221 additions and 42 deletions

View file

@ -1,29 +1,34 @@
// <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.
// <AgentDetailPopover> — the agent detail panel from `/agents`, 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 dialog opens, found by
// 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 { Dialog } from "../../ui/dialog/Dialog.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 AgentDetailDialog({
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.
@ -36,7 +41,7 @@ export function AgentDetailDialog({
}
});
// Fetched on each open, so what the dialog shows is current rather than
// Fetched on each open, so what the popover shows is current rather than
// what the page last saw.
useEffect(() => {
if (!open) return;
@ -61,24 +66,27 @@ export function AgentDetailDialog({
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>
<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}
</>
);

View file

@ -2,7 +2,7 @@
// its wanted state (up / offline / paused / destroyed) and link an account to
// it, with the confirm and form dialogs those need. Shared by the agent
// roster (`AgentsPage`) and the detail dialog on a terminal page
// (`AgentDetailDialog`), so a state change reads the same from either; each
// (`AgentDetailPopover`), so a state change reads the same from either; each
// caller renders the returned `dialogs` once.
//
// "offline" and "paused" are the *same* confirm shape — same button