Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/agent/detail/AgentClaudeLogin.tsx
iris e9ddfd61ec swarm ui: a log in button in the claude login row of an agent's settings
An agent with no stored login, or one to log in again, can be logged in from
the settings panel instead of waiting for its turns to fail with a login needed
badge first. The button opens the same dialog as the card's.
2026-11-15 16:13:15 +01:00

138 lines
4.4 KiB
TypeScript

// <AgentClaudeLogin> — the swarm's stored claude login for one agent: when it
// was written (`GET …/claude-login`, read from the secret store's metadata),
// or that none is, a button that logs the agent in (or in again) without
// waiting for its turns to fail first, and a delete that removes every version
// of it (`DELETE …/claude-login/stored`) after a confirmation.
//
// Its state belongs to one agent: callers key it by agent name.
import { useCallback, useEffect, useState } from "preact/hooks";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
import { Badge } from "@hive/shared/badge.js";
import { ClaudeLoginDialog } from "../claude-login/ClaudeLoginDialog.js";
import { ConfirmDialog } from "../../ui/confirm-dialog/ConfirmDialog.js";
import { Hint } from "../../ui/hint/Hint.js";
import { RelativeTime } from "../../ui/relative-time/RelativeTime.js";
import { showToast } from "../../ui/toast/toasts.js";
import {
deleteMessage,
deleteStoredLoginPath,
storedAtMs,
storedLoginPath,
type StoredLogin,
} from "../claude-login/StoredLogin.js";
export function AgentClaudeLogin({ agent }: { agent: string }) {
const [login, setLogin] = useState<StoredLogin | null>(null);
const [error, setError] = useState<ProblemDetails | null>(null);
const [confirming, setConfirming] = useState(false);
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState<ProblemDetails | null>(null);
const [loggingIn, setLoggingIn] = useState(false);
const load = useCallback(async () => {
try {
const r = await fetch(storedLoginPath(agent));
if (!r.ok) {
setError(await readApiError(r));
return;
}
setLogin((await r.json()) as StoredLogin);
setError(null);
} catch (err) {
setError({ detail: String(err) });
}
}, [agent]);
useEffect(() => {
void load();
}, [load]);
async function confirmDelete() {
setDeleting(true);
setDeleteError(null);
try {
const r = await fetch(deleteStoredLoginPath(agent), { method: "DELETE" });
if (!r.ok) {
setDeleteError(await readApiError(r));
return;
}
setConfirming(false);
// Nothing is stored now, whatever the re-read says.
setLogin({ stored_at: null });
showToast(`Stored login for ${agent} deleted.`, "ok");
await load();
} catch (err) {
setDeleteError({ detail: String(err) });
} finally {
setDeleting(false);
}
}
// Shown as text, not a hover title. A login already read stays under a
// failed refetch.
const errorPanel = error ? (
<ApiErrorPanel context="reading the stored login failed" problem={error} />
) : null;
if (login === null) {
return errorPanel ?? <Hint as="span">…</Hint>;
}
const at = storedAtMs(login);
const loginBadge = (
<Badge
variant="quiet"
value={login.stored_at === null ? "log in" : "log in again"}
title={`log ${agent} in to claude from here`}
onClick={() => setLoggingIn(true)}
/>
);
return (
<>
{errorPanel}
{loggingIn ? (
<ClaudeLoginDialog
agent={agent}
onClose={() => {
setLoggingIn(false);
void load();
}}
/>
) : null}
{login.stored_at === null ? (
<>
<Hint as="span">none stored</Hint> {loginBadge}
</>
) : (
<>
<span title={login.stored_at}>
stored{" "}
{at === null ? login.stored_at : <RelativeTime epochMs={at} />}
</span>{" "}
{loginBadge}{" "}
<Badge
variant="quiet"
value="delete login"
title={`delete the stored claude login for ${agent}`}
onClick={() => {
setDeleteError(null);
setConfirming(true);
}}
/>
</>
)}
<ConfirmDialog
open={confirming}
label={`delete the stored claude login for ${agent}`}
confirmLabel={deleting ? "deleting…" : "delete login"}
confirmDisabled={deleting}
onCancel={() => setConfirming(false)}
onConfirm={() => void confirmDelete()}
>
<p>{deleteMessage(agent)}</p>
{deleteError ? (
<ApiErrorPanel context="delete failed" problem={deleteError} />
) : null}
</ConfirmDialog>
</>
);
}