Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/agent/detail/AgentDetail.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

285 lines
7.6 KiB
TypeScript

// <AgentDetail> — one agent's details: its status, wanted state, hive, config
// PR, inference settings, resource caps and linked accounts, plus the actions on
// them. Two layouts. In an opened card on `/agents` (`inCard`) the card header
// already shows status, message and wanted state, so the body is four folded
// sections (`sections.ts`): overview, settings, accounts, and the terminal with
// its subagents, each built when first opened. In the detail dialog on a terminal
// page it is one flat list with those three rows, and no terminal, since the
// page is already beside one.
//
// Takes the agent's *live* row and the shared `AgentActions`, so a state
// change or a link made here shows wherever else the same row is drawn.
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { Badge } from "@hive/shared/badge.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import type { AgentActions } from "../roster/AgentActions.js";
import { AgentInference } from "./AgentInference.js";
import { AgentInstructions } from "./AgentInstructions.js";
import { AgentLimits } from "./AgentLimits.js";
import { AgentPermissions } from "./AgentPermissions.js";
import { AllowedDomains } from "./AllowedDomains.js";
import { AgentStateRow } from "./AgentStateRow.js";
import { AgentTermPreview } from "../terminal/AgentTermPreview.js";
import { FRESHNESS, type AgentRow } from "../AgentTypes.js";
import { LinkAccountMenu } from "./LinkAccountMenu.js";
import { LinkedAccounts } from "./LinkedAccounts.js";
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
import {
SECTION_TITLE,
sectionsFor,
startsOpen,
type DetailSection,
} from "./sections.js";
import { SubagentTerms } from "../terminal/SubagentTerms.js";
import { WantedMenu } from "../roster/WantedMenu.js";
import "./AgentDetail.css";
/** The rows the card header already shows: status, message and wanted. */
function HeaderRows({
row,
actions,
}: {
row: AgentRow;
actions: AgentActions;
}) {
const { tone, label } = FRESHNESS[row.freshness];
const declareError = actions.actionErrors.get(row.name);
return (
<>
<dt>status</dt>
<dd>
<Badge
tone={tone}
value={
<>
{label}
{row.last_seen_unix !== null ? (
<>
{" "}
(<RelativeTime epochMs={row.last_seen_unix * 1000} />)
</>
) : null}
</>
}
/>
</dd>
<dt>message</dt>
<dd>{row.snapshot?.status_text ?? "—"}</dd>
<dt>wanted</dt>
<dd>
{/* Full menu (destroy included) — mara: "destroy is already
available via wanted state", no separate button needed. */}
<WantedMenu
row={row}
pending={actions.pendingAgents.has(row.name)}
onSelectUp={actions.selectUp}
onSelectOffline={actions.selectOffline}
onSelectPaused={actions.selectPaused}
onDestroy={actions.requestDestroy}
/>
{/* A failed declaration shows here too: on a terminal page's
dialog there is no roster card to carry the "failed" badge. */}
{declareError ? (
<ApiErrorPanel
context="the declaration failed"
problem={declareError}
/>
) : null}
</dd>
</>
);
}
function OverviewRows({ row }: { row: AgentRow }) {
return (
<>
<dt>state</dt>
<dd>
<AgentStateRow key={row.name} agent={row.name} />
</dd>
<dt>hive</dt>
<dd>{row.hive ?? "—"}</dd>
<dt>config PR</dt>
<dd>
{row.config_pr ? (
<Badge
tone="warning"
value={
row.config_pr.html_url ? (
<a
href={row.config_pr.html_url}
target="_blank"
rel="noreferrer"
>
#{row.config_pr.pr_number}
</a>
) : (
`#${row.config_pr.pr_number}`
)
}
/>
) : (
"—"
)}
</dd>
</>
);
}
function SettingsRows({ agent }: { agent: string }) {
return (
<>
<dt>inference</dt>
<dd>
<AgentInference key={agent} agent={agent} />
</dd>
<dt>instructions</dt>
<dd>
<AgentInstructions key={agent} agent={agent} />
</dd>
<dt>resource caps</dt>
<dd>
<AgentLimits key={agent} agent={agent} />
</dd>
<dt>permissions</dt>
<dd>
<AgentPermissions key={agent} agent={agent} />
</dd>
<dt>allowed domains</dt>
<dd>
<AllowedDomains key={agent} agent={agent} />
</dd>
</>
);
}
function AccountsRows({
row,
actions,
}: {
row: AgentRow;
actions: AgentActions;
}) {
return (
<>
<dt>accounts</dt>
<dd>
{row.hive ? (
<LinkedAccounts
key={`${row.hive}/${row.name}`}
hive={row.hive}
agent={row.name}
version={actions.linkVersion}
/>
) : (
"—"
)}
</dd>
</>
);
}
function LinkAccountAction({
row,
actions,
}: {
row: AgentRow;
actions: AgentActions;
}) {
return (
<div class="ui-agent-detail-actions">
<LinkAccountMenu
agent={row.name}
hasHive={row.hive !== null}
onSelect={(kind) => actions.openLink(row, kind)}
/>
</div>
);
}
/** The body of one folded section of an opened card. */
function SectionBody({
section,
row,
actions,
}: {
section: DetailSection;
row: AgentRow;
actions: AgentActions;
}) {
switch (section) {
case "overview":
return (
<dl class="ui-agent-detail-fields">
<OverviewRows row={row} />
</dl>
);
case "settings":
return (
<dl class="ui-agent-detail-fields">
<SettingsRows agent={row.name} />
</dl>
);
case "accounts":
return (
<>
<dl class="ui-agent-detail-fields">
<AccountsRows row={row} actions={actions} />
</dl>
<LinkAccountAction row={row} actions={actions} />
</>
);
case "terminal":
return (
<>
{/* The agent's live terminal and its input box. */}
<AgentTermPreview agentName={row.name} />
<SubagentTerms key={row.name} agentName={row.name} />
</>
);
}
}
export function AgentDetail({
row,
actions,
withTerminal = false,
inCard = false,
}: {
row: AgentRow;
actions: AgentActions;
/** Add the live terminal and subagents: for the flat layout, below the rows. */
withTerminal?: boolean;
/** The folded sections of an opened card, without the rows its header shows. */
inCard?: boolean;
}) {
if (inCard) {
return (
<div class="ui-agent-detail ui-agent-detail-sections">
{sectionsFor(withTerminal).map((section) => (
<LazyDisclosure
key={section}
title={SECTION_TITLE[section]}
startOpen={startsOpen(section)}
>
<SectionBody section={section} row={row} actions={actions} />
</LazyDisclosure>
))}
</div>
);
}
return (
<div class="ui-agent-detail">
<dl class="ui-agent-detail-fields">
<HeaderRows row={row} actions={actions} />
<OverviewRows row={row} />
<SettingsRows agent={row.name} />
<AccountsRows row={row} actions={actions} />
</dl>
<LinkAccountAction row={row} actions={actions} />
{withTerminal ? (
<SectionBody section="terminal" row={row} actions={actions} />
) : null}
</div>
);
}