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:
parent
66df552f68
commit
2c1a9fa488
4 changed files with 75 additions and 55 deletions
|
|
@ -35,6 +35,7 @@ import { AgentTermPreview } from "./AgentTermPreview.js";
|
||||||
import { SubagentTerms } from "./SubagentTerms.js";
|
import { SubagentTerms } from "./SubagentTerms.js";
|
||||||
import { FRESHNESS, type AgentRow } from "./AgentTypes.js";
|
import { FRESHNESS, type AgentRow } from "./AgentTypes.js";
|
||||||
import { LinkedAccounts } from "./LinkedAccounts.js";
|
import { LinkedAccounts } from "./LinkedAccounts.js";
|
||||||
|
import { LinkAccountMenu } from "./LinkAccountMenu.js";
|
||||||
import { Button } from "../../ui/button/Button.js";
|
import { Button } from "../../ui/button/Button.js";
|
||||||
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
|
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
|
||||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||||
|
|
@ -602,53 +603,14 @@ export function AgentsPage() {
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="ui-agent-detail-actions">
|
<div class="ui-agent-detail-actions">
|
||||||
<Badge
|
<LinkAccountMenu
|
||||||
variant="quiet"
|
agent={detailTarget.name}
|
||||||
icon={<LinkIcon />}
|
hasHive={detailTarget.hive !== null}
|
||||||
value="link matrix account"
|
onSelect={(kind) => {
|
||||||
onClick={
|
if (kind === "matrix") setMatrixTarget(detailTarget);
|
||||||
detailTarget.hive
|
else if (kind === "forge") setForgeTarget(detailTarget);
|
||||||
? () => setMatrixTarget(detailTarget)
|
else setGithubTarget(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"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* The agent's live terminal and its input box. */}
|
{/* The agent's live terminal and its input box. */}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-subagent-terms-heading,
|
.ui-subagent-terms-heading {
|
||||||
.ui-subagent-terms-empty {
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,16 +37,15 @@ export function SubagentTerms({ agentName }: { agentName: string }) {
|
||||||
refresh().catch((e: unknown) => setError({ detail: String(e) }));
|
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 (
|
return (
|
||||||
<section class="ui-subagent-terms" aria-label={`${agentName}'s subagents`}>
|
<section class="ui-subagent-terms" aria-label={`${agentName}'s subagents`}>
|
||||||
<div class="ui-subagent-terms-list">
|
<div class="ui-subagent-terms-list">
|
||||||
<span class="ui-subagent-terms-heading">subagents</span>
|
<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) => (
|
{names?.map((name) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={name}
|
key={name}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue