mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
feat(tui): double-press confirm on /clear and /new
Prevents accidental session loss: the first press prints "press /clear again within 3s to confirm"; a second press inside the window actually starts a new session. Outside the window the gate re-arms. Opt out with HERMES_TUI_NO_CONFIRM=1 for scripted / muscle-memory workflows. Refs #4069.
This commit is contained in:
52
ui-tui/src/__tests__/destructive.test.ts
Normal file
52
ui-tui/src/__tests__/destructive.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { CONFIRM_WINDOW_MS, createDestructiveGate } from '../domain/destructive.js'
|
||||
|
||||
describe('createDestructiveGate', () => {
|
||||
it('first request is not confirmed — it arms the gate', () => {
|
||||
const g = createDestructiveGate()
|
||||
expect(g.request('clear', 0)).toBe(false)
|
||||
})
|
||||
|
||||
it('second request within window with same key is confirmed', () => {
|
||||
const g = createDestructiveGate()
|
||||
g.request('clear', 0)
|
||||
expect(g.request('clear', 2_500)).toBe(true)
|
||||
})
|
||||
|
||||
it('second request outside the window re-arms and is not confirmed', () => {
|
||||
const g = createDestructiveGate()
|
||||
g.request('clear', 0)
|
||||
expect(g.request('clear', CONFIRM_WINDOW_MS + 1)).toBe(false)
|
||||
})
|
||||
|
||||
it('different key re-arms the gate, does not confirm', () => {
|
||||
const g = createDestructiveGate()
|
||||
g.request('clear', 0)
|
||||
expect(g.request('undo', 500)).toBe(false)
|
||||
expect(g.request('undo', 900)).toBe(true)
|
||||
})
|
||||
|
||||
it('confirmation consumes the pending state so a third press re-arms', () => {
|
||||
const g = createDestructiveGate()
|
||||
g.request('clear', 0)
|
||||
g.request('clear', 500)
|
||||
expect(g.request('clear', 600)).toBe(false)
|
||||
})
|
||||
|
||||
it('reset clears pending state', () => {
|
||||
const g = createDestructiveGate()
|
||||
g.request('clear', 0)
|
||||
g.reset()
|
||||
expect(g.request('clear', 500)).toBe(false)
|
||||
})
|
||||
|
||||
it('respects a custom window', () => {
|
||||
const g = createDestructiveGate(100)
|
||||
g.request('clear', 0)
|
||||
expect(g.request('clear', 50)).toBe(true)
|
||||
|
||||
g.request('clear', 0)
|
||||
expect(g.request('clear', 150)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
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,
|
||||
@@ -13,6 +15,8 @@ import { patchOverlayState } from '../../overlayStore.js'
|
||||
import { patchUiState } from '../../uiStore.js'
|
||||
import type { SlashCommand } from '../types.js'
|
||||
|
||||
const destructiveGate = createDestructiveGate()
|
||||
|
||||
const flagFromArg = (arg: string, current: boolean): boolean | null => {
|
||||
if (!arg) {
|
||||
return !current
|
||||
@@ -82,6 +86,12 @@ export const coreCommands: SlashCommand[] = [
|
||||
return
|
||||
}
|
||||
|
||||
const label = cmd.startsWith('/new') ? '/new' : '/clear'
|
||||
|
||||
if (!NO_CONFIRM_DESTRUCTIVE && !destructiveGate.request('clear')) {
|
||||
return ctx.transcript.sys(`press ${label} again within 3s to confirm (starts a new session)`)
|
||||
}
|
||||
|
||||
patchUiState({ status: 'forging session…' })
|
||||
ctx.session.newSession(cmd.startsWith('/new') ? 'new session started' : undefined)
|
||||
}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export const STARTUP_RESUME_ID = (process.env.HERMES_TUI_RESUME ?? '').trim()
|
||||
export const MOUSE_TRACKING = !/^(?:1|true|yes|on)$/i.test((process.env.HERMES_TUI_DISABLE_MOUSE ?? '').trim())
|
||||
export const NO_CONFIRM_DESTRUCTIVE = /^(?:1|true|yes|on)$/i.test(
|
||||
(process.env.HERMES_TUI_NO_CONFIRM ?? '').trim()
|
||||
)
|
||||
|
||||
23
ui-tui/src/domain/destructive.ts
Normal file
23
ui-tui/src/domain/destructive.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const CONFIRM_WINDOW_MS = 3_000
|
||||
|
||||
export interface DestructiveGate {
|
||||
request: (key: string, now?: number) => boolean
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const createDestructiveGate = (windowMs = CONFIRM_WINDOW_MS): DestructiveGate => {
|
||||
let pending: { at: number; key: string } | null = null
|
||||
|
||||
return {
|
||||
request: (key, now = Date.now()) => {
|
||||
const confirmed = pending?.key === key && now - pending.at < windowMs
|
||||
|
||||
pending = confirmed ? null : { at: now, key }
|
||||
|
||||
return confirmed
|
||||
},
|
||||
reset: () => {
|
||||
pending = null
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user