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:
@@ -3,7 +3,6 @@ import type { SlashExecResponse } from '../gatewayTypes.js'
|
||||
import { asCommandDispatch, rpcErrorMessage } from '../lib/rpc.js'
|
||||
|
||||
import type { SlashHandlerContext } from './interfaces.js'
|
||||
import { destructiveGate, isDestructiveCommand } from './slash/commands/core.js'
|
||||
import { findSlashCommand } from './slash/registry.js'
|
||||
import type { SlashRunCtx } from './slash/types.js'
|
||||
import { getUiState } from './uiStore.js'
|
||||
@@ -41,17 +40,11 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b
|
||||
const found = findSlashCommand(parsed.name)
|
||||
|
||||
if (found) {
|
||||
if (!isDestructiveCommand(found.name)) {
|
||||
destructiveGate.reset()
|
||||
}
|
||||
|
||||
found.run(parsed.arg, runCtx, cmd)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
destructiveGate.reset()
|
||||
|
||||
if (catalog?.canon) {
|
||||
const needle = `/${parsed.name}`.toLowerCase()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ActivityItem,
|
||||
ApprovalReq,
|
||||
ClarifyReq,
|
||||
ConfirmReq,
|
||||
DetailsMode,
|
||||
Msg,
|
||||
PanelSection,
|
||||
@@ -53,6 +54,7 @@ export interface GatewayProviderProps {
|
||||
export interface OverlayState {
|
||||
approval: ApprovalReq | null
|
||||
clarify: ClarifyReq | null
|
||||
confirm: ConfirmReq | null
|
||||
modelPicker: boolean
|
||||
pager: null | PagerState
|
||||
picker: boolean
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { OverlayState } from './interfaces.js'
|
||||
const buildOverlayState = (): OverlayState => ({
|
||||
approval: null,
|
||||
clarify: null,
|
||||
confirm: null,
|
||||
modelPicker: false,
|
||||
pager: null,
|
||||
picker: false,
|
||||
@@ -17,8 +18,8 @@ export const $overlayState = atom<OverlayState>(buildOverlayState())
|
||||
|
||||
export const $isBlocked = computed(
|
||||
$overlayState,
|
||||
({ approval, clarify, modelPicker, pager, picker, secret, skillsHub, sudo }) =>
|
||||
Boolean(approval || clarify || modelPicker || pager || picker || secret || skillsHub || sudo)
|
||||
({ approval, clarify, confirm, modelPicker, pager, picker, secret, skillsHub, sudo }) =>
|
||||
Boolean(approval || clarify || confirm || modelPicker || pager || picker || secret || skillsHub || sudo)
|
||||
)
|
||||
|
||||
export const getOverlayState = () => $overlayState.get()
|
||||
|
||||
@@ -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?'
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user