diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index 1dfe06ae4..80e1fe309 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -31,7 +31,7 @@ struct GeneralSettings: View { VStack(alignment: .leading, spacing: 18) { if !self.state.onboardingSeen { Button { - OnboardingController.shared.show() + DebugActions.restartOnboarding() } label: { Text("Complete onboarding to finish setup") .font(.callout.weight(.semibold)) diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift index 062c90d37..8a56202b6 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift @@ -173,6 +173,24 @@ extension OnboardingView { .shadow(color: .black.opacity(0.06), radius: 8, y: 3)) } + func onboardingGlassCard( + spacing: CGFloat = 12, + padding: CGFloat = 16, + @ViewBuilder _ content: () -> some View) -> some View + { + VStack(alignment: .leading, spacing: spacing) { + content() + } + .padding(padding) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .strokeBorder(Color.white.opacity(0.10), lineWidth: 1))) + } + func featureRow(title: String, subtitle: String, systemImage: String) -> some View { HStack(alignment: .top, spacing: 12) { Image(systemName: systemImage) diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift index 97f7144c1..e6e902113 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift @@ -654,7 +654,7 @@ extension OnboardingView { .frame(maxWidth: 520) .fixedSize(horizontal: false, vertical: true) - self.onboardingCard(padding: 8) { + self.onboardingGlassCard(padding: 8) { ClawdbotChatView(viewModel: self.onboardingChatModel, style: .onboarding) .frame(maxHeight: .infinity) } diff --git a/apps/macos/Sources/Clawdbot/PermissionManager.swift b/apps/macos/Sources/Clawdbot/PermissionManager.swift index 353a52ff5..c398da5a5 100644 --- a/apps/macos/Sources/Clawdbot/PermissionManager.swift +++ b/apps/macos/Sources/Clawdbot/PermissionManager.swift @@ -10,8 +10,16 @@ import Speech import UserNotifications enum PermissionManager { - static func isLocationAuthorized(status: CLAuthorizationStatus, requireAlways _: Bool) -> Bool { - status == .authorizedAlways + static func isLocationAuthorized(status: CLAuthorizationStatus, requireAlways: Bool) -> Bool { + if requireAlways { return status == .authorizedAlways } + switch status { + case .authorizedAlways, .authorizedWhenInUse: + return true + case .authorized: // deprecated, but still shows up on some macOS versions + return true + default: + return false + } } static func ensure(_ caps: [Capability], interactive: Bool) async -> [Capability: Bool] { @@ -150,7 +158,7 @@ enum PermissionManager { } let status = CLLocationManager().authorizationStatus switch status { - case .authorizedAlways: + case .authorizedAlways, .authorizedWhenInUse, .authorized: return true case .notDetermined: guard interactive else { return false } diff --git a/apps/macos/Sources/Clawdbot/PermissionsSettings.swift b/apps/macos/Sources/Clawdbot/PermissionsSettings.swift index 1994ba406..d7ee5339c 100644 --- a/apps/macos/Sources/Clawdbot/PermissionsSettings.swift +++ b/apps/macos/Sources/Clawdbot/PermissionsSettings.swift @@ -15,7 +15,7 @@ struct PermissionsSettings: View { .padding(.horizontal, 2) .padding(.vertical, 6) - Button("Show onboarding") { self.showOnboarding() } + Button("Restart onboarding") { self.showOnboarding() } .buttonStyle(.bordered) Spacer() } diff --git a/apps/macos/Sources/Clawdbot/SettingsRootView.swift b/apps/macos/Sources/Clawdbot/SettingsRootView.swift index 22c348ea1..87ecd3800 100644 --- a/apps/macos/Sources/Clawdbot/SettingsRootView.swift +++ b/apps/macos/Sources/Clawdbot/SettingsRootView.swift @@ -58,7 +58,7 @@ struct SettingsRootView: View { PermissionsSettings( status: self.permissionMonitor.status, refresh: self.refreshPerms, - showOnboarding: { OnboardingController.shared.show() }) + showOnboarding: { DebugActions.restartOnboarding() }) .tabItem { Label("Permissions", systemImage: "lock.shield") } .tag(SettingsTab.permissions) diff --git a/apps/macos/Sources/ClawdbotDiscovery/GatewayDiscoveryModel.swift b/apps/macos/Sources/ClawdbotDiscovery/GatewayDiscoveryModel.swift index 7547a6dba..7b4d61159 100644 --- a/apps/macos/Sources/ClawdbotDiscovery/GatewayDiscoveryModel.swift +++ b/apps/macos/Sources/ClawdbotDiscovery/GatewayDiscoveryModel.swift @@ -166,26 +166,22 @@ public final class GatewayDiscoveryModel { } private func recomputeGateways() { - var next = self.gatewaysByDomain.values - .flatMap(\.self) - .sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending } - if self.gatewaysByDomain[ClawdbotBonjour.wideAreaBridgeServiceDomain]?.isEmpty ?? true, - !self.wideAreaFallbackGateways.isEmpty - { - next.append(contentsOf: self.wideAreaFallbackGateways) + let primary = self.sortedDeduped(gateways: self.gatewaysByDomain.values.flatMap(\.self)) + let primaryFiltered = self.filterLocalGateways ? primary.filter { !$0.isLocal } : primary + if !primaryFiltered.isEmpty { + self.gateways = primaryFiltered + return } - var seen = Set() - let deduped = next.filter { gateway in - if seen.contains(gateway.stableID) { return false } - seen.insert(gateway.stableID) - return true + + // Bonjour can return only "local" results for the wide-area domain (or no results at all), + // which makes onboarding look empty even though Tailscale DNS-SD can already see bridges. + guard !self.wideAreaFallbackGateways.isEmpty else { + self.gateways = primaryFiltered + return } - let sorted = deduped.sorted { - $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending - } - self.gateways = self.filterLocalGateways - ? sorted.filter { !$0.isLocal } - : sorted + + let combined = self.sortedDeduped(gateways: primary + self.wideAreaFallbackGateways) + self.gateways = self.filterLocalGateways ? combined.filter { !$0.isLocal } : combined } private func updateGateways(for domain: String) { @@ -240,7 +236,7 @@ public final class GatewayDiscoveryModel { .sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending } if domain == ClawdbotBonjour.wideAreaBridgeServiceDomain, - !(self.gatewaysByDomain[domain]?.isEmpty ?? true) + self.hasUsableWideAreaResults { self.wideAreaFallbackGateways = [] } @@ -256,7 +252,7 @@ public final class GatewayDiscoveryModel { let startedAt = Date() while !Task.isCancelled, Date().timeIntervalSince(startedAt) < 35.0 { let hasResults = await MainActor.run { - !(self.gatewaysByDomain[domain]?.isEmpty ?? true) + self.hasUsableWideAreaResults } if hasResults { return } @@ -279,6 +275,25 @@ public final class GatewayDiscoveryModel { } } + private var hasUsableWideAreaResults: Bool { + let domain = ClawdbotBonjour.wideAreaBridgeServiceDomain + guard let gateways = self.gatewaysByDomain[domain], !gateways.isEmpty else { return false } + if !self.filterLocalGateways { return true } + return gateways.contains(where: { !$0.isLocal }) + } + + private func sortedDeduped(gateways: [DiscoveredGateway]) -> [DiscoveredGateway] { + var seen = Set() + let deduped = gateways.filter { gateway in + if seen.contains(gateway.stableID) { return false } + seen.insert(gateway.stableID) + return true + } + return deduped.sorted { + $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending + } + } + private nonisolated static var isRunningTests: Bool { // Keep discovery background work from running forever during SwiftPM test runs. if Bundle.allBundles.contains(where: { $0.bundleURL.pathExtension == "xctest" }) { return true }