From 259b236ad1d0ad5bc62d08a62aebf6dab8ec6fab Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 29 May 2026 16:51:15 +0200 Subject: [PATCH] =?UTF-8?q?agent:=20=F0=9F=94=93=20logout=20in=20overflow?= =?UTF-8?q?=20menu=20+=20/logout=20slash=20command=20(#576=20frontend=20ha?= =?UTF-8?q?lf)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with damocles PR #582 (POST /api/logout backend on the per-agent web UI). Three additions to the agent's app.js: - New `postLogout` helper next to postCancelTurn / postCompact / postNewSession. Same postSimple shape. - New entry in SLASH_COMMANDS so /help lists /logout. - New /logout case in handleSlashCommand with window.confirm. - New '🔓 logout' item in populateOverflowMenu's overflow popover, mirroring the new-session item's pattern (confirm before POST, disable button while in-flight, closeOverflowMenu before fire). Confirm dialog spells out the consequences (SIGINT, creds wiped, park in needs-login) so the operator doesn't accidentally drop a production session. Tooltip on the menu item links the action back to the credentials directory + post-logout state. Wire-level: POST /api/logout, no body. Backend returns 200 with a text body describing the wipe outcome — postSimple ignores it (success → no terminal note; failure → red turn-end-fail row). --- frontend/packages/agent/src/app.js | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 88e49215..112dfda1 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -218,6 +218,34 @@ window.marked = marked; }); menu.append(newSessBtn); + // 🔓 logout (#576) — SIGINTs claude, wipes the credentials dir, + // flips the harness LoginState to NeedsLogin. The turn loop's + // next iteration parks in wait_for_login; a fresh `claude auth + // login` from the dashboard re-arms it (#542 mtime resumption). + // Destructive — the operator has to re-paste OAuth creds on the + // login screen after — so we confirm with a clear-eyed prompt. + const logoutBtn = el('button', { + type: 'button', + class: 'overflow-item overflow-item-logout', + role: 'menuitem', + id: 'logout-btn', + title: 'wipe ~/.claude/ credentials + park in needs-login until a fresh `claude auth login` runs', + }, + el('span', { class: 'overflow-item-icon', 'aria-hidden': 'true' }, '🔓'), + 'logout', + ); + logoutBtn.addEventListener('click', () => { + if (!window.confirm( + `log ${label} out? this SIGINTs any running claude turn, wipes the credentials directory, ` + + `and parks the agent in 'needs login' until you paste a fresh OAuth code from the login screen. ` + + `prior --continue context is not affected (only the OAuth creds).` + )) return; + logoutBtn.disabled = true; + closeOverflowMenu(); + postLogout().finally(() => { logoutBtn.disabled = false; }); + }); + menu.append(logoutBtn); + overflowMenuPopulated = true; } @@ -363,6 +391,7 @@ window.marked = marked; { name: '/compact', desc: 'compact the persistent claude session' }, { name: '/model', desc: '/model — switch claude model for future turns' }, { name: '/new-session', desc: 'next turn runs without --continue (fresh claude session)' }, + { name: '/logout', desc: 'wipe ~/.claude/ credentials + park in needs-login' }, ]; async function postModel(name) { @@ -402,6 +431,7 @@ window.marked = marked; const postCancelTurn = () => postSimple('/api/cancel', '/cancel'); const postCompact = () => postSimple('/api/compact', '/compact'); const postNewSession = () => postSimple('/api/new-session', '/new-session'); + const postLogout = () => postSimple('/api/logout', '/logout'); function handleSlashCommand(line) { if (!termAPI) return false; @@ -430,6 +460,15 @@ window.marked = marked; postNewSession(); } return true; + case '/logout': + if (window.confirm( + `log out? this SIGINTs any running claude turn, wipes the credentials directory, ` + + `and parks the agent in 'needs login' until you paste a fresh OAuth code from the login screen. ` + + `prior --continue context is not affected (only the OAuth creds).` + )) { + postLogout(); + } + return true; case '/model': { const parts = trimmed.split(/\s+/); if (parts.length < 2 || !parts[1]) {