11import { useEffect , useRef , useState , useCallback } from 'react'
22import { createPortal } from 'react-dom'
33import { nanoid } from 'nanoid'
4- import { X , Send , Sparkles , Trash2 , Square , ShieldCheck , Wrench , AlertCircle , ChevronDown , Check , Eye , EyeOff , ShieldAlert , RotateCcw , Download } from 'lucide-react'
4+ import { X , Send , Sparkles , Trash2 , Square , ShieldCheck , Wrench , AlertCircle , ChevronDown , Check , Eye , EyeOff , ShieldAlert , RotateCcw , Download , Zap } from 'lucide-react'
55import { useAppStore , AiMessage as AiMessageType , AiPermission , AiApproval } from '../../store'
66import { cn , stripAnsi } from '../../lib/utils'
77import { AiMessage } from './AiMessage'
@@ -47,6 +47,21 @@ function formatTokens(n: number): string {
4747 return String ( n )
4848}
4949
50+ // ── Unified agent mode (combines permission + approval) ────────────────────────
51+ type AiMode = 'read-only' | 'fix' | 'auto-pilot'
52+
53+ function modeToPermission ( m : AiMode ) : AiPermission {
54+ return m === 'read-only' ? 'troubleshoot' : 'full-access'
55+ }
56+ function modeToApproval ( m : AiMode ) : AiApproval {
57+ return m === 'auto-pilot' ? 'auto' : 'ask'
58+ }
59+ function permApprovalToMode ( p : AiPermission , a : AiApproval ) : AiMode {
60+ if ( p === 'troubleshoot' ) return 'read-only'
61+ if ( a === 'auto' ) return 'auto-pilot'
62+ return 'fix'
63+ }
64+
5065export function AiPanel ( { activeSession, splitSession, allSessions, getTerminalContext, sendToTerminal, sendToSession } : Props ) : JSX . Element {
5166 const {
5267 aiMessages, aiStreaming, aiAgentActive, aiPermission, aiApproval, aiBlacklist, aiTokens,
@@ -57,22 +72,24 @@ export function AiPanel({ activeSession, splitSession, allSessions, getTerminalC
5772
5873 // Per-session overrides — start from global settings, can be changed mid-chat
5974 // Reset to global defaults when conversation is cleared
60- const [ sessionPermission , setSessionPermission ] = useState < AiPermission > ( aiPermission )
61- const [ sessionApproval , setSessionApproval ] = useState < AiApproval > ( aiApproval )
62- const [ sessionBlacklist , setSessionBlacklist ] = useState < string [ ] > ( aiBlacklist )
63- const [ autoWatch , setAutoWatch ] = useState ( true )
64- const [ historyCommands , setHistoryCommands ] = useState < string [ ] > ( [ ] )
65- const [ privacyDismissed , setPrivacyDismissed ] = useState ( ( ) =>
75+ const [ sessionMode , setSessionMode ] = useState < AiMode > ( ( ) => permApprovalToMode ( aiPermission , aiApproval ) )
76+ const [ sessionBlacklist , setSessionBlacklist ] = useState < string [ ] > ( aiBlacklist )
77+ const [ autoWatch , setAutoWatch ] = useState ( true )
78+ const [ historyCommands , setHistoryCommands ] = useState < string [ ] > ( [ ] )
79+ const [ privacyDismissed , setPrivacyDismissed ] = useState ( ( ) =>
6680 localStorage . getItem ( 'aria-privacy-notice-accepted' ) === '1'
6781 )
82+
83+ // Derived — keeps the rest of the component working without changes
84+ const sessionPermission = modeToPermission ( sessionMode )
85+ const sessionApproval = modeToApproval ( sessionMode )
6886 const prevMessageCount = useRef ( 0 )
6987
7088 // Sequential command queue — prevents race condition when auto-executing multiple commands
7189 const commandQueueRef = useRef < Promise < void > > ( Promise . resolve ( ) )
7290 useEffect ( ( ) => {
7391 if ( aiMessages . length === 0 && prevMessageCount . current > 0 ) {
74- setSessionPermission ( aiPermission )
75- setSessionApproval ( aiApproval )
92+ setSessionMode ( permApprovalToMode ( aiPermission , aiApproval ) )
7693 setSessionBlacklist ( aiBlacklist )
7794 }
7895 prevMessageCount . current = aiMessages . length
@@ -750,8 +767,7 @@ export function AiPanel({ activeSession, splitSession, allSessions, getTerminalC
750767
751768 { /* Toolbar */ }
752769 < div className = "flex items-center gap-0.5 px-2.5 pb-2.5 pt-1" >
753- < ModeSelector value = { sessionPermission } onChange = { setSessionPermission } />
754- < ApprovalSelector value = { sessionApproval } onChange = { setSessionApproval } />
770+ < AgentModeSelector value = { sessionMode } onChange = { setSessionMode } />
755771 < BlacklistButton blacklist = { sessionBlacklist } onChange = { setSessionBlacklist } />
756772
757773 < button
@@ -904,41 +920,26 @@ function PillSelect<T extends string>({
904920 )
905921}
906922
907- function ModeSelector ( { value, onChange } : { value : AiPermission ; onChange : ( v : AiPermission ) => void } ) : JSX . Element {
923+ function AgentModeSelector ( { value, onChange } : { value : AiMode ; onChange : ( v : AiMode ) => void } ) : JSX . Element {
908924 return (
909925 < PillSelect
910926 value = { value }
911927 onChange = { onChange }
912928 options = { [
913929 {
914- id : 'troubleshoot ' , label : 'Troubleshoot ' , short : 'Scan ' ,
930+ id : 'read-only ' , label : 'Read Only ' , short : 'Read Only ' ,
915931 icon : < ShieldCheck className = "w-3 h-3" /> ,
916- dimColor : 'text-amber -500/70' , activeColor : 'text-amber -400' ,
932+ dimColor : 'text-emerald -500/70' , activeColor : 'text-emerald -400' ,
917933 } ,
918934 {
919- id : 'full-access ' , label : 'Full Access ' , short : 'Full ' ,
935+ id : 'fix ' , label : 'Fix Mode ' , short : 'Fix Mode ' ,
920936 icon : < Wrench className = "w-3 h-3" /> ,
921- dimColor : 'text-red-500/70' , activeColor : 'text-red-400' ,
922- } ,
923- ] }
924- />
925- )
926- }
927-
928- function ApprovalSelector ( { value, onChange } : { value : AiApproval ; onChange : ( v : AiApproval ) => void } ) : JSX . Element {
929- return (
930- < PillSelect
931- value = { value }
932- onChange = { onChange }
933- align = "right"
934- options = { [
935- {
936- id : 'ask' , label : 'Ask each time' , short : 'Ask' ,
937- dimColor : 'text-primary/60' , activeColor : 'text-primary' ,
937+ dimColor : 'text-amber-500/70' , activeColor : 'text-amber-400' ,
938938 } ,
939939 {
940- id : 'auto' , label : 'Auto-approve' , short : 'Auto' ,
941- dimColor : 'text-emerald-500/60' , activeColor : 'text-emerald-400' ,
940+ id : 'auto-pilot' , label : 'Auto Pilot' , short : 'Auto Pilot' ,
941+ icon : < Zap className = "w-3 h-3" /> ,
942+ dimColor : 'text-red-500/70' , activeColor : 'text-red-400' ,
942943 } ,
943944 ] }
944945 />
@@ -991,7 +992,7 @@ function BlacklistButton({ blacklist, onChange }: { blacklist: string[]; onChang
991992 ) }
992993 >
993994 < ShieldAlert className = "w-3 h-3" />
994- < span > Patterns </ span >
995+ < span > Blocked </ span >
995996 { activeCount > 0 && (
996997 < span className = "text-[11px] opacity-70" > ({ activeCount } )</ span >
997998 ) }
0 commit comments