feat(tui): replace /clear double-press gate with a proper confirm overlay

The time-window gate felt wrong — users would hit /clear, read the
prompt, retype, and consistently blow past the window. Swapping to a
real yes/no overlay that blocks input like the existing Approval and
Clarify prompts.

- add ConfirmReq type + OverlayState.confirm + $isBlocked coverage
- ConfirmPrompt component (prompts.tsx): cancel row on top as the
  default, danger-coloured confirm row on the bottom, Y/N hotkeys,
  Enter on default = cancel, Esc/Ctrl+C cancel
- wire into PromptZone (appOverlays.tsx)
- /clear + /new now push onto the overlay instead of arming a timer
- HERMES_TUI_NO_CONFIRM=1 still skips the prompt for scripting
- drop the destructiveGate + createSlashHandler reset wiring
  (destructive.ts and its tests removed)

Refs #4069.
This commit is contained in:
Brooklyn Nicholson 2026-04-18 18:04:08 -05:00
parent 75377feb07
commit df5ca5065f
9 changed files with 132 additions and 115 deletions

View file

@ -1,7 +1,6 @@
import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js'
import { dailyFortune, randomFortune } from '../../../content/fortunes.js'
import { HOTKEYS } from '../../../content/hotkeys.js'
import { createDestructiveGate } from '../../../domain/destructive.js'
import { nextDetailsMode, parseDetailsMode } from '../../../domain/details.js'
import type {
ConfigGetValueResponse,
@ -15,12 +14,6 @@ import { patchOverlayState } from '../../overlayStore.js'
import { patchUiState } from '../../uiStore.js'
import type { SlashCommand } from '../types.js'
export const destructiveGate = createDestructiveGate()
const DESTRUCTIVE_COMMANDS = new Set(['clear', 'new'])
export const isDestructiveCommand = (name: string) => DESTRUCTIVE_COMMANDS.has(name)
const flagFromArg = (arg: string, current: boolean): boolean | null => {
if (!arg) {
return !current
@ -90,14 +83,27 @@ export const coreCommands: SlashCommand[] = [
return
}
const label = cmd.startsWith('/new') ? '/new' : '/clear'
const isNew = cmd.startsWith('/new')
if (!NO_CONFIRM_DESTRUCTIVE && !destructiveGate.request('clear')) {
return ctx.transcript.sys(`press ${label} again to confirm — starts a new session`)
const commit = () => {
patchUiState({ status: 'forging session…' })
ctx.session.newSession(isNew ? 'new session started' : undefined)
}
patchUiState({ status: 'forging session…' })
ctx.session.newSession(cmd.startsWith('/new') ? 'new session started' : undefined)
if (NO_CONFIRM_DESTRUCTIVE) {
return commit()
}
patchOverlayState({
confirm: {
cancelLabel: 'No, keep going',
confirmLabel: isNew ? 'Yes, start a new session' : 'Yes, clear the session',
danger: true,
detail: 'This ends the current conversation and clears the transcript.',
onConfirm: commit,
title: isNew ? 'Start a new session?' : 'Clear the current session?'
}
})
}
},