Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/pages/agents/AgentTerminalPage.tsx
iris f957e19d0d 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.
2026-10-12 00:19:33 +02:00

85 lines
3.3 KiB
TypeScript

// <AgentTerminalPage> — the swarm terminal tab's full page, mounted at
// `/agents/:name/terminal`. The MVP `AgentTermPreview` (below `AgentsPage`'s
// detail panel) is height-capped; this is that same
// component's `fullHeight` mount — no 12em cap, floating header badges
// shown (there's no detail-panel `<dl>` here to duplicate them).
//
// Registers itself in the dynamic-tabs row on mount (not just when opened
// via the preview's expand trigger) — a bookmark, a shared link, or a
// plain page reload should all re-open the tab instead of landing on a
// page with no way back into the tab row. `useDynamicTabs().open()` is
// idempotent (dedupes by href), so this is safe to call every mount.
import { useEffect, useRef, useState } from "preact/hooks";
import type { RouteComponentProps } from "wouter-preact";
import { Badge } from "@hive/shared/badge.js";
import { AgentDetailPopover } from "../../agent/detail/AgentDetailPopover.js";
import { AgentIcon } from "../../agent/AgentIcon.js";
import {
AgentTermPreview,
termPaths,
} from "../../agent/terminal/AgentTermPreview.js";
import { useAgentFavicon } from "../../agent/terminal/useAgentFavicon.js";
import { useDynamicTabs } from "../../shell/useDynamicTabs.js";
import "./AgentTerminalPage.css";
// Also mounted at `/agents/:name/subagents/:subagent/terminal`, for one of
// the agent's subagents.
export function AgentTerminalPage({
params,
}: RouteComponentProps<{ name: string; subagent?: string }>) {
const { open } = useDynamicTabs();
const name = decodeURIComponent(params.name);
const subagent =
params.subagent === undefined
? undefined
: decodeURIComponent(params.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
// would just be redundant work against an already-deduped list, but
// there's no reason to pay it more than once per identity.
useEffect(() => {
open({ href: page, label });
// eslint-disable-next-line react-hooks/exhaustive-deps -- `open` is a
// fresh function identity every render (`useDynamicTabs` doesn't
// memoize it); including it would re-fire this effect every render
// instead of once per terminal `page`.
}, [page]);
// The agent's details open in a popover under its badge, so reading or
// changing them never means leaving the terminal, or dimming it. A
// subagent's terminal opens its agent's details: a subagent has none of its
// own.
const [detailOpen, setDetailOpen] = useState(false);
const badgeRef = useRef<HTMLSpanElement>(null);
return (
<div class="ui-agent-terminal-page">
<AgentTermPreview
agentName={name}
subagent={subagent}
fullHeight
showHeaderBadges
leading={
<span ref={badgeRef}>
<Badge
icon={<AgentIcon name={name} small />}
value={name}
title={`${detailOpen ? "close" : "open"} ${name}'s details`}
expanded={detailOpen}
onClick={() => setDetailOpen((o) => !o)}
/>
</span>
}
/>
<AgentDetailPopover
agent={name}
open={detailOpen}
onClose={() => setDetailOpen(false)}
anchorRef={badgeRef}
/>
</div>
);
}