Skip to content

Commit adca7e4

Browse files
feat: unify AI agent mode into three clear presets
Replace the two separate Permission + Approval dropdowns with a single AgentModeSelector showing three self-explanatory options: • Read Only — Troubleshoot + Ask (read-only commands, confirm each) • Fix Mode — Full Access + Ask (config changes, confirm each step) • Auto Pilot — Full Access + Auto (executes automatically, use with caution) Rename the blacklist toolbar button from "Patterns" to "Blocked". Merge the two Settings → ARIA sections ("Agent Mode" + "Command Execution") into one section with colored badge labels describing the risk level of each mode.
1 parent 84d2c67 commit adca7e4

2 files changed

Lines changed: 91 additions & 113 deletions

File tree

src/renderer/src/components/ai/AiPanel.tsx

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useRef, useState, useCallback } from 'react'
22
import { createPortal } from 'react-dom'
33
import { 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'
55
import { useAppStore, AiMessage as AiMessageType, AiPermission, AiApproval } from '../../store'
66
import { cn, stripAnsi } from '../../lib/utils'
77
import { 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+
5065
export 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
)}

src/renderer/src/components/dialogs/SettingsDialog.tsx

Lines changed: 55 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,36 +1102,6 @@ function AiSection(): JSX.Element {
11021102
setTimeout(() => setSettingsSaved(false), 2500)
11031103
}
11041104

1105-
const permissionOptions: { id: AiPermission; label: string; desc: string; icon: JSX.Element }[] = [
1106-
{
1107-
id: 'troubleshoot',
1108-
label: 'Troubleshoot',
1109-
desc: 'Read-only commands only — show, ping, ls, ps, etc. Safe for monitoring.',
1110-
icon: <ShieldCheck className="w-4 h-4 text-amber-400" />,
1111-
},
1112-
{
1113-
id: 'full-access',
1114-
label: 'Full Access',
1115-
desc: 'Any command including configuration changes. Use with caution.',
1116-
icon: <Wrench className="w-4 h-4 text-red-400" />,
1117-
},
1118-
]
1119-
1120-
const approvalOptions: { id: AiApproval; label: string; desc: string; icon: JSX.Element }[] = [
1121-
{
1122-
id: 'ask',
1123-
label: 'Ask before each command',
1124-
desc: 'AI shows the command and waits for your approval before running.',
1125-
icon: <Sparkles className="w-4 h-4 text-primary" />,
1126-
},
1127-
{
1128-
id: 'auto',
1129-
label: 'Auto-approve all',
1130-
desc: 'AI executes commands immediately without asking. Fastest workflow.',
1131-
icon: <Zap className="w-4 h-4 text-emerald-400" />,
1132-
},
1133-
]
1134-
11351105
return (
11361106
<div className="space-y-6">
11371107
<SectionHeader
@@ -1213,57 +1183,64 @@ function AiSection(): JSX.Element {
12131183
)}
12141184
</SettingsGroup>
12151185

1216-
{/* Permission Mode */}
1186+
{/* Agent Mode — unified 3-option selector */}
12171187
<SettingsGroup label="Agent Mode">
12181188
<div className="space-y-2">
1219-
{permissionOptions.map((opt) => (
1220-
<button
1221-
key={opt.id}
1222-
onClick={() => setAiPermission(opt.id)}
1223-
className={cn(
1224-
'w-full flex items-start gap-3 p-3 rounded-lg border text-left transition-colors',
1225-
aiPermission === opt.id
1226-
? 'border-primary/50 bg-primary/10'
1227-
: 'border-border hover:border-border/80 hover:bg-accent/50'
1228-
)}
1229-
>
1230-
<div className="mt-0.5 shrink-0">{opt.icon}</div>
1231-
<div className="flex-1 min-w-0">
1232-
<div className="flex items-center gap-2">
1233-
<span className="text-sm font-medium text-foreground">{opt.label}</span>
1234-
{aiPermission === opt.id && <Check className="w-3 h-3 text-primary" />}
1235-
</div>
1236-
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">{opt.desc}</p>
1237-
</div>
1238-
</button>
1239-
))}
1240-
</div>
1241-
</SettingsGroup>
1242-
1243-
{/* Approval Setting */}
1244-
<SettingsGroup label="Command Execution">
1245-
<div className="space-y-2">
1246-
{approvalOptions.map((opt) => (
1247-
<button
1248-
key={opt.id}
1249-
onClick={() => setAiApproval(opt.id)}
1250-
className={cn(
1251-
'w-full flex items-start gap-3 p-3 rounded-lg border text-left transition-colors',
1252-
aiApproval === opt.id
1253-
? 'border-primary/50 bg-primary/10'
1254-
: 'border-border hover:border-border/80 hover:bg-accent/50'
1255-
)}
1256-
>
1257-
<div className="mt-0.5 shrink-0">{opt.icon}</div>
1258-
<div className="flex-1 min-w-0">
1259-
<div className="flex items-center gap-2">
1260-
<span className="text-sm font-medium text-foreground">{opt.label}</span>
1261-
{aiApproval === opt.id && <Check className="w-3 h-3 text-primary" />}
1189+
{[
1190+
{
1191+
permission: 'troubleshoot' as AiPermission,
1192+
approval: 'ask' as AiApproval,
1193+
label: 'Read Only',
1194+
desc: 'Diagnose and inspect — read-only commands only (show, ping, ls…). Nothing is changed on the device.',
1195+
icon: <ShieldCheck className="w-4 h-4 text-emerald-400" />,
1196+
badge: 'Safest',
1197+
badgeCls: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20',
1198+
},
1199+
{
1200+
permission: 'full-access' as AiPermission,
1201+
approval: 'ask' as AiApproval,
1202+
label: 'Fix Mode',
1203+
desc: 'ARIA can apply configuration changes, but pauses and asks your approval before running each command.',
1204+
icon: <Wrench className="w-4 h-4 text-amber-400" />,
1205+
badge: 'Confirm each step',
1206+
badgeCls: 'text-amber-400 bg-amber-500/10 border-amber-500/20',
1207+
},
1208+
{
1209+
permission: 'full-access' as AiPermission,
1210+
approval: 'auto' as AiApproval,
1211+
label: 'Auto Pilot',
1212+
desc: 'ARIA executes commands automatically without stopping. Fastest workflow — use only on non-production devices.',
1213+
icon: <Zap className="w-4 h-4 text-red-400" />,
1214+
badge: 'Use with caution',
1215+
badgeCls: 'text-red-400 bg-red-500/10 border-red-500/20',
1216+
},
1217+
].map((opt) => {
1218+
const isActive = aiPermission === opt.permission && aiApproval === opt.approval
1219+
return (
1220+
<button
1221+
key={opt.label}
1222+
onClick={() => { setAiPermission(opt.permission); setAiApproval(opt.approval) }}
1223+
className={cn(
1224+
'w-full flex items-start gap-3 p-3 rounded-lg border text-left transition-colors',
1225+
isActive
1226+
? 'border-primary/50 bg-primary/10'
1227+
: 'border-border hover:border-border/80 hover:bg-accent/50'
1228+
)}
1229+
>
1230+
<div className="mt-0.5 shrink-0">{opt.icon}</div>
1231+
<div className="flex-1 min-w-0">
1232+
<div className="flex items-center gap-2 flex-wrap">
1233+
<span className="text-sm font-medium text-foreground">{opt.label}</span>
1234+
<span className={cn('text-[10px] px-1.5 py-0.5 rounded-full border font-medium', opt.badgeCls)}>
1235+
{opt.badge}
1236+
</span>
1237+
{isActive && <Check className="w-3 h-3 text-primary ml-auto shrink-0" />}
1238+
</div>
1239+
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">{opt.desc}</p>
12621240
</div>
1263-
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">{opt.desc}</p>
1264-
</div>
1265-
</button>
1266-
))}
1241+
</button>
1242+
)
1243+
})}
12671244
</div>
12681245
</SettingsGroup>
12691246

0 commit comments

Comments
 (0)