fix(tui): honor destructive slash confirmation config

This commit is contained in:
YuYigeng
2026-07-26 23:41:12 +08:00
committed by Teknium
parent 08f32a6335
commit 77f35add0c
7 changed files with 95 additions and 1 deletions

View File

@@ -482,6 +482,19 @@ describe('createSlashHandler', () => {
expect(ctx.gateway.rpc).not.toHaveBeenCalled()
})
it.each([
['/new sprint planning', 'new session started', 'sprint planning'],
['/clear', undefined, undefined]
])('skips the confirmation for %s when config disables it', (command, message, title) => {
patchUiState({ destructiveSlashConfirm: false })
const ctx = buildCtx()
expect(createSlashHandler(ctx)(command)).toBe(true)
expect(getOverlayState().confirm).toBeNull()
expect(ctx.session.newSession).toHaveBeenCalledWith(message, title)
})
it('routes the /reset catalog alias through the local fresh-session lifecycle', () => {
const ctx = buildCtx({
local: {
@@ -501,6 +514,26 @@ describe('createSlashHandler', () => {
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
})
it('skips the confirmation for the /reset alias when config disables it', () => {
patchUiState({ destructiveSlashConfirm: false })
const ctx = buildCtx({
local: {
catalog: {
canon: {
'/new': '/new',
'/reset': '/new'
}
}
}
})
expect(createSlashHandler(ctx)('/reset')).toBe(true)
expect(getOverlayState().confirm).toBeNull()
expect(ctx.session.newSession).toHaveBeenCalledWith('new session started', undefined)
})
it('keeps visible scrollback when branching a TUI session', async () => {
patchUiState({ sid: 'sid-parent' })
const rpc = vi.fn(() => Promise.resolve({ session_id: 'sid-branch', title: 'branch title' }))

View File

@@ -47,6 +47,54 @@ describe('applyDisplay', () => {
expect(s.streaming).toBe(false)
})
it('hydrates the destructive slash confirmation policy from approvals', () => {
const setBell = vi.fn()
applyDisplay(
{
config: {
approvals: { destructive_slash_confirm: false },
display: {}
}
},
setBell
)
expect($uiState.get().destructiveSlashConfirm).toBe(false)
applyDisplay(
{
config: {
approvals: { destructive_slash_confirm: true },
display: {}
}
},
setBell
)
expect($uiState.get().destructiveSlashConfirm).toBe(true)
})
it('defaults destructive slash confirmation on and preserves it across config RPC failure', () => {
const setBell = vi.fn()
applyDisplay({ config: { display: {} } }, setBell)
expect($uiState.get().destructiveSlashConfirm).toBe(true)
applyDisplay(
{
config: {
approvals: { destructive_slash_confirm: false },
display: {}
}
},
setBell
)
applyDisplay(null, setBell)
expect($uiState.get().destructiveSlashConfirm).toBe(false)
})
it('coerces legacy true + "on" alias to top', () => {
const setBell = vi.fn()

View File

@@ -322,6 +322,7 @@ export interface UiState {
busy: boolean
busyInputMode: BusyInputMode
compact: boolean
destructiveSlashConfirm: boolean
detailsMode: DetailsMode
detailsModeCommandOverride: boolean
// Focus view (/focus) — display-only reduced-output mode. Drives the

View File

@@ -199,7 +199,7 @@ export const coreCommands: SlashCommand[] = [
ctx.session.newSession(isNew ? 'new session started' : undefined, requestedTitle || undefined)
}
if (NO_CONFIRM_DESTRUCTIVE) {
if (NO_CONFIRM_DESTRUCTIVE || !ctx.ui.destructiveSlashConfirm) {
return commit()
}

View File

@@ -14,6 +14,7 @@ const buildUiState = (): UiState => ({
busy: false,
busyInputMode: 'queue',
compact: false,
destructiveSlashConfirm: true,
detailsMode: 'collapsed',
detailsModeCommandOverride: false,
focusView: false,

View File

@@ -253,6 +253,7 @@ export const applyDisplay = (
setVoiceRecordKey?: (v: ParsedVoiceRecordKey) => void
) => {
const d = cfg?.config?.display ?? {}
const approvals = cfg?.config?.approvals
setBell(!!d.bell_on_complete)
@@ -273,6 +274,10 @@ export const applyDisplay = (
battery: !!d.battery,
busyInputMode: normalizeBusyInputMode(d.busy_input_mode),
compact: !!d.tui_compact,
// Fail safe: only YAML boolean false disables the prompt. A transient
// config RPC failure (cfg=null) preserves the last known policy instead
// of silently changing approval behavior until the next successful poll.
...(cfg ? { destructiveSlashConfirm: approvals?.destructive_slash_confirm !== false } : {}),
detailsMode: resolveDetailsMode(d),
detailsModeCommandOverride: false,
focusView: !!d.focus_view,

View File

@@ -119,8 +119,14 @@ export interface ConfigVoiceConfig {
submit_mode?: unknown
}
export interface ConfigApprovalsConfig {
// Raw config value: only the explicit boolean false disables the safety gate.
destructive_slash_confirm?: unknown
}
export interface ConfigFullResponse {
config?: {
approvals?: ConfigApprovalsConfig
display?: ConfigDisplayConfig
voice?: ConfigVoiceConfig
paste_collapse_threshold?: number