From 8de40e0c10255786adf10082b1ac6beb0df31ccb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Jan 2026 15:27:33 +0000 Subject: [PATCH] feat(macos): add Camera permission to onboarding flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 'camera' case to Capability enum - Add UI strings (title, subtitle, icon) in PermissionsSettings - Add ensureCamera() and camera status check in PermissionManager - Add CameraPermissionHelper for opening System Settings 🦞 Clawd's first code contribution! --- .../Sources/Clawdis/PermissionManager.swift | 38 +++++++++++++++++++ .../Sources/Clawdis/PermissionsSettings.swift | 3 ++ apps/macos/Sources/ClawdisIPC/IPC.swift | 1 + 3 files changed, 42 insertions(+) diff --git a/apps/macos/Sources/Clawdis/PermissionManager.swift b/apps/macos/Sources/Clawdis/PermissionManager.swift index e6de4e58f..8afb9745f 100644 --- a/apps/macos/Sources/Clawdis/PermissionManager.swift +++ b/apps/macos/Sources/Clawdis/PermissionManager.swift @@ -31,6 +31,8 @@ enum PermissionManager { await self.ensureMicrophone(interactive: interactive) case .speechRecognition: await self.ensureSpeechRecognition(interactive: interactive) + case .camera: + await self.ensureCamera(interactive: interactive) } } @@ -114,6 +116,24 @@ enum PermissionManager { return SFSpeechRecognizer.authorizationStatus() == .authorized } + private static func ensureCamera(interactive: Bool) async -> Bool { + let status = AVCaptureDevice.authorizationStatus(for: .video) + switch status { + case .authorized: + return true + case .notDetermined: + guard interactive else { return false } + return await AVCaptureDevice.requestAccess(for: .video) + case .denied, .restricted: + if interactive { + CameraPermissionHelper.openSettings() + } + return false + @unknown default: + return false + } + } + static func voiceWakePermissionsGranted() -> Bool { let mic = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized let speech = SFSpeechRecognizer.authorizationStatus() == .authorized @@ -153,6 +173,9 @@ enum PermissionManager { case .speechRecognition: results[cap] = SFSpeechRecognizer.authorizationStatus() == .authorized + + case .camera: + results[cap] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized } } return results @@ -189,6 +212,21 @@ enum MicrophonePermissionHelper { } } +enum CameraPermissionHelper { + static func openSettings() { + let candidates = [ + "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera", + "x-apple.systempreferences:com.apple.preference.security", + ] + + for candidate in candidates { + if let url = URL(string: candidate), NSWorkspace.shared.open(url) { + return + } + } + } +} + enum AppleScriptPermission { private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "AppleScriptPermission") diff --git a/apps/macos/Sources/Clawdis/PermissionsSettings.swift b/apps/macos/Sources/Clawdis/PermissionsSettings.swift index fd2040f22..955e17edb 100644 --- a/apps/macos/Sources/Clawdis/PermissionsSettings.swift +++ b/apps/macos/Sources/Clawdis/PermissionsSettings.swift @@ -120,6 +120,7 @@ struct PermissionRow: View { case .screenRecording: "Screen Recording" case .microphone: "Microphone" case .speechRecognition: "Speech Recognition" + case .camera: "Camera" } } @@ -132,6 +133,7 @@ struct PermissionRow: View { case .screenRecording: "Capture the screen for context or screenshots" case .microphone: "Allow Voice Wake and audio capture" case .speechRecognition: "Transcribe Voice Wake trigger phrases on-device" + case .camera: "Capture photos and video from the camera" } } @@ -143,6 +145,7 @@ struct PermissionRow: View { case .screenRecording: "display" case .microphone: "mic" case .speechRecognition: "waveform" + case .camera: "camera" } } } diff --git a/apps/macos/Sources/ClawdisIPC/IPC.swift b/apps/macos/Sources/ClawdisIPC/IPC.swift index 469d80405..0a7bea442 100644 --- a/apps/macos/Sources/ClawdisIPC/IPC.swift +++ b/apps/macos/Sources/ClawdisIPC/IPC.swift @@ -11,6 +11,7 @@ public enum Capability: String, Codable, CaseIterable, Sendable { case screenRecording case microphone case speechRecognition + case camera } public enum CameraFacing: String, Codable, Sendable {