diff --git a/apps/macos/Sources/Clawdbot/ExecApprovals.swift b/apps/macos/Sources/Clawdbot/ExecApprovals.swift index 53e0b10a8..537ceeaad 100644 --- a/apps/macos/Sources/Clawdbot/ExecApprovals.swift +++ b/apps/macos/Sources/Clawdbot/ExecApprovals.swift @@ -475,8 +475,8 @@ enum ExecApprovalsStore { private static func mergeAgents( current: ExecApprovalsAgent, - legacy: ExecApprovalsAgent - ) -> ExecApprovalsAgent { + legacy: ExecApprovalsAgent) -> ExecApprovalsAgent + { var seen = Set() var allowlist: [ExecAllowlistEntry] = [] func append(_ entry: ExecAllowlistEntry) { @@ -486,8 +486,12 @@ enum ExecApprovalsStore { seen.insert(key) allowlist.append(entry) } - for entry in current.allowlist ?? [] { append(entry) } - for entry in legacy.allowlist ?? [] { append(entry) } + for entry in current.allowlist ?? [] { + append(entry) + } + for entry in legacy.allowlist ?? [] { + append(entry) + } return ExecApprovalsAgent( security: current.security ?? legacy.security, diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift index 4b8389625..dbe80ecfe 100644 --- a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift +++ b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift @@ -63,26 +63,38 @@ final class ExecApprovalsGatewayPrompter { 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) + return Self.shouldPresent( + mode: mode, + activeSession: activeSession, + requestSession: requestSession, + lastInputSeconds: Self.lastInputSeconds(), + thresholdSeconds: 120) + } - if let session = requestSession, !session.isEmpty { - if let active = activeSession, !active.isEmpty { + private static func shouldPresent( + mode: AppState.ConnectionMode, + activeSession: String?, + requestSession: String?, + lastInputSeconds: Int?, + thresholdSeconds: Int) -> Bool + { + let active = activeSession?.trimmingCharacters(in: .whitespacesAndNewlines) + let requested = requestSession?.trimmingCharacters(in: .whitespacesAndNewlines) + let recentlyActive = lastInputSeconds.map { $0 <= thresholdSeconds } ?? (mode == .local) + + if let session = requested, !session.isEmpty { + if let active, !active.isEmpty { return active == session } return recentlyActive } - if let active = activeSession, !active.isEmpty { + if let active, !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) @@ -90,3 +102,22 @@ final class ExecApprovalsGatewayPrompter { return Int(seconds.rounded()) } } + +#if DEBUG +extension ExecApprovalsGatewayPrompter { + static func _testShouldPresent( + mode: AppState.ConnectionMode, + activeSession: String?, + requestSession: String?, + lastInputSeconds: Int?, + thresholdSeconds: Int = 120) -> Bool + { + self.shouldPresent( + mode: mode, + activeSession: activeSession, + requestSession: requestSession, + lastInputSeconds: lastInputSeconds, + thresholdSeconds: thresholdSeconds) + } +} +#endif diff --git a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift index 418d0b810..633b7d872 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift @@ -560,13 +560,13 @@ actor GatewayEndpointStore { { switch bindMode { case "tailnet": - return tailscaleIP ?? "127.0.0.1" + tailscaleIP ?? "127.0.0.1" case "auto": - return "127.0.0.1" + "127.0.0.1" case "custom": - return customBindHost ?? "127.0.0.1" + customBindHost ?? "127.0.0.1" default: - return "127.0.0.1" + "127.0.0.1" } } } diff --git a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift index 6aab7910e..b39138277 100644 --- a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift +++ b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift @@ -192,7 +192,8 @@ extension MenuSessionsInjector { let headerItem = NSMenuItem() headerItem.tag = self.tag headerItem.isEnabled = false - let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState)) + let statusText = self + .cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState)) let hosted = self.makeHostedView( rootView: AnyView(MenuSessionsHeaderView( count: rows.count, diff --git a/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift new file mode 100644 index 000000000..88fb02f2b --- /dev/null +++ b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift @@ -0,0 +1,56 @@ +import Testing +@testable import Clawdbot + +@Suite +@MainActor +struct ExecApprovalsGatewayPrompterTests { + @Test func sessionMatchPrefersActiveSession() { + let matches = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: " main ", + requestSession: "main", + lastInputSeconds: nil) + #expect(matches) + + let mismatched = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: "other", + requestSession: "main", + lastInputSeconds: 0) + #expect(!mismatched) + } + + @Test func sessionFallbackUsesRecentActivity() { + let recent = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: nil, + requestSession: "main", + lastInputSeconds: 10, + thresholdSeconds: 120) + #expect(recent) + + let stale = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: nil, + requestSession: "main", + lastInputSeconds: 200, + thresholdSeconds: 120) + #expect(!stale) + } + + @Test func defaultBehaviorMatchesMode() { + let local = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .local, + activeSession: nil, + requestSession: nil, + lastInputSeconds: 400) + #expect(local) + + let remote = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: nil, + requestSession: nil, + lastInputSeconds: 400) + #expect(!remote) + } +} diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift index 18e529389..05c96f8be 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import Foundation import os import Testing