exec-config-panel.tsx
1 'use client' 2 3 import { useCallback, useEffect, useState } from 'react' 4 import { api } from '@/lib/app/api-client' 5 import type { ExecApprovalConfig, ExecApprovalSnapshot } from '@/types' 6 7 interface Props { 8 agentId: string 9 } 10 11 export function ExecConfigPanel({ agentId }: Props) { 12 const [config, setConfig] = useState<ExecApprovalConfig>({ security: 'deny', askMode: 'off', patterns: [] }) 13 const [hash, setHash] = useState('') 14 const [loading, setLoading] = useState(true) 15 const [saving, setSaving] = useState(false) 16 const [error, setError] = useState('') 17 const [newPattern, setNewPattern] = useState('') 18 19 const load = useCallback(async () => { 20 setLoading(true) 21 setError('') 22 try { 23 const snap = await api<ExecApprovalSnapshot>('GET', `/openclaw/exec-config?agentId=${agentId}`) 24 setConfig(snap.file) 25 setHash(snap.hash) 26 } catch (err: unknown) { 27 setError(err instanceof Error ? err.message : 'Failed to load') 28 } finally { 29 setLoading(false) 30 } 31 }, [agentId]) 32 33 useEffect(() => { load() }, [load]) 34 35 const save = async (patch: Partial<ExecApprovalConfig>) => { 36 const updated = { ...config, ...patch } 37 setConfig(updated) 38 setSaving(true) 39 setError('') 40 try { 41 const result = await api<{ ok: boolean; hash: string }>('PUT', '/openclaw/exec-config', { 42 agentId, 43 config: updated, 44 baseHash: hash, 45 }) 46 setHash(result.hash) 47 } catch (err: unknown) { 48 setError(err instanceof Error ? err.message : 'Save failed') 49 } finally { 50 setSaving(false) 51 } 52 } 53 54 const addPattern = () => { 55 const p = newPattern.trim() 56 if (!p || config.patterns.includes(p)) return 57 save({ patterns: [...config.patterns, p] }) 58 setNewPattern('') 59 } 60 61 const removePattern = (idx: number) => { 62 save({ patterns: config.patterns.filter((_, i) => i !== idx) }) 63 } 64 65 if (loading) return <div className="p-4 text-[13px] text-text-3/50">Loading exec config...</div> 66 67 return ( 68 <div className="flex flex-col gap-4"> 69 {/* Security Level */} 70 <div> 71 <label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-2">Security Level</label> 72 <select 73 value={config.security} 74 onChange={(e) => save({ security: e.target.value as ExecApprovalConfig['security'] })} 75 disabled={saving} 76 className="w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none" 77 > 78 <option value="deny">Deny (block all)</option> 79 <option value="allowlist">Allowlist (matched patterns only)</option> 80 <option value="full">Full (allow all)</option> 81 </select> 82 </div> 83 84 {/* Ask Mode */} 85 <div> 86 <label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-2">Ask Mode</label> 87 <select 88 value={config.askMode} 89 onChange={(e) => save({ askMode: e.target.value as ExecApprovalConfig['askMode'] })} 90 disabled={saving} 91 className="w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none" 92 > 93 <option value="off">Off</option> 94 <option value="on-miss">On miss (ask when no pattern matches)</option> 95 <option value="always">Always ask</option> 96 </select> 97 </div> 98 99 {/* Patterns */} 100 {config.security === 'allowlist' && ( 101 <div> 102 <label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-2"> 103 Allowed Patterns 104 </label> 105 <div className="flex flex-col gap-1 mb-2"> 106 {config.patterns.map((p, i) => ( 107 <div key={i} className="flex items-center gap-2 py-1 px-2 rounded-[8px] bg-white/[0.02] border border-white/[0.04]"> 108 <span className="text-[12px] text-text font-mono truncate flex-1">{p}</span> 109 <button 110 onClick={() => removePattern(i)} 111 disabled={saving} 112 className="text-red-400/60 hover:text-red-400 text-[10px] bg-transparent border-none cursor-pointer" 113 > 114 Remove 115 </button> 116 </div> 117 ))} 118 {!config.patterns.length && ( 119 <span className="text-[12px] text-text-3/40">No patterns configured</span> 120 )} 121 </div> 122 <div className="flex gap-2"> 123 <input 124 type="text" 125 value={newPattern} 126 onChange={(e) => setNewPattern(e.target.value)} 127 onKeyDown={(e) => e.key === 'Enter' && addPattern()} 128 placeholder="e.g. npm run *" 129 className="flex-1 px-3 py-1.5 rounded-[8px] border border-white/[0.06] bg-black/20 text-[12px] text-text font-mono outline-none placeholder:text-text-3/40" 130 /> 131 <button 132 onClick={addPattern} 133 disabled={saving || !newPattern.trim()} 134 className="px-3 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[11px] font-600 cursor-pointer disabled:opacity-30 transition-all" 135 style={{ fontFamily: 'inherit' }} 136 > 137 Add 138 </button> 139 </div> 140 </div> 141 )} 142 143 {error && <p className="text-[12px] text-red-400">{error}</p>} 144 {saving && <p className="text-[11px] text-text-3/50">Saving...</p>} 145 </div> 146 ) 147 }