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.
138 lines
4.4 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|