diff --git a/src/main/AryxAppService.ts b/src/main/AryxAppService.ts index e78384a..5757192 100644 --- a/src/main/AryxAppService.ts +++ b/src/main/AryxAppService.ts @@ -699,6 +699,7 @@ export class AryxAppService extends EventEmitter { sessionId: string, approvalId: string, decision: ApprovalDecision, + alwaysApprove?: boolean, ): Promise { const workspace = await this.loadWorkspace(); const session = this.requireSession(workspace, sessionId); @@ -726,6 +727,13 @@ export class AryxAppService extends EventEmitter { this.setSessionPendingApprovalState(session, dequeuePendingApprovalState(session, approvalId)); session.updatedAt = resolvedAt; + if (decision === 'approved' && alwaysApprove && approval.toolName) { + const existing = session.approvalSettings?.autoApprovedToolNames ?? []; + if (!existing.includes(approval.toolName)) { + session.approvalSettings = { autoApprovedToolNames: [...existing, approval.toolName] }; + } + } + const updatedRun = this.updateSessionRun(session, handle.requestId, (run) => upsertRunApprovalEvent(run, resolvedApproval)); diff --git a/src/main/ipc/registerIpcHandlers.ts b/src/main/ipc/registerIpcHandlers.ts index 5586472..218d6b5 100644 --- a/src/main/ipc/registerIpcHandlers.ts +++ b/src/main/ipc/registerIpcHandlers.ts @@ -113,7 +113,7 @@ export function registerIpcHandlers(window: BrowserWindow, service: AryxAppServi service.cancelSessionTurn(input.sessionId), ); ipcMain.handle(ipcChannels.resolveSessionApproval, (_event, input: ResolveSessionApprovalInput) => - service.resolveSessionApproval(input.sessionId, input.approvalId, input.decision), + service.resolveSessionApproval(input.sessionId, input.approvalId, input.decision, input.alwaysApprove), ); ipcMain.handle(ipcChannels.resolveSessionUserInput, (_event, input: ResolveSessionUserInputInput) => service.resolveSessionUserInput(input.sessionId, input.userInputId, input.answer, input.wasFreeform), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 4bab4e8..a2cbfae 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -247,8 +247,8 @@ export default function App() { api.sendSessionMessage({ sessionId: selectedSession.id, content: c })} onCancelTurn={() => { void api.cancelSessionTurn({ sessionId: selectedSession.id }); }} - onResolveApproval={(approvalId, decision) => - api.resolveSessionApproval({ sessionId: selectedSession.id, approvalId, decision }) + onResolveApproval={(approvalId, decision, alwaysApprove) => + api.resolveSessionApproval({ sessionId: selectedSession.id, approvalId, decision, alwaysApprove }) } onResolveUserInput={(userInputId, answer, wasFreeform) => api.resolveSessionUserInput({ sessionId: selectedSession.id, userInputId, answer, wasFreeform }) diff --git a/src/renderer/components/ChatPane.tsx b/src/renderer/components/ChatPane.tsx index 54682e5..c5964ae 100644 --- a/src/renderer/components/ChatPane.tsx +++ b/src/renderer/components/ChatPane.tsx @@ -39,7 +39,7 @@ interface ChatPaneProps { runtimeTools?: ReadonlyArray; onSend: (content: string) => Promise; onCancelTurn?: () => void; - onResolveApproval?: (approvalId: string, decision: ApprovalDecision) => Promise; + onResolveApproval?: (approvalId: string, decision: ApprovalDecision, alwaysApprove?: boolean) => Promise; onResolveUserInput?: (userInputId: string, answer: string, wasFreeform: boolean) => Promise; onSetInteractionMode?: (mode: InteractionMode) => void; onDismissPlanReview?: () => void; @@ -178,14 +178,14 @@ export function ChatPane({ } } - async function handleResolveApproval(decision: ApprovalDecision) { + async function handleResolveApproval(decision: ApprovalDecision, alwaysApprove?: boolean) { if (!pendingApproval || !onResolveApproval || isResolvingApproval) return; setApprovalError(undefined); setIsResolvingApproval(true); try { - await onResolveApproval(pendingApproval.id, decision); + await onResolveApproval(pendingApproval.id, decision, alwaysApprove); } catch (error) { setApprovalError(error instanceof Error ? error.message : String(error)); } finally { @@ -384,7 +384,7 @@ export function ChatPane({ void handleResolveApproval(decision)} + onResolve={(decision, alwaysApprove) => void handleResolveApproval(decision, alwaysApprove)} position={totalPendingCount > 1 ? 1 : undefined} total={totalPendingCount > 1 ? totalPendingCount : undefined} /> diff --git a/src/renderer/components/chat/ApprovalBanner.tsx b/src/renderer/components/chat/ApprovalBanner.tsx index 2964456..5c0629b 100644 --- a/src/renderer/components/chat/ApprovalBanner.tsx +++ b/src/renderer/components/chat/ApprovalBanner.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Bot, Check, ChevronDown, Loader2, ShieldAlert, ShieldCheck, X } from 'lucide-react'; +import { Bot, Check, ChevronDown, Loader2, ShieldAlert, ShieldBan, ShieldCheck, X } from 'lucide-react'; import { MarkdownContent } from '@renderer/components/MarkdownContent'; import { permissionDetailSummary, PermissionDetailView } from '@renderer/components/chat/PermissionDetailView'; @@ -15,7 +15,7 @@ export function ApprovalBanner({ total, }: { approval: PendingApprovalRecord; - onResolve: (decision: ApprovalDecision) => void; + onResolve: (decision: ApprovalDecision, alwaysApprove?: boolean) => void; isResolving: boolean; position?: number; total?: number; @@ -23,6 +23,7 @@ export function ApprovalBanner({ const kindLabel = approval.kind === 'final-response' ? 'Final response review' : 'Tool call approval'; const hasMessages = approval.messages && approval.messages.length > 0; const showPosition = position !== undefined && total !== undefined && total > 1; + const canAlwaysApprove = approval.kind === 'tool-call' && !!approval.toolName; return (
@@ -87,6 +88,19 @@ export function ApprovalBanner({ {isResolving ? : } Approve + {canAlwaysApprove && ( + + )}