diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift
index 8dda277eef..1dfe06ae45 100644
--- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift
+++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift
@@ -157,13 +157,18 @@ struct GeneralSettings: View {
private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool {
guard mode != .off else { return true }
+ guard CLLocationManager.locationServicesEnabled() else {
+ await MainActor.run { LocationPermissionHelper.openSettings() }
+ return false
+ }
+
let status = CLLocationManager().authorizationStatus
- // Note: macOS only supports authorizedAlways, not authorizedWhenInUse (iOS only)
- if status == .authorizedAlways {
+ let requireAlways = mode == .always
+ if PermissionManager.isLocationAuthorized(status: status, requireAlways: requireAlways) {
return true
}
- let updated = await LocationPermissionRequester.shared.request(always: mode == .always)
- return updated == .authorizedAlways
+ let updated = await LocationPermissionRequester.shared.request(always: requireAlways)
+ return PermissionManager.isLocationAuthorized(status: updated, requireAlways: requireAlways)
}
private var connectionSection: some View {
diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift
index b439c66eaa..92a86529b8 100644
--- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift
+++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift
@@ -222,7 +222,15 @@ actor MacNodeRuntime {
(Self.locationPreciseEnabled() ? .precise : .balanced)
let services = await self.mainActorServices()
let status = await services.locationAuthorizationStatus()
- if status != .authorizedAlways {
+ let hasPermission = switch mode {
+ case .always:
+ status == .authorizedAlways
+ case .whileUsing:
+ status == .authorizedAlways
+ case .off:
+ false
+ }
+ if !hasPermission {
return BridgeInvokeResponse(
id: req.id,
ok: false,
diff --git a/apps/macos/Sources/Clawdbot/Onboarding.swift b/apps/macos/Sources/Clawdbot/Onboarding.swift
index 3e94dc79de..17c34150f3 100644
--- a/apps/macos/Sources/Clawdbot/Onboarding.swift
+++ b/apps/macos/Sources/Clawdbot/Onboarding.swift
@@ -32,7 +32,7 @@ final class OnboardingController {
let hosting = NSHostingController(rootView: OnboardingView())
let window = NSWindow(contentViewController: hosting)
window.title = UIStrings.welcomeTitle
- window.setContentSize(NSSize(width: 630, height: 684))
+ window.setContentSize(NSSize(width: OnboardingView.windowWidth, height: OnboardingView.windowHeight))
window.styleMask = [.titled, .closable, .fullSizeContentView]
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
@@ -98,7 +98,10 @@ struct OnboardingView: View {
@Bindable var state: AppState
var permissionMonitor: PermissionMonitor
- let pageWidth: CGFloat = 630
+ static let windowWidth: CGFloat = 630
+ static let windowHeight: CGFloat = 752 // ~+10% to fit full onboarding content
+
+ let pageWidth: CGFloat = Self.windowWidth
let contentHeight: CGFloat = 460
let connectionPageIndex = 1
let anthropicAuthPageIndex = 2
diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift
index e312537385..062c90d375 100644
--- a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift
+++ b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift
@@ -27,7 +27,7 @@ extension OnboardingView {
Spacer(minLength: 0)
self.navigationBar
}
- .frame(width: self.pageWidth, height: 684)
+ .frame(width: self.pageWidth, height: Self.windowHeight)
.background(Color(NSColor.windowBackgroundColor))
.onAppear {
self.currentPage = 0
diff --git a/apps/macos/Sources/Clawdbot/PermissionManager.swift b/apps/macos/Sources/Clawdbot/PermissionManager.swift
index 3104087309..353a52ff5e 100644
--- a/apps/macos/Sources/Clawdbot/PermissionManager.swift
+++ b/apps/macos/Sources/Clawdbot/PermissionManager.swift
@@ -10,6 +10,10 @@ import Speech
import UserNotifications
enum PermissionManager {
+ static func isLocationAuthorized(status: CLAuthorizationStatus, requireAlways _: Bool) -> Bool {
+ status == .authorizedAlways
+ }
+
static func ensure(_ caps: [Capability], interactive: Bool) async -> [Capability: Bool] {
var results: [Capability: Bool] = [:]
for cap in caps {
@@ -138,18 +142,23 @@ enum PermissionManager {
}
private static func ensureLocation(interactive: Bool) async -> Bool {
+ guard CLLocationManager.locationServicesEnabled() else {
+ if interactive {
+ await MainActor.run { LocationPermissionHelper.openSettings() }
+ }
+ return false
+ }
let status = CLLocationManager().authorizationStatus
switch status {
- // Note: macOS only supports authorizedAlways, not authorizedWhenInUse (iOS only)
case .authorizedAlways:
return true
case .notDetermined:
guard interactive else { return false }
let updated = await LocationPermissionRequester.shared.request(always: false)
- return updated == .authorizedAlways
+ return self.isLocationAuthorized(status: updated, requireAlways: false)
case .denied, .restricted:
if interactive {
- LocationPermissionHelper.openSettings()
+ await MainActor.run { LocationPermissionHelper.openSettings() }
}
return false
@unknown default:
@@ -202,8 +211,8 @@ enum PermissionManager {
case .location:
let status = CLLocationManager().authorizationStatus
- // Note: macOS only supports authorizedAlways
- results[cap] = status == .authorizedAlways
+ results[cap] = CLLocationManager.locationServicesEnabled()
+ && self.isLocationAuthorized(status: status, requireAlways: false)
}
}
return results
@@ -282,13 +291,21 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate {
}
func request(always: Bool) async -> CLAuthorizationStatus {
- if always {
- self.manager.requestAlwaysAuthorization()
- } else {
- self.manager.requestWhenInUseAuthorization()
+ let current = self.manager.authorizationStatus
+ if PermissionManager.isLocationAuthorized(status: current, requireAlways: always) {
+ return current
}
+
return await withCheckedContinuation { cont in
self.continuation = cont
+ if always {
+ self.manager.requestAlwaysAuthorization()
+ } else {
+ self.manager.requestWhenInUseAuthorization()
+ }
+
+ // On macOS, requesting an actual fix makes the prompt more reliable.
+ self.manager.requestLocation()
}
}
diff --git a/apps/macos/Sources/Clawdbot/Resources/Info.plist b/apps/macos/Sources/Clawdbot/Resources/Info.plist
index 2e69df3de9..e01130f3ff 100644
--- a/apps/macos/Sources/Clawdbot/Resources/Info.plist
+++ b/apps/macos/Sources/Clawdbot/Resources/Info.plist
@@ -47,10 +47,14 @@
Clawdbot captures the screen when the agent needs screenshots for context.
NSCameraUsageDescription
Clawdbot can capture photos or short video clips when requested by the agent.
- NSLocationUsageDescription
- Clawdbot can share your location when requested by the agent.
- NSMicrophoneUsageDescription
- Clawdbot needs the mic for Voice Wake tests and agent audio capture.
+ NSLocationUsageDescription
+ Clawdbot can share your location when requested by the agent.
+ NSLocationWhenInUseUsageDescription
+ Clawdbot can share your location when requested by the agent.
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ Clawdbot can share your location when requested by the agent.
+ NSMicrophoneUsageDescription
+ Clawdbot needs the mic for Voice Wake tests and agent audio capture.
NSSpeechRecognitionUsageDescription
Clawdbot uses speech recognition to detect your Voice Wake trigger phrase.
NSAppleEventsUsageDescription
diff --git a/apps/macos/Tests/ClawdbotIPCTests/PermissionManagerLocationTests.swift b/apps/macos/Tests/ClawdbotIPCTests/PermissionManagerLocationTests.swift
new file mode 100644
index 0000000000..d84f264cb9
--- /dev/null
+++ b/apps/macos/Tests/ClawdbotIPCTests/PermissionManagerLocationTests.swift
@@ -0,0 +1,20 @@
+import CoreLocation
+import Testing
+
+@testable import Clawdbot
+
+@Suite("PermissionManager Location")
+struct PermissionManagerLocationTests {
+ @Test("authorizedAlways counts for both modes")
+ func authorizedAlwaysCountsForBothModes() {
+ #expect(PermissionManager.isLocationAuthorized(status: .authorizedAlways, requireAlways: false))
+ #expect(PermissionManager.isLocationAuthorized(status: .authorizedAlways, requireAlways: true))
+ }
+
+ @Test("other statuses not authorized")
+ func otherStatusesNotAuthorized() {
+ #expect(!PermissionManager.isLocationAuthorized(status: .notDetermined, requireAlways: false))
+ #expect(!PermissionManager.isLocationAuthorized(status: .denied, requireAlways: false))
+ #expect(!PermissionManager.isLocationAuthorized(status: .restricted, requireAlways: false))
+ }
+}
diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift
index d9df658ffc..5376470014 100644
--- a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift
+++ b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift
@@ -52,8 +52,10 @@ public struct ClawdbotChatView: View {
public var body: some View {
ZStack {
- ClawdbotChatTheme.background
- .ignoresSafeArea()
+ if self.style == .standard {
+ ClawdbotChatTheme.background
+ .ignoresSafeArea()
+ }
VStack(spacing: Layout.stackSpacing) {
self.messageList
diff --git a/scripts/codesign-mac-app.sh b/scripts/codesign-mac-app.sh
index 5b55d86e35..798aa45d21 100755
--- a/scripts/codesign-mac-app.sh
+++ b/scripts/codesign-mac-app.sh
@@ -146,6 +146,8 @@ cat > "$ENT_TMP_APP_BASE" <<'PLIST'
com.apple.security.device.camera
+ com.apple.security.personal-information.location
+
PLIST
@@ -176,6 +178,8 @@ cat > "$ENT_TMP_APP" <<'PLIST'
com.apple.security.device.camera
+ com.apple.security.personal-information.location
+
PLIST