fix(ios): eliminate Swift warnings and clean build logs

This commit is contained in:
Peter Steinberger
2026-02-26 22:42:20 +00:00
parent 22c74d416b
commit c35368c6dd
17 changed files with 250 additions and 115 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<OpenClawPedometerPayload, Error>) 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)

View File

@@ -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 {

View File

@@ -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<Void, Error>) in
center.add(request) { error in
if let error {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)
}
)
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -22,17 +22,17 @@ enum HostEnvSecurityPolicy {
"PS4",
"GCONV_PATH",
"IFS",
"SSLKEYLOGFILE"
"SSLKEYLOGFILE",
]
static let blockedOverrideKeys: Set<String> = [
"HOME",
"ZDOTDIR"
"ZDOTDIR",
]
static let blockedPrefixes: [String] = [
"DYLD_",
"LD_",
"BASH_FUNC_"
"BASH_FUNC_",
]
}

View File

@@ -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 {