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]) {