Watch
0
0
Fork
You've already forked hyperhive
0

swarm-ui: link a matrix account to an agent

New per-row action on AgentsPage: a quiet-variant icon badge (LinkIcon,
matching the LinksMenu/SettingsMenu chrome-not-chip convention) opens a
dialog with an account/token/homeserver form, PUTting
/api/hives/{hive}/agents/{agent}/matrix-accounts/{account} per the
contract atlas posted on hyperhive#3726 (issuecomment-72355).

Built against the contract before the backend endpoint exists per
atlas's explicit note that it doesn't change when the implementation
lands -- this 404s until that item merges. No linked-accounts list:
no route exposes one, and a credential store shouldn't hand a secret
back out anyway, so this is a blind set/update action, matching
mara's 1:1-for-now ruling on the issue.

Verified: tsc --noEmit and nix fmt clean, esbuild build clean. Real
DOM-interaction screenshots against a throwaway mock server (deleted
before this commit, never tracked) -- table column render + disabled
state on a hiveless row, dialog open, form filled with the token
masked, and the success path end to end (token field clears, success
message shows) against a mocked 200 response.
This commit is contained in:
iris 2026-09-08 13:51:20 +02:00 • committed by mara
commit 767f87610a
3 changed files with 220 additions and 0 deletions

View file

@ -32,6 +32,7 @@ import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge, type BadgeTone } from "@hive/shared/badge.js";
import { Dropdown, type DropdownOption } from "@hive/shared/dropdown.js";
import { LinkIcon } from "@hive/shared/icons.js";
import { Button } from "../ui/button/Button.js";
import { ConfirmDialog } from "../ui/confirm-dialog/ConfirmDialog.js";
import { Dialog } from "../ui/dialog/Dialog.js";
@ -44,6 +45,7 @@ import {
} from "../ui/refresh-interval/RefreshInterval.js";
import { Table, type TableColumn } from "../ui/table/Table.js";
import { CreateAgentForm } from "./CreateAgentForm.js";
import { LinkMatrixAccountForm } from "./LinkMatrixAccountForm.js";
interface ConfigPrStatus {
pr_number: number;
@ -200,6 +202,12 @@ export function AgentsPage() {
// one's JSX below reads standalone.
const [destroyTarget, setDestroyTarget] = useState<AgentRow | null>(null);
const [stopTarget, setStopTarget] = useState<AgentRow | null>(null);
// The row currently showing the "link a matrix account" dialog — same
// null-means-closed shape as `destroyTarget`/`stopTarget`, own piece of
// state rather than folded into either since this dialog isn't a
// confirmation of a `declareState` call, it's an unrelated action with
// its own form (`LinkMatrixAccountForm`).
const [matrixTarget, setMatrixTarget] = useState<AgentRow | null>(null);
async function refresh() {
const res = await fetch("/api/agents/status");
@ -395,6 +403,30 @@ export function AgentsPage() {
);
},
},
{
key: "matrix",
header: "matrix",
// Icon-only trigger, quiet variant (no permanent pill fill) — same
// "chrome, not a status chip" reasoning as `LinksMenu`/`SettingsMenu`'s
// own `Badge` triggers (see `@hive/shared/badge/Badge.tsx`'s
// `BadgeVariant` doc). Disabled with no hive on record, same guard
// `WantedMenu` uses — the endpoint is hive-scoped, there's nothing
// to PUT against without one.
render: (a) => (
<Badge
variant="quiet"
icon={<LinkIcon />}
value="link account"
onClick={a.hive ? () => setMatrixTarget(a) : undefined}
disabled={!a.hive}
title={
a.hive
? `link a matrix account to ${a.name}`
: "no hive on record for this agent — nothing to link against"
}
/>
),
},
{
key: "config-pr",
header: "config PR",
@ -459,6 +491,21 @@ export function AgentsPage() {
>
<CreateAgentForm />
</Dialog>
<Dialog
open={matrixTarget !== null}
onClose={() => setMatrixTarget(null)}
label="link a matrix account"
>
{/* `matrixTarget.hive` is non-null here — the trigger badge above
is disabled without one, so this can only open with a real
hive to PUT against. */}
{matrixTarget?.hive ? (
<LinkMatrixAccountForm
hive={matrixTarget.hive}
agent={matrixTarget.name}
/>
) : null}
</Dialog>
<ConfirmDialog
open={stopTarget !== null}
label="stop agent"