agent: 🔓 logout in overflow menu + /logout slash command (#576 frontend half)

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).
This commit is contained in:
iris 2026-05-29 16:51:15 +02:00 committed by Mara
commit 259b236ad1

View file

@ -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 <name> — 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]) {