fix(tui): honor destructive slash confirmation config
This commit is contained in:
@@ -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' }))
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const buildUiState = (): UiState => ({
|
||||
busy: false,
|
||||
busyInputMode: 'queue',
|
||||
compact: false,
|
||||
destructiveSlashConfirm: true,
|
||||
detailsMode: 'collapsed',
|
||||
detailsModeCommandOverride: false,
|
||||
focusView: false,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user