import ClawdbotIPC import SwiftUI struct PermissionsSettings: View { let status: [Capability: Bool] let refresh: () async -> Void let showOnboarding: () -> Void var body: some View { VStack(alignment: .leading, spacing: 14) { Text("Allow these so Clawdbot can notify and capture when needed.") .padding(.top, 4) PermissionStatusList(status: self.status, refresh: self.refresh) .padding(.horizontal, 2) .padding(.vertical, 6) Button("Restart onboarding") { self.showOnboarding() } .buttonStyle(.bordered) Spacer() } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) } } struct PermissionStatusList: View { let status: [Capability: Bool] let refresh: () async -> Void var body: some View { VStack(alignment: .leading, spacing: 12) { ForEach(Capability.allCases, id: \.self) { cap in PermissionRow(capability: cap, status: self.status[cap] ?? false) { Task { await self.handle(cap) } } } Button { Task { await self.refresh() } } label: { Label("Refresh", systemImage: "arrow.clockwise") } .buttonStyle(.bordered) .controlSize(.small) .font(.footnote) .padding(.top, 2) .help("Refresh status") if (self.status[.accessibility] ?? false) == false || (self.status[.screenRecording] ?? false) == false { VStack(alignment: .leading, spacing: 8) { Text( "Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.") .font(.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) Button { LaunchdManager.startClawdbot() } label: { Label("Restart Clawdbot", systemImage: "arrow.counterclockwise") } .buttonStyle(.bordered) .controlSize(.small) } .padding(.top, 4) } } } @MainActor private func handle(_ cap: Capability) async { _ = await PermissionManager.ensure([cap], interactive: true) await self.refresh() } } struct PermissionRow: View { let capability: Capability let status: Bool let compact: Bool let action: () -> Void init(capability: Capability, status: Bool, compact: Bool = false, action: @escaping () -> Void) { self.capability = capability self.status = status self.compact = compact self.action = action } var body: some View { HStack(spacing: self.compact ? 10 : 12) { ZStack { Circle().fill(self.status ? Color.green.opacity(0.2) : Color.gray.opacity(0.15)) .frame(width: self.iconSize, height: self.iconSize) Image(systemName: self.icon) .foregroundStyle(self.status ? Color.green : Color.secondary) } VStack(alignment: .leading, spacing: 2) { Text(self.title).font(.body.weight(.semibold)) Text(self.subtitle).font(.caption).foregroundStyle(.secondary) } Spacer() if self.status { Label("Granted", systemImage: "checkmark.circle.fill") .foregroundStyle(.green) } else { Button("Grant") { self.action() } .buttonStyle(.bordered) } } .padding(.vertical, self.compact ? 4 : 6) } private var iconSize: CGFloat { self.compact ? 28 : 32 } private var title: String { switch self.capability { case .appleScript: "Automation (AppleScript)" case .notifications: "Notifications" case .accessibility: "Accessibility" case .screenRecording: "Screen Recording" case .microphone: "Microphone" case .speechRecognition: "Speech Recognition" case .camera: "Camera" case .location: "Location" } } private var subtitle: String { switch self.capability { case .appleScript: "Control other apps (e.g. Terminal) for automation actions" case .notifications: "Show desktop alerts for agent activity" case .accessibility: "Control UI elements when an action requires it" 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" case .location: "Share location when requested by the agent" } } private var icon: String { switch self.capability { case .appleScript: "applescript" case .notifications: "bell" case .accessibility: "hand.raised" case .screenRecording: "display" case .microphone: "mic" case .speechRecognition: "waveform" case .camera: "camera" case .location: "location" } } } #if DEBUG struct PermissionsSettings_Previews: PreviewProvider { static var previews: some View { PermissionsSettings( status: [ .appleScript: true, .notifications: true, .accessibility: false, .screenRecording: false, .microphone: true, .speechRecognition: false, ], refresh: {}, showOnboarding: {}) .frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight) } } #endif