From 1868a79d9a57cbeda63f0928b1a81a1af47d39bb Mon Sep 17 00:00:00 2001 From: David Kaya Date: Fri, 27 Mar 2026 19:34:50 +0100 Subject: [PATCH] feat: add 'Always approve' button for tool-call approvals When a tool-call approval banner appears, users can now click 'Always approve' to both approve the current call AND add the tool to the session's autoApprovedToolNames. Future calls to that tool are then auto-approved without showing the banner. - Add alwaysApprove?: boolean to ResolveSessionApprovalInput - In resolveSessionApproval, atomically append tool to session approvalSettings when alwaysApprove is true (avoids running guard) - Add 'Always approve' button in ApprovalBanner (tool-call kind only) - Wire through ChatPane and App.tsx with updated callback signatures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/main/AryxAppService.ts | 8 ++++++++ src/main/ipc/registerIpcHandlers.ts | 2 +- src/renderer/App.tsx | 4 ++-- src/renderer/components/ChatPane.tsx | 8 ++++---- .../components/chat/ApprovalBanner.tsx | 18 ++++++++++++++++-- src/shared/contracts/ipc.ts | 1 + 6 files changed, 32 insertions(+), 9 deletions(-) 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 && ( + + )}