diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index 80e1fe309..327b3ceb2 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -33,9 +33,16 @@ struct GeneralSettings: View { Button { DebugActions.restartOnboarding() } label: { - Text("Complete onboarding to finish setup") - .font(.callout.weight(.semibold)) - .foregroundColor(.accentColor) + HStack(spacing: 8) { + Label("Complete onboarding to finish setup", systemImage: "arrow.counterclockwise") + .font(.callout.weight(.semibold)) + .foregroundStyle(Color.accentColor) + Spacer(minLength: 0) + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundStyle(.tertiary) + } + .contentShape(Rectangle()) } .buttonStyle(.plain) .padding(.bottom, 2) diff --git a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift index 8490e8312..af967e310 100644 --- a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift +++ b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift @@ -456,17 +456,28 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { private func makeMessageItem(text: String, symbolName: String, width: CGFloat, maxLines: Int? = 2) -> NSMenuItem { let view = AnyView( - Label(text, systemImage: symbolName) - .font(.caption) - .foregroundStyle(.secondary) - .multilineTextAlignment(.leading) - .lineLimit(maxLines) - .truncationMode(.tail) - .fixedSize(horizontal: false, vertical: true) - .padding(.leading, 18) - .padding(.trailing, 12) - .padding(.vertical, 6) - .frame(width: max(1, width), alignment: .leading)) + HStack(alignment: .top, spacing: 8) { + Image(systemName: symbolName) + .font(.caption) + .foregroundStyle(.secondary) + .frame(width: 14, alignment: .leading) + .padding(.top, 1) + + Text(text) + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.leading) + .lineLimit(maxLines) + .truncationMode(.tail) + .fixedSize(horizontal: false, vertical: true) + .layoutPriority(1) + + Spacer(minLength: 0) + } + .padding(.leading, 18) + .padding(.trailing, 12) + .padding(.vertical, 6) + .frame(width: max(1, width), alignment: .leading)) let item = NSMenuItem() item.tag = self.tag diff --git a/apps/macos/Sources/Clawdbot/Onboarding.swift b/apps/macos/Sources/Clawdbot/Onboarding.swift index 17c34150f..0271912c5 100644 --- a/apps/macos/Sources/Clawdbot/Onboarding.swift +++ b/apps/macos/Sources/Clawdbot/Onboarding.swift @@ -166,7 +166,8 @@ struct OnboardingView: View { state: AppState = AppStateStore.shared, permissionMonitor: PermissionMonitor = .shared, discoveryModel: GatewayDiscoveryModel = GatewayDiscoveryModel( - localDisplayName: InstanceIdentity.displayName)) + localDisplayName: InstanceIdentity.displayName, + filterLocalGateways: false)) { self.state = state self.permissionMonitor = permissionMonitor diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift index 8a56202b6..b8e9a1248 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift @@ -178,17 +178,15 @@ extension OnboardingView { padding: CGFloat = 16, @ViewBuilder _ content: () -> some View) -> some View { - VStack(alignment: .leading, spacing: spacing) { + let shape = RoundedRectangle(cornerRadius: 16, style: .continuous) + return 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))) + .background(Color.clear) + .clipShape(shape) + .overlay(shape.strokeBorder(Color.white.opacity(0.10), lineWidth: 1)) } func featureRow(title: String, subtitle: String, systemImage: String) -> some View { diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Monitoring.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Monitoring.swift index f4259da1e..751f8b388 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Monitoring.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Monitoring.swift @@ -33,7 +33,7 @@ extension OnboardingView { if shouldMonitor, !self.monitoringDiscovery { self.monitoringDiscovery = true Task { @MainActor in - try? await Task.sleep(nanoseconds: 550_000_000) + try? await Task.sleep(nanoseconds: 150_000_000) guard self.monitoringDiscovery else { return } self.gatewayDiscovery.start() await self.refreshLocalGatewayProbe() diff --git a/apps/macos/Sources/Clawdbot/PermissionManager.swift b/apps/macos/Sources/Clawdbot/PermissionManager.swift index c398da5a5..fbd2f766a 100644 --- a/apps/macos/Sources/Clawdbot/PermissionManager.swift +++ b/apps/macos/Sources/Clawdbot/PermissionManager.swift @@ -292,6 +292,7 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate { static let shared = LocationPermissionRequester() private let manager = CLLocationManager() private var continuation: CheckedContinuation? + private var timeoutTask: Task? override init() { super.init() @@ -306,6 +307,16 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate { return await withCheckedContinuation { cont in self.continuation = cont + self.timeoutTask?.cancel() + self.timeoutTask = Task { [weak self] in + try? await Task.sleep(nanoseconds: 3_000_000_000) + await MainActor.run { [weak self] in + guard let self else { return } + guard self.continuation != nil else { return } + LocationPermissionHelper.openSettings() + self.finish(status: self.manager.authorizationStatus) + } + } if always { self.manager.requestAlwaysAuthorization() } else { @@ -317,13 +328,43 @@ final class LocationPermissionRequester: NSObject, CLLocationManagerDelegate { } } + private func finish(status: CLAuthorizationStatus) { + self.timeoutTask?.cancel() + self.timeoutTask = nil + guard let cont = self.continuation else { return } + self.continuation = nil + cont.resume(returning: status) + } + // nonisolated for Swift 6 strict concurrency compatibility nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { let status = manager.authorizationStatus Task { @MainActor in - guard let cont = self.continuation else { return } - self.continuation = nil - cont.resume(returning: status) + self.finish(status: status) + } + } + + // Legacy callback (still used on some macOS versions / configurations). + nonisolated func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + Task { @MainActor in + self.finish(status: status) + } + } + + nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + let status = manager.authorizationStatus + Task { @MainActor in + if status == .denied || status == .restricted { + LocationPermissionHelper.openSettings() + } + self.finish(status: status) + } + } + + nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + let status = manager.authorizationStatus + Task { @MainActor in + self.finish(status: status) } } } diff --git a/docs/platforms/mac/bundled-gateway.md b/docs/platforms/mac/bundled-gateway.md index 7a8331653..dc25cd0df 100644 --- a/docs/platforms/mac/bundled-gateway.md +++ b/docs/platforms/mac/bundled-gateway.md @@ -20,7 +20,7 @@ App bundle layout: - `Clawdbot.app/Contents/Resources/Relay/dist/` - Compiled CLI/gateway payload from `pnpm exec tsc` - `Clawdbot.app/Contents/Resources/Relay/node_modules/` - - Production dependencies staged via `pnpm deploy --prod --no-optional --legacy` + - Production dependencies staged via `pnpm deploy --prod --legacy` (includes optional native addons) - `Clawdbot.app/Contents/Resources/Relay/clawdbot` - Wrapper script that execs the bundled Node + dist entrypoint - `Clawdbot.app/Contents/Resources/Relay/package.json` @@ -47,7 +47,7 @@ Packaging script: It builds: - TS: `pnpm exec tsc` - Swift app + helper: `swift build …` -- Relay payload: `pnpm deploy --prod --no-optional --legacy` + copy `dist/` +- Relay payload: `pnpm deploy --prod --legacy` + copy `dist/` - Node runtime: downloads the latest Node release (override via `NODE_VERSION`) Important knobs: @@ -90,6 +90,10 @@ Node uses JIT. The bundled runtime is signed with: This is applied by `scripts/codesign-mac-app.sh`. +Note: because the relay runs under hardened runtime, any bundled `*.node` native +addons must be signed with the same Team ID as the relay `node` binary. +`scripts/codesign-mac-app.sh` re-signs `Contents/Resources/Relay/**/*.node` for this. + ## Image processing To avoid shipping native `sharp` addons inside the bundle, the gateway defaults