Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: one link-account menu, and no subagents section without subagents

The three link-account badges were wider than the agent detail panel and
spilled out of it; they are now a single badge opening a dropdown of the
three kinds. The subagents row, heading included, is not rendered for an
agent that has none.
This commit is contained in:
iris 2026-10-04 15:55:18 +02:00 • committed by mara
commit 2c1a9fa488
4 changed files with 75 additions and 55 deletions

View file

@ -35,6 +35,7 @@ import { AgentTermPreview } from "./AgentTermPreview.js";
import { SubagentTerms } from "./SubagentTerms.js";
import { FRESHNESS, type AgentRow } from "./AgentTypes.js";
import { LinkedAccounts } from "./LinkedAccounts.js";
import { LinkAccountMenu } from "./LinkAccountMenu.js";
import { Button } from "../../ui/button/Button.js";
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
import { Dialog } from "../../ui/dialog/Dialog.js";
@ -602,53 +603,14 @@ export function AgentsPage() {
</dd>
</dl>
<div class="ui-agent-detail-actions">
<Badge
variant="quiet"
icon={<LinkIcon />}
value="link matrix account"
onClick={
detailTarget.hive
? () => setMatrixTarget(detailTarget)
: undefined
}
disabled={!detailTarget.hive}
title={
detailTarget.hive
? `link a matrix account to ${detailTarget.name}`
: "no hive on record for this agent — nothing to link against"
}
/>
<Badge
variant="quiet"
icon={<LinkIcon />}
value="link forge account"
onClick={
detailTarget.hive
? () => setForgeTarget(detailTarget)
: undefined
}
disabled={!detailTarget.hive}
title={
detailTarget.hive
? `link an external forge account to ${detailTarget.name}`
: "no hive on record for this agent — nothing to link against"
}
/>
<Badge
variant="quiet"
icon={<LinkIcon />}
value="link github account"
onClick={
detailTarget.hive
? () => setGithubTarget(detailTarget)
: undefined
}
disabled={!detailTarget.hive}
title={
detailTarget.hive
? `link a github account to ${detailTarget.name}`
: "no hive on record for this agent — nothing to link against"
}
<LinkAccountMenu
agent={detailTarget.name}
hasHive={detailTarget.hive !== null}
onSelect={(kind) => {
if (kind === "matrix") setMatrixTarget(detailTarget);
else if (kind === "forge") setForgeTarget(detailTarget);
else setGithubTarget(detailTarget);
}}
/>
</div>
{/* The agent's live terminal and its input box. */}

View file

@ -0,0 +1,60 @@
// <LinkAccountMenu> — one "link account" badge that opens a `Dropdown` with
// the three kinds of account an agent can be linked to, in place of one
// badge per kind: three side-by-side badges were wider than the detail
// panel and spilled out of it. Presentational only, same split as
// `WantedMenu`: the caller owns what each selection opens.
import { useRef, useState } from "preact/hooks";
import { Badge } from "@hive/shared/badge.js";
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
import { LinkIcon } from "@hive/shared/icons.js";
export type LinkKind = "matrix" | "forge" | "github";
const OPTIONS: DropdownOption[] = [
{ value: "matrix", label: "matrix account" },
{ value: "forge", label: "forge account" },
{ value: "github", label: "github account" },
];
export function LinkAccountMenu({
agent,
hasHive,
onSelect,
}: {
agent: string;
/** Linking writes against the agent's hive, so without one there is
* nothing to link against and the menu stays shut. */
hasHive: boolean;
onSelect: (kind: LinkKind) => void;
}) {
const [open, setOpen] = useState(false);
const anchorRef = useRef<HTMLDivElement>(null);
return (
<div class="ui-dropdown-anchor" ref={anchorRef}>
<Badge
variant="quiet"
icon={<LinkIcon />}
value="link account"
onClick={hasHive ? () => setOpen((o) => !o) : undefined}
expanded={open}
disabled={!hasHive}
title={
hasHive
? `link a matrix, forge or github account to ${agent}`
: "no hive on record for this agent — nothing to link against"
}
/>
<Dropdown
open={open}
options={OPTIONS}
label={`link an account to ${agent}`}
onSelect={(value) => {
setOpen(false);
onSelect(value as LinkKind);
}}
onClose={() => setOpen(false)}
anchorRef={anchorRef}
/>
</div>
);
}

View file

@ -11,7 +11,6 @@
gap: 0.5em;
}
.ui-subagent-terms-heading,
.ui-subagent-terms-empty {
.ui-subagent-terms-heading {
color: var(--muted);
}

View file

@ -37,16 +37,15 @@ export function SubagentTerms({ agentName }: { agentName: string }) {
refresh().catch((e: unknown) => setError({ detail: String(e) }));
});
// An agent with no subagents shows nothing here — no heading, no
// "none" line — and so does the first fetch while it is in flight. A
// failed fetch still shows, so the error is not swallowed.
if (!error && (names === null || names.length === 0)) return null;
return (
<section class="ui-subagent-terms" aria-label={`${agentName}'s subagents`}>
<div class="ui-subagent-terms-list">
<span class="ui-subagent-terms-heading">subagents</span>
{names === null && !error && <Badge value="loading…" />}
{names?.length === 0 && (
<span class="ui-subagent-terms-empty">
none with output on record
</span>
)}
{names?.map((name) => (
<Badge
key={name}