diff --git a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx
index a6c70284..28faff3f 100644
--- a/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx
+++ b/frontend/packages/swarm-ui/src/pages/agents/AgentsPage.tsx
@@ -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() {
- }
- 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"
- }
- />
- }
- 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"
- }
- />
- }
- 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"
- }
+ {
+ if (kind === "matrix") setMatrixTarget(detailTarget);
+ else if (kind === "forge") setForgeTarget(detailTarget);
+ else setGithubTarget(detailTarget);
+ }}
/>
{/* The agent's live terminal and its input box. */}
diff --git a/frontend/packages/swarm-ui/src/pages/agents/LinkAccountMenu.tsx b/frontend/packages/swarm-ui/src/pages/agents/LinkAccountMenu.tsx
new file mode 100644
index 00000000..0af8bc65
--- /dev/null
+++ b/frontend/packages/swarm-ui/src/pages/agents/LinkAccountMenu.tsx
@@ -0,0 +1,60 @@
+// — 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(null);
+ return (
+
+ }
+ 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"
+ }
+ />
+ {
+ setOpen(false);
+ onSelect(value as LinkKind);
+ }}
+ onClose={() => setOpen(false)}
+ anchorRef={anchorRef}
+ />
+
+ );
+}
diff --git a/frontend/packages/swarm-ui/src/pages/agents/SubagentTerms.css b/frontend/packages/swarm-ui/src/pages/agents/SubagentTerms.css
index 3d2247a8..ec8a7a81 100644
--- a/frontend/packages/swarm-ui/src/pages/agents/SubagentTerms.css
+++ b/frontend/packages/swarm-ui/src/pages/agents/SubagentTerms.css
@@ -11,7 +11,6 @@
gap: 0.5em;
}
-.ui-subagent-terms-heading,
-.ui-subagent-terms-empty {
+.ui-subagent-terms-heading {
color: var(--muted);
}
diff --git a/frontend/packages/swarm-ui/src/pages/agents/SubagentTerms.tsx b/frontend/packages/swarm-ui/src/pages/agents/SubagentTerms.tsx
index 7c1bfd67..a1a78cbf 100644
--- a/frontend/packages/swarm-ui/src/pages/agents/SubagentTerms.tsx
+++ b/frontend/packages/swarm-ui/src/pages/agents/SubagentTerms.tsx
@@ -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 (
subagents
- {names === null && !error && }
- {names?.length === 0 && (
-
- none with output on record
-
- )}
{names?.map((name) => (