From ced9efd964d64db17e1bef7bb5dd0620fb719062 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 22 Jan 2026 09:53:36 +0000 Subject: [PATCH] fix: avoid duplicate exec approval prompts --- .../ExecApprovalsGatewayPrompter.swift | 33 +++++++++++++++++++ .../Clawdbot/ExecApprovalsSocket.swift | 4 ++- .../Clawdbot/NodeMode/MacNodeRuntime.swift | 3 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift index 4cd79d5f6..4b8389625 100644 --- a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift +++ b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift @@ -1,5 +1,6 @@ import ClawdbotKit import ClawdbotProtocol +import CoreGraphics import Foundation import OSLog @@ -44,6 +45,7 @@ final class ExecApprovalsGatewayPrompter { do { let data = try JSONEncoder().encode(payload) let request = try JSONDecoder().decode(GatewayApprovalRequest.self, from: data) + guard self.shouldPresent(request: request) else { return } let decision = ExecApprovalsPromptPresenter.prompt(request.request) try await GatewayConnection.shared.requestVoid( method: .execApprovalResolve, @@ -56,4 +58,35 @@ final class ExecApprovalsGatewayPrompter { self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)") } } + + private func shouldPresent(request: GatewayApprovalRequest) -> Bool { + let mode = AppStateStore.shared.connectionMode + let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines) + let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines) + let recentlyActive = self.isRecentlyActive(mode: mode, thresholdSeconds: 120) + + if let session = requestSession, !session.isEmpty { + if let active = activeSession, !active.isEmpty { + return active == session + } + return recentlyActive + } + + if let active = activeSession, !active.isEmpty { + return true + } + return mode == .local + } + + private func isRecentlyActive(mode: AppState.ConnectionMode, thresholdSeconds: Int) -> Bool { + guard let seconds = Self.lastInputSeconds() else { return mode == .local } + return seconds <= thresholdSeconds + } + + private static func lastInputSeconds() -> Int? { + let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null + let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent) + if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil } + return Int(seconds.rounded()) + } } diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift index b5b74bec8..68f8e906d 100644 --- a/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift +++ b/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift @@ -13,6 +13,7 @@ struct ExecApprovalPromptRequest: Codable, Sendable { var ask: String? var agentId: String? var resolvedPath: String? + var sessionKey: String? } private struct ExecApprovalSocketRequest: Codable { @@ -412,7 +413,8 @@ private enum ExecHostExecutor { security: context.security.rawValue, ask: context.ask.rawValue, agentId: context.trimmedAgent, - resolvedPath: context.resolution?.resolvedPath)) + resolvedPath: context.resolution?.resolvedPath, + sessionKey: request.sessionKey)) switch decision { case .deny: diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift index 184164262..c3eacb8a1 100644 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift +++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift @@ -679,7 +679,8 @@ actor MacNodeRuntime { security: context.security.rawValue, ask: context.ask.rawValue, agentId: context.agentId, - resolvedPath: context.resolution?.resolvedPath)) + resolvedPath: context.resolution?.resolvedPath, + sessionKey: context.sessionKey)) } switch decision { case .deny: