/** * @license * Copyright 2026 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Box, Text } from 'ink'; import { PolicyDecision, type ApprovalMode, type PolicyEngine, type PolicyRule, listEditableRules, addEditableRule, updateEditableRule, deleteEditableRule, duplicateEditableRule, reloadUserPolicyRules, type EditablePolicyRule, } from '@vybestack/llxprt-code-core'; import { Colors } from '../colors.js'; import { theme } from '../semantic-colors.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { getBorderStyle } from '../contexts/UnicodeRenderingContext.js'; import type { HistoryItemWithoutId } from '../types.js'; import { MessageType } from '../types.js'; import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js'; import { PolicyForm, PolicyStackView, buildMenuItems, ruleSummary, ACTION_ITEMS, type MenuItem, } from './policiesDialogViews.js'; /** Narrow runtime surface the dialog depends on. */ export interface PoliciesDialogRuntime { getPolicyEngine(): PolicyEngine; getApprovalMode(): ApprovalMode; } interface PoliciesDialogProps { config?: PoliciesDialogRuntime; addItem: UseHistoryManagerReturn['addItem']; onExit: () => void; } type View = 'menu' | 'actions' | 'form' | 'stack'; interface DialogState { view: View; rules: EditablePolicyRule[]; engineRules: readonly PolicyRule[]; selectedIndex: number; formMode: 'add' | 'edit'; message: string | null; messageType: MessageType | null; busy: boolean; } function initialDialogState(): DialogState { return { view: 'menu', rules: [], engineRules: [], selectedIndex: -1, formMode: 'add', message: null, messageType: null, busy: false, }; } /** Extract a human-readable detail string from a caught error. */ function errorDetail(error: unknown): string { return error instanceof Error ? error.message : String(error); } interface RunMutationDeps { busyRef: React.MutableRefObject; mountedRef: React.MutableRefObject; patch: (p: Partial) => void; refreshRules: () => Promise; recordMessage: (type: MessageType, text: string) => void; } /** Execute a mutation, then refresh. Report distinct errors for each phase. */ function makeRunMutation(deps: RunMutationDeps) { const { busyRef, mountedRef, patch, refreshRules, recordMessage } = deps; return async (label: string, fn: () => Promise): Promise => { if (busyRef.current) return; busyRef.current = true; patch({ busy: true }); try { await fn(); } catch (error) { if (!mountedRef.current) return; recordMessage( MessageType.ERROR, `Policy update failed: ${errorDetail(error)}`, ); return; } finally { busyRef.current = false; if (mountedRef.current) patch({ busy: false }); } // Mutation succeeded; refresh may still fail. Report a distinct error // so the user knows the change was written but the display is stale. try { await refreshRules(); } catch (error) { if (!mountedRef.current) return; recordMessage( MessageType.ERROR, `Policy: ${label}. Refresh failed: ${errorDetail(error)}`, ); return; } if (!mountedRef.current) return; recordMessage(MessageType.INFO, `Policy: ${label}`); patch({ view: 'menu', selectedIndex: -1 }); }; } interface PoliciesDialogStateProps { engine: PolicyEngine | undefined; approvalMode: ApprovalMode; addItem: UseHistoryManagerReturn['addItem']; } function usePoliciesDialogState({ engine, approvalMode, addItem, }: PoliciesDialogStateProps) { const [state, setState] = useState(initialDialogState); const busyRef = useRef(false); const mountedRef = useRef(true); const patch = useCallback( (p: Partial) => setState((s) => ({ ...s, ...p })), [], ); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const refreshRules = useCallback(async () => { if (engine === undefined) return; const editable = await listEditableRules(); await reloadUserPolicyRules(engine, approvalMode); if (!mountedRef.current) return; patch({ rules: editable, engineRules: engine.getRules() }); }, [engine, approvalMode, patch]); useEffect(() => { void refreshRules().catch((error) => { if (!mountedRef.current) return; patch({ message: `Policy refresh failed: ${errorDetail(error)}`, messageType: MessageType.ERROR, }); }); }, [refreshRules, patch]); const recordMessage = useCallback( (type: MessageType, text: string) => { patch({ message: text, messageType: type }); addItem({ type, text } as HistoryItemWithoutId, Date.now()); }, [addItem, patch], ); const runMutation = useMemo( () => makeRunMutation({ busyRef, mountedRef, patch, refreshRules, recordMessage, }), [refreshRules, recordMessage, patch], ); const handleAdd = useCallback( (rule: EditablePolicyRule) => { void runMutation(`added ${ruleSummary(rule)}`, async () => { await addEditableRule(rule); }); }, [runMutation], ); return { state, patch, refreshRules, runMutation, handleAdd, recordMessage }; } function useMutationHandlers( state: DialogState, runMutation: (label: string, fn: () => Promise) => Promise, patch: (p: Partial) => void, ) { const handleEdit = useCallback( (rule: EditablePolicyRule) => { void runMutation(`updated to ${ruleSummary(rule)}`, async () => { await updateEditableRule(state.selectedIndex, rule); }); }, [runMutation, state.selectedIndex], ); const handleDelete = useCallback(() => { if (state.selectedIndex < 0 || state.selectedIndex >= state.rules.length) { return; } const target = state.rules[state.selectedIndex]; void runMutation(`deleted ${ruleSummary(target)}`, () => deleteEditableRule(state.selectedIndex), ); }, [state.rules, state.selectedIndex, runMutation]); const handleDuplicate = useCallback(() => { void runMutation( `duplicated rule #${state.selectedIndex + 1}`, async () => { await duplicateEditableRule(state.selectedIndex); }, ); }, [state.selectedIndex, runMutation]); const handleMenuSelect = useCallback( (value: string) => { if (value === '__add__') { patch({ formMode: 'add', view: 'form' }); } else if (value === '__stack__') { patch({ view: 'stack' }); } else if (value === '__close__') { // Close handled by parent via onExit } else { patch({ selectedIndex: Number.parseInt(value, 10), view: 'actions', }); } }, [patch], ); const handleActionSelect = useCallback( (value: string) => { if (value === 'edit') { patch({ formMode: 'edit', view: 'form' }); } else if (value === 'delete') { handleDelete(); } else if (value === 'duplicate') { handleDuplicate(); } else { patch({ view: 'menu' }); } }, [patch, handleDelete, handleDuplicate], ); return { handleEdit, handleDelete, handleDuplicate, handleMenuSelect, handleActionSelect, }; } export const PoliciesDialog: React.FC = ({ config, addItem, onExit, }) => { const engine = config?.getPolicyEngine(); const approvalMode = config?.getApprovalMode() ?? ('default' as ApprovalMode); const { state, patch, runMutation, handleAdd } = usePoliciesDialogState({ engine, approvalMode, addItem, }); const { handleEdit, handleMenuSelect, handleActionSelect } = useMutationHandlers(state, runMutation, patch); useKeypress( (key) => { if (state.busy) return; if (key.name === 'escape') { if (state.view === 'menu') { onExit(); } else if (state.view !== 'form') { patch({ view: 'menu' }); } } }, { isActive: state.view !== 'form' }, ); if (engine === undefined) { return ( Policy engine not available. Cannot manage policies. ); } return ( { if (v === '__close__') onExit(); else handleMenuSelect(v); }} onActionSelect={handleActionSelect} onAdd={handleAdd} onEdit={handleEdit} onCancel={() => patch({ view: 'menu' })} onStackBack={() => patch({ view: 'menu' })} /> ); }; interface PolicyDialogShellProps { state: DialogState; menuItems: MenuItem[]; onMenuSelect: (value: string) => void; onActionSelect: (value: string) => void; onAdd: (rule: EditablePolicyRule) => void; onEdit: (rule: EditablePolicyRule) => void; onCancel: () => void; onStackBack: () => void; } const PolicyDialogShell: React.FC = (props) => { const { state, menuItems, onMenuSelect, onActionSelect, onAdd, onEdit, onCancel, onStackBack, } = props; return ( {state.view === 'menu' && ( )} {state.view === 'actions' && ( )} {state.view === 'form' && ( = 0 && state.selectedIndex < state.rules.length ? state.rules[state.selectedIndex] : { toolName: '', decision: PolicyDecision.ALLOW, priority: 100, } } onSubmit={state.formMode === 'add' ? onAdd : onEdit} onCancel={onCancel} /> )} {state.view === 'stack' && ( )} {state.busy && ( {'Applying\u2026'} )} (Use arrow keys to navigate, Enter to select, Esc to go back/exit) ); }; const DialogHeader: React.FC<{ message: string | null; messageType: MessageType | null; }> = ({ message, messageType }) => ( <> Policy Manager Editable overrides (auto-saved.toml) apply immediately. Default & system tiers are read-only. {message !== null && ( {message} )} ); const MenuView: React.FC<{ rules: readonly EditablePolicyRule[]; menuItems: MenuItem[]; onSelect: (value: string) => void; busy: boolean; }> = ({ rules, menuItems, onSelect, busy }) => ( {rules.length === 0 && ( No user overrides yet. Add a rule to get started. )} ); const ActionsView: React.FC<{ rules: readonly EditablePolicyRule[]; selectedIndex: number; onSelect: (value: string) => void; busy: boolean; }> = ({ rules, selectedIndex, onSelect, busy }) => { const inBounds = selectedIndex >= 0 && selectedIndex < rules.length; return ( Selected: {inBounds ? ruleSummary(rules[selectedIndex]) : '(none)'} ); }; const StackView: React.FC<{ rules: readonly PolicyRule[]; onBack: () => void; }> = ({ rules, onBack }) => ( (Esc or Enter to go back to the menu) ); const StackBackHandler: React.FC<{ onBack: () => void }> = ({ onBack }) => { useKeypress( (key) => { if (key.name === 'return') { onBack(); } }, { isActive: true }, ); return null; };