From e9ddfd61ec6c27094d21cb139894dbd1f1f09e1b Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 15 Nov 2026 15:11:41 +0100 Subject: [PATCH] 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. --- docs/2-handbook/swarm/claude-login.md | 5 ++++ docs/4-implementation/web-ui/swarm-ui.md | 5 ++-- .../src/agent/detail/AgentClaudeLogin.tsx | 29 +++++++++++++++++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/2-handbook/swarm/claude-login.md b/docs/2-handbook/swarm/claude-login.md index 42243b7b0..4f1e24cfb 100644 --- a/docs/2-handbook/swarm/claude-login.md +++ b/docs/2-handbook/swarm/claude-login.md @@ -20,6 +20,11 @@ A started login waits 15 minutes for its code, then stops. Starting another logi for the same agent replaces the running one. A refused code fails the login, and the dialog shows what claude printed. +You don't have to wait for the agent to fail first: the **claude login** row in the +card's settings has a **log in** button too, which reads **log in again** once the swarm has a +login for the agent, so a fresh agent or a forced relogin starts there. It opens the +same dialog. + The agent's own web UI login (`/login`) still works, unchanged. It goes away once you have confirmed the swarm login works. diff --git a/docs/4-implementation/web-ui/swarm-ui.md b/docs/4-implementation/web-ui/swarm-ui.md index fc9a2d979..8bfd47480 100644 --- a/docs/4-implementation/web-ui/swarm-ui.md +++ b/docs/4-implementation/web-ui/swarm-ui.md @@ -71,8 +71,9 @@ the code back. When the secret store has the login, the dialog closes and a short toast in the top-right corner says so. The credentials go to the agent through the secret store ([`claude-login.md`](../../2-handbook/swarm/claude-login.md)). The agent's own web UI login still works too. The settings section of an opened card has a **claude login** row: when -the swarm stored the agent's login, or that none is, with a **delete login** -action that asks first. +the swarm stored the agent's login, or that none is, with a **log in** button +(**log in again** when the swarm holds one) that opens the same dialog, and a **delete +login** action that asks first. A card shows **updating** when the agent's status snapshot has `updating` set. The hive sets it when an update stops a running container and clears it when diff --git a/frontend/packages/swarm-ui/src/agent/detail/AgentClaudeLogin.tsx b/frontend/packages/swarm-ui/src/agent/detail/AgentClaudeLogin.tsx index dbf52a963..b15735b8a 100644 --- a/frontend/packages/swarm-ui/src/agent/detail/AgentClaudeLogin.tsx +++ b/frontend/packages/swarm-ui/src/agent/detail/AgentClaudeLogin.tsx @@ -1,13 +1,15 @@ // — 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, and a delete that removes every version of it -// (`DELETE …/claude-login/stored`) after a confirmation. +// 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"; @@ -26,6 +28,7 @@ export function AgentClaudeLogin({ agent }: { agent: string }) { const [confirming, setConfirming] = useState(false); const [deleting, setDeleting] = useState(false); const [deleteError, setDeleteError] = useState(null); + const [loggingIn, setLoggingIn] = useState(false); const load = useCallback(async () => { try { @@ -75,17 +78,37 @@ export function AgentClaudeLogin({ agent }: { agent: string }) { return errorPanel ?? …; } const at = storedAtMs(login); + const loginBadge = ( + setLoggingIn(true)} + /> + ); return ( <> {errorPanel} + {loggingIn ? ( + { + setLoggingIn(false); + void load(); + }} + /> + ) : null} {login.stored_at === null ? ( - none stored + <> + none stored {loginBadge} + ) : ( <> stored{" "} {at === null ? login.stored_at : } {" "} + {loginBadge}{" "}