From 8d257e93626780b04975d878212e6e14c5b66def Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 21 Sep 2026 15:05:26 +0200 Subject: [PATCH] agent ui: restore a clickable logout affordance to the status menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mara: 'hive ui agent term: logout button gone? must have dropped with one of the redesigns, pls bring it back or tell me where to find it' It did drop, silently. The old app.js overflow menu had an unconditional 🔓 logout item (dashboard-link + rebuild + new-session + logout); the Preact rewrite's Root.tsx comment explicitly documents dashboard-link moving to MetaNav and rebuild being removed outright, but says nothing about logout — it just didn't come along. `/logout` still works (TermInput's slash-command list, type-twice-to-confirm), but nothing in the UI points at it or offers a click path anymore. Fix: add a 'logout' entry to the status badge's dropdown (StatusChips' StatusMenu) — the same menu that already hosts pause/resume and cancel-turn, reusing its exact click-then-confirm-click pattern (danger: true, armed/confirm state reset on close). Unlike cancel-turn it's not gated on `thinking` — always offered, matching the old overflow menu's unconditional entry. Wired through onLogout -> termActions.ts's existing postLogout (already used by the slash command, so the only new code is the menu entry + prop threading). Scoped to just what was asked: /new-session has the identical gone-from-any-menu gap (same old overflow item, no restored click path), left alone here and flagged separately rather than folded in. tsc --noEmit clean, nix fmt clean. --- frontend/packages/agent/src/Root.tsx | 12 ++++++- .../agent/src/components/StatusChips.tsx | 36 ++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/frontend/packages/agent/src/Root.tsx b/frontend/packages/agent/src/Root.tsx index dacd3424..168f515a 100644 --- a/frontend/packages/agent/src/Root.tsx +++ b/frontend/packages/agent/src/Root.tsx @@ -27,7 +27,7 @@ import { fmtAge, fmtTokens } from "./lib/format.js"; import { resolveDashboardBase } from "./lib/dashboardBase.js"; import { submitPauseResume } from "./lib/pauseAction.js"; import { postModel, postEffort } from "./lib/modelEffort.js"; -import { postCancelTurn } from "./lib/termActions.js"; +import { postCancelTurn, postLogout } from "./lib/termActions.js"; import type { TokenUsage } from "./types.js"; type OpenPanel = "inbox" | "todos" | null; @@ -225,6 +225,7 @@ export function Root() { onTogglePause={() => {}} thinking={false} onCancelTurn={() => Promise.resolve()} + onLogout={() => Promise.resolve()} /> )} @@ -331,6 +332,15 @@ export function Root() { refresh(); }) } + onLogout={() => + postLogout().then((r) => { + if (!r.ok) + liveStreamRef.current?.pushNote( + `✗ /logout failed${r.detail ? ": " + r.detail : ""}`, + ); + refresh(); + }) + } /> )} diff --git a/frontend/packages/agent/src/components/StatusChips.tsx b/frontend/packages/agent/src/components/StatusChips.tsx index f354c9bd..8a1f1eaf 100644 --- a/frontend/packages/agent/src/components/StatusChips.tsx +++ b/frontend/packages/agent/src/components/StatusChips.tsx @@ -37,6 +37,11 @@ export interface StatusChipsProps { lastTurnLabel?: string; thinking: boolean; onCancelTurn: () => Promise; + /** Rotates OAuth creds + parks the agent in needs-login. Always + * offered (unlike cancel-turn, not gated on `thinking`) — the old + * page's overflow menu had it unconditionally too. See + * `termActions.ts`'s `postLogout` for what it actually does. */ + onLogout: () => Promise; } const MODEL_DESCRIPTIONS: Record = { @@ -114,6 +119,7 @@ function StatusMenu({ onTogglePause, thinking, onCancelTurn, + onLogout, }: Pick< StatusChipsProps, | "statusLabel" @@ -123,14 +129,17 @@ function StatusMenu({ | "onTogglePause" | "thinking" | "onCancelTurn" + | "onLogout" >) { const [open, setOpen] = useState(false); const [confirmCancel, setConfirmCancel] = useState(false); + const [confirmLogout, setConfirmLogout] = useState(false); const anchorRef = useRef(null); function close() { setOpen(false); setConfirmCancel(false); + setConfirmLogout(false); } const options: DropdownOption[] = [ @@ -143,6 +152,13 @@ function StatusMenu({ danger: true, }); } + // Always offered, regardless of turn state — same as the old overflow + // menu's unconditional 🔓 logout entry. + options.push({ + value: "logout", + label: confirmLogout ? "🔓 click again to confirm" : "🔓 logout", + danger: true, + }); return (
@@ -163,12 +179,22 @@ function StatusMenu({ close(); return; } - // value === 'cancel-turn': first click arms it, second fires. - if (!confirmCancel) { - setConfirmCancel(true); + if (value === "cancel-turn") { + // First click arms it, second fires. + if (!confirmCancel) { + setConfirmCancel(true); + return; + } + onCancelTurn(); + close(); return; } - onCancelTurn(); + // value === 'logout': same arm-then-fire shape as cancel-turn. + if (!confirmLogout) { + setConfirmLogout(true); + return; + } + onLogout(); close(); }} onClose={close} @@ -196,6 +222,7 @@ export function StatusChips({ lastTurnLabel, thinking, onCancelTurn, + onLogout, }: StatusChipsProps) { return (
@@ -207,6 +234,7 @@ export function StatusChips({ onTogglePause={onTogglePause} thinking={thinking} onCancelTurn={onCancelTurn} + onLogout={onLogout} />