diff --git a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift index 9571839059d..67f01138803 100644 --- a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift +++ b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift @@ -54,7 +54,12 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable { idempotencyKey: String, attachments: [OpenClawChatAttachmentPayload]) async throws -> OpenClawChatSendResponse { - Self.logger.info("chat.send start sessionKey=\(sessionKey, privacy: .public) len=\(message.count, privacy: .public) attachments=\(attachments.count, privacy: .public)") + let startLogMessage = + "chat.send start sessionKey=\(sessionKey) " + + "len=\(message.count) attachments=\(attachments.count)" + Self.logger.info( + "\(startLogMessage, privacy: .public)" + ) struct Params: Codable { var sessionKey: String var message: String diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index a770fcb2c6f..53e32684988 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -212,7 +212,7 @@ final class GatewayConnectionController { await self.connectManual(host: host, port: port, useTLS: useTLS) case let .discovered(stableID, _): guard let gateway = self.gateways.first(where: { $0.stableID == stableID }) else { return } - await self.connectDiscoveredGateway(gateway) + _ = await self.connectDiscoveredGateway(gateway) } } @@ -399,7 +399,7 @@ final class GatewayConnectionController { self.didAutoConnect = true Task { [weak self] in guard let self else { return } - await self.connectDiscoveredGateway(target) + _ = await self.connectDiscoveredGateway(target) } return } @@ -411,7 +411,7 @@ final class GatewayConnectionController { self.didAutoConnect = true Task { [weak self] in guard let self else { return } - await self.connectDiscoveredGateway(gateway) + _ = await self.connectDiscoveredGateway(gateway) } return } @@ -632,7 +632,8 @@ final class GatewayConnectionController { 0, NI_NUMERICHOST) guard rc == 0 else { return nil } - return String(cString: buffer) + let bytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) } + return String(bytes: bytes, encoding: .utf8) } if let host, !host.isEmpty { @@ -889,11 +890,9 @@ final class GatewayConnectionController { permissions["contacts"] = contactsStatus == .authorized || contactsStatus == .limited let calendarStatus = EKEventStore.authorizationStatus(for: .event) - permissions["calendar"] = - calendarStatus == .authorized || calendarStatus == .fullAccess || calendarStatus == .writeOnly + permissions["calendar"] = Self.hasEventKitAccess(calendarStatus) let remindersStatus = EKEventStore.authorizationStatus(for: .reminder) - permissions["reminders"] = - remindersStatus == .authorized || remindersStatus == .fullAccess || remindersStatus == .writeOnly + permissions["reminders"] = Self.hasEventKitAccess(remindersStatus) let motionStatus = CMMotionActivityManager.authorizationStatus() let pedometerStatus = CMPedometer.authorizationStatus() @@ -911,13 +910,17 @@ final class GatewayConnectionController { private static func isLocationAuthorized(status: CLAuthorizationStatus) -> Bool { switch status { - case .authorizedAlways, .authorizedWhenInUse, .authorized: + case .authorizedAlways, .authorizedWhenInUse: return true default: return false } } + private static func hasEventKitAccess(_ status: EKAuthorizationStatus) -> Bool { + status == .fullAccess || status == .writeOnly + } + private static func motionAvailable() -> Bool { CMMotionActivityManager.isActivityAvailable() || CMPedometer.isStepCountingAvailable() } @@ -986,7 +989,7 @@ extension GatewayConnectionController { } #endif -private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate { +private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @unchecked Sendable { private let url: URL private let timeoutSeconds: Double private let onComplete: (String?) -> Void diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index d763a3b908f..ca9c3f9d0c3 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -46,6 +46,7 @@ private enum IOSDeepLinkAgentPolicy { @MainActor @Observable +// swiftlint:disable type_body_length file_length final class NodeAppModel { struct AgentDeepLinkPrompt: Identifiable, Equatable { let id: String @@ -414,8 +415,10 @@ final class NodeAppModel { } let wasSuppressed = self.backgroundReconnectSuppressed self.backgroundReconnectSuppressed = false - self.pushWakeLogger.info( - "Background reconnect lease reason=\(reason, privacy: .public) seconds=\(leaseSeconds, privacy: .public) wasSuppressed=\(wasSuppressed, privacy: .public)") + let leaseLogMessage = + "Background reconnect lease reason=\(reason) " + + "seconds=\(leaseSeconds) wasSuppressed=\(wasSuppressed)" + self.pushWakeLogger.info("\(leaseLogMessage, privacy: .public)") } private func suppressBackgroundReconnect(reason: String, disconnectIfNeeded: Bool) { @@ -425,8 +428,10 @@ final class NodeAppModel { self.backgroundReconnectLeaseUntil = nil self.backgroundReconnectSuppressed = true guard changed else { return } - self.pushWakeLogger.info( - "Background reconnect suppressed reason=\(reason, privacy: .public) disconnect=\(disconnectIfNeeded, privacy: .public)") + let suppressLogMessage = + "Background reconnect suppressed reason=\(reason) " + + "disconnect=\(disconnectIfNeeded)" + self.pushWakeLogger.info("\(suppressLogMessage, privacy: .public)") guard disconnectIfNeeded else { return } Task { [weak self] in guard let self else { return } @@ -607,7 +612,7 @@ final class NodeAppModel { self.voiceWakeSyncTask = Task { [weak self] in guard let self else { return } - if !(await self.isGatewayHealthMonitorDisabled()) { + if !self.isGatewayHealthMonitorDisabled() { await self.refreshWakeWordsFromGateway() } @@ -662,9 +667,13 @@ final class NodeAppModel { self.gatewayHealthMonitor.start( check: { [weak self] in guard let self else { return false } - if await self.isGatewayHealthMonitorDisabled() { return true } + if await MainActor.run(body: { self.isGatewayHealthMonitorDisabled() }) { return true } do { - let data = try await self.operatorGateway.request(method: "health", paramsJSON: nil, timeoutSeconds: 6) + let data = try await self.operatorGateway.request( + method: "health", + paramsJSON: nil, + timeoutSeconds: 6 + ) guard let decoded = try? JSONDecoder().decode(OpenClawGatewayHealthOK.self, from: data) else { return false } @@ -1765,7 +1774,10 @@ private extension NodeAppModel { try? await Task.sleep(nanoseconds: 1_000_000_000) continue } - if self.shouldPauseReconnectLoopInBackground(source: "operator_loop") { try? await Task.sleep(nanoseconds: 2_000_000_000); continue } + if self.shouldPauseReconnectLoopInBackground(source: "operator_loop") { + try? await Task.sleep(nanoseconds: 2_000_000_000) + continue + } if await self.isOperatorConnected() { try? await Task.sleep(nanoseconds: 1_000_000_000) continue @@ -1830,6 +1842,8 @@ private extension NodeAppModel { } } + // Legacy reconnect state machine; follow-up refactor needed to split into helpers. + // swiftlint:disable:next function_body_length func startNodeGatewayLoop( url: URL, stableID: String, @@ -1854,7 +1868,10 @@ private extension NodeAppModel { try? await Task.sleep(nanoseconds: 1_000_000_000) continue } - if self.shouldPauseReconnectLoopInBackground(source: "node_loop") { try? await Task.sleep(nanoseconds: 2_000_000_000); continue } + if self.shouldPauseReconnectLoopInBackground(source: "node_loop") { + try? await Task.sleep(nanoseconds: 2_000_000_000) + continue + } if await self.isGatewayConnected() { try? await Task.sleep(nanoseconds: 1_000_000_000) continue @@ -1898,7 +1915,10 @@ private extension NodeAppModel { sessionKey: relayData.sessionKey, deliveryChannel: relayData.deliveryChannel, deliveryTo: relayData.deliveryTo)) - GatewayDiagnostics.log("gateway connected host=\(url.host ?? "?") scheme=\(url.scheme ?? "?")") + GatewayDiagnostics.log( + "gateway connected host=\(url.host ?? "?") " + + "scheme=\(url.scheme ?? "?")" + ) if let addr = await self.nodeGateway.currentRemoteAddress() { await MainActor.run { self.gatewayRemoteAddress = addr } } @@ -1993,9 +2013,11 @@ private extension NodeAppModel { self.gatewayPairingRequestId = requestId if let requestId, !requestId.isEmpty { self.gatewayStatusText = - "Pairing required (requestId: \(requestId)). Approve on gateway and return to OpenClaw." + "Pairing required (requestId: \(requestId)). " + + "Approve on gateway and return to OpenClaw." } else { - self.gatewayStatusText = "Pairing required. Approve on gateway and return to OpenClaw." + self.gatewayStatusText = + "Pairing required. Approve on gateway and return to OpenClaw." } } // Hard stop the underlying WebSocket watchdog reconnects so the UI stays stable and @@ -2213,12 +2235,16 @@ extension NodeAppModel { key: event.replyId) do { try await self.sendAgentRequest(link: link) - self.watchReplyLogger.info( - "watch reply forwarded replyId=\(event.replyId, privacy: .public) action=\(event.actionId, privacy: .public)") + let forwardedMessage = + "watch reply forwarded replyId=\(event.replyId) " + + "action=\(event.actionId)" + self.watchReplyLogger.info("\(forwardedMessage, privacy: .public)") self.openChatRequestID &+= 1 } catch { - self.watchReplyLogger.error( - "watch reply forwarding failed replyId=\(event.replyId, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + let failedMessage = + "watch reply forwarding failed replyId=\(event.replyId) " + + "error=\(error.localizedDescription)" + self.watchReplyLogger.error("\(failedMessage, privacy: .public)") self.queuedWatchReplies.insert(event, at: 0) } } @@ -2252,21 +2278,37 @@ extension NodeAppModel { return false } let pushKind = Self.openclawPushKind(userInfo) - self.pushWakeLogger.info( - "Silent push received wakeId=\(wakeId, privacy: .public) kind=\(pushKind, privacy: .public) backgrounded=\(self.isBackgrounded, privacy: .public) autoReconnect=\(self.gatewayAutoReconnectEnabled, privacy: .public)") + let receivedMessage = + "Silent push received wakeId=\(wakeId) " + + "kind=\(pushKind) " + + "backgrounded=\(self.isBackgrounded) " + + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" + self.pushWakeLogger.info("\(receivedMessage, privacy: .public)") let result = await self.reconnectGatewaySessionsForSilentPushIfNeeded(wakeId: wakeId) - self.pushWakeLogger.info( - "Silent push outcome wakeId=\(wakeId, privacy: .public) applied=\(result.applied, privacy: .public) reason=\(result.reason, privacy: .public) durationMs=\(result.durationMs, privacy: .public)") + let outcomeMessage = + "Silent push outcome wakeId=\(wakeId) " + + "applied=\(result.applied) " + + "reason=\(result.reason) " + + "durationMs=\(result.durationMs)" + self.pushWakeLogger.info("\(outcomeMessage, privacy: .public)") return result.applied } func handleBackgroundRefreshWake(trigger: String = "bg_app_refresh") async -> Bool { let wakeId = Self.makePushWakeAttemptID() - self.pushWakeLogger.info( - "Background refresh wake received wakeId=\(wakeId, privacy: .public) trigger=\(trigger, privacy: .public) backgrounded=\(self.isBackgrounded, privacy: .public) autoReconnect=\(self.gatewayAutoReconnectEnabled, privacy: .public)") + let receivedMessage = + "Background refresh wake received wakeId=\(wakeId) " + + "trigger=\(trigger) " + + "backgrounded=\(self.isBackgrounded) " + + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" + self.pushWakeLogger.info("\(receivedMessage, privacy: .public)") let result = await self.reconnectGatewaySessionsForSilentPushIfNeeded(wakeId: wakeId) - self.pushWakeLogger.info( - "Background refresh wake outcome wakeId=\(wakeId, privacy: .public) applied=\(result.applied, privacy: .public) reason=\(result.reason, privacy: .public) durationMs=\(result.durationMs, privacy: .public)") + let outcomeMessage = + "Background refresh wake outcome wakeId=\(wakeId) " + + "applied=\(result.applied) " + + "reason=\(result.reason) " + + "durationMs=\(result.durationMs)" + self.pushWakeLogger.info("\(outcomeMessage, privacy: .public)") return result.applied } @@ -2283,17 +2325,26 @@ extension NodeAppModel { if let last = self.lastSignificantLocationWakeAt, now.timeIntervalSince(last) < throttleWindowSeconds { - self.locationWakeLogger.info( - "Location wake throttled wakeId=\(wakeId, privacy: .public) elapsedSec=\(now.timeIntervalSince(last), privacy: .public)") + let throttledMessage = + "Location wake throttled wakeId=\(wakeId) " + + "elapsedSec=\(now.timeIntervalSince(last))" + self.locationWakeLogger.info("\(throttledMessage, privacy: .public)") return } self.lastSignificantLocationWakeAt = now - self.locationWakeLogger.info( - "Location wake begin wakeId=\(wakeId, privacy: .public) backgrounded=\(self.isBackgrounded, privacy: .public) autoReconnect=\(self.gatewayAutoReconnectEnabled, privacy: .public)") + let beginMessage = + "Location wake begin wakeId=\(wakeId) " + + "backgrounded=\(self.isBackgrounded) " + + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" + self.locationWakeLogger.info("\(beginMessage, privacy: .public)") let result = await self.reconnectGatewaySessionsForSilentPushIfNeeded(wakeId: wakeId) - self.locationWakeLogger.info( - "Location wake trigger wakeId=\(wakeId, privacy: .public) applied=\(result.applied, privacy: .public) reason=\(result.reason, privacy: .public) durationMs=\(result.durationMs, privacy: .public)") + let triggerMessage = + "Location wake trigger wakeId=\(wakeId) " + + "applied=\(result.applied) " + + "reason=\(result.reason) " + + "durationMs=\(result.durationMs)" + self.locationWakeLogger.info("\(triggerMessage, privacy: .public)") guard result.applied else { return } let connected = await self.waitForGatewayConnection(timeoutMs: 5000, pollMs: 250) @@ -2451,14 +2502,18 @@ extension NodeAppModel { extension NodeAppModel { private func refreshWakeWordsFromGateway() async { do { - let data = try await self.operatorGateway.request(method: "voicewake.get", paramsJSON: "{}", timeoutSeconds: 8) + let data = try await self.operatorGateway.request( + method: "voicewake.get", + paramsJSON: "{}", + timeoutSeconds: 8 + ) guard let triggers = VoiceWakePreferences.decodeGatewayTriggers(from: data) else { return } VoiceWakePreferences.saveTriggerWords(triggers) } catch { if let gatewayError = error as? GatewayResponseError { let lower = gatewayError.message.lowercased() if lower.contains("unauthorized role") || lower.contains("missing scope") { - await self.setGatewayHealthMonitorDisabled(true) + self.setGatewayHealthMonitorDisabled(true) return } } @@ -2513,7 +2568,8 @@ extension NodeAppModel { ) if message.count > IOSDeepLinkAgentPolicy.maxMessageChars { - self.screen.errorText = "Deep link too large (message exceeds \(IOSDeepLinkAgentPolicy.maxMessageChars) characters)." + self.screen.errorText = "Deep link too large (message exceeds " + + "\(IOSDeepLinkAgentPolicy.maxMessageChars) characters)." self.recordShareEvent("Rejected: message too large (\(message.count) chars).") return } @@ -2728,3 +2784,4 @@ extension NodeAppModel { } } #endif +// swiftlint:enable type_body_length file_length diff --git a/apps/ios/Sources/Motion/MotionService.swift b/apps/ios/Sources/Motion/MotionService.swift index f108e0b560b..e126b3bd20d 100644 --- a/apps/ios/Sources/Motion/MotionService.swift +++ b/apps/ios/Sources/Motion/MotionService.swift @@ -20,7 +20,7 @@ final class MotionService: MotionServicing { let limit = max(1, min(params.limit ?? 200, 1000)) let manager = CMMotionActivityManager() - let mapped = try await withCheckedThrowingContinuation { (cont: CheckedContinuation<[OpenClawMotionActivityEntry], Error>) in + let mapped: [OpenClawMotionActivityEntry] = try await withCheckedThrowingContinuation { cont in manager.queryActivityStarting(from: start, to: end, to: OperationQueue()) { activity, error in if let error { cont.resume(throwing: error) @@ -62,7 +62,7 @@ final class MotionService: MotionServicing { let (start, end) = Self.resolveRange(startISO: params.startISO, endISO: params.endISO) let pedometer = CMPedometer() - let payload = try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in + let payload: OpenClawPedometerPayload = try await withCheckedThrowingContinuation { cont in pedometer.queryPedometerData(from: start, to: end) { data, error in if let error { cont.resume(throwing: error) diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index c0e872b2ceb..b0dbdc13639 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -134,7 +134,10 @@ struct OnboardingWizardView: View { Button("Done") { UIApplication.shared.sendAction( #selector(UIResponder.resignFirstResponder), - to: nil, from: nil, for: nil) + to: nil, + from: nil, + for: nil + ) } } } @@ -716,8 +719,10 @@ struct OnboardingWizardView: View { private func detectQRCode(from data: Data) -> String? { guard let ciImage = CIImage(data: data) else { return nil } let detector = CIDetector( - ofType: CIDetectorTypeQRCode, context: nil, - options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) + ofType: CIDetectorTypeQRCode, + context: nil, + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh] + ) let features = detector?.features(in: ciImage) ?? [] for feature in features { if let qr = feature as? CIQRCodeFeature, let message = qr.messageString { diff --git a/apps/ios/Sources/OpenClawApp.swift b/apps/ios/Sources/OpenClawApp.swift index 0dc0c4cac26..27f7f5e02ca 100644 --- a/apps/ios/Sources/OpenClawApp.swift +++ b/apps/ios/Sources/OpenClawApp.swift @@ -4,7 +4,7 @@ import OpenClawKit import os import UIKit import BackgroundTasks -import UserNotifications +@preconcurrency import UserNotifications private struct PendingWatchPromptAction { var promptId: String? @@ -119,11 +119,19 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc request.earliestBeginDate = Date().addingTimeInterval(max(60, delay)) do { try BGTaskScheduler.shared.submit(request) + let scheduledLogMessage = + "Scheduled background wake refresh reason=\(reason) " + + "delaySeconds=\(max(60, delay))" self.backgroundWakeLogger.info( - "Scheduled background wake refresh reason=\(reason, privacy: .public) delaySeconds=\(max(60, delay), privacy: .public)") + "\(scheduledLogMessage, privacy: .public)" + ) } catch { + let failedLogMessage = + "Failed scheduling background wake refresh reason=\(reason) " + + "error=\(error.localizedDescription)" self.backgroundWakeLogger.error( - "Failed scheduling background wake refresh reason=\(reason, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + "\(failedLogMessage, privacy: .public)" + ) } } @@ -418,7 +426,9 @@ enum WatchPromptNotificationBridge { } } - private static func notificationAuthorizationStatus(center: UNUserNotificationCenter) async -> UNAuthorizationStatus { + private static func notificationAuthorizationStatus( + center: UNUserNotificationCenter + ) async -> UNAuthorizationStatus { await withCheckedContinuation { continuation in center.getNotificationSettings { settings in continuation.resume(returning: settings.authorizationStatus) @@ -440,7 +450,10 @@ enum WatchPromptNotificationBridge { } } - private static func addNotificationRequest(_ request: UNNotificationRequest, center: UNUserNotificationCenter) async throws { + private static func addNotificationRequest( + _ request: UNNotificationRequest, + center: UNUserNotificationCenter + ) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in center.add(request) { error in if let error { diff --git a/apps/ios/Sources/Reminders/RemindersService.swift b/apps/ios/Sources/Reminders/RemindersService.swift index 249f439fb17..8c347b2282b 100644 --- a/apps/ios/Sources/Reminders/RemindersService.swift +++ b/apps/ios/Sources/Reminders/RemindersService.swift @@ -17,7 +17,7 @@ final class RemindersService: RemindersServicing { let statusFilter = params.status ?? .incomplete let predicate = store.predicateForReminders(in: nil) - let payload = try await withCheckedThrowingContinuation { (cont: CheckedContinuation<[OpenClawReminderPayload], Error>) in + let payload: [OpenClawReminderPayload] = try await withCheckedThrowingContinuation { cont in store.fetchReminders(matching: predicate) { items in let formatter = ISO8601DateFormatter() let filtered = (items ?? []).filter { reminder in diff --git a/apps/ios/Sources/Services/NodeServiceProtocols.swift b/apps/ios/Sources/Services/NodeServiceProtocols.swift index 27ee7cc2776..1eba72e7d6a 100644 --- a/apps/ios/Sources/Services/NodeServiceProtocols.swift +++ b/apps/ios/Sources/Services/NodeServiceProtocols.swift @@ -3,10 +3,13 @@ import Foundation import OpenClawKit import UIKit +typealias OpenClawCameraSnapResult = (format: String, base64: String, width: Int, height: Int) +typealias OpenClawCameraClipResult = (format: String, base64: String, durationMs: Int, hasAudio: Bool) + protocol CameraServicing: Sendable { func listDevices() async -> [CameraController.CameraDeviceInfo] - func snap(params: OpenClawCameraSnapParams) async throws -> (format: String, base64: String, width: Int, height: Int) - func clip(params: OpenClawCameraClipParams) async throws -> (format: String, base64: String, durationMs: Int, hasAudio: Bool) + func snap(params: OpenClawCameraSnapParams) async throws -> OpenClawCameraSnapResult + func clip(params: OpenClawCameraClipParams) async throws -> OpenClawCameraClipResult } protocol ScreenRecordingServicing: Sendable { diff --git a/apps/ios/Sources/Services/WatchMessagingService.swift b/apps/ios/Sources/Services/WatchMessagingService.swift index 3511a06c2db..e173a63c8e2 100644 --- a/apps/ios/Sources/Services/WatchMessagingService.swift +++ b/apps/ios/Sources/Services/WatchMessagingService.swift @@ -148,11 +148,15 @@ final class WatchMessagingService: NSObject, WatchMessagingServicing, @unchecked private func sendReachableMessage(_ payload: [String: Any], with session: WCSession) async throws { try await withCheckedThrowingContinuation { continuation in - session.sendMessage(payload, replyHandler: { _ in - continuation.resume() - }, errorHandler: { error in - continuation.resume(throwing: error) - }) + session.sendMessage( + payload, + replyHandler: { _ in + continuation.resume() + }, + errorHandler: { error in + continuation.resume(throwing: error) + } + ) } } diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index d9e1efd772d..7186c7205b5 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -5,6 +5,7 @@ import os import SwiftUI import UIKit +// swiftlint:disable type_body_length struct SettingsTab: View { private struct FeatureHelp: Identifiable { let id = UUID() @@ -228,7 +229,10 @@ struct SettingsTab: View { .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + .background( + .thinMaterial, + in: RoundedRectangle(cornerRadius: 10, style: .continuous) + ) } } } label: { @@ -275,7 +279,9 @@ struct SettingsTab: View { self.featureToggle( "Allow Camera", isOn: self.$cameraEnabled, - help: "Allows the gateway to request photos or short video clips while OpenClaw is foregrounded.") + help: "Allows the gateway to request photos or short video clips " + + "while OpenClaw is foregrounded." + ) HStack(spacing: 8) { Text("Location Access") @@ -283,7 +289,11 @@ struct SettingsTab: View { Button { self.activeFeatureHelp = FeatureHelp( title: "Location Access", - message: "Controls location permissions for OpenClaw. Off disables location tools, While Using enables foreground location, and Always enables background location.") + message: "Controls location permissions for OpenClaw. " + + "Off disables location tools, While Using enables " + + "foreground location, and Always enables " + + "background location." + ) } label: { Image(systemName: "info.circle") .foregroundStyle(.secondary) @@ -313,7 +323,11 @@ struct SettingsTab: View { LabeledContent( "API Key", value: self.appModel.talkMode.gatewayTalkConfigLoaded - ? (self.appModel.talkMode.gatewayTalkApiKeyConfigured ? "Configured" : "Not configured") + ? ( + self.appModel.talkMode.gatewayTalkApiKeyConfigured + ? "Configured" + : "Not configured" + ) : "Not loaded") LabeledContent( "Default Model", @@ -340,7 +354,9 @@ struct SettingsTab: View { Button { self.activeFeatureHelp = FeatureHelp( title: "Default Share Instruction", - message: "Appends this instruction when sharing content into OpenClaw from iOS.") + message: "Appends this instruction when sharing content " + + "into OpenClaw from iOS." + ) } label: { Image(systemName: "info.circle") .foregroundStyle(.secondary) @@ -393,7 +409,9 @@ struct SettingsTab: View { Button("Cancel", role: .cancel) {} } message: { Text( - "This will disconnect, clear saved gateway connection + credentials, and reopen the onboarding wizard.") + "This will disconnect, clear saved gateway connection + credentials, " + + "and reopen the onboarding wizard." + ) } .alert(item: self.$activeFeatureHelp) { help in Alert( @@ -701,7 +719,9 @@ struct SettingsTab: View { let hasToken = !self.gatewayToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty let hasPassword = !self.gatewayPassword.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty GatewayDiagnostics.log( - "setup code applied host=\(host) port=\(resolvedPort ?? -1) tls=\(self.manualGatewayTLS) token=\(hasToken) password=\(hasPassword)") + "setup code applied host=\(host) port=\(resolvedPort ?? -1) " + + "tls=\(self.manualGatewayTLS) token=\(hasToken) password=\(hasPassword)" + ) guard let port = resolvedPort else { self.setupStatusText = "Failed: invalid port" return @@ -1009,3 +1029,4 @@ struct SettingsTab: View { return lines } } +// swiftlint:enable type_body_length diff --git a/apps/ios/Sources/Status/StatusPill.swift b/apps/ios/Sources/Status/StatusPill.swift index ea5e425c49d..8c0885fc516 100644 --- a/apps/ios/Sources/Status/StatusPill.swift +++ b/apps/ios/Sources/Status/StatusPill.swift @@ -51,7 +51,11 @@ struct StatusPill: View { Circle() .fill(self.gateway.color) .frame(width: 9, height: 9) - .scaleEffect(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.15 : 0.85) : 1.0) + .scaleEffect( + self.gateway == .connecting && !self.reduceMotion + ? (self.pulse ? 1.15 : 0.85) + : 1.0 + ) .opacity(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.0 : 0.6) : 1.0) Text(self.gateway.title) diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index 239cb1868ad..5210921a5a7 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -10,7 +10,7 @@ import Speech // This file intentionally centralizes talk mode state + behavior. // It's large, and splitting would force `private` -> `fileprivate` across many members. // We'll refactor into smaller files when the surface stabilizes. -// swiftlint:disable type_body_length +// swiftlint:disable type_body_length file_length @MainActor @Observable final class TalkModeManager: NSObject { @@ -156,9 +156,7 @@ final class TalkModeManager: NSObject { let micOk = await Self.requestMicrophonePermission() guard micOk else { self.logger.warning("start blocked: microphone permission denied") - self.statusText = Self.permissionMessage( - kind: "Microphone", - status: AVAudioSession.sharedInstance().recordPermission) + self.statusText = "Microphone permission denied" return } let speechOk = await Self.requestSpeechPermission() @@ -300,9 +298,7 @@ final class TalkModeManager: NSObject { if !self.allowSimulatorCapture { let micOk = await Self.requestMicrophonePermission() guard micOk else { - self.statusText = Self.permissionMessage( - kind: "Microphone", - status: AVAudioSession.sharedInstance().recordPermission) + self.statusText = "Microphone permission denied" throw NSError(domain: "TalkMode", code: 4, userInfo: [ NSLocalizedDescriptionKey: "Microphone permission denied", ]) @@ -470,14 +466,15 @@ final class TalkModeManager: NSObject { private func startRecognition() throws { #if targetEnvironment(simulator) + if self.allowSimulatorCapture { + self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest() + self.recognitionRequest?.shouldReportPartialResults = true + return + } if !self.allowSimulatorCapture { throw NSError(domain: "TalkMode", code: 2, userInfo: [ NSLocalizedDescriptionKey: "Talk mode is not supported on the iOS simulator", ]) - } else { - self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest() - self.recognitionRequest?.shouldReportPartialResults = true - return } #endif @@ -525,7 +522,9 @@ final class TalkModeManager: NSObject { self.noiseFloorSamples.removeAll(keepingCapacity: true) let threshold = min(0.35, max(0.12, avg + 0.10)) GatewayDiagnostics.log( - "talk audio: noiseFloor=\(String(format: "%.3f", avg)) threshold=\(String(format: "%.3f", threshold))") + "talk audio: noiseFloor=\(String(format: "%.3f", avg)) " + + "threshold=\(String(format: "%.3f", threshold))" + ) } } @@ -549,7 +548,9 @@ final class TalkModeManager: NSObject { self.loggedPartialThisCycle = false GatewayDiagnostics.log( - "talk speech: recognition started mode=\(String(describing: self.captureMode)) engineRunning=\(self.audioEngine.isRunning)") + "talk speech: recognition started mode=\(String(describing: self.captureMode)) " + + "engineRunning=\(self.audioEngine.isRunning)" + ) self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in guard let self else { return } if let error { @@ -1316,11 +1317,11 @@ final class TalkModeManager: NSObject { try Task.checkCancellation() chunks.append(chunk) } - await self?.completeIncrementalPrefetch(id: id, chunks: chunks) + self?.completeIncrementalPrefetch(id: id, chunks: chunks) } catch is CancellationError { - await self?.clearIncrementalPrefetch(id: id) + self?.clearIncrementalPrefetch(id: id) } catch { - await self?.failIncrementalPrefetch(id: id, error: error) + self?.failIncrementalPrefetch(id: id, error: error) } } self.incrementalSpeechPrefetch = IncrementalSpeechPrefetchState( @@ -1426,7 +1427,10 @@ final class TalkModeManager: NSObject { for await evt in stream { if Task.isCancelled { return } guard evt.event == "agent", let payload = evt.payload else { continue } - guard let agentEvent = try? GatewayPayloadDecoding.decode(payload, as: OpenClawAgentEventPayload.self) else { + guard let agentEvent = try? GatewayPayloadDecoding.decode( + payload, + as: OpenClawAgentEventPayload.self + ) else { continue } guard agentEvent.runId == runId, agentEvent.stream == "assistant" else { continue } @@ -1726,23 +1730,20 @@ private struct IncrementalSpeechBuffer { extension TalkModeManager { nonisolated static func requestMicrophonePermission() async -> Bool { - let session = AVAudioSession.sharedInstance() - switch session.recordPermission { + switch AVAudioApplication.shared.recordPermission { case .granted: return true case .denied: return false case .undetermined: - break + return await self.requestPermissionWithTimeout { completion in + AVAudioApplication.requestRecordPermission(completionHandler: { ok in + completion(ok) + }) + } @unknown default: return false } - - return await self.requestPermissionWithTimeout { completion in - AVAudioSession.sharedInstance().requestRecordPermission { ok in - completion(ok) - } - } } nonisolated static func requestSpeechPermission() async -> Bool { @@ -1766,7 +1767,7 @@ extension TalkModeManager { } private nonisolated static func requestPermissionWithTimeout( - _ operation: @escaping @Sendable (@escaping (Bool) -> Void) -> Void) async -> Bool + _ operation: @escaping @Sendable (@escaping @Sendable (Bool) -> Void) -> Void) async -> Bool { do { return try await AsyncTimeout.withTimeout( @@ -1910,7 +1911,7 @@ extension TalkModeManager { } let providerID = Self.normalizedTalkProviderID(rawProvider) ?? - normalizedProviders.keys.sorted().first ?? + normalizedProviders.keys.min() ?? Self.defaultTalkProvider return TalkProviderConfigSelection( provider: providerID, @@ -1920,7 +1921,11 @@ extension TalkModeManager { func reloadConfig() async { guard let gateway else { return } do { - let res = try await gateway.request(method: "talk.config", paramsJSON: "{\"includeSecrets\":true}", timeoutSeconds: 8) + let res = try await gateway.request( + method: "talk.config", + paramsJSON: "{\"includeSecrets\":true}", + timeoutSeconds: 8 + ) guard let json = try JSONSerialization.jsonObject(with: res) as? [String: Any] else { return } guard let config = json["config"] as? [String: Any] else { return } let talk = config["talk"] as? [String: Any] @@ -2007,10 +2012,18 @@ extension TalkModeManager { private static func describeAudioSession() -> String { let session = AVAudioSession.sharedInstance() - let inputs = session.currentRoute.inputs.map { "\($0.portType.rawValue):\($0.portName)" }.joined(separator: ",") - let outputs = session.currentRoute.outputs.map { "\($0.portType.rawValue):\($0.portName)" }.joined(separator: ",") - let available = session.availableInputs?.map { "\($0.portType.rawValue):\($0.portName)" }.joined(separator: ",") ?? "" - return "category=\(session.category.rawValue) mode=\(session.mode.rawValue) opts=\(session.categoryOptions.rawValue) inputAvail=\(session.isInputAvailable) routeIn=[\(inputs)] routeOut=[\(outputs)] availIn=[\(available)]" + let inputs = session.currentRoute.inputs + .map { "\($0.portType.rawValue):\($0.portName)" } + .joined(separator: ",") + let outputs = session.currentRoute.outputs + .map { "\($0.portType.rawValue):\($0.portName)" } + .joined(separator: ",") + let available = session.availableInputs? + .map { "\($0.portType.rawValue):\($0.portName)" } + .joined(separator: ",") ?? "" + return "category=\(session.category.rawValue) mode=\(session.mode.rawValue) " + + "opts=\(session.categoryOptions.rawValue) inputAvail=\(session.isInputAvailable) " + + "routeIn=[\(inputs)] routeOut=[\(outputs)] availIn=[\(available)]" } } @@ -2078,7 +2091,9 @@ private final class AudioTapDiagnostics: @unchecked Sendable { guard shouldLog else { return } GatewayDiagnostics.log( - "\(label) mic: buffers=\(count) frames=\(frames) rate=\(Int(rate))Hz ch=\(ch) rms=\(String(format: "%.4f", resolvedRms)) max=\(String(format: "%.4f", maxRms))") + "\(label) mic: buffers=\(count) frames=\(frames) rate=\(Int(rate))Hz ch=\(ch) " + + "rms=\(String(format: "%.4f", resolvedRms)) max=\(String(format: "%.4f", maxRms))" + ) } } @@ -2135,4 +2150,4 @@ private struct IncrementalPrefetchedAudio { let outputFormat: String? } -// swiftlint:enable type_body_length +// swiftlint:enable type_body_length file_length diff --git a/apps/ios/project.yml b/apps/ios/project.yml index b433ca8a2bb..63a959d0f18 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -133,11 +133,13 @@ targets: - path: ShareExtension dependencies: - package: OpenClawKit + - sdk: AppIntents.framework settings: base: CODE_SIGN_IDENTITY: "Apple Development" CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)" DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)" + ENABLE_APPINTENTS_METADATA: NO PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_SHARE_BUNDLE_ID)" PROVISIONING_PROFILE_SPECIFIER: "$(OPENCLAW_SHARE_PROFILE)" SWIFT_VERSION: "6.0" @@ -171,6 +173,7 @@ targets: Release: Config/Signing.xcconfig settings: base: + ENABLE_APPINTENTS_METADATA: NO PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_WATCH_APP_BUNDLE_ID)" info: path: WatchApp/Info.plist diff --git a/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift b/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift index 1417589ae4a..2c308b3eeb6 100644 --- a/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift +++ b/apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift @@ -355,9 +355,9 @@ private enum ExecHostExecutor { static func handle(_ request: ExecHostRequest) async -> ExecHostResponse { let validatedRequest: ExecHostValidatedRequest switch ExecHostRequestEvaluator.validateRequest(request) { - case .success(let request): + case let .success(request): validatedRequest = request - case .failure(let error): + case let .failure(error): return self.errorResponse(error) } @@ -370,7 +370,7 @@ private enum ExecHostExecutor { context: context, approvalDecision: request.approvalDecision) { - case .deny(let error): + case let .deny(error): return self.errorResponse(error) case .allow: break @@ -401,7 +401,7 @@ private enum ExecHostExecutor { context: context, approvalDecision: followupDecision) { - case .deny(let error): + case let .deny(error): return self.errorResponse(error) case .allow: break diff --git a/apps/macos/Sources/OpenClaw/ExecHostRequestEvaluator.swift b/apps/macos/Sources/OpenClaw/ExecHostRequestEvaluator.swift index fe38d7ea18f..4e0ff4173de 100644 --- a/apps/macos/Sources/OpenClaw/ExecHostRequestEvaluator.swift +++ b/apps/macos/Sources/OpenClaw/ExecHostRequestEvaluator.swift @@ -26,9 +26,9 @@ enum ExecHostRequestEvaluator { command: command, rawCommand: request.rawCommand) switch validatedCommand { - case .ok(let resolved): + case let .ok(resolved): return .success(ExecHostValidatedRequest(command: command, displayCommand: resolved.displayCommand)) - case .invalid(let message): + case let .invalid(message): return .failure( ExecHostError( code: "INVALID_REQUEST", diff --git a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift index b126d03de21..e4927331b4f 100644 --- a/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift +++ b/apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift @@ -22,17 +22,17 @@ enum HostEnvSecurityPolicy { "PS4", "GCONV_PATH", "IFS", - "SSLKEYLOGFILE" + "SSLKEYLOGFILE", ] static let blockedOverrideKeys: Set = [ "HOME", - "ZDOTDIR" + "ZDOTDIR", ] static let blockedPrefixes: [String] = [ "DYLD_", "LD_", - "BASH_FUNC_" + "BASH_FUNC_", ] } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift index a96e288d7f4..0b012586672 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift @@ -105,7 +105,9 @@ enum ChatMarkdownPreprocessor { outputLines.append(currentLine) } - return outputLines.joined(separator: "\n").replacingOccurrences(of: #"^\n+"#, with: "", options: .regularExpression) + return outputLines + .joined(separator: "\n") + .replacingOccurrences(of: #"^\n+"#, with: "", options: .regularExpression) } private static func stripPrefixedTimestamps(_ raw: String) -> String {