From b294f7c467632c19bf3b84f4d5faa00bdd5ccd50 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 01:41:59 +0100 Subject: [PATCH] fix: harden ios app build hygiene --- CHANGELOG.md | 1 + Swabble/Package.resolved | 38 +- Swabble/Sources/SwabbleKit/WakeWordGate.swift | 25 +- .../ActivityWidget/OpenClawLiveActivity.swift | 18 +- apps/ios/CHANGELOG.md | 2 + apps/ios/README.md | 8 +- .../ShareExtension/ShareViewController.swift | 9 +- .../ios/Sources/Camera/CameraController.swift | 20 +- .../Chat/IOSGatewayChatTransport.swift | 9 +- .../Sources/Contacts/ContactsService.swift | 23 +- .../ios/Sources/Device/DeviceInfoHelper.swift | 7 +- apps/ios/Sources/Device/NodeDisplayName.swift | 12 +- .../EventKit/EventKitAuthorization.swift | 1 - .../Gateway/GatewayConnectConfig.swift | 2 +- .../Gateway/GatewayConnectionController.swift | 38 +- .../Gateway/GatewayConnectionIssue.swift | 26 +- .../Gateway/GatewayDiscoveryModel.swift | 7 +- .../Gateway/GatewayHealthMonitor.swift | 8 +- .../Sources/Gateway/GatewayProblemView.swift | 61 ++-- .../Gateway/GatewayServiceResolver.swift | 6 +- .../Gateway/GatewaySettingsStore.swift | 23 +- apps/ios/Sources/Gateway/TCPProbe.swift | 1 - apps/ios/Sources/HomeToolbar.swift | 6 +- .../Sources/Location/LocationService.swift | 2 +- .../Location/SignificantLocationMonitor.swift | 4 +- .../Sources/Media/PhotoLibraryService.swift | 4 +- apps/ios/Sources/Model/NodeAppModel.swift | 327 ++++++++++-------- apps/ios/Sources/Motion/MotionService.swift | 3 +- .../Onboarding/GatewayOnboardingView.swift | 8 +- .../Onboarding/OnboardingStateStore.swift | 16 +- .../Onboarding/OnboardingWizardView.swift | 238 +++++++------ apps/ios/Sources/OpenClawApp.swift | 52 ++- .../Push/ExecApprovalNotificationBridge.swift | 14 +- .../Push/PushRegistrationManager.swift | 4 +- apps/ios/Sources/Push/PushRelayClient.swift | 60 +--- .../Sources/Reminders/RemindersService.swift | 6 +- apps/ios/Sources/RootCanvas.swift | 21 +- .../ios/Sources/Screen/ScreenController.swift | 5 +- .../Sources/Screen/ScreenRecordService.swift | 1 - .../Services/NodeServiceProtocols.swift | 10 +- .../Services/NotificationService.swift | 2 +- .../Services/WatchConnectivityTransport.swift | 9 +- .../Services/WatchMessagingPayloadCodec.swift | 20 +- apps/ios/Sources/Settings/SettingsTab.swift | 48 ++- .../Settings/VoiceWakeWordsSettingsView.swift | 2 +- .../Status/StatusActivityBuilder.swift | 11 +- apps/ios/Sources/Status/StatusGlassCard.swift | 7 +- apps/ios/Sources/Status/StatusPill.swift | 3 +- .../Sources/Voice/TalkModeGatewayConfig.swift | 4 +- apps/ios/Sources/Voice/TalkModeManager.swift | 111 +++--- apps/ios/Sources/Voice/TalkSpeechLocale.swift | 14 +- apps/ios/SwiftSources.input.xcfilelist | 79 ++++- .../Sources/WatchConnectivityReceiver.swift | 24 +- .../Sources/WatchInboxStore.swift | 62 ++-- .../Sources/WatchInboxView.swift | 14 +- .../fastlane/metadata/en-US/release_notes.txt | 2 + apps/ios/project.yml | 2 + .../Sources/OpenClawChatUI/ChatComposer.swift | 21 +- .../ChatMarkdownPreprocessor.swift | 14 +- .../OpenClawChatUI/ChatMarkdownRenderer.swift | 2 +- .../OpenClawChatUI/ChatMessageViews.swift | 24 +- .../Sources/OpenClawChatUI/ChatModels.swift | 12 +- .../OpenClawChatUI/ChatPayloadDecoding.swift | 2 +- .../Sources/OpenClawChatUI/ChatSessions.swift | 8 +- .../Sources/OpenClawChatUI/ChatTheme.swift | 4 +- .../Sources/OpenClawChatUI/ChatView.swift | 21 +- .../OpenClawChatUI/ChatViewModel.swift | 15 +- .../OpenClawKit/AnyCodable+Helpers.swift | 16 +- .../Sources/OpenClawKit/AnyCodable.swift | 1 - .../Sources/OpenClawKit/BonjourTypes.swift | 4 +- .../OpenClawKit/CaptureRateLimits.swift | 4 +- .../Sources/OpenClawKit/DeepLinks.swift | 2 +- .../OpenClawKit/DeviceAuthPayload.swift | 4 +- .../Sources/OpenClawKit/DeviceAuthStore.swift | 25 +- .../Sources/OpenClawKit/DeviceIdentity.swift | 5 +- .../Sources/OpenClawKit/GatewayChannel.swift | 112 +++--- .../GatewayConnectChallengeSupport.swift | 4 +- .../GatewayConnectionProblem.swift | 114 ++++-- .../GatewayDiscoveryStatusText.swift | 1 - .../Sources/OpenClawKit/GatewayErrors.swift | 40 ++- .../OpenClawKit/GatewayNodeSession.swift | 29 +- .../OpenClawKit/GatewayPayloadDecoding.swift | 2 +- .../OpenClawKit/GatewayTLSPinning.swift | 13 +- .../GenericPasswordKeychainStore.swift | 8 +- .../OpenClawKit/InstanceIdentity.swift | 40 +-- .../OpenClawKit/LocationCurrentRequest.swift | 3 +- .../OpenClawKit/LocationServiceSupport.swift | 10 +- .../OpenClawKit/OpenClawKitResources.swift | 8 +- .../Sources/OpenClawKit/PhotoCapture.swift | 5 +- .../OpenClawKit/ShareToAgentDeepLink.swift | 2 +- .../OpenClawKit/TalkConfigParsing.swift | 20 +- .../OpenClawKit/TalkPromptBuilder.swift | 7 +- .../TalkSystemSpeechSynthesizer.swift | 12 +- .../Sources/OpenClawProtocol/AnyCodable.swift | 4 +- .../OpenClawProtocol/WizardHelpers.swift | 6 +- docs/platforms/ios.md | 18 +- docs/plugins/codex-computer-use.md | 12 + 97 files changed, 1150 insertions(+), 1044 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a184d9703bd..9e4a6c8d676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai - Memory Core: stream fallback vector search scoring with a bounded top-K result set so large indexes do not materialize every chunk embedding when sqlite-vec is unavailable. (#73069) Thanks @parkertoddbrooks. - Memory/Ollama: add `memorySearch.remote.nonBatchConcurrency` for inline embedding indexing, default Ollama non-batch indexing to one request at a time, and keep batch concurrency separate from non-batch concurrency so local embedding backfills avoid timeout storms on smaller hosts. Carries forward #57733. Thanks @itilys. - macOS app: update Peekaboo, ElevenLabsKit, and MLX TTS helper dependencies, make canvas file watching and config/exec-approval state writes reliable under concurrent app/test activity, and keep the app plus helper builds warning-free. Thanks @Blaizzy. +- iOS app: refresh SwiftPM/XcodeGen source hygiene, make app, extension, watch, and curated shared Swift files pass the prebuild SwiftFormat and SwiftLint checks, move relay registration off deprecated StoreKit receipt APIs, and keep simulator builds and logic tests warning-free. Thanks @ngutman. - Docs/tools: clarify that `tools.profile: "messaging"` is intentionally narrow and that `tools.profile: "full"` is the unrestricted baseline for broader command/control access. Carries forward #39954. Thanks @posigit. - Control UI/Agents: redact tool-call args, partial/final results, derived exec output, and configured custom secret patterns before streaming tool events to the Control UI, so tool output cannot expose provider or channel credentials. Fixes #72283. (#72319) Thanks @volcano303 and @BunsDev. - Agents/sessions: keep `sessions_history` recall redaction enabled even when general log redaction is disabled, and clarify that safety-boundary UI/tool/diagnostic payloads still redact independently of `logging.redactSensitive`. Carries forward #72319. Thanks @volcano303 and @BunsDev. diff --git a/Swabble/Package.resolved b/Swabble/Package.resolved index f52a51fbe53..24de6ea3a7c 100644 --- a/Swabble/Package.resolved +++ b/Swabble/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "24a723309d7a0039d3df3051106f77ac1ed7068a02508e3a6804e41d757e6c72", + "originHash" : "c0677e232394b5f6b0191b6dbb5bae553d55264f65ae725cd03a8ffdfda9cdd3", "pins" : [ { "identity" : "commander", @@ -10,24 +10,6 @@ "version" : "0.2.1" } }, - { - "identity" : "elevenlabskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/steipete/ElevenLabsKit", - "state" : { - "revision" : "7e3c948d8340abe3977014f3de020edf221e9269", - "version" : "0.1.0" - } - }, - { - "identity" : "swift-concurrency-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", - "state" : { - "revision" : "5a3825302b1a0d744183200915a47b508c828e6f", - "version" : "1.3.2" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -45,24 +27,6 @@ "revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211", "version" : "0.99.0" } - }, - { - "identity" : "swiftui-math", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/swiftui-math", - "state" : { - "revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71", - "version" : "0.1.0" - } - }, - { - "identity" : "textual", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/textual", - "state" : { - "revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38", - "version" : "0.3.1" - } } ], "version" : 3 diff --git a/Swabble/Sources/SwabbleKit/WakeWordGate.swift b/Swabble/Sources/SwabbleKit/WakeWordGate.swift index b36855c1dc7..628f77f9d73 100644 --- a/Swabble/Sources/SwabbleKit/WakeWordGate.swift +++ b/Swabble/Sources/SwabbleKit/WakeWordGate.swift @@ -13,7 +13,9 @@ public struct WakeWordSegment: Sendable, Equatable { self.range = range } - public var end: TimeInterval { start + duration } + public var end: TimeInterval { + self.start + self.duration + } } public struct WakeWordGateConfig: Sendable, Equatable { @@ -24,7 +26,8 @@ public struct WakeWordGateConfig: Sendable, Equatable { public init( triggers: [String], minPostTriggerGap: TimeInterval = 0.45, - minCommandLength: Int = 1) { + minCommandLength: Int = 1) + { self.triggers = triggers self.minPostTriggerGap = minPostTriggerGap self.minCommandLength = minCommandLength @@ -78,10 +81,10 @@ public enum WakeWordGate { segments: [WakeWordSegment], config: WakeWordGateConfig) -> WakeWordGateMatch? { - let triggerTokens = normalizeTriggers(config.triggers) + let triggerTokens = self.normalizeTriggers(config.triggers) guard !triggerTokens.isEmpty else { return nil } - let tokens = normalizeSegments(segments) + let tokens = self.normalizeSegments(segments) guard !tokens.isEmpty else { return nil } var best: MatchCandidate? @@ -115,7 +118,7 @@ public enum WakeWordGate { } guard let best else { return nil } - let command = commandText(transcript: transcript, segments: segments, triggerEndTime: best.triggerEnd) + let command = self.commandText(transcript: transcript, segments: segments, triggerEndTime: best.triggerEnd) .trimmingCharacters(in: Self.whitespaceAndPunctuation) guard command.count >= config.minCommandLength else { return nil } return WakeWordGateMatch( @@ -145,7 +148,7 @@ public enum WakeWordGate { guard !text.isEmpty else { return false } let normalized = text.lowercased() for trigger in triggers { - let token = trigger.trimmingCharacters(in: whitespaceAndPunctuation).lowercased() + let token = trigger.trimmingCharacters(in: self.whitespaceAndPunctuation).lowercased() if token.isEmpty { continue } if normalized.contains(token) { return true } } @@ -155,11 +158,11 @@ public enum WakeWordGate { public static func stripWake(text: String, triggers: [String]) -> String { var out = text for trigger in triggers { - let token = trigger.trimmingCharacters(in: whitespaceAndPunctuation) + let token = trigger.trimmingCharacters(in: self.whitespaceAndPunctuation) guard !token.isEmpty else { continue } out = out.replacingOccurrences(of: token, with: "", options: [.caseInsensitive]) } - return out.trimmingCharacters(in: whitespaceAndPunctuation) + return out.trimmingCharacters(in: self.whitespaceAndPunctuation) } private static func normalizeTriggers(_ triggers: [String]) -> [TriggerTokens] { @@ -167,7 +170,7 @@ public enum WakeWordGate { for trigger in triggers { let tokens = trigger .split(whereSeparator: { $0.isWhitespace }) - .map { normalizeToken(String($0)) } + .map { self.normalizeToken(String($0)) } .filter { !$0.isEmpty } if tokens.isEmpty { continue } output.append(TriggerTokens(source: tokens.joined(separator: " "), tokens: tokens)) @@ -177,7 +180,7 @@ public enum WakeWordGate { private static func normalizeSegments(_ segments: [WakeWordSegment]) -> [Token] { segments.compactMap { segment in - let normalized = normalizeToken(segment.text) + let normalized = self.normalizeToken(segment.text) guard !normalized.isEmpty else { return nil } return Token( normalized: normalized, @@ -190,7 +193,7 @@ public enum WakeWordGate { private static func normalizeToken(_ token: String) -> String { token - .trimmingCharacters(in: whitespaceAndPunctuation) + .trimmingCharacters(in: self.whitespaceAndPunctuation) .lowercased() } diff --git a/apps/ios/ActivityWidget/OpenClawLiveActivity.swift b/apps/ios/ActivityWidget/OpenClawLiveActivity.swift index 497fbd45a08..d076dc82d00 100644 --- a/apps/ios/ActivityWidget/OpenClawLiveActivity.swift +++ b/apps/ios/ActivityWidget/OpenClawLiveActivity.swift @@ -5,11 +5,11 @@ import WidgetKit struct OpenClawLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: OpenClawActivityAttributes.self) { context in - lockScreenView(context: context) + self.lockScreenView(context: context) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { - statusDot(state: context.state) + self.statusDot(state: context.state) } DynamicIslandExpandedRegion(.center) { Text(context.state.statusText) @@ -17,25 +17,24 @@ struct OpenClawLiveActivity: Widget { .lineLimit(1) } DynamicIslandExpandedRegion(.trailing) { - trailingView(state: context.state) + self.trailingView(state: context.state) } } compactLeading: { - statusDot(state: context.state) + self.statusDot(state: context.state) } compactTrailing: { Text(context.state.statusText) .font(.caption2) .lineLimit(1) .frame(maxWidth: 64) } minimal: { - statusDot(state: context.state) + self.statusDot(state: context.state) } } } - @ViewBuilder private func lockScreenView(context: ActivityViewContext) -> some View { HStack(spacing: 8) { - statusDot(state: context.state) + self.statusDot(state: context.state) .frame(width: 10, height: 10) VStack(alignment: .leading, spacing: 2) { Text("OpenClaw") @@ -45,7 +44,7 @@ struct OpenClawLiveActivity: Widget { .foregroundStyle(.secondary) } Spacer() - trailingView(state: context.state) + self.trailingView(state: context.state) } .padding(.horizontal, 12) .padding(.vertical, 4) @@ -69,10 +68,9 @@ struct OpenClawLiveActivity: Widget { } } - @ViewBuilder private func statusDot(state: OpenClawActivityAttributes.ContentState) -> some View { Circle() - .fill(dotColor(state: state)) + .fill(self.dotColor(state: state)) .frame(width: 6, height: 6) } diff --git a/apps/ios/CHANGELOG.md b/apps/ios/CHANGELOG.md index ac0cf5fa5c3..263fd8ab230 100644 --- a/apps/ios/CHANGELOG.md +++ b/apps/ios/CHANGELOG.md @@ -4,6 +4,8 @@ Maintenance update for the current OpenClaw development release. +- Refreshed build hygiene for the iOS app, Share extension, Activity widget, Watch app, and curated shared Swift sources; relay registration now uses StoreKit app transaction JWS data instead of deprecated receipt APIs. + ## 2026.4.25 - 2026-04-25 Maintenance update for the current OpenClaw development release. diff --git a/apps/ios/README.md b/apps/ios/README.md index e7948bf42e2..6e927987fad 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -213,7 +213,7 @@ See `apps/ios/VERSIONING.md` for the detailed spec. - The relay registration is bound to the gateway identity fetched from `gateway.identity.get`, so another gateway cannot reuse that stored registration. - The app persists the relay handle metadata locally so reconnects can republish the gateway registration without re-registering on every connect. - If the relay base URL changes in a later build, the app refreshes the relay registration instead of reusing the old relay origin. -- Relay mode requires a reachable relay base URL and uses App Attest plus the app receipt during registration. +- Relay mode requires a reachable relay base URL and uses App Attest plus a StoreKit app transaction JWS during registration. - Gateway-side relay sending is configured through `gateway.push.apns.relay.baseUrl` in `openclaw.json`. `OPENCLAW_APNS_RELAY_BASE_URL` remains a temporary env override only. ## Official Build Relay Trust Model @@ -222,7 +222,7 @@ See `apps/ios/VERSIONING.md` for the detailed spec. - The app must pair with the gateway and establish both node and operator sessions. - The operator session is used to fetch `gateway.identity.get`. - `iOS -> relay` - - The app registers with the relay over HTTPS using App Attest plus the app receipt. + - The app registers with the relay over HTTPS using App Attest plus a StoreKit app transaction JWS. - The relay requires the official production/TestFlight distribution path, which is why local Xcode/dev installs cannot use the hosted relay. - `gateway delegation` @@ -247,6 +247,10 @@ gateway can only send pushes for iOS devices that paired with that gateway. - iPhone node commands in foreground: camera snap/clip, canvas present/navigate/eval/snapshot, screen record, location, contacts, calendar, reminders, photos, motion, local notifications. - Share extension deep-link forwarding into the connected gateway session. +## Computer Use Relationship + +The iOS app is not a Codex Computer Use backend. Computer Use and `cua-driver mcp` are macOS desktop-control paths; iOS exposes device capabilities as OpenClaw node commands through the gateway. Agents can drive the iPhone canvas, camera, screen, location, voice, and other node capabilities with `node.invoke`, subject to iOS foreground/background limits. + ## Location Automation Use Case (Testing) Use this for automation signals ("I moved", "I arrived", "I left"), not as a keep-awake mechanism. diff --git a/apps/ios/ShareExtension/ShareViewController.swift b/apps/ios/ShareExtension/ShareViewController.swift index 00f1b06f9dc..248cdbc0706 100644 --- a/apps/ios/ShareExtension/ShareViewController.swift +++ b/apps/ios/ShareExtension/ShareViewController.swift @@ -90,8 +90,8 @@ final class ShareViewController: UIViewController { let payload = extracted.payload self.pendingAttachments = extracted.attachments self.logger.info( - "share payload trace=\(traceId, privacy: .public) titleChars=\(payload.title?.count ?? 0) textChars=\(payload.text?.count ?? 0) hasURL=\(payload.url != nil) imageAttachments=\(self.pendingAttachments.count)" - ) + // swiftlint:disable:next line_length + "share payload trace=\(traceId, privacy: .public) titleChars=\(payload.title?.count ?? 0) textChars=\(payload.text?.count ?? 0) hasURL=\(payload.url != nil) imageAttachments=\(self.pendingAttachments.count)") let message = self.composeDraft(from: payload) await MainActor.run { self.draftTextView.text = message @@ -287,7 +287,7 @@ final class ShareViewController: UIViewController { let isInvalidConnectParams = (code.contains("invalid") && code.contains("connect")) || message.contains("invalid connect params") - if isInvalidConnectParams && mentionsClientIdPath { + if isInvalidConnectParams, mentionsClientIdPath { return true } } @@ -405,7 +405,6 @@ final class ShareViewController: UIViewController { } else { unknownCount += 1 } - } } @@ -475,7 +474,7 @@ final class ShareViewController: UIViewController { if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) { if let text = await self.loadTextValue(from: provider, typeIdentifier: UTType.text.identifier), let url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines)), - url.scheme != nil + url.scheme != nil { return url } diff --git a/apps/ios/Sources/Camera/CameraController.swift b/apps/ios/Sources/Camera/CameraController.swift index 6b7a0db892c..86e75b9d4df 100644 --- a/apps/ios/Sources/Camera/CameraController.swift +++ b/apps/ios/Sources/Camera/CameraController.swift @@ -1,17 +1,17 @@ import AVFoundation -import OpenClawKit import Foundation +import OpenClawKit import os actor CameraController { - struct CameraDeviceInfo: Codable, Sendable { + struct CameraDeviceInfo: Codable { var id: String var name: String var position: String var deviceType: String } - enum CameraError: LocalizedError, Sendable { + enum CameraError: LocalizedError { case cameraUnavailable case microphoneUnavailable case permissionDenied(kind: String) @@ -142,7 +142,7 @@ actor CameraController { } func listDevices() -> [CameraDeviceInfo] { - return Self.discoverVideoDevices().map { device in + Self.discoverVideoDevices().map { device in CameraDeviceInfo( id: device.uniqueID, name: device.localizedName, @@ -152,7 +152,7 @@ actor CameraController { } private func ensureAccess(for mediaType: AVMediaType) async throws { - if !(await CameraAuthorization.isAuthorized(for: mediaType)) { + if await !(CameraAuthorization.isAuthorized(for: mediaType)) { throw CameraError.permissionDenied(kind: mediaType == .video ? "Camera" : "Microphone") } } @@ -162,7 +162,7 @@ actor CameraController { deviceId: String?) -> AVCaptureDevice? { if let deviceId, !deviceId.isEmpty { - if let match = Self.discoverVideoDevices().first(where: { $0.uniqueID == deviceId }) { + if let match = discoverVideoDevices().first(where: { $0.uniqueID == deviceId }) { return match } } @@ -270,8 +270,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat func photoOutput( _ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, - error: Error? - ) { + error: Error?) + { let alreadyResumed = self.resumed.withLock { old in let was = old old = true @@ -303,8 +303,8 @@ private final class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegat func photoOutput( _ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, - error: Error? - ) { + error: Error?) + { guard let error else { return } let alreadyResumed = self.resumed.withLock { old in let was = old diff --git a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift index 264d76dc961..d5add00d2db 100644 --- a/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift +++ b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift @@ -1,10 +1,10 @@ +import Foundation import OpenClawChatUI import OpenClawKit import OpenClawProtocol -import Foundation import OSLog -struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable { +struct IOSGatewayChatTransport: OpenClawChatTransport { private static let logger = Logger(subsystem: "ai.openclaw", category: "ios.chat.transport") private let gateway: GatewayNodeSession @@ -70,10 +70,9 @@ struct IOSGatewayChatTransport: OpenClawChatTransport, Sendable { { let startLogMessage = "chat.send start sessionKey=\(sessionKey) " - + "len=\(message.count) attachments=\(attachments.count)" + + "len=\(message.count) attachments=\(attachments.count)" Self.logger.info( - "\(startLogMessage, privacy: .public)" - ) + "\(startLogMessage, privacy: .public)") struct Params: Codable { var sessionKey: String var message: String diff --git a/apps/ios/Sources/Contacts/ContactsService.swift b/apps/ios/Sources/Contacts/ContactsService.swift index efe89f8a218..c2881436a04 100644 --- a/apps/ios/Sources/Contacts/ContactsService.swift +++ b/apps/ios/Sources/Contacts/ContactsService.swift @@ -72,7 +72,7 @@ final class ContactsService: ContactsServicing { contact.givenName = givenName ?? "" contact.familyName = familyName ?? "" contact.organizationName = organizationName ?? "" - if contact.givenName.isEmpty && contact.familyName.isEmpty, let displayName { + if contact.givenName.isEmpty, contact.familyName.isEmpty, let displayName { contact.givenName = displayName } contact.phoneNumbers = phoneNumbers.map { @@ -86,13 +86,12 @@ final class ContactsService: ContactsServicing { save.add(contact, toContainerWithIdentifier: nil) try store.execute(save) - let persisted: CNContact - if !contact.identifier.isEmpty { - persisted = try store.unifiedContact( + let persisted: CNContact = if !contact.identifier.isEmpty { + try store.unifiedContact( withIdentifier: contact.identifier, keysToFetch: Self.payloadKeys) } else { - persisted = contact + contact } return OpenClawContactsAddPayload(contact: Self.payload(from: persisted)) @@ -137,7 +136,7 @@ final class ContactsService: ContactsServicing { phoneNumbers: [String], emails: [String]) throws -> CNContact? { - if phoneNumbers.isEmpty && emails.isEmpty { + if phoneNumbers.isEmpty, emails.isEmpty { return nil } @@ -163,13 +162,13 @@ final class ContactsService: ContactsServicing { phoneNumbers: [String], emails: [String]) -> CNContact? { - let normalizedPhones = Set(phoneNumbers.map { normalizePhone($0) }.filter { !$0.isEmpty }) + let normalizedPhones = Set(phoneNumbers.map { self.normalizePhone($0) }.filter { !$0.isEmpty }) let normalizedEmails = Set(emails.map { $0.lowercased() }.filter { !$0.isEmpty }) var seen = Set() for contact in contacts { guard seen.insert(contact.identifier).inserted else { continue } - let contactPhones = Set(contact.phoneNumbers.map { normalizePhone($0.value.stringValue) }) + let contactPhones = Set(contact.phoneNumbers.map { self.normalizePhone($0.value.stringValue) }) let contactEmails = Set(contact.emailAddresses.map { String($0.value).lowercased() }) if !normalizedPhones.isEmpty, !contactPhones.isDisjoint(with: normalizedPhones) { @@ -198,13 +197,13 @@ final class ContactsService: ContactsServicing { givenName: contact.givenName, familyName: contact.familyName, organizationName: contact.organizationName, - phoneNumbers: contact.phoneNumbers.map { $0.value.stringValue }, + phoneNumbers: contact.phoneNumbers.map(\.value.stringValue), emails: contact.emailAddresses.map { String($0.value) }) } -#if DEBUG + #if DEBUG static func _test_matches(contact: CNContact, phoneNumbers: [String], emails: [String]) -> Bool { - matchContacts(contacts: [contact], phoneNumbers: phoneNumbers, emails: emails) != nil + self.matchContacts(contacts: [contact], phoneNumbers: phoneNumbers, emails: emails) != nil } -#endif + #endif } diff --git a/apps/ios/Sources/Device/DeviceInfoHelper.swift b/apps/ios/Sources/Device/DeviceInfoHelper.swift index 28302c889ba..db5178ce4ca 100644 --- a/apps/ios/Sources/Device/DeviceInfoHelper.swift +++ b/apps/ios/Sources/Device/DeviceInfoHelper.swift @@ -1,8 +1,7 @@ +import Darwin import Foundation import UIKit -import Darwin - /// Shared device and platform info for Settings, gateway node payloads, and device status. enum DeviceInfoHelper { /// e.g. "iOS 18.0.0" or "iPadOS 18.0.0" by interface idiom. Use for gateway/device payloads. @@ -65,8 +64,8 @@ enum DeviceInfoHelper { /// Display string for Settings: "1.2.3" or "1.2.3 (456)" when build differs. static func openClawVersionString() -> String { - let version = appVersion() - let build = appBuild() + let version = self.appVersion() + let build = self.appBuild() if build.isEmpty || build == version { return version } diff --git a/apps/ios/Sources/Device/NodeDisplayName.swift b/apps/ios/Sources/Device/NodeDisplayName.swift index 9ddf38b24a7..5d941034279 100644 --- a/apps/ios/Sources/Device/NodeDisplayName.swift +++ b/apps/ios/Sources/Device/NodeDisplayName.swift @@ -5,25 +5,25 @@ enum NodeDisplayName { private static let genericNames: Set = ["iOS Node", "iPhone Node", "iPad Node"] static func isGeneric(_ name: String) -> Bool { - Self.genericNames.contains(name) + self.genericNames.contains(name) } static func defaultValue(for interfaceIdiom: UIUserInterfaceIdiom) -> String { switch interfaceIdiom { case .phone: - return "iPhone Node" + "iPhone Node" case .pad: - return "iPad Node" + "iPad Node" default: - return "iOS Node" + "iOS Node" } } static func resolve( existing: String?, deviceName: String, - interfaceIdiom: UIUserInterfaceIdiom - ) -> String { + interfaceIdiom: UIUserInterfaceIdiom) -> String + { let trimmedExisting = existing?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !trimmedExisting.isEmpty, !Self.isGeneric(trimmedExisting) { return trimmedExisting diff --git a/apps/ios/Sources/EventKit/EventKitAuthorization.swift b/apps/ios/Sources/EventKit/EventKitAuthorization.swift index c27e9a3efde..6d3df782c62 100644 --- a/apps/ios/Sources/EventKit/EventKitAuthorization.swift +++ b/apps/ios/Sources/EventKit/EventKitAuthorization.swift @@ -31,4 +31,3 @@ enum EventKitAuthorization { } } } - diff --git a/apps/ios/Sources/Gateway/GatewayConnectConfig.swift b/apps/ios/Sources/Gateway/GatewayConnectConfig.swift index 0abea0e312c..4e909615278 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectConfig.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectConfig.swift @@ -9,7 +9,7 @@ import OpenClawKit /// /// Both sessions should derive all connection inputs from this config so we /// don't accidentally persist gateway-scoped state under different keys. -struct GatewayConnectConfig: Sendable { +struct GatewayConnectConfig { let url: URL let stableID: String let tls: GatewayTLSParams? diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index 5700356fbac..25aea75b4f0 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -3,12 +3,12 @@ import Contacts import CoreLocation import CoreMotion import CryptoKit +import Darwin import EventKit import Foundation -import Darwin -import OpenClawKit import Network import Observation +import OpenClawKit import os import Photos import ReplayKit @@ -28,7 +28,9 @@ final class GatewayConnectionController { let fingerprintSha256: String let isManual: Bool - var id: String { self.stableID } + var id: String { + self.stableID + } } private(set) var gateways: [GatewayDiscoveryModel.DiscoveredGateway] = [] @@ -86,7 +88,6 @@ final class GatewayConnectionController { self.updateFromDiscovery() } - /// Returns `nil` when a connect attempt was started, otherwise returns a user-facing error. func connectWithDiagnostics(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async -> String? { await self.connectDiscoveredGateway(gateway) @@ -177,7 +178,7 @@ final class GatewayConnectionController { guard let fp = await self.probeTLSFingerprint(url: url) else { self.appModel?.gatewayStatusText = "TLS handshake failed for \(host):\(resolvedPort). " - + "Remote gateways must use HTTPS/WSS." + + "Remote gateways must use HTTPS/WSS." return } self.pendingTrustConnect = (url: url, stableID: stableID, isManual: true) @@ -557,11 +558,11 @@ final class GatewayConnectionController { private func resolveHostPortFromBonjourEndpoint(_ endpoint: NWEndpoint) async -> (host: String, port: Int)? { switch endpoint { case let .hostPort(host, port): - return (host: host.debugDescription, port: Int(port.rawValue)) + (host: host.debugDescription, port: Int(port.rawValue)) case let .service(name, type, domain, _): - return await Self.resolveBonjourServiceToHostPort(name: name, type: type, domain: domain) + await Self.resolveBonjourServiceToHostPort(name: name, type: type, domain: domain) default: - return nil + nil } } @@ -569,8 +570,8 @@ final class GatewayConnectionController { name: String, type: String, domain: String, - timeoutSeconds: TimeInterval = 3.0 - ) async -> (host: String, port: Int)? { + timeoutSeconds: TimeInterval = 3.0) async -> (host: String, port: Int)? + { // NetService callbacks are delivered via a run loop. If we resolve from a thread without one, // we can end up never receiving callbacks, which in turn leaks the continuation and leaves // the UI stuck "connecting". Keep the whole lifecycle on the main run loop and always @@ -636,8 +637,8 @@ final class GatewayConnectionController { } guard let addrs = svc.addresses else { return nil } - for addrData in addrs { - let host = addrData.withUnsafeBytes { ptr -> String? in + for addrData in addrs { + let host = addrData.withUnsafeBytes { ptr -> String? in guard let base = ptr.baseAddress, !ptr.isEmpty else { return nil } var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) @@ -764,7 +765,8 @@ final class GatewayConnectionController { private func resolvedClientId(defaults: UserDefaults, stableID: String?) -> String { if let stableID, - let override = GatewaySettingsStore.loadGatewayClientIdOverride(stableID: stableID) { + let override = GatewaySettingsStore.loadGatewayClientIdOverride(stableID: stableID) + { return override } let manualClientId = defaults.string(forKey: "gateway.manual.clientId")? @@ -781,7 +783,7 @@ final class GatewayConnectionController { } let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedHost.isEmpty else { return nil } - if useTLS && self.shouldForceTLS(host: trimmedHost) { + if useTLS, self.shouldForceTLS(host: trimmedHost) { return 443 } return 18789 @@ -929,9 +931,9 @@ final class GatewayConnectionController { private static func isLocationAuthorized(status: CLAuthorizationStatus) -> Bool { switch status { case .authorizedAlways, .authorizedWhenInUse: - return true + true default: - return false + false } } @@ -1045,8 +1047,8 @@ private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, @u func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let trust = challenge.protectionSpace.serverTrust else { diff --git a/apps/ios/Sources/Gateway/GatewayConnectionIssue.swift b/apps/ios/Sources/Gateway/GatewayConnectionIssue.swift index 2a790431582..72aa57d12f1 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionIssue.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionIssue.swift @@ -19,9 +19,9 @@ enum GatewayConnectionIssue: Equatable { var needsAuthToken: Bool { switch self { case .tokenMissing, .unauthorized: - return true + true default: - return false + false } } @@ -40,17 +40,17 @@ enum GatewayConnectionIssue: Equatable { } switch problem.kind { case .deviceIdentityRequired, - .deviceSignatureExpired, - .deviceNonceRequired, - .deviceNonceMismatch, - .deviceSignatureInvalid, - .devicePublicKeyInvalid, - .deviceIdMismatch, - .tailscaleIdentityMissing, - .tailscaleProxyMissing, - .tailscaleWhoisFailed, - .tailscaleIdentityMismatch, - .authRateLimited: + .deviceSignatureExpired, + .deviceNonceRequired, + .deviceNonceMismatch, + .deviceSignatureInvalid, + .devicePublicKeyInvalid, + .deviceIdMismatch, + .tailscaleIdentityMissing, + .tailscaleProxyMissing, + .tailscaleWhoisFailed, + .tailscaleIdentityMismatch, + .authRateLimited: return .unauthorized case .timeout, .connectionRefused, .reachabilityFailed, .websocketCancelled: return .network diff --git a/apps/ios/Sources/Gateway/GatewayDiscoveryModel.swift b/apps/ios/Sources/Gateway/GatewayDiscoveryModel.swift index 1090904f0b9..e042f19fec1 100644 --- a/apps/ios/Sources/Gateway/GatewayDiscoveryModel.swift +++ b/apps/ios/Sources/Gateway/GatewayDiscoveryModel.swift @@ -1,7 +1,7 @@ -import OpenClawKit import Foundation import Network import Observation +import OpenClawKit @MainActor @Observable @@ -13,7 +13,10 @@ final class GatewayDiscoveryModel { } struct DiscoveredGateway: Identifiable, Equatable { - var id: String { self.stableID } + var id: String { + self.stableID + } + var name: String var endpoint: NWEndpoint var stableID: String diff --git a/apps/ios/Sources/Gateway/GatewayHealthMonitor.swift b/apps/ios/Sources/Gateway/GatewayHealthMonitor.swift index 182df942c9d..3f4d45c54c7 100644 --- a/apps/ios/Sources/Gateway/GatewayHealthMonitor.swift +++ b/apps/ios/Sources/Gateway/GatewayHealthMonitor.swift @@ -3,7 +3,7 @@ import OpenClawKit @MainActor final class GatewayHealthMonitor { - struct Config: Sendable { + struct Config { var intervalSeconds: Double var timeoutSeconds: Double var maxFailures: Int @@ -17,8 +17,8 @@ final class GatewayHealthMonitor { config: Config = Config(intervalSeconds: 15, timeoutSeconds: 5, maxFailures: 3), sleep: @escaping @Sendable (UInt64) async -> Void = { nanoseconds in try? await Task.sleep(nanoseconds: nanoseconds) - } - ) { + }) + { self.config = config self.sleep = sleep } @@ -67,7 +67,7 @@ final class GatewayHealthMonitor { { let timeout = max(0.0, timeoutSeconds) if timeout == 0 { - return (try? await check()) ?? false + return await (try? check()) ?? false } do { let timeoutError = NSError( diff --git a/apps/ios/Sources/Gateway/GatewayProblemView.swift b/apps/ios/Sources/Gateway/GatewayProblemView.swift index b71676668bb..9333b2cf120 100644 --- a/apps/ios/Sources/Gateway/GatewayProblemView.swift +++ b/apps/ios/Sources/Gateway/GatewayProblemView.swift @@ -59,58 +59,57 @@ struct GatewayProblemBanner: View { .padding(14) .background( .thinMaterial, - in: RoundedRectangle(cornerRadius: 16, style: .continuous) - ) + in: RoundedRectangle(cornerRadius: 16, style: .continuous)) } private var iconName: String { switch self.problem.kind { case .pairingRequired, - .pairingRoleUpgradeRequired, - .pairingScopeUpgradeRequired, - .pairingMetadataUpgradeRequired: - return "person.crop.circle.badge.clock" + .pairingRoleUpgradeRequired, + .pairingScopeUpgradeRequired, + .pairingMetadataUpgradeRequired: + "person.crop.circle.badge.clock" case .timeout, .connectionRefused, .reachabilityFailed, .websocketCancelled: - return "wifi.exclamationmark" + "wifi.exclamationmark" case .deviceIdentityRequired, - .deviceSignatureExpired, - .deviceNonceRequired, - .deviceNonceMismatch, - .deviceSignatureInvalid, - .devicePublicKeyInvalid, - .deviceIdMismatch: - return "lock.shield" + .deviceSignatureExpired, + .deviceNonceRequired, + .deviceNonceMismatch, + .deviceSignatureInvalid, + .devicePublicKeyInvalid, + .deviceIdMismatch: + "lock.shield" default: - return "exclamationmark.triangle.fill" + "exclamationmark.triangle.fill" } } private var tint: Color { switch self.problem.kind { case .pairingRequired, - .pairingRoleUpgradeRequired, - .pairingScopeUpgradeRequired, - .pairingMetadataUpgradeRequired: - return .orange + .pairingRoleUpgradeRequired, + .pairingScopeUpgradeRequired, + .pairingMetadataUpgradeRequired: + .orange case .timeout, .connectionRefused, .reachabilityFailed, .websocketCancelled: - return .yellow + .yellow default: - return .red + .red } } private var ownerLabel: String { switch self.problem.owner { case .gateway: - return "Fix on gateway" + "Fix on gateway" case .iphone: - return "Fix on iPhone" + "Fix on iPhone" case .both: - return "Check both" + "Check both" case .network: - return "Check network" + "Check network" case .unknown: - return "Needs attention" + "Needs attention" } } } @@ -218,15 +217,15 @@ struct GatewayProblemDetailsSheet: View { private var ownerSummary: String { switch self.problem.owner { case .gateway: - return "Primary fix: gateway" + "Primary fix: gateway" case .iphone: - return "Primary fix: this iPhone" + "Primary fix: this iPhone" case .both: - return "Primary fix: check both this iPhone and the gateway" + "Primary fix: check both this iPhone and the gateway" case .network: - return "Primary fix: network or remote access" + "Primary fix: network or remote access" case .unknown: - return "Primary fix: review details and retry" + "Primary fix: review details and retry" } } } diff --git a/apps/ios/Sources/Gateway/GatewayServiceResolver.swift b/apps/ios/Sources/Gateway/GatewayServiceResolver.swift index dab3b4787cf..93a1d6296e8 100644 --- a/apps/ios/Sources/Gateway/GatewayServiceResolver.swift +++ b/apps/ios/Sources/Gateway/GatewayServiceResolver.swift @@ -1,8 +1,8 @@ import Foundation import OpenClawKit -// NetService-based resolver for Bonjour services. -// Used to resolve the service endpoint (SRV + A/AAAA) without trusting TXT for routing. +/// NetService-based resolver for Bonjour services. +/// Used to resolve the service endpoint (SRV + A/AAAA) without trusting TXT for routing. final class GatewayServiceResolver: NSObject, NetServiceDelegate { private let service: NetService private let completion: ((host: String, port: Int)?) -> Void @@ -38,7 +38,7 @@ final class GatewayServiceResolver: NSObject, NetServiceDelegate { self.finish(result: nil) } - private func finish(result: ((host: String, port: Int))?) { + private func finish(result: (host: String, port: Int)?) { guard !self.didFinish else { return } self.didFinish = true self.service.stop() diff --git a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift index 71d09912c67..a69d70d0b40 100644 --- a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift +++ b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift @@ -52,8 +52,7 @@ enum GatewaySettingsStore { static func loadPreferredGatewayStableID() -> String? { if let value = KeychainStore.loadString( service: self.gatewayService, - account: self.preferredGatewayStableIDAccount - )?.trimmingCharacters(in: .whitespacesAndNewlines), + account: self.preferredGatewayStableIDAccount)?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty { return value @@ -79,8 +78,7 @@ enum GatewaySettingsStore { static func loadLastDiscoveredGatewayStableID() -> String? { if let value = KeychainStore.loadString( service: self.gatewayService, - account: self.lastDiscoveredGatewayStableIDAccount - )?.trimmingCharacters(in: .whitespacesAndNewlines), + account: self.lastDiscoveredGatewayStableIDAccount)?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty { return value @@ -160,18 +158,18 @@ enum GatewaySettingsStore { var stableID: String { switch self { case let .manual(_, _, _, stableID): - return stableID + stableID case let .discovered(stableID, _): - return stableID + stableID } } var useTLS: Bool { switch self { case let .manual(_, _, useTLS, _): - return useTLS + useTLS case let .discovered(_, useTLS): - return useTLS + useTLS } } } @@ -446,7 +444,6 @@ enum GatewaySettingsStore { defaults.set(stored, forKey: self.lastDiscoveredGatewayStableIDDefaultsKey) } } - } enum GatewayDiagnostics { @@ -518,7 +515,7 @@ enum GatewayDiagnostics { static func bootstrap() { guard let url = fileURL else { return } - queue.async { + self.queue.async { self.truncateLogIfNeeded(url: url) let timestamp = self.isoTimestamp() let line = "[\(timestamp)] gateway diagnostics started\n" @@ -532,10 +529,10 @@ enum GatewayDiagnostics { static func log(_ message: String) { let timestamp = self.isoTimestamp() let line = "[\(timestamp)] \(message)" - logger.info("\(line, privacy: .public)") + self.logger.info("\(line, privacy: .public)") guard let url = fileURL else { return } - queue.async { + self.queue.async { let shouldTruncate = self.logWritesSinceCheck.withLock { count in count += 1 if count >= self.logSizeCheckEveryWrites { @@ -556,7 +553,7 @@ enum GatewayDiagnostics { static func reset() { guard let url = fileURL else { return } - queue.async { + self.queue.async { try? FileManager.default.removeItem(at: url) } } diff --git a/apps/ios/Sources/Gateway/TCPProbe.swift b/apps/ios/Sources/Gateway/TCPProbe.swift index e22da96298f..24d4de49639 100644 --- a/apps/ios/Sources/Gateway/TCPProbe.swift +++ b/apps/ios/Sources/Gateway/TCPProbe.swift @@ -40,4 +40,3 @@ enum TCPProbe { } } } - diff --git a/apps/ios/Sources/HomeToolbar.swift b/apps/ios/Sources/HomeToolbar.swift index af4e9dab8c3..6e978f48f5f 100644 --- a/apps/ios/Sources/HomeToolbar.swift +++ b/apps/ios/Sources/HomeToolbar.swift @@ -98,8 +98,7 @@ private struct HomeToolbarStatusButton: View { .scaleEffect( self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.15 : 0.85) - : 1.0 - ) + : 1.0) .opacity(self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.0 : 0.6) : 1.0) Text(self.gateway.title) @@ -214,8 +213,7 @@ private struct HomeToolbarActionButton: View { (self.tint ?? .white).opacity( self.isActive ? 0.34 - : (self.contrast == .increased ? 0.4 : (self.brighten ? 0.22 : 0.16)) - ), + : (self.contrast == .increased ? 0.4 : (self.brighten ? 0.22 : 0.16))), lineWidth: self.contrast == .increased ? 1.0 : (self.isActive ? 0.8 : 0.6)) } } diff --git a/apps/ios/Sources/Location/LocationService.swift b/apps/ios/Sources/Location/LocationService.swift index f974e84cfd4..b04fe037d5a 100644 --- a/apps/ios/Sources/Location/LocationService.swift +++ b/apps/ios/Sources/Location/LocationService.swift @@ -1,6 +1,6 @@ -import OpenClawKit import CoreLocation import Foundation +import OpenClawKit @MainActor final class LocationService: NSObject, CLLocationManagerDelegate, LocationServiceCommon { diff --git a/apps/ios/Sources/Location/SignificantLocationMonitor.swift b/apps/ios/Sources/Location/SignificantLocationMonitor.swift index 1b8d5ca2a0d..f6da3f1b58b 100644 --- a/apps/ios/Sources/Location/SignificantLocationMonitor.swift +++ b/apps/ios/Sources/Location/SignificantLocationMonitor.swift @@ -11,8 +11,8 @@ enum SignificantLocationMonitor { locationService: any LocationServicing, locationMode: OpenClawLocationMode, gateway: GatewayNodeSession, - beforeSend: (@MainActor @Sendable () async -> Void)? = nil - ) { + beforeSend: (@MainActor @Sendable () async -> Void)? = nil) + { guard locationMode == .always else { return } let status = locationService.authorizationStatus() guard status == .authorizedAlways else { return } diff --git a/apps/ios/Sources/Media/PhotoLibraryService.swift b/apps/ios/Sources/Media/PhotoLibraryService.swift index f66beb3e707..823d737d175 100644 --- a/apps/ios/Sources/Media/PhotoLibraryService.swift +++ b/apps/ios/Sources/Media/PhotoLibraryService.swift @@ -1,6 +1,6 @@ import Foundation -import Photos import OpenClawKit +import Photos import UIKit final class PhotoLibraryService: PhotosServicing { @@ -139,7 +139,7 @@ final class PhotoLibraryService: PhotosServicing { if newWidth >= currentImage.size.width { break } - currentImage = resize(image: currentImage, targetWidth: newWidth) + currentImage = self.resize(image: currentImage, targetWidth: newWidth) } throw NSError(domain: "Photos", code: 4, userInfo: [ diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 0ffe96ace70..8727eef7fdd 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -1,15 +1,15 @@ +import Observation import OpenClawChatUI import OpenClawKit import OpenClawProtocol -import Observation import os import Security import SwiftUI import UIKit import UserNotifications -// Wrap errors without pulling non-Sendable types into async notification paths. -private struct NotificationCallError: Error, Sendable { +/// Wrap errors without pulling non-Sendable types into async notification paths. +private struct NotificationCallError: Error { let message: String } @@ -18,7 +18,7 @@ private struct GatewayRelayIdentityResponse: Decodable { let publicKey: String } -// Ensures notification requests return promptly even if the system prompt blocks. +/// Ensures notification requests return promptly even if the system prompt blocks. private final class NotificationInvokeLatch: @unchecked Sendable { private let lock = NSLock() private var continuation: CheckedContinuation, Never>? @@ -61,7 +61,7 @@ final class NodeAppModel { let request: AgentDeepLink } - struct ExecApprovalPrompt: Identifiable, Equatable, Codable, Sendable { + struct ExecApprovalPrompt: Identifiable, Equatable, Codable { let id: String let commandText: String let commandPreview: String? @@ -124,6 +124,7 @@ final class NodeAppModel { var gatewayDisplayStatusText: String { self.lastGatewayProblem?.statusText ?? self.gatewayStatusText } + var seamColorHex: String? private var mainSessionBaseKey: String = "main" var selectedAgentId: String? @@ -141,7 +142,7 @@ final class NodeAppModel { private var lastAgentDeepLinkPromptAt: Date = .distantPast @ObservationIgnored private var queuedAgentDeepLinkPromptTask: Task? - // Primary "node" connection: used for device capabilities and node.invoke requests. + /// Primary "node" connection: used for device capabilities and node.invoke requests. private let nodeGateway = GatewayNodeSession() // Secondary "operator" connection: used for chat/talk/config/voicewake requests. private let operatorGateway = GatewayNodeSession() @@ -188,8 +189,14 @@ final class NodeAppModel { private var apnsDeviceTokenHex: String? private var apnsLastRegisteredTokenHex: String? @ObservationIgnored private let pushRegistrationManager = PushRegistrationManager() - var gatewaySession: GatewayNodeSession { self.nodeGateway } - var operatorSession: GatewayNodeSession { self.operatorGateway } + var gatewaySession: GatewayNodeSession { + self.nodeGateway + } + + var operatorSession: GatewayNodeSession { + self.operatorGateway + } + private(set) var activeGatewayConnectConfig: GatewayConnectConfig? private static let watchExecApprovalBridgeStateKey = "watch.execApproval.bridge.state.v1" @@ -377,7 +384,6 @@ final class NodeAppModel { } } - func setScenePhase(_ phase: ScenePhase) { let keepTalkActive = UserDefaults.standard.bool(forKey: "talk.background.enabled") GatewayDiagnostics.log("node app model: scene phase=\(String(describing: phase))") @@ -429,7 +435,7 @@ final class NodeAppModel { let operatorWasConnected = await MainActor.run { self.operatorConnected } if operatorWasConnected { // Prefer keeping the connection if it's healthy; reconnect only when needed. - let healthy = (try? await self.operatorGateway.request( + let healthy = await (try? self.operatorGateway.request( method: "health", paramsJSON: nil, timeoutSeconds: 2)) != nil @@ -512,7 +518,7 @@ final class NodeAppModel { self.backgroundReconnectSuppressed = false let leaseLogMessage = "Background reconnect lease reason=\(reason) " - + "seconds=\(leaseSeconds) wasSuppressed=\(wasSuppressed)" + + "seconds=\(leaseSeconds) wasSuppressed=\(wasSuppressed)" self.pushWakeLogger.info("\(leaseLogMessage, privacy: .public)") } @@ -525,7 +531,7 @@ final class NodeAppModel { guard changed else { return } let suppressLogMessage = "Background reconnect suppressed reason=\(reason) " - + "disconnect=\(disconnectIfNeeded)" + + "disconnect=\(disconnectIfNeeded)" self.pushWakeLogger.info("\(suppressLogMessage, privacy: .public)") guard disconnectIfNeeded else { return } Task { [weak self] in @@ -646,7 +652,7 @@ final class NodeAppModel { self.applyMainSessionKey(decoded.mainkey) let selected = (self.selectedAgentId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - if !selected.isEmpty && !decoded.agents.contains(where: { $0.id == selected }) { + if !selected.isEmpty, !decoded.agents.contains(where: { $0.id == selected }) { self.selectedAgentId = nil } self.talkMode.updateMainSessionKey(self.mainSessionKey) @@ -769,8 +775,7 @@ final class NodeAppModel { let data = try await self.operatorGateway.request( method: "health", paramsJSON: nil, - timeoutSeconds: 6 - ) + timeoutSeconds: 6) guard let decoded = try? JSONDecoder().decode(OpenClawGatewayHealthOK.self, from: data) else { return false } @@ -1057,6 +1062,7 @@ final class NodeAppModel { """ let resultJSON = try await self.screen.eval(javaScript: js) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: resultJSON) + default: return BridgeInvokeResponse( id: req.id, @@ -1294,8 +1300,8 @@ final class NodeAppModel { } private static func isNotificationAuthorizationAllowed( - _ status: NotificationAuthorizationStatus - ) -> Bool { + _ status: NotificationAuthorizationStatus) -> Bool + { switch status { case .authorized, .provisional, .ephemeral: true @@ -1306,8 +1312,8 @@ final class NodeAppModel { private func runNotificationCall( timeoutSeconds: Double, - operation: @escaping @Sendable () async throws -> T - ) async -> Result { + operation: @escaping @Sendable () async throws -> T) async -> Result + { let latch = NotificationInvokeLatch() var opTask: Task? var timeoutTask: Task? @@ -1481,12 +1487,11 @@ final class NodeAppModel { error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) } } - } -private extension NodeAppModel { - // Central registry for node invoke routing to keep commands in one place. - func buildCapabilityRouter() -> NodeCapabilityRouter { +extension NodeAppModel { + /// Central registry for node invoke routing to keep commands in one place. + private func buildCapabilityRouter() -> NodeCapabilityRouter { var handlers: [String: NodeCapabilityRouter.Handler] = [:] func register(_ commands: [String], handler: @escaping NodeCapabilityRouter.Handler) { @@ -1610,7 +1615,7 @@ private extension NodeAppModel { return NodeCapabilityRouter(handlers: handlers) } - func handleWatchInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { + private func handleWatchInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { switch req.command { case OpenClawWatchCommand.status.rawValue: let status = await self.watchMessagingService.status() @@ -1627,7 +1632,7 @@ private extension NodeAppModel { let normalizedParams = Self.normalizeWatchNotifyParams(params) let title = normalizedParams.title let body = normalizedParams.body - if title.isEmpty && body.isEmpty { + if title.isEmpty, body.isEmpty { return BridgeInvokeResponse( id: req.id, ok: false, @@ -1670,18 +1675,18 @@ private extension NodeAppModel { } } - func locationMode() -> OpenClawLocationMode { + private func locationMode() -> OpenClawLocationMode { let raw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off" return OpenClawLocationMode(rawValue: raw) ?? .off } - func isLocationPreciseEnabled() -> Bool { + private func isLocationPreciseEnabled() -> Bool { // iOS settings now expose a single location mode control. // Default location tool precision stays high unless a command explicitly requests balanced. true } - static func decodeParams(_ type: T.Type, from json: String?) throws -> T { + fileprivate static func decodeParams(_ type: T.Type, from json: String?) throws -> T { guard let json, let data = json.data(using: .utf8) else { throw NSError(domain: "Gateway", code: 20, userInfo: [ NSLocalizedDescriptionKey: "INVALID_REQUEST: paramsJSON required", @@ -1690,7 +1695,7 @@ private extension NodeAppModel { return try JSONDecoder().decode(type, from: data) } - static func encodePayload(_ obj: some Encodable) throws -> String { + fileprivate static func encodePayload(_ obj: some Encodable) throws -> String { let data = try JSONEncoder().encode(obj) guard let json = String(bytes: data, encoding: .utf8) else { throw NSError(domain: "NodeAppModel", code: 21, userInfo: [ @@ -1700,17 +1705,17 @@ private extension NodeAppModel { return json } - func isCameraEnabled() -> Bool { + private func isCameraEnabled() -> Bool { // Default-on: if the key doesn't exist yet, treat it as enabled. if UserDefaults.standard.object(forKey: "camera.enabled") == nil { return true } return UserDefaults.standard.bool(forKey: "camera.enabled") } - func triggerCameraFlash() { + private func triggerCameraFlash() { self.cameraFlashNonce &+= 1 } - func showCameraHUD(text: String, kind: CameraHUDKind, autoHideSeconds: Double? = nil) { + private func showCameraHUD(text: String, kind: CameraHUDKind, autoHideSeconds: Double? = nil) { self.cameraHUDDismissTask?.cancel() withAnimation(.spring(response: 0.25, dampingFraction: 0.85)) { @@ -1854,8 +1859,8 @@ extension NodeAppModel { } } -private extension NodeAppModel { - func prepareForGatewayConnect(url: URL, stableID: String) { +extension NodeAppModel { + private func prepareForGatewayConnect(url: URL, stableID: String) { self.gatewayAutoReconnectEnabled = true self.gatewayPairingPaused = false self.gatewayPairingRequestId = nil @@ -1878,13 +1883,13 @@ private extension NodeAppModel { self.apnsLastRegisteredTokenHex = nil } - func clearGatewayConnectionProblem() { + private func clearGatewayConnectionProblem() { self.lastGatewayProblem = nil self.gatewayPairingPaused = false self.gatewayPairingRequestId = nil } - func applyGatewayConnectionProblem(_ problem: GatewayConnectionProblem) { + private func applyGatewayConnectionProblem(_ problem: GatewayConnectionProblem) { self.lastGatewayProblem = problem self.gatewayStatusText = problem.statusText self.gatewayServerName = nil @@ -1903,14 +1908,14 @@ private extension NodeAppModel { } } - func shouldKeepGatewayProblemStatus(forDisconnectReason reason: String) -> Bool { + private func shouldKeepGatewayProblemStatus(forDisconnectReason reason: String) -> Bool { guard let lastGatewayProblem else { return false } return GatewayConnectionProblemMapper.shouldPreserve( previousProblem: lastGatewayProblem, overDisconnectReason: reason) } - func shouldStartOperatorGatewayLoop( + private func shouldStartOperatorGatewayLoop( token: String?, bootstrapToken: String?, password: String?, @@ -1923,12 +1928,12 @@ private extension NodeAppModel { hasStoredOperatorToken: self.hasStoredGatewayRoleToken("operator")) } - func hasStoredGatewayRoleToken(_ role: String) -> Bool { + private func hasStoredGatewayRoleToken(_ role: String) -> Bool { let identity = DeviceIdentityStore.loadOrCreate() return DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: role) != nil } - nonisolated static func shouldStartOperatorGatewayLoop( + fileprivate nonisolated static func shouldStartOperatorGatewayLoop( token: String?, bootstrapToken: String?, password: String?, @@ -1949,7 +1954,8 @@ private extension NodeAppModel { return hasStoredOperatorToken } - nonisolated static func clearingBootstrapToken(in config: GatewayConnectConfig?) -> GatewayConnectConfig? { + fileprivate nonisolated static func clearingBootstrapToken(in config: GatewayConnectConfig?) + -> GatewayConnectConfig? { guard let config else { return nil } let trimmedBootstrapToken = config.bootstrapToken? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" @@ -1964,7 +1970,7 @@ private extension NodeAppModel { nodeOptions: config.nodeOptions) } - func currentGatewayReconnectAuth( + private func currentGatewayReconnectAuth( fallbackToken: String?, fallbackBootstrapToken: String?, fallbackPassword: String?) -> (token: String?, bootstrapToken: String?, password: String?) @@ -1975,7 +1981,7 @@ private extension NodeAppModel { return (fallbackToken, fallbackBootstrapToken, fallbackPassword) } - func clearPersistedGatewayBootstrapTokenIfNeeded() { + private func clearPersistedGatewayBootstrapTokenIfNeeded() { // Always drop the in-memory bootstrap token after the first successful // bootstrap connect so reconnect loops cannot reuse a spent token. self.activeGatewayConnectConfig = Self.clearingBootstrapToken(in: self.activeGatewayConnectConfig) @@ -1999,7 +2005,7 @@ private extension NodeAppModel { sessionBox: WebSocketSessionBox?) async { self.clearPersistedGatewayBootstrapTokenIfNeeded() - if self.operatorGatewayTask == nil && self.shouldStartOperatorGatewayLoop( + if self.operatorGatewayTask == nil, self.shouldStartOperatorGatewayLoop( token: token, bootstrapToken: nil, password: password, @@ -2020,7 +2026,7 @@ private extension NodeAppModel { _ = await self.requestNotificationAuthorizationIfNeeded() } - func refreshBackgroundReconnectSuppressionIfNeeded(source: String) { + private func refreshBackgroundReconnectSuppressionIfNeeded(source: String) { guard self.isBackgrounded else { return } guard !self.backgroundReconnectSuppressed else { return } guard let leaseUntil = self.backgroundReconnectLeaseUntil else { @@ -2032,12 +2038,12 @@ private extension NodeAppModel { } } - func shouldPauseReconnectLoopInBackground(source: String) -> Bool { + private func shouldPauseReconnectLoopInBackground(source: String) -> Bool { self.refreshBackgroundReconnectSuppressionIfNeeded(source: source) return self.isBackgrounded && self.backgroundReconnectSuppressed } - func startOperatorGatewayLoop( + private func startOperatorGatewayLoop( url: URL, stableID: String, token: String?, @@ -2141,7 +2147,7 @@ private extension NodeAppModel { // Legacy reconnect state machine; follow-up refactor needed to split into helpers. // swiftlint:disable:next function_body_length - func startNodeGatewayLoop( + private func startNodeGatewayLoop( url: URL, stableID: String, token: String?, @@ -2216,7 +2222,7 @@ private extension NodeAppModel { let usedBootstrapToken = reconnectAuth.token?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty != false && reconnectAuth.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines) - .isEmpty == false + .isEmpty == false if usedBootstrapToken { await self.handleSuccessfulBootstrapGatewayOnboarding( url: url, @@ -2230,8 +2236,7 @@ private extension NodeAppModel { ( sessionKey: self.mainSessionKey, deliveryChannel: self.shareDeliveryChannel, - deliveryTo: self.shareDeliveryTo - ) + deliveryTo: self.shareDeliveryTo) } ShareGatewayRelaySettings.saveConfig( ShareGatewayRelayConfig( @@ -2243,8 +2248,7 @@ private extension NodeAppModel { deliveryTo: relayData.deliveryTo)) GatewayDiagnostics.log( "gateway connected host=\(url.host ?? "?") " - + "scheme=\(url.scheme ?? "?")" - ) + + "scheme=\(url.scheme ?? "?")") if let addr = await self.nodeGateway.currentRemoteAddress() { await MainActor.run { self.gatewayRemoteAddress = addr } } @@ -2295,8 +2299,8 @@ private extension NodeAppModel { if Task.isCancelled { break } if !didFallbackClientId, let fallbackClientId = self.legacyClientIdFallback( - currentClientId: currentOptions.clientId, - error: error) + currentClientId: currentOptions.clientId, + error: error) { didFallbackClientId = true currentOptions.clientId = fallbackClientId @@ -2368,7 +2372,7 @@ private extension NodeAppModel { } } - func shouldRequestOperatorApprovalScope(token: String?, password: String?) -> Bool { + private func shouldRequestOperatorApprovalScope(token: String?, password: String?) -> Bool { let identity = DeviceIdentityStore.loadOrCreate() let storedOperatorScopes = DeviceAuthStore .loadToken(deviceId: identity.deviceId, role: "operator")? @@ -2379,11 +2383,11 @@ private extension NodeAppModel { storedOperatorScopes: storedOperatorScopes) } - nonisolated static func shouldRequestOperatorApprovalScope( + fileprivate nonisolated static func shouldRequestOperatorApprovalScope( token: String?, password: String?, - storedOperatorScopes: [String] - ) -> Bool { + storedOperatorScopes: [String]) -> Bool + { let trimmedToken = token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !trimmedToken.isEmpty { return true @@ -2395,11 +2399,11 @@ private extension NodeAppModel { return storedOperatorScopes.contains("operator.approvals") } - func makeOperatorConnectOptions( + private func makeOperatorConnectOptions( clientId: String, displayName: String?, - includeApprovalScope: Bool - ) -> GatewayConnectOptions { + includeApprovalScope: Bool) -> GatewayConnectOptions + { var scopes = ["operator.read", "operator.write", "operator.talk.secrets"] // Preserve reconnect compatibility for older paired operator tokens that were // approved before iOS requested operator.approvals by default. @@ -2418,7 +2422,7 @@ private extension NodeAppModel { includeDeviceIdentity: true) } - func legacyClientIdFallback(currentClientId: String, error: Error) -> String? { + private func legacyClientIdFallback(currentClientId: String, error: Error) -> String? { let normalizedClientId = currentClientId.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() guard normalizedClientId == "openclaw-ios" else { return nil } let message = error.localizedDescription.lowercased() @@ -2428,7 +2432,7 @@ private extension NodeAppModel { return "moltbot-ios" } - func isOperatorConnected() async -> Bool { + private func isOperatorConnected() async -> Bool { self.operatorConnected } } @@ -2568,8 +2572,10 @@ extension NodeAppModel { PendingForegroundNodeActionsResponse.self, from: payload) guard !decoded.actions.isEmpty else { return } - // swiftlint:disable:next line_length - self.pendingActionLogger.info("Pending actions pulled trigger=\(trigger, privacy: .public) count=\(decoded.actions.count, privacy: .public)") + self.pendingActionLogger + .info( + // swiftlint:disable:next line_length + "Pending actions pulled trigger=\(trigger, privacy: .public) count=\(decoded.actions.count, privacy: .public)") await self.applyPendingForegroundNodeActions(decoded.actions, trigger: trigger) } catch { // Best-effort only. @@ -2591,8 +2597,10 @@ extension NodeAppModel { command: action.command, paramsJSON: action.paramsJSON) let result = await self.handleInvoke(req) - // swiftlint:disable:next line_length - self.pendingActionLogger.info("Pending action replay trigger=\(trigger, privacy: .public) id=\(action.id, privacy: .public) command=\(action.command, privacy: .public) ok=\(result.ok, privacy: .public)") + self.pendingActionLogger + .info( + // swiftlint:disable:next line_length + "Pending action replay trigger=\(trigger, privacy: .public) id=\(action.id, privacy: .public) command=\(action.command, privacy: .public) ok=\(result.ok, privacy: .public)") guard result.ok else { return } let acked = await self.ackPendingForegroundNodeAction( id: action.id, @@ -2616,17 +2624,19 @@ extension NodeAppModel { timeoutSeconds: 6) return true } catch { - // swiftlint:disable:next line_length - self.pendingActionLogger.error("Pending action ack failed trigger=\(trigger, privacy: .public) id=\(id, privacy: .public) command=\(command, privacy: .public) error=\(String(describing: error), privacy: .public)") + self.pendingActionLogger + .error( + // swiftlint:disable:next line_length + "Pending action ack failed trigger=\(trigger, privacy: .public) id=\(id, privacy: .public) command=\(command, privacy: .public) error=\(String(describing: error), privacy: .public)") return false } } private func handleWatchQuickReply(_ event: WatchQuickReplyEvent) async { - switch self.watchReplyCoordinator.ingest(event, isGatewayConnected: await self.isGatewayConnected()) { + switch await self.watchReplyCoordinator.ingest(event, isGatewayConnected: self.isGatewayConnected()) { case .dropMissingFields: self.watchReplyLogger.info("watch reply dropped: missing replyId/actionId") - case .deduped(let replyId): + case let .deduped(replyId): self.watchReplyLogger.debug( "watch reply deduped replyId=\(replyId, privacy: .public)") case let .queue(replyId, actionId): @@ -2638,7 +2648,7 @@ extension NodeAppModel { } private func flushQueuedWatchRepliesIfConnected() async { - for event in self.watchReplyCoordinator.drainIfConnected(await self.isGatewayConnected()) { + for event in await self.watchReplyCoordinator.drainIfConnected(self.isGatewayConnected()) { await self.forwardWatchReplyToAgent(event) } } @@ -2660,13 +2670,13 @@ extension NodeAppModel { try await self.sendAgentRequest(link: link) let forwardedMessage = "watch reply forwarded replyId=\(event.replyId) " - + "action=\(event.actionId)" + + "action=\(event.actionId)" self.watchReplyLogger.info("\(forwardedMessage, privacy: .public)") self.openChatRequestID &+= 1 } catch { let failedMessage = "watch reply forwarding failed replyId=\(event.replyId) " - + "error=\(error.localizedDescription)" + + "error=\(error.localizedDescription)" self.watchReplyLogger.error("\(failedMessage, privacy: .public)") self.watchReplyCoordinator.requeueFront(event) } @@ -2811,7 +2821,7 @@ extension NodeAppModel { risk: nil) } - nonisolated private static func shouldResetWatchExecApprovalResolvingStateOnPrompt( + private nonisolated static func shouldResetWatchExecApprovalResolvingStateOnPrompt( reason: String) -> Bool { reason == "resolve_retry" @@ -2828,8 +2838,10 @@ extension NodeAppModel { self.watchExecApprovalLogger.debug( "watch exec approval prompt sent id=\(prompt.id, privacy: .public) reason=\(reason, privacy: .public)") } catch { - // swiftlint:disable:next line_length - self.watchExecApprovalLogger.error("watch exec approval prompt failed id=\(prompt.id, privacy: .public) reason=\(reason, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + self.watchExecApprovalLogger + .error( + // swiftlint:disable:next line_length + "watch exec approval prompt failed id=\(prompt.id, privacy: .public) reason=\(reason, privacy: .public) error=\(error.localizedDescription, privacy: .public)") } await self.syncWatchExecApprovalSnapshot(reason: "\(reason)_snapshot") } @@ -2850,8 +2862,10 @@ extension NodeAppModel { do { _ = try await self.watchMessagingService.sendExecApprovalResolved(message) } catch { - // swiftlint:disable:next line_length - self.watchExecApprovalLogger.error("watch exec approval resolved update failed id=\(normalizedApprovalID, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + self.watchExecApprovalLogger + .error( + // swiftlint:disable:next line_length + "watch exec approval resolved update failed id=\(normalizedApprovalID, privacy: .public) error=\(error.localizedDescription, privacy: .public)") } await self.syncWatchExecApprovalSnapshot(reason: "resolved_snapshot") } @@ -2870,8 +2884,10 @@ extension NodeAppModel { do { _ = try await self.watchMessagingService.sendExecApprovalExpired(message) } catch { - // swiftlint:disable:next line_length - self.watchExecApprovalLogger.error("watch exec approval expiry update failed id=\(normalizedApprovalID, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + self.watchExecApprovalLogger + .error( + // swiftlint:disable:next line_length + "watch exec approval expiry update failed id=\(normalizedApprovalID, privacy: .public) error=\(error.localizedDescription, privacy: .public)") } await self.syncWatchExecApprovalSnapshot(reason: "expired_\(reason.rawValue)") } @@ -2900,13 +2916,17 @@ extension NodeAppModel { _ = try await self.watchMessagingService.syncExecApprovalSnapshot(message) GatewayDiagnostics.log( "watch exec approval: sync snapshot sent reason=\(reason) count=\(approvals.count)") - // swiftlint:disable:next line_length - self.watchExecApprovalLogger.debug("watch exec approval snapshot sent reason=\(reason, privacy: .public) count=\(approvals.count, privacy: .public)") + self.watchExecApprovalLogger + .debug( + // swiftlint:disable:next line_length + "watch exec approval snapshot sent reason=\(reason, privacy: .public) count=\(approvals.count, privacy: .public)") } catch { GatewayDiagnostics.log( "watch exec approval: sync snapshot failed reason=\(reason) error=\(error.localizedDescription)") - // swiftlint:disable:next line_length - self.watchExecApprovalLogger.error("watch exec approval snapshot failed reason=\(reason, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + self.watchExecApprovalLogger + .error( + // swiftlint:disable:next line_length + "watch exec approval snapshot failed reason=\(reason, privacy: .public) error=\(error.localizedDescription, privacy: .public)") } } @@ -2917,7 +2937,7 @@ extension NodeAppModel { GatewayDiagnostics.log("watch exec approval: refresh on demand end reason=\(reason)") } - nonisolated private static func watchExecApprovalIDsNeedingFetch( + private nonisolated static func watchExecApprovalIDsNeedingFetch( candidateIDs: [String], cachedApprovalIDs: [String]) -> [String] { @@ -2972,8 +2992,10 @@ extension NodeAppModel { forApprovalID: approvalId, notificationCenter: self.notificationCenter) case let .failed(message): - // swiftlint:disable:next line_length - self.watchExecApprovalLogger.error("watch exec approval hydrate failed id=\(approvalId, privacy: .public) reason=\(reason, privacy: .public) error=\(message, privacy: .public)") + self.watchExecApprovalLogger + .error( + // swiftlint:disable:next line_length + "watch exec approval hydrate failed id=\(approvalId, privacy: .public) reason=\(reason, privacy: .public) error=\(message, privacy: .public)") } } } @@ -3054,8 +3076,10 @@ extension NodeAppModel { reason: .notFound) return true case let .failed(message): - // swiftlint:disable:next line_length - self.watchExecApprovalLogger.error("watch exec approval push fetch failed id=\(normalizedApprovalID, privacy: .public) error=\(message, privacy: .public)") + self.watchExecApprovalLogger + .error( + // swiftlint:disable:next line_length + "watch exec approval push fetch failed id=\(normalizedApprovalID, privacy: .public) error=\(message, privacy: .public)") return false } } @@ -3084,9 +3108,9 @@ extension NodeAppModel { let pushKind = Self.openclawPushKind(userInfo) let receivedMessage = "Silent push received wakeId=\(wakeId) " - + "kind=\(pushKind) " - + "backgrounded=\(self.isBackgrounded) " - + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" + + "kind=\(pushKind) " + + "backgrounded=\(self.isBackgrounded) " + + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" self.pushWakeLogger.info("\(receivedMessage, privacy: .public)") if await ExecApprovalNotificationBridge.handleResolvedPushIfNeeded( @@ -3108,8 +3132,10 @@ extension NodeAppModel { { let handled = await self.handleExecApprovalRequestedRemotePush(approvalId: approvalId) if handled { - // swiftlint:disable:next line_length - self.execApprovalNotificationLogger.info("Handled exec approval request push wakeId=\(wakeId, privacy: .public) id=\(approvalId, privacy: .public)") + self.execApprovalNotificationLogger + .info( + // swiftlint:disable:next line_length + "Handled exec approval request push wakeId=\(wakeId, privacy: .public) id=\(approvalId, privacy: .public)") } return handled } @@ -3117,9 +3143,9 @@ extension NodeAppModel { let result = await self.reconnectGatewaySessionsForSilentPushIfNeeded(wakeId: wakeId) let outcomeMessage = "Silent push outcome wakeId=\(wakeId) " - + "applied=\(result.applied) " - + "reason=\(result.reason) " - + "durationMs=\(result.durationMs)" + + "applied=\(result.applied) " + + "reason=\(result.reason) " + + "durationMs=\(result.durationMs)" self.pushWakeLogger.info("\(outcomeMessage, privacy: .public)") return result.applied } @@ -3128,16 +3154,16 @@ extension NodeAppModel { let wakeId = Self.makePushWakeAttemptID() let receivedMessage = "Background refresh wake received wakeId=\(wakeId) " - + "trigger=\(trigger) " - + "backgrounded=\(self.isBackgrounded) " - + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" + + "trigger=\(trigger) " + + "backgrounded=\(self.isBackgrounded) " + + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" self.pushWakeLogger.info("\(receivedMessage, privacy: .public)") let result = await self.reconnectGatewaySessionsForSilentPushIfNeeded(wakeId: wakeId) let outcomeMessage = "Background refresh wake outcome wakeId=\(wakeId) " - + "applied=\(result.applied) " - + "reason=\(result.reason) " - + "durationMs=\(result.durationMs)" + + "applied=\(result.applied) " + + "reason=\(result.reason) " + + "durationMs=\(result.durationMs)" self.pushWakeLogger.info("\(outcomeMessage, privacy: .public)") return result.applied } @@ -3157,7 +3183,7 @@ extension NodeAppModel { { let throttledMessage = "Location wake throttled wakeId=\(wakeId) " - + "elapsedSec=\(now.timeIntervalSince(last))" + + "elapsedSec=\(now.timeIntervalSince(last))" self.locationWakeLogger.info("\(throttledMessage, privacy: .public)") return } @@ -3165,15 +3191,15 @@ extension NodeAppModel { let beginMessage = "Location wake begin wakeId=\(wakeId) " - + "backgrounded=\(self.isBackgrounded) " - + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" + + "backgrounded=\(self.isBackgrounded) " + + "autoReconnect=\(self.gatewayAutoReconnectEnabled)" self.locationWakeLogger.info("\(beginMessage, privacy: .public)") let result = await self.reconnectGatewaySessionsForSilentPushIfNeeded(wakeId: wakeId) let triggerMessage = "Location wake trigger wakeId=\(wakeId) " - + "applied=\(result.applied) " - + "reason=\(result.reason) " - + "durationMs=\(result.durationMs)" + + "applied=\(result.applied) " + + "reason=\(result.reason) " + + "durationMs=\(result.durationMs)" self.locationWakeLogger.info("\(triggerMessage, privacy: .public)") guard result.applied else { return } @@ -3201,7 +3227,7 @@ extension NodeAppModel { return } let usesRelayTransport = await self.pushRegistrationManager.usesRelayTransport - if !usesRelayTransport && token == self.apnsLastRegisteredTokenHex { + if !usesRelayTransport, token == self.apnsLastRegisteredTokenHex { return } guard let topic = Bundle.main.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines), @@ -3330,8 +3356,10 @@ extension NodeAppModel { self.clearPendingExecApprovalPromptIfMatches(approvalId) await self.publishWatchExecApprovalExpired(approvalId: approvalId, reason: .notFound) case let .failed(message): - // swiftlint:disable:next line_length - self.execApprovalNotificationLogger.error("Exec approval prompt fetch failed id=\(approvalId, privacy: .public) reason=\(message, privacy: .public)") + self.execApprovalNotificationLogger + .error( + // swiftlint:disable:next line_length + "Exec approval prompt fetch failed id=\(approvalId, privacy: .public) reason=\(message, privacy: .public)") } } @@ -3369,7 +3397,7 @@ extension NodeAppModel { expiresAtMs: details.expiresAtMs) } - nonisolated private static func shouldUseBackgroundAwareExecApprovalReconnect( + private nonisolated static func shouldUseBackgroundAwareExecApprovalReconnect( sourceReason: String, isBackgrounded: Bool) -> Bool { @@ -3387,24 +3415,22 @@ extension NodeAppModel { sourceReason: String? = nil) async -> ExecApprovalPromptFetchOutcome { let normalizedSourceReason = sourceReason?.trimmingCharacters(in: .whitespacesAndNewlines) - let fetchReason: String - if let normalizedSourceReason, !normalizedSourceReason.isEmpty { - fetchReason = normalizedSourceReason + let fetchReason: String = if let normalizedSourceReason, !normalizedSourceReason.isEmpty { + normalizedSourceReason } else { - fetchReason = "direct" + "direct" } GatewayDiagnostics.log( "watch exec approval: fetch prompt start id=\(approvalId) reason=\(fetchReason)") - let connected: Bool - if Self.shouldUseBackgroundAwareExecApprovalReconnect( + let connected: Bool = if Self.shouldUseBackgroundAwareExecApprovalReconnect( sourceReason: fetchReason, isBackgrounded: self.isBackgrounded) { - connected = await self.ensureOperatorApprovalConnectionForWatchReview( - timeoutMs: 12_000, + await self.ensureOperatorApprovalConnectionForWatchReview( + timeoutMs: 12000, reason: fetchReason) } else { - connected = await self.ensureOperatorApprovalConnection(timeoutMs: 12_000) + await self.ensureOperatorApprovalConnection(timeoutMs: 12000) } guard connected else { GatewayDiagnostics.log( @@ -3472,8 +3498,8 @@ extension NodeAppModel { func handleExecApprovalNotificationDecision( approvalId: String, - decision: String - ) async { + decision: String) async + { let normalizedApprovalID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines) guard !normalizedApprovalID.isEmpty else { return } @@ -3499,8 +3525,8 @@ extension NodeAppModel { private func resolveExecApprovalNotificationDecision( approvalId: String, decision: String, - sourceReason: String? = nil - ) async -> ExecApprovalResolutionOutcome { + sourceReason: String? = nil) async -> ExecApprovalResolutionOutcome + { let normalizedApprovalID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines) let normalizedDecision = decision.trimmingCharacters(in: .whitespacesAndNewlines) let normalizedSourceReason = sourceReason?.trimmingCharacters(in: .whitespacesAndNewlines) @@ -3509,16 +3535,15 @@ extension NodeAppModel { return .failed(message: "Invalid approval request.") } - let connected: Bool - if Self.shouldUseBackgroundAwareExecApprovalReconnect( + let connected: Bool = if Self.shouldUseBackgroundAwareExecApprovalReconnect( sourceReason: resolutionReason, isBackgrounded: self.isBackgrounded) { - connected = await self.ensureOperatorApprovalConnectionForWatchReview( - timeoutMs: 12_000, + await self.ensureOperatorApprovalConnectionForWatchReview( + timeoutMs: 12000, reason: resolutionReason) } else { - connected = await self.ensureOperatorApprovalConnection(timeoutMs: 12_000) + await self.ensureOperatorApprovalConnection(timeoutMs: 12000) } guard connected else { self.execApprovalNotificationLogger.error( @@ -3573,7 +3598,7 @@ extension NodeAppModel { self.dismissPendingExecApprovalPrompt() } - nonisolated private static func isApprovalNotificationStaleError(_ error: Error) -> Bool { + private nonisolated static func isApprovalNotificationStaleError(_ error: Error) -> Bool { guard let gatewayError = error as? GatewayResponseError else { return false } if gatewayError.code != "INVALID_REQUEST" { return false @@ -3584,7 +3609,7 @@ extension NodeAppModel { return gatewayError.message.lowercased().contains("unknown or expired approval id") } - nonisolated private static func isApprovalNotificationUnavailableError(_ error: Error) -> Bool { + private nonisolated static func isApprovalNotificationUnavailableError(_ error: Error) -> Bool { guard let gatewayError = error as? GatewayResponseError else { return false } if gatewayError.code != "INVALID_REQUEST" { return false @@ -3698,7 +3723,7 @@ extension NodeAppModel { GatewayDiagnostics.log( "watch exec approval: watch_request_reconnect_begin reason=\(reconnectReason) backgrounded=true") - let leaseSeconds = min(45.0, max(15.0, Double(max(timeoutMs, 1_000)) / 1000.0 + 8.0)) + let leaseSeconds = min(45.0, max(15.0, Double(max(timeoutMs, 1000)) / 1000.0 + 8.0)) self.grantBackgroundReconnectLease(seconds: leaseSeconds, reason: "watch_review_\(reconnectReason)") GatewayDiagnostics.log( "watch exec approval: watch_request_reconnect_lease_granted " @@ -3722,7 +3747,7 @@ extension NodeAppModel { "watch exec approval: watch_request_reconnect_loop_\(hadReconnectLoop ? "reused" : "started") " + "reason=\(reconnectReason)") - let initialWaitMs = min(2_500, max(750, timeoutMs / 4)) + let initialWaitMs = min(2500, max(750, timeoutMs / 4)) GatewayDiagnostics.log( "watch exec approval: watch_request_reconnect_wait " + "reason=\(reconnectReason) phase=initial timeoutMs=\(initialWaitMs)") @@ -3772,8 +3797,8 @@ extension NodeAppModel { } private func reconnectGatewaySessionsForSilentPushIfNeeded( - wakeId: String - ) async -> SilentPushWakeAttemptResult { + wakeId: String) async -> SilentPushWakeAttemptResult + { let startedAt = Date() let makeResult: (Bool, String) -> SilentPushWakeAttemptResult = { applied, reason in let durationMs = Int(Date().timeIntervalSince(startedAt) * 1000) @@ -3817,8 +3842,7 @@ extension NodeAppModel { let data = try await self.operatorGateway.request( method: "voicewake.get", paramsJSON: "{}", - timeoutSeconds: 8 - ) + timeoutSeconds: 8) guard let triggers = VoiceWakePreferences.decodeGatewayTriggers(from: data) else { return } VoiceWakePreferences.saveTriggerWords(triggers) } catch { @@ -3876,8 +3900,8 @@ extension NodeAppModel { let message = link.message.trimmingCharacters(in: .whitespacesAndNewlines) guard !message.isEmpty else { return } self.deepLinkLogger.info( - "agent deep link received messageChars=\(message.count) url=\(originalURL.absoluteString, privacy: .public)" - ) + // swiftlint:disable:next line_length + "agent deep link received messageChars=\(message.count) url=\(originalURL.absoluteString, privacy: .public)") if message.count > IOSDeepLinkAgentPolicy.maxMessageChars { self.screen.errorText = "Deep link too large (message exceeds " @@ -4173,8 +4197,8 @@ extension NodeAppModel { func _test_makeOperatorConnectOptions( clientId: String, displayName: String?, - includeApprovalScope: Bool - ) -> GatewayConnectOptions { + includeApprovalScope: Bool) -> GatewayConnectOptions + { self.makeOperatorConnectOptions( clientId: clientId, displayName: displayName, @@ -4244,8 +4268,8 @@ extension NodeAppModel { host: String?, nodeId: String?, agentId: String?, - expiresAtMs: Int? - ) -> ExecApprovalPrompt? { + expiresAtMs: Int?) -> ExecApprovalPrompt? + { self.makeExecApprovalPrompt( from: ExecApprovalGetResponse( id: id, @@ -4282,8 +4306,8 @@ extension NodeAppModel { nonisolated static func _test_shouldRequestOperatorApprovalScope( token: String?, password: String?, - storedOperatorScopes: [String] - ) -> Bool { + storedOperatorScopes: [String]) -> Bool + { self.shouldRequestOperatorApprovalScope( token: token, password: password, @@ -4291,8 +4315,8 @@ extension NodeAppModel { } nonisolated static func _test_clearingBootstrapToken( - in config: GatewayConnectConfig? - ) -> GatewayConnectConfig? { + in config: GatewayConnectConfig?) -> GatewayConnectConfig? + { self.clearingBootstrapToken(in: config) } @@ -4313,7 +4337,6 @@ extension NodeAppModel { clientDisplayName: nil), sessionBox: nil) } - } #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 e126b3bd20d..d58428adb77 100644 --- a/apps/ios/Sources/Motion/MotionService.swift +++ b/apps/ios/Sources/Motion/MotionService.swift @@ -62,7 +62,7 @@ final class MotionService: MotionServicing { let (start, end) = Self.resolveRange(startISO: params.startISO, endISO: params.endISO) let pedometer = CMPedometer() - let payload: OpenClawPedometerPayload = try await withCheckedThrowingContinuation { cont in + return try await withCheckedThrowingContinuation { cont in pedometer.queryPedometerData(from: start, to: end) { data, error in if let error { cont.resume(throwing: error) @@ -79,7 +79,6 @@ final class MotionService: MotionServicing { } } } - return payload } private static func resolveRange(startISO: String?, endISO: String?) -> (Date, Date) { diff --git a/apps/ios/Sources/Onboarding/GatewayOnboardingView.swift b/apps/ios/Sources/Onboarding/GatewayOnboardingView.swift index 1d16f7a9ee9..a6c440781d7 100644 --- a/apps/ios/Sources/Onboarding/GatewayOnboardingView.swift +++ b/apps/ios/Sources/Onboarding/GatewayOnboardingView.swift @@ -95,7 +95,6 @@ private struct AutoDetectStep: View { } return nil } - } private struct ManualEntryStep: View { @@ -229,7 +228,7 @@ private struct ManualEntryStep: View { private func manualPortValue() -> Int? { let trimmed = self.manualPortText.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } - return Int(trimmed.filter { $0.isNumber }) + return Int(trimmed.filter(\.isNumber)) } private func resetManualForm() { @@ -334,7 +333,6 @@ private func resetGatewayConnectionState( } @MainActor -@ViewBuilder private func gatewayConnectionStatusSection( appModel: NodeAppModel, gatewayController: GatewayConnectionController, @@ -373,8 +371,8 @@ private struct ConnectionStatusBox: View { static func defaultLines( appModel: NodeAppModel, - gatewayController: GatewayConnectionController - ) -> [String] { + gatewayController: GatewayConnectionController) -> [String] + { var lines: [String] = [ "gateway: \(appModel.gatewayDisplayStatusText)", "discovery: \(gatewayController.discoveryStatusText)", diff --git a/apps/ios/Sources/Onboarding/OnboardingStateStore.swift b/apps/ios/Sources/Onboarding/OnboardingStateStore.swift index dc2859d86d9..2fa1a7b73ff 100644 --- a/apps/ios/Sources/Onboarding/OnboardingStateStore.swift +++ b/apps/ios/Sources/Onboarding/OnboardingStateStore.swift @@ -25,7 +25,7 @@ enum OnboardingStateStore { @MainActor static func shouldPresentOnLaunch(appModel: NodeAppModel, defaults: UserDefaults = .standard) -> Bool { - if defaults.bool(forKey: Self.completedDefaultsKey) { return false } + if defaults.bool(forKey: self.completedDefaultsKey) { return false } // If we have a last-known connection config, don't force onboarding on launch. Auto-connect // should handle reconnecting, and users can always open onboarding manually if needed. if GatewaySettingsStore.loadLastGatewayConnection() != nil { return false } @@ -33,28 +33,28 @@ enum OnboardingStateStore { } static func markCompleted(mode: OnboardingConnectionMode? = nil, defaults: UserDefaults = .standard) { - defaults.set(true, forKey: Self.completedDefaultsKey) + defaults.set(true, forKey: self.completedDefaultsKey) if let mode { - defaults.set(mode.rawValue, forKey: Self.lastModeDefaultsKey) + defaults.set(mode.rawValue, forKey: self.lastModeDefaultsKey) } defaults.set(Int(Date().timeIntervalSince1970), forKey: Self.lastSuccessTimeDefaultsKey) } static func shouldPresentFirstRunIntro(defaults: UserDefaults = .standard) -> Bool { - !defaults.bool(forKey: Self.firstRunIntroSeenDefaultsKey) + !defaults.bool(forKey: self.firstRunIntroSeenDefaultsKey) } static func markFirstRunIntroSeen(defaults: UserDefaults = .standard) { - defaults.set(true, forKey: Self.firstRunIntroSeenDefaultsKey) + defaults.set(true, forKey: self.firstRunIntroSeenDefaultsKey) } static func markIncomplete(defaults: UserDefaults = .standard) { - defaults.set(false, forKey: Self.completedDefaultsKey) + defaults.set(false, forKey: self.completedDefaultsKey) } static func reset(defaults: UserDefaults = .standard) { - defaults.set(false, forKey: Self.completedDefaultsKey) - defaults.set(false, forKey: Self.firstRunIntroSeenDefaultsKey) + defaults.set(false, forKey: self.completedDefaultsKey) + defaults.set(false, forKey: self.firstRunIntroSeenDefaultsKey) } static func lastMode(defaults: UserDefaults = .standard) -> OnboardingConnectionMode? { diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index 55327d6af80..5bfc83c929d 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -1,5 +1,5 @@ -import CoreImage import Combine +import CoreImage import OpenClawKit import PhotosUI import SwiftUI @@ -151,8 +151,7 @@ struct OnboardingWizardView: View { #selector(UIResponder.resignFirstResponder), to: nil, from: nil, - for: nil - ) + for: nil) } } } @@ -160,137 +159,136 @@ struct OnboardingWizardView: View { .gatewayTrustPromptAlert() .alert("QR Scanner Unavailable", isPresented: Binding( get: { self.scannerError != nil }, - set: { if !$0 { self.scannerError = nil } } - )) { + set: { if !$0 { self.scannerError = nil } })) + { Button("OK", role: .cancel) {} } message: { Text(self.scannerError ?? "") } .sheet(isPresented: self.$showQRScanner) { - NavigationStack { - QRScannerView( - onGatewayLink: { link in - self.handleScannedLink(link) - }, - onError: { error in - self.showQRScanner = false - self.statusLine = "Scanner error: \(error)" - self.scannerError = error - }, - onDismiss: { - self.showQRScanner = false - }) - .ignoresSafeArea() - .navigationTitle("Scan QR Code") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button("Cancel") { self.showQRScanner = false } - } - ToolbarItem(placement: .topBarTrailing) { - PhotosPicker(selection: self.$selectedPhoto, matching: .images) { - Label("Photos", systemImage: "photo") + NavigationStack { + QRScannerView( + onGatewayLink: { link in + self.handleScannedLink(link) + }, + onError: { error in + self.showQRScanner = false + self.statusLine = "Scanner error: \(error)" + self.scannerError = error + }, + onDismiss: { + self.showQRScanner = false + }) + .ignoresSafeArea() + .navigationTitle("Scan QR Code") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Cancel") { self.showQRScanner = false } + } + ToolbarItem(placement: .topBarTrailing) { + PhotosPicker(selection: self.$selectedPhoto, matching: .images) { + Label("Photos", systemImage: "photo") + } + } + } + } + .onChange(of: self.selectedPhoto) { _, newValue in + guard let item = newValue else { return } + self.selectedPhoto = nil + Task { + guard let data = try? await item.loadTransferable(type: Data.self) else { + self.showQRScanner = false + self.scannerError = "Could not load the selected image." + return + } + if let message = self.detectQRCode(from: data) { + if let link = GatewayConnectDeepLink.fromSetupCode(message) { + self.handleScannedLink(link) + return + } + if let url = URL(string: message), + let route = DeepLinkParser.parse(url), + case let .gateway(link) = route + { + self.handleScannedLink(link) + return } } - } - } - .onChange(of: self.selectedPhoto) { _, newValue in - guard let item = newValue else { return } - self.selectedPhoto = nil - Task { - guard let data = try? await item.loadTransferable(type: Data.self) else { self.showQRScanner = false - self.scannerError = "Could not load the selected image." - return + self.scannerError = "No valid QR code found in the selected image." } - if let message = self.detectQRCode(from: data) { - if let link = GatewayConnectDeepLink.fromSetupCode(message) { - self.handleScannedLink(link) - return - } - if let url = URL(string: message), - let route = DeepLinkParser.parse(url), - case let .gateway(link) = route - { - self.handleScannedLink(link) - return - } - } - self.showQRScanner = false - self.scannerError = "No valid QR code found in the selected image." } } - } - .sheet(isPresented: self.$showGatewayProblemDetails) { - if let currentProblem = self.currentProblem { - GatewayProblemDetailsSheet( - problem: currentProblem, - primaryActionTitle: "Retry", - onPrimaryAction: { - Task { await self.retryLastAttempt() } - }) + .sheet(isPresented: self.$showGatewayProblemDetails) { + if let currentProblem = self.currentProblem { + GatewayProblemDetailsSheet( + problem: currentProblem, + primaryActionTitle: "Retry", + onPrimaryAction: { + Task { await self.retryLastAttempt() } + }) + } } - } - .onAppear { - self.initializeState() - } - .onDisappear { - self.discoveryRestartTask?.cancel() - self.discoveryRestartTask = nil - } - .onChange(of: self.discoveryDomain) { _, _ in - self.scheduleDiscoveryRestart() - } - .onChange(of: self.manualPortText) { _, newValue in - let digits = newValue.filter(\.isNumber) - if digits != newValue { - self.manualPortText = digits - return + .onAppear { + self.initializeState() } - guard let parsed = Int(digits), parsed > 0 else { - self.manualPort = 0 - return + .onDisappear { + self.discoveryRestartTask?.cancel() + self.discoveryRestartTask = nil } - self.manualPort = min(parsed, 65535) - } - .onChange(of: self.manualPort) { _, newValue in - let normalized = newValue > 0 ? String(newValue) : "" - if self.manualPortText != normalized { - self.manualPortText = normalized + .onChange(of: self.discoveryDomain) { _, _ in + self.scheduleDiscoveryRestart() } - } - .onChange(of: self.gatewayToken) { _, newValue in - self.saveGatewayCredentials(token: newValue, password: self.gatewayPassword) - } - .onChange(of: self.gatewayPassword) { _, newValue in - self.saveGatewayCredentials(token: self.gatewayToken, password: newValue) - } - .onChange(of: self.appModel.lastGatewayProblem) { _, newValue in - self.updateConnectionIssue(problem: newValue, statusText: self.appModel.gatewayStatusText) - } - .onChange(of: self.appModel.gatewayStatusText) { _, newValue in - self.updateConnectionIssue(problem: self.appModel.lastGatewayProblem, statusText: newValue) - } - .onChange(of: self.appModel.gatewayServerName) { _, newValue in - guard newValue != nil else { return } - self.showQRScanner = false - self.statusLine = "Connected." - if !self.didMarkCompleted, let selectedMode { - OnboardingStateStore.markCompleted(mode: selectedMode) - self.didMarkCompleted = true + .onChange(of: self.manualPortText) { _, newValue in + let digits = newValue.filter(\.isNumber) + if digits != newValue { + self.manualPortText = digits + return + } + guard let parsed = Int(digits), parsed > 0 else { + self.manualPort = 0 + return + } + self.manualPort = min(parsed, 65535) + } + .onChange(of: self.manualPort) { _, newValue in + let normalized = newValue > 0 ? String(newValue) : "" + if self.manualPortText != normalized { + self.manualPortText = normalized + } + } + .onChange(of: self.gatewayToken) { _, newValue in + self.saveGatewayCredentials(token: newValue, password: self.gatewayPassword) + } + .onChange(of: self.gatewayPassword) { _, newValue in + self.saveGatewayCredentials(token: self.gatewayToken, password: newValue) + } + .onChange(of: self.appModel.lastGatewayProblem) { _, newValue in + self.updateConnectionIssue(problem: newValue, statusText: self.appModel.gatewayStatusText) + } + .onChange(of: self.appModel.gatewayStatusText) { _, newValue in + self.updateConnectionIssue(problem: self.appModel.lastGatewayProblem, statusText: newValue) + } + .onChange(of: self.appModel.gatewayServerName) { _, newValue in + guard newValue != nil else { return } + self.showQRScanner = false + self.statusLine = "Connected." + if !self.didMarkCompleted, let selectedMode { + OnboardingStateStore.markCompleted(mode: selectedMode) + self.didMarkCompleted = true + } + self.onClose() + } + .onChange(of: self.scenePhase) { _, newValue in + guard newValue == .active else { return } + self.attemptAutomaticPairingResumeIfNeeded() + } + .onReceive(Self.pairingAutoResumeTicker) { _ in + self.attemptAutomaticPairingResumeIfNeeded() } - self.onClose() - } - .onChange(of: self.scenePhase) { _, newValue in - guard newValue == .active else { return } - self.attemptAutomaticPairingResumeIfNeeded() - } - .onReceive(Self.pairingAutoResumeTicker) { _ in - self.attemptAutomaticPairingResumeIfNeeded() - } } - @ViewBuilder private var introStep: some View { VStack(spacing: 0) { Spacer() @@ -369,7 +367,6 @@ struct OnboardingWizardView: View { } } - @ViewBuilder private var welcomeStep: some View { VStack(spacing: 0) { Spacer() @@ -712,7 +709,6 @@ struct OnboardingWizardView: View { } } - @ViewBuilder private func manualConnectionFieldsSection(title: String) -> some View { Section(title) { TextField("Host", text: self.$manualHost) @@ -868,8 +864,7 @@ struct OnboardingWizardView: View { let detector = CIDetector( ofType: CIDetectorTypeQRCode, context: nil, - options: [CIDetectorAccuracy: CIDetectorAccuracyHigh] - ) + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) let features = detector?.features(in: ciImage) ?? [] for feature in features { if let qr = feature as? CIQRCodeFeature, let message = qr.messageString { @@ -891,6 +886,7 @@ struct OnboardingWizardView: View { self.connectMessage = nil self.step = target } + private var canConnectManual: Bool { let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines) return !host.isEmpty && self.manualPort > 0 && self.manualPort <= 65535 @@ -919,7 +915,7 @@ struct OnboardingWizardView: View { if self.selectedMode == nil { self.selectedMode = OnboardingStateStore.lastMode() } - if self.selectedMode == .developerLocal && self.manualHost == "openclaw.local" { + if self.selectedMode == .developerLocal, self.manualHost == "openclaw.local" { self.manualHost = "localhost" self.manualTLS = false } diff --git a/apps/ios/Sources/OpenClawApp.swift b/apps/ios/Sources/OpenClawApp.swift index 043229605f3..5c829d4f781 100644 --- a/apps/ios/Sources/OpenClawApp.swift +++ b/apps/ios/Sources/OpenClawApp.swift @@ -1,9 +1,9 @@ -import SwiftUI +import BackgroundTasks import Foundation import OpenClawKit import os +import SwiftUI import UIKit -import BackgroundTasks @preconcurrency import UserNotifications private struct PendingWatchPromptAction { @@ -88,16 +88,15 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc self.appModel ?? OpenClawAppModelRegistry.appModel } -#if DEBUG + #if DEBUG func _test_resolvedAppModel() -> NodeAppModel? { self.resolvedAppModel() } -#endif + #endif func application( _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { GatewayDiagnostics.log("app delegate: didFinishLaunching") if self.appModel == nil { @@ -151,7 +150,7 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc guard let appModel = self.resolvedAppModel() else { if ExecApprovalNotificationBridge.payloadKind(userInfo: userInfo) == ExecApprovalNotificationBridge.requestedKind, - let approvalId = ExecApprovalNotificationBridge.approvalID(from: userInfo) + let approvalId = ExecApprovalNotificationBridge.approvalID(from: userInfo) { self.pendingExecApprovalRequestedPushIDs.append(approvalId) } @@ -179,8 +178,8 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc private func registerBackgroundWakeRefreshTask() { BGTaskScheduler.shared.register( forTaskWithIdentifier: Self.wakeRefreshTaskIdentifier, - using: nil - ) { [weak self] task in + using: nil) + { [weak self] task in guard let refreshTask = task as? BGAppRefreshTask else { task.setTaskCompleted(success: false) return @@ -196,17 +195,15 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc try BGTaskScheduler.shared.submit(request) let scheduledLogMessage = "Scheduled background wake refresh reason=\(reason) " - + "delaySeconds=\(max(60, delay))" + + "delaySeconds=\(max(60, delay))" self.backgroundWakeLogger.info( - "\(scheduledLogMessage, privacy: .public)" - ) + "\(scheduledLogMessage, privacy: .public)") } catch { let failedLogMessage = "Failed scheduling background wake refresh reason=\(reason) " - + "error=\(error.localizedDescription)" + + "error=\(error.localizedDescription)" self.backgroundWakeLogger.error( - "\(failedLogMessage, privacy: .public)" - ) + "\(failedLogMessage, privacy: .public)") } } @@ -475,14 +472,13 @@ enum WatchPromptNotificationBridge { private static func categoryActions(_ actions: [OpenClawWatchAction]) -> [UNNotificationAction] { actions.enumerated().map { index, action in - let identifier: String - switch index { + let identifier: String = switch index { case 0: - identifier = self.actionPrimaryIdentifier + self.actionPrimaryIdentifier case 1: - identifier = self.actionSecondaryIdentifier + self.actionSecondaryIdentifier default: - identifier = "\(self.actionIdentifierPrefix)\(index)" + "\(self.actionIdentifierPrefix)\(index)" } return UNNotificationAction( identifier: identifier, @@ -494,12 +490,12 @@ enum WatchPromptNotificationBridge { private static func notificationActionOptions(style: String?) -> UNNotificationActionOptions { switch style?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { case "destructive": - return [.destructive] + [.destructive] case "foreground": // For mirrored watch actions, keep handling in background when possible. - return [] + [] default: - return [] + [] } } @@ -510,7 +506,7 @@ enum WatchPromptNotificationBridge { case .authorized, .provisional, .ephemeral: return true case .notDetermined: - let granted = (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false + let granted = await (try? center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false if !granted { return false } let updatedStatus = await self.notificationAuthorizationStatus(center: center) if self.isAuthorizationStatusAllowed(updatedStatus) { @@ -540,8 +536,8 @@ enum WatchPromptNotificationBridge { } private static func notificationAuthorizationStatus( - center: UNUserNotificationCenter - ) async -> UNAuthorizationStatus { + center: UNUserNotificationCenter) async -> UNAuthorizationStatus + { await withCheckedContinuation { continuation in center.getNotificationSettings { settings in continuation.resume(returning: settings.authorizationStatus) @@ -565,8 +561,8 @@ enum WatchPromptNotificationBridge { private static func addNotificationRequest( _ request: UNNotificationRequest, - center: UNUserNotificationCenter - ) async throws { + center: UNUserNotificationCenter) async throws + { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in center.add(request) { error in ThrowingContinuationSupport.resumeVoid(continuation, error: error) diff --git a/apps/ios/Sources/Push/ExecApprovalNotificationBridge.swift b/apps/ios/Sources/Push/ExecApprovalNotificationBridge.swift index 939bf17a6ed..3a89c4b082b 100644 --- a/apps/ios/Sources/Push/ExecApprovalNotificationBridge.swift +++ b/apps/ios/Sources/Push/ExecApprovalNotificationBridge.swift @@ -1,7 +1,7 @@ import Foundation -import UserNotifications +@preconcurrency import UserNotifications -struct ExecApprovalNotificationPrompt: Sendable, Equatable { +struct ExecApprovalNotificationPrompt: Equatable { let approvalId: String } @@ -38,8 +38,7 @@ enum ExecApprovalNotificationBridge { static func parsePrompt( actionIdentifier: String, - userInfo: [AnyHashable: Any] - ) -> ExecApprovalNotificationPrompt? + userInfo: [AnyHashable: Any]) -> ExecApprovalNotificationPrompt? { guard actionIdentifier == UNNotificationDefaultActionIdentifier || actionIdentifier == self.reviewActionIdentifier @@ -54,8 +53,7 @@ enum ExecApprovalNotificationBridge { @MainActor static func handleResolvedPushIfNeeded( userInfo: [AnyHashable: Any], - notificationCenter: NotificationCentering - ) async -> Bool + notificationCenter: NotificationCentering) async -> Bool { guard self.payloadKind(userInfo: userInfo) == self.resolvedKind, let approvalId = self.approvalID(from: userInfo) @@ -70,8 +68,8 @@ enum ExecApprovalNotificationBridge { @MainActor static func removeNotifications( forApprovalID approvalId: String, - notificationCenter: NotificationCentering - ) async { + notificationCenter: NotificationCentering) async + { let normalizedID = approvalId.trimmingCharacters(in: .whitespacesAndNewlines) guard !normalizedID.isEmpty else { return } diff --git a/apps/ios/Sources/Push/PushRegistrationManager.swift b/apps/ios/Sources/Push/PushRegistrationManager.swift index 77f54f8d108..d784ab89f92 100644 --- a/apps/ios/Sources/Push/PushRegistrationManager.swift +++ b/apps/ios/Sources/Push/PushRegistrationManager.swift @@ -84,7 +84,7 @@ actor PushRegistrationManager { } guard let installationId = GatewaySettingsStore.loadStableInstanceID()? .trimmingCharacters(in: .whitespacesAndNewlines), - !installationId.isEmpty + !installationId.isEmpty else { throw PushRelayError.relayMisconfigured("Missing stable installation ID for relay registration") } @@ -145,7 +145,7 @@ actor PushRegistrationManager { guard let expiresAtMs else { return true } let nowMs = Int64(Date().timeIntervalSince1970 * 1000) // Refresh shortly before expiry so reconnect-path republishes a live handle. - return expiresAtMs <= nowMs + 60_000 + return expiresAtMs <= nowMs + 60000 } private static func sha256Hex(_ value: String) -> String { diff --git a/apps/ios/Sources/Push/PushRelayClient.swift b/apps/ios/Sources/Push/PushRelayClient.swift index 07bb5caa3b7..a59c379f968 100644 --- a/apps/ios/Sources/Push/PushRelayClient.swift +++ b/apps/ios/Sources/Push/PushRelayClient.swift @@ -24,7 +24,7 @@ enum PushRelayError: LocalizedError { case .unsupportedAppAttest: "App Attest unavailable on this device" case .missingReceipt: - "App Store receipt missing after refresh" + "App Store app transaction missing after refresh" } } } @@ -85,33 +85,6 @@ private struct RelayErrorResponse: Decodable { var reason: String? } -private final class PushRelayReceiptRefreshCoordinator: NSObject, SKRequestDelegate { - private var continuation: CheckedContinuation? - private var activeRequest: SKReceiptRefreshRequest? - - func refresh() async throws { - try await withCheckedThrowingContinuation { continuation in - self.continuation = continuation - let request = SKReceiptRefreshRequest() - self.activeRequest = request - request.delegate = self - request.start() - } - } - - func requestDidFinish(_ request: SKRequest) { - self.continuation?.resume(returning: ()) - self.continuation = nil - self.activeRequest = nil - } - - func request(_ request: SKRequest, didFailWithError error: Error) { - self.continuation?.resume(throwing: error) - self.continuation = nil - self.activeRequest = nil - } -} - private struct PushRelayAppAttestProof { var keyId: String var attestationObject: String? @@ -197,25 +170,27 @@ private final class PushRelayAppAttestService { private final class PushRelayReceiptProvider { func loadReceiptBase64() async throws -> String { - if let receipt = self.readReceiptData() { - return receipt.base64EncodedString() + do { + let result = try await AppTransaction.shared + return try Self.appTransactionBase64(result) + } catch { + let refreshed = try await AppTransaction.refresh() + return try Self.appTransactionBase64(refreshed) } - let refreshCoordinator = PushRelayReceiptRefreshCoordinator() - try await refreshCoordinator.refresh() - if let refreshed = self.readReceiptData() { - return refreshed.base64EncodedString() - } - throw PushRelayError.missingReceipt } - private func readReceiptData() -> Data? { - guard let url = Bundle.main.appStoreReceiptURL else { return nil } - guard let data = try? Data(contentsOf: url), !data.isEmpty else { return nil } - return data + private static func appTransactionBase64( + _ result: StoreKit.VerificationResult) throws -> String + { + let jws = result.jwsRepresentation.trimmingCharacters(in: .whitespacesAndNewlines) + guard !jws.isEmpty else { + throw PushRelayError.missingReceipt + } + return Data(jws.utf8).base64EncodedString() } } -// The client is constructed once and used behind PushRegistrationManager actor isolation. +/// The client is constructed once and used behind PushRegistrationManager actor isolation. final class PushRelayClient: @unchecked Sendable { private let baseURL: URL private let session: URLSession @@ -294,8 +269,7 @@ final class PushRelayClient: @unchecked Sendable { status: status, message: Self.decodeErrorMessage(data: data)) } - let decoded = try self.decode(PushRelayRegisterResponse.self, from: data) - return decoded + return try self.decode(PushRelayRegisterResponse.self, from: data) } private func fetchChallenge() async throws -> PushRelayChallengeResponse { diff --git a/apps/ios/Sources/Reminders/RemindersService.swift b/apps/ios/Sources/Reminders/RemindersService.swift index 8c347b2282b..19591908fb9 100644 --- a/apps/ios/Sources/Reminders/RemindersService.swift +++ b/apps/ios/Sources/Reminders/RemindersService.swift @@ -23,11 +23,11 @@ final class RemindersService: RemindersServicing { let filtered = (items ?? []).filter { reminder in switch statusFilter { case .all: - return true + true case .completed: - return reminder.isCompleted + reminder.isCompleted case .incomplete: - return !reminder.isCompleted + !reminder.isCompleted } } let selected = Array(filtered.prefix(limit)) diff --git a/apps/ios/Sources/RootCanvas.swift b/apps/ios/Sources/RootCanvas.swift index 5f8b0729e35..5440796387a 100644 --- a/apps/ios/Sources/RootCanvas.swift +++ b/apps/ios/Sources/RootCanvas.swift @@ -1,6 +1,6 @@ +import OpenClawProtocol import SwiftUI import UIKit -import OpenClawProtocol struct RootCanvas: View { @Environment(NodeAppModel.self) private var appModel @@ -262,7 +262,7 @@ struct RootCanvas: View { eyebrow: "Connected to \(gatewayLabel)", title: "Your agents are ready", subtitle: - "This phone stays dormant until the gateway needs it, then wakes, syncs, and goes back to sleep.", + "This phone stays dormant until the gateway needs it, then wakes, syncs, and goes back to sleep.", gatewayLabel: gatewayLabel, activeAgentName: self.appModel.activeAgentName, activeAgentBadge: agents.first(where: { $0.isActive })?.badge ?? "OC", @@ -276,7 +276,7 @@ struct RootCanvas: View { eyebrow: "Reconnecting", title: "OpenClaw is syncing back up", subtitle: - "The gateway session is coming back online. " + "The gateway session is coming back online. " + "Agent shortcuts should settle automatically in a moment.", gatewayLabel: gatewayLabel, activeAgentName: self.appModel.activeAgentName, @@ -291,7 +291,7 @@ struct RootCanvas: View { eyebrow: "Welcome to OpenClaw", title: "Your phone stays quiet until it is needed", subtitle: - "Pair this device to your gateway to wake it only for real work, " + "Pair this device to your gateway to wake it only for real work, " + "keep a live agent overview handy, and avoid battery-draining background loops.", gatewayLabel: gatewayLabel, activeAgentName: "Main", @@ -300,7 +300,7 @@ struct RootCanvas: View { agentCount: agents.count, agents: Array(agents.prefix(4)), footer: - "When connected, the gateway can wake the phone with a silent push " + "When connected, the gateway can wake the phone with a silent push " + "instead of holding an always-on session.") } } @@ -352,7 +352,7 @@ struct RootCanvas: View { let words = self.homeCanvasName(for: agent) .split(whereSeparator: { $0.isWhitespace || $0 == "-" || $0 == "_" }) .prefix(2) - let initials = words.compactMap { $0.first }.map(String.init).joined() + let initials = words.compactMap(\.first).map(String.init).joined() if !initials.isEmpty { return initials.uppercased() } @@ -468,8 +468,13 @@ private struct CanvasContent: View { var openSettings: () -> Void var retryGatewayConnection: () -> Void - private var brightenButtons: Bool { self.systemColorScheme == .light } - private var talkActive: Bool { self.appModel.talkMode.isEnabled || self.talkEnabled } + private var brightenButtons: Bool { + self.systemColorScheme == .light + } + + private var talkActive: Bool { + self.appModel.talkMode.isEnabled || self.talkEnabled + } var body: some View { ZStack { diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index f476dd3c397..a2a43d59a97 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -1,5 +1,5 @@ -import OpenClawKit import Observation +import OpenClawKit import UIKit import WebKit @@ -194,7 +194,7 @@ final class ScreenController { NSLocalizedDescriptionKey: "web view unavailable", ]) } - let image: UIImage = try await withCheckedThrowingContinuation { cont in + return try await withCheckedThrowingContinuation { cont in webView.takeSnapshot(with: config) { image, error in if let error { cont.resume(throwing: error) @@ -209,7 +209,6 @@ final class ScreenController { cont.resume(returning: image) } } - return image } func attachWebView(_ webView: WKWebView) { diff --git a/apps/ios/Sources/Screen/ScreenRecordService.swift b/apps/ios/Sources/Screen/ScreenRecordService.swift index 4bea2724dca..a6148731e15 100644 --- a/apps/ios/Sources/Screen/ScreenRecordService.swift +++ b/apps/ios/Sources/Screen/ScreenRecordService.swift @@ -319,7 +319,6 @@ final class ScreenRecordService: @unchecked Sendable { } } } - } @MainActor diff --git a/apps/ios/Sources/Services/NodeServiceProtocols.swift b/apps/ios/Sources/Services/NodeServiceProtocols.swift index afb018221b6..c750e98a5c7 100644 --- a/apps/ios/Sources/Services/NodeServiceProtocols.swift +++ b/apps/ios/Sources/Services/NodeServiceProtocols.swift @@ -69,7 +69,7 @@ protocol MotionServicing: Sendable { func pedometer(params: OpenClawPedometerParams) async throws -> OpenClawPedometerPayload } -struct WatchMessagingStatus: Sendable, Equatable { +struct WatchMessagingStatus: Equatable { var supported: Bool var paired: Bool var appInstalled: Bool @@ -77,7 +77,7 @@ struct WatchMessagingStatus: Sendable, Equatable { var activationState: String } -struct WatchQuickReplyEvent: Sendable, Equatable { +struct WatchQuickReplyEvent: Equatable { var replyId: String var promptId: String var actionId: String @@ -88,7 +88,7 @@ struct WatchQuickReplyEvent: Sendable, Equatable { var transport: String } -struct WatchExecApprovalResolveEvent: Sendable, Equatable { +struct WatchExecApprovalResolveEvent: Equatable { var replyId: String var approvalId: String var decision: OpenClawWatchExecApprovalDecision @@ -96,13 +96,13 @@ struct WatchExecApprovalResolveEvent: Sendable, Equatable { var transport: String } -struct WatchExecApprovalSnapshotRequestEvent: Sendable, Equatable { +struct WatchExecApprovalSnapshotRequestEvent: Equatable { var requestId: String var sentAtMs: Int? var transport: String } -struct WatchNotificationSendResult: Sendable, Equatable { +struct WatchNotificationSendResult: Equatable { var deliveredImmediately: Bool var queuedForDelivery: Bool var transport: String diff --git a/apps/ios/Sources/Services/NotificationService.swift b/apps/ios/Sources/Services/NotificationService.swift index 28e1d9dfd67..bffcb7f16dc 100644 --- a/apps/ios/Sources/Services/NotificationService.swift +++ b/apps/ios/Sources/Services/NotificationService.swift @@ -6,7 +6,7 @@ struct NotificationSnapshot: @unchecked Sendable { let userInfo: [AnyHashable: Any] } -enum NotificationAuthorizationStatus: Sendable { +enum NotificationAuthorizationStatus { case notDetermined case denied case authorized diff --git a/apps/ios/Sources/Services/WatchConnectivityTransport.swift b/apps/ios/Sources/Services/WatchConnectivityTransport.swift index 65482151148..f96a0f1ed20 100644 --- a/apps/ios/Sources/Services/WatchConnectivityTransport.swift +++ b/apps/ios/Sources/Services/WatchConnectivityTransport.swift @@ -21,13 +21,12 @@ private func sendReachableWatchMessage(_ payload: [String: Any], with session: W }, errorHandler: { error in continuation.resume(throwing: error) - } - ) + }) } } final class WatchConnectivityTransport: NSObject, @unchecked Sendable { - nonisolated private static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging") + private nonisolated static let logger = Logger(subsystem: "ai.openclaw", category: "watch.messaging") private let session: WCSession? private let callbacksLock = NSLock() @@ -228,7 +227,7 @@ final class WatchConnectivityTransport: NSObject, @unchecked Sendable { } } - nonisolated private static func status(for session: WCSession) -> WatchMessagingStatus { + private nonisolated static func status(for session: WCSession) -> WatchMessagingStatus { WatchMessagingStatus( supported: true, paired: session.isPaired, @@ -237,7 +236,7 @@ final class WatchConnectivityTransport: NSObject, @unchecked Sendable { activationState: self.activationStateLabel(session.activationState)) } - nonisolated private static func activationStateLabel(_ state: WCSessionActivationState) -> String { + private nonisolated static func activationStateLabel(_ state: WCSessionActivationState) -> String { switch state { case .notActivated: "notActivated" diff --git a/apps/ios/Sources/Services/WatchMessagingPayloadCodec.swift b/apps/ios/Sources/Services/WatchMessagingPayloadCodec.swift index 5322a6bc65f..c7147d88ca9 100644 --- a/apps/ios/Sources/Services/WatchMessagingPayloadCodec.swift +++ b/apps/ios/Sources/Services/WatchMessagingPayloadCodec.swift @@ -21,7 +21,7 @@ enum WatchMessagingPayloadCodec { "title": params.title, "body": params.body, "priority": params.priority?.rawValue ?? OpenClawNotificationPriority.active.rawValue, - "sentAtMs": nowMs(), + "sentAtMs": self.nowMs(), ] if let promptId = nonEmpty(params.promptId) { payload["promptId"] = promptId @@ -88,7 +88,7 @@ enum WatchMessagingPayloadCodec { { var payload: [String: Any] = [ "type": OpenClawWatchPayloadType.execApprovalPrompt.rawValue, - "approval": encodeExecApprovalItem(message.approval), + "approval": self.encodeExecApprovalItem(message.approval), ] if let sentAtMs = message.sentAtMs { payload["sentAtMs"] = sentAtMs @@ -140,7 +140,7 @@ enum WatchMessagingPayloadCodec { { var payload: [String: Any] = [ "type": OpenClawWatchPayloadType.execApprovalSnapshot.rawValue, - "approvals": message.approvals.map(encodeExecApprovalItem), + "approvals": message.approvals.map(self.encodeExecApprovalItem), ] if let sentAtMs = message.sentAtMs { payload["sentAtMs"] = sentAtMs @@ -161,11 +161,11 @@ enum WatchMessagingPayloadCodec { guard let actionId = nonEmpty(payload["actionId"] as? String) else { return nil } - let promptId = nonEmpty(payload["promptId"] as? String) ?? "unknown" - let replyId = nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString - let actionLabel = nonEmpty(payload["actionLabel"] as? String) - let sessionKey = nonEmpty(payload["sessionKey"] as? String) - let note = nonEmpty(payload["note"] as? String) + let promptId = self.nonEmpty(payload["promptId"] as? String) ?? "unknown" + let replyId = self.nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString + let actionLabel = self.nonEmpty(payload["actionLabel"] as? String) + let sessionKey = self.nonEmpty(payload["sessionKey"] as? String) + let note = self.nonEmpty(payload["note"] as? String) let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue return WatchQuickReplyEvent( @@ -192,7 +192,7 @@ enum WatchMessagingPayloadCodec { else { return nil } - let replyId = nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString + let replyId = self.nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue return WatchExecApprovalResolveEvent( replyId: replyId, @@ -209,7 +209,7 @@ enum WatchMessagingPayloadCodec { guard (payload["type"] as? String) == OpenClawWatchPayloadType.execApprovalSnapshotRequest.rawValue else { return nil } - let requestId = nonEmpty(payload["requestId"] as? String) ?? UUID().uuidString + let requestId = self.nonEmpty(payload["requestId"] as? String) ?? UUID().uuidString let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue return WatchExecApprovalSnapshotRequestEvent( requestId: requestId, diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index f1a3f7bb0d9..839a22da705 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -1,6 +1,6 @@ -import OpenClawKit import Network import Observation +import OpenClawKit import os import SwiftUI import UIKit @@ -247,8 +247,7 @@ struct SettingsTab: View { .padding(10) .background( .thinMaterial, - in: RoundedRectangle(cornerRadius: 10, style: .continuous) - ) + in: RoundedRectangle(cornerRadius: 10, style: .continuous)) } } } label: { @@ -270,15 +269,17 @@ struct SettingsTab: View { self.featureToggle( "Voice Wake", isOn: self.$voiceWakeEnabled, - help: "Enables wake-word activation to start a hands-free session.") { newValue in - self.appModel.setVoiceWakeEnabled(newValue) - } + help: "Enables wake-word activation to start a hands-free session.") + { newValue in + self.appModel.setVoiceWakeEnabled(newValue) + } self.featureToggle( "Talk Mode", isOn: self.$talkEnabled, - help: "Enables voice conversation mode with your connected OpenClaw agent.") { newValue in - self.appModel.setTalkEnabled(newValue) - } + help: "Enables voice conversation mode with your connected OpenClaw agent.") + { newValue in + self.appModel.setTalkEnabled(newValue) + } Picker("Speech Language", selection: self.$talkSpeechLocale) { ForEach(TalkSpeechLocale.supportedOptions()) { option in Text(option.label).tag(option.id) @@ -301,8 +302,7 @@ struct SettingsTab: View { "Allow Camera", isOn: self.$cameraEnabled, help: "Allows the gateway to request photos or short video clips " - + "while OpenClaw is foregrounded." - ) + + "while OpenClaw is foregrounded.") HStack(spacing: 8) { Text("Location Access") @@ -313,8 +313,7 @@ struct SettingsTab: View { message: "Controls location permissions for OpenClaw. " + "Off disables location tools, While Using enables " + "foreground location, and Always enables " - + "background location." - ) + + "background location.") } label: { Image(systemName: "info.circle") .foregroundStyle(.secondary) @@ -347,8 +346,7 @@ struct SettingsTab: View { ? ( self.appModel.talkMode.gatewayTalkApiKeyConfigured ? "Configured" - : "Not configured" - ) + : "Not configured") : "Not loaded") LabeledContent( "Default Model", @@ -365,7 +363,7 @@ struct SettingsTab: View { isOn: self.$talkButtonEnabled, help: "Shows the Talk control in the main toolbar.") TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical) - .lineLimit(2 ... 6) + .lineLimit(2...6) .textInputAutocapitalization(.sentences) HStack(spacing: 8) { Text("Default Share Instruction") @@ -376,8 +374,7 @@ struct SettingsTab: View { self.activeFeatureHelp = FeatureHelp( title: "Default Share Instruction", message: "Appends this instruction when sharing content " - + "into OpenClaw from iOS." - ) + + "into OpenClaw from iOS.") } label: { Image(systemName: "info.circle") .foregroundStyle(.secondary) @@ -441,8 +438,7 @@ struct SettingsTab: View { } message: { Text( "This will disconnect, clear saved gateway connection + credentials, " - + "and reopen the onboarding wizard." - ) + + "and reopen the onboarding wizard.") } .alert(item: self.$activeFeatureHelp) { help in Alert( @@ -635,8 +631,8 @@ struct SettingsTab: View { _ title: String, isOn: Binding, help: String, - onChange: ((Bool) -> Void)? = nil - ) -> some View { + onChange: ((Bool) -> Void)? = nil) -> some View + { HStack(spacing: 8) { Toggle(title, isOn: isOn) Button { @@ -754,8 +750,7 @@ struct SettingsTab: View { 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)" - ) + + "tls=\(self.manualGatewayTLS) token=\(hasToken) password=\(hasPassword)") guard let port = resolvedPort else { self.setupStatusText = "Failed: invalid port" return @@ -858,7 +853,7 @@ struct SettingsTab: View { } let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } - if self.manualGatewayTLS && trimmed.lowercased().hasSuffix(".ts.net") { + if self.manualGatewayTLS, trimmed.lowercased().hasSuffix(".ts.net") { return 443 } return 18789 @@ -868,7 +863,7 @@ struct SettingsTab: View { let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return false } - if Self.isTailnetHostOrIP(trimmed) && !Self.hasTailnetIPv4() { + if Self.isTailnetHostOrIP(trimmed), !Self.hasTailnetIPv4() { let msg = "Tailscale is off on this iPhone. Turn it on, then try again." self.setupStatusText = msg GatewayDiagnostics.log("preflight fail: tailnet missing host=\(trimmed)") @@ -1095,4 +1090,5 @@ struct SettingsTab: View { return lines } } + // swiftlint:enable type_body_length diff --git a/apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift b/apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift index e00e87e55d6..50cee9dd972 100644 --- a/apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift +++ b/apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift @@ -1,5 +1,5 @@ -import SwiftUI import Combine +import SwiftUI struct VoiceWakeWordsSettingsView: View { @Environment(NodeAppModel.self) private var appModel diff --git a/apps/ios/Sources/Status/StatusActivityBuilder.swift b/apps/ios/Sources/Status/StatusActivityBuilder.swift index 21a2edd2973..6ed93f96b23 100644 --- a/apps/ios/Sources/Status/StatusActivityBuilder.swift +++ b/apps/ios/Sources/Status/StatusActivityBuilder.swift @@ -6,8 +6,8 @@ enum StatusActivityBuilder { appModel: NodeAppModel, voiceWakeEnabled: Bool, cameraHUDText: String?, - cameraHUDKind: NodeAppModel.CameraHUDKind? - ) -> StatusPill.Activity? { + cameraHUDKind: NodeAppModel.CameraHUDKind?) -> StatusPill.Activity? + { // Keep the top pill consistent across tabs (camera + voice wake + pairing states). if appModel.isBackgrounded { return StatusPill.Activity( @@ -19,9 +19,9 @@ enum StatusActivityBuilder { if let gatewayProblem = appModel.lastGatewayProblem { switch gatewayProblem.kind { case .pairingRequired, - .pairingRoleUpgradeRequired, - .pairingScopeUpgradeRequired, - .pairingMetadataUpgradeRequired: + .pairingRoleUpgradeRequired, + .pairingScopeUpgradeRequired, + .pairingMetadataUpgradeRequired: return StatusPill.Activity( title: "Approval pending", systemImage: "person.crop.circle.badge.clock", @@ -93,4 +93,3 @@ enum StatusActivityBuilder { return nil } } - diff --git a/apps/ios/Sources/Status/StatusGlassCard.swift b/apps/ios/Sources/Status/StatusGlassCard.swift index 6ee9ae0e403..999634aef9b 100644 --- a/apps/ios/Sources/Status/StatusGlassCard.swift +++ b/apps/ios/Sources/Status/StatusGlassCard.swift @@ -18,8 +18,7 @@ private struct StatusGlassCardModifier: ViewModifier { RoundedRectangle(cornerRadius: 14, style: .continuous) .strokeBorder( .white.opacity(self.contrast == .increased ? 0.5 : (self.brighten ? 0.24 : 0.18)), - lineWidth: self.contrast == .increased ? 1.0 : 0.5 - ) + lineWidth: self.contrast == .increased ? 1.0 : 0.5) } .shadow(color: .black.opacity(0.25), radius: 12, y: 6) } @@ -32,8 +31,6 @@ extension View { StatusGlassCardModifier( brighten: brighten, verticalPadding: verticalPadding, - horizontalPadding: horizontalPadding - ) - ) + horizontalPadding: horizontalPadding)) } } diff --git a/apps/ios/Sources/Status/StatusPill.swift b/apps/ios/Sources/Status/StatusPill.swift index d6f94185b40..34c1a9b61fa 100644 --- a/apps/ios/Sources/Status/StatusPill.swift +++ b/apps/ios/Sources/Status/StatusPill.swift @@ -54,8 +54,7 @@ struct StatusPill: View { .scaleEffect( self.gateway == .connecting && !self.reduceMotion ? (self.pulse ? 1.15 : 0.85) - : 1.0 - ) + : 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/TalkModeGatewayConfig.swift b/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift index b8054165121..bcc075becbf 100644 --- a/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift +++ b/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift @@ -20,8 +20,8 @@ enum TalkModeGatewayConfigParser { config: [String: Any], defaultProvider: String, defaultModelIdFallback: String, - defaultSilenceTimeoutMs: Int - ) -> TalkModeGatewayConfigState { + defaultSilenceTimeoutMs: Int) -> TalkModeGatewayConfigState + { let talk = TalkConfigParsing.bridgeFoundationDictionary(config["talk"] as? [String: Any]) let selection = TalkConfigParsing.selectProviderConfig( talk, diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index ec46bf9bcde..158d8294a07 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -1,9 +1,9 @@ import AVFAudio +import Foundation +import Observation import OpenClawChatUI import OpenClawKit import OpenClawProtocol -import Foundation -import Observation import OSLog import Speech @@ -99,7 +99,7 @@ final class TalkModeManager: NSObject { private var gateway: GatewayNodeSession? private var gatewayConnected = false - private var silenceWindow: TimeInterval = TimeInterval(TalkModeManager.defaultSilenceTimeoutMs) / 1000 + private var silenceWindow: TimeInterval = .init(TalkModeManager.defaultSilenceTimeoutMs) / 1000 private var lastAudioActivity: Date? private var noiseFloorSamples: [Double] = [] private var noiseFloor: Double? @@ -488,16 +488,16 @@ 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", - ]) - } + 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", + ]) + } #endif self.stopRecognition() @@ -550,8 +550,7 @@ final class TalkModeManager: NSObject { 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))" - ) + + "threshold=\(String(format: "%.3f", threshold))") } } @@ -576,8 +575,7 @@ final class TalkModeManager: NSObject { GatewayDiagnostics.log( "talk speech: recognition started mode=\(String(describing: self.captureMode)) " - + "engineRunning=\(self.audioEngine.isRunning)" - ) + + "engineRunning=\(self.audioEngine.isRunning)") self.recognitionTask = recognizer.recognitionTask(with: request) { [weak self] result, error in guard let self else { return } if let error { @@ -722,7 +720,7 @@ final class TalkModeManager: NSObject { guard self.isListening, !self.isSpeechOutputActive else { return } let transcript = self.lastTranscript.trimmingCharacters(in: .whitespacesAndNewlines) guard !transcript.isEmpty else { return } - let lastActivity = [self.lastHeard, self.lastAudioActivity].compactMap { $0 }.max() + let lastActivity = [self.lastHeard, self.lastAudioActivity].compactMap(\.self).max() guard let lastActivity else { return } if Date().timeIntervalSince(lastActivity) < self.silenceWindow { return } await self.processTranscript(transcript, restartAfter: true) @@ -733,13 +731,13 @@ final class TalkModeManager: NSObject { guard self.isListening, !self.isSpeaking, self.isPushToTalkActive else { return } let transcript = self.lastTranscript.trimmingCharacters(in: .whitespacesAndNewlines) guard !transcript.isEmpty else { return } - let lastActivity = [self.lastHeard, self.lastAudioActivity].compactMap { $0 }.max() + let lastActivity = [self.lastHeard, self.lastAudioActivity].compactMap(\.self).max() guard let lastActivity else { return } if Date().timeIntervalSince(lastActivity) < self.silenceWindow { return } _ = await self.endPushToTalk() } - // Guardrail for PTT once so we don't stay open indefinitely. + /// Guardrail for PTT once so we don't stay open indefinitely. private func schedulePTTTimeout(seconds: TimeInterval) { guard seconds > 0 else { return } let nanos = UInt64(seconds * 1_000_000_000) @@ -1103,7 +1101,10 @@ final class TalkModeManager: NSObject { result = await self.mp3Player.play(stream: rawStream) } let duration = Date().timeIntervalSince(started) - self.logger.info("elevenlabs stream finished=\(result.finished, privacy: .public) dur=\(duration, privacy: .public)s") + self.logger + .info( + // swiftlint:disable:next line_length + "elevenlabs stream finished=\(result.finished, privacy: .public) dur=\(duration, privacy: .public)s") if !result.finished, let interruptedAt = result.interruptedAt { self.lastInterruptedAtSeconds = interruptedAt } @@ -1186,9 +1187,9 @@ final class TalkModeManager: NSObject { return !route.outputs.contains { output in switch output.portType { case .builtInSpeaker, .builtInReceiver: - return true + true default: - return false + false } } } @@ -1392,8 +1393,7 @@ final class TalkModeManager: NSObject { private func consumeIncrementalPrefetchedAudioIfAvailable( for segment: String, - context: IncrementalSpeechContext? - ) async -> IncrementalPrefetchedAudio? + context: IncrementalSpeechContext?) async -> IncrementalPrefetchedAudio? { guard let context else { self.cancelIncrementalPrefetch() @@ -1467,8 +1467,8 @@ final class TalkModeManager: NSObject { guard evt.event == "agent", let payload = evt.payload else { continue } guard let agentEvent = try? GatewayPayloadDecoding.decode( payload, - as: OpenClawAgentEventPayload.self - ) else { + as: OpenClawAgentEventPayload.self) + else { continue } guard agentEvent.runId == runId, agentEvent.stream == "assistant" else { continue } @@ -1550,8 +1550,7 @@ final class TalkModeManager: NSObject { private func makeIncrementalTTSRequest( text: String, context: IncrementalSpeechContext, - outputFormat: String? - ) -> ElevenLabsTTSRequest + outputFormat: String?) -> ElevenLabsTTSRequest { ElevenLabsTTSRequest( text: text, @@ -1579,8 +1578,7 @@ final class TalkModeManager: NSObject { private static func monitorStreamFailures( _ stream: AsyncThrowingStream, - failureBox: StreamFailureBox - ) -> AsyncThrowingStream + failureBox: StreamFailureBox) -> AsyncThrowingStream { AsyncThrowingStream { continuation in let task = Task { @@ -1622,8 +1620,7 @@ final class TalkModeManager: NSObject { private func speakIncrementalSegment( _ text: String, context preferredContext: IncrementalSpeechContext? = nil, - prefetchedAudio: IncrementalPrefetchedAudio? = nil - ) async + prefetchedAudio: IncrementalPrefetchedAudio? = nil) async { let context: IncrementalSpeechContext if let preferredContext { @@ -1651,11 +1648,10 @@ final class TalkModeManager: NSObject { text: text, context: context, outputFormat: context.outputFormat) - let rawStream: AsyncThrowingStream - if let prefetchedAudio, !prefetchedAudio.chunks.isEmpty { - rawStream = Self.makeBufferedAudioStream(chunks: prefetchedAudio.chunks) + let rawStream: AsyncThrowingStream = if let prefetchedAudio, !prefetchedAudio.chunks.isEmpty { + Self.makeBufferedAudioStream(chunks: prefetchedAudio.chunks) } else { - rawStream = client.streamSynthesize(voiceId: voiceId, request: request) + client.streamSynthesize(voiceId: voiceId, request: request) } let playbackFormat = prefetchedAudio?.outputFormat ?? context.outputFormat let sampleRate = TalkTTSValidation.pcmSampleRate(from: playbackFormat) @@ -1689,7 +1685,6 @@ final class TalkModeManager: NSObject { self.lastInterruptedAtSeconds = interruptedAt } } - } private struct IncrementalSpeechBuffer { @@ -1818,7 +1813,7 @@ private struct IncrementalSpeechBuffer { } private static func isSoftBoundary(_ ch: Character, bufferedChars: Int) -> Bool { - bufferedChars >= Self.softBoundaryMinChars && ch.isWhitespace + bufferedChars >= self.softBoundaryMinChars && ch.isWhitespace } } @@ -1987,8 +1982,7 @@ extension TalkModeManager { let res = try await gateway.request( method: "talk.config", paramsJSON: "{\"includeSecrets\":true}", - timeoutSeconds: 8 - ) + 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 parsed = TalkModeGatewayConfigParser.parse( @@ -2060,7 +2054,7 @@ extension TalkModeManager { .allowBluetoothHFP, .defaultToSpeaker, ]) - try? session.setPreferredSampleRate(48_000) + try? session.setPreferredSampleRate(48000) try? session.setPreferredIOBufferDuration(0.02) try session.setActive(true, options: []) } @@ -2101,19 +2095,19 @@ private final class AudioTapDiagnostics: @unchecked Sendable { var shouldLog = false var shouldEmitLevel = false var count = 0 - lock.lock() - bufferCount += 1 - count = bufferCount + self.lock.lock() + self.bufferCount += 1 + count = self.bufferCount let now = Date() - if now.timeIntervalSince(lastLoggedAt) >= 1.0 { - lastLoggedAt = now + if now.timeIntervalSince(self.lastLoggedAt) >= 1.0 { + self.lastLoggedAt = now shouldLog = true } - if now.timeIntervalSince(lastLevelEmitAt) >= 0.12 { - lastLevelEmitAt = now + if now.timeIntervalSince(self.lastLevelEmitAt) >= 0.12 { + self.lastLevelEmitAt = now shouldEmitLevel = true } - lock.unlock() + self.lock.unlock() let rate = buffer.format.sampleRate let ch = buffer.format.channelCount @@ -2133,12 +2127,12 @@ private final class AudioTapDiagnostics: @unchecked Sendable { } let resolvedRms = rms ?? 0 - lock.lock() - lastRms = resolvedRms - if resolvedRms > maxRmsWindow { maxRmsWindow = resolvedRms } - let maxRms = maxRmsWindow - if shouldLog { maxRmsWindow = 0 } - lock.unlock() + self.lock.lock() + self.lastRms = resolvedRms + if resolvedRms > self.maxRmsWindow { self.maxRmsWindow = resolvedRms } + let maxRms = self.maxRmsWindow + if shouldLog { self.maxRmsWindow = 0 } + self.lock.unlock() if shouldEmitLevel, let onLevel { onLevel(resolvedRms) @@ -2146,9 +2140,8 @@ 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))" - ) + "\(self.label) mic: buffers=\(count) frames=\(frames) rate=\(Int(rate))Hz ch=\(ch) " + + "rms=\(String(format: "%.4f", resolvedRms)) max=\(String(format: "%.4f", maxRms))") } } diff --git a/apps/ios/Sources/Voice/TalkSpeechLocale.swift b/apps/ios/Sources/Voice/TalkSpeechLocale.swift index 07cdf045cb3..80fc1ae8ac3 100644 --- a/apps/ios/Sources/Voice/TalkSpeechLocale.swift +++ b/apps/ios/Sources/Voice/TalkSpeechLocale.swift @@ -13,8 +13,8 @@ enum TalkSpeechLocale { } static func supportedOptions( - supportedLocales: Set = SFSpeechRecognizer.supportedLocales() - ) -> [Option] { + supportedLocales: Set = SFSpeechRecognizer.supportedLocales()) -> [Option] + { var seen = Set() let dynamic: [Option] = supportedLocales .compactMap { locale in @@ -33,8 +33,8 @@ enum TalkSpeechLocale { gatewaySelection: String?, deviceLocaleID: String = Locale.autoupdatingCurrent.identifier, fallbackLocaleID: String = Self.fallbackLocaleID, - supportedLocaleIDs: Set - ) -> String? { + supportedLocaleIDs: Set) -> String? + { TalkConfigParsing.resolvedSpeechRecognitionLocaleID( preferredLocaleIDs: [ TalkConfigParsing.normalizedExplicitSpeechLocaleID(localSelection), @@ -48,8 +48,10 @@ enum TalkSpeechLocale { static func makeRecognizer( localSelection: String?, gatewaySelection: String?, - supportedLocales: Set = SFSpeechRecognizer.supportedLocales() - ) -> (recognizer: SFSpeechRecognizer?, localeID: String?) { + supportedLocales: Set = SFSpeechRecognizer.supportedLocales()) -> ( + recognizer: SFSpeechRecognizer?, + localeID: String?) + { let supportedIDs = Set(supportedLocales.map(\.identifier)) guard let localeID = self.resolvedLocaleID( localSelection: localSelection, diff --git a/apps/ios/SwiftSources.input.xcfilelist b/apps/ios/SwiftSources.input.xcfilelist index ad55607e9a4..b9808a27743 100644 --- a/apps/ios/SwiftSources.input.xcfilelist +++ b/apps/ios/SwiftSources.input.xcfilelist @@ -1,34 +1,89 @@ -Sources/Gateway/GatewayConnectionController.swift -Sources/Gateway/GatewayDiscoveryDebugLogView.swift -Sources/Gateway/GatewayDiscoveryModel.swift -Sources/Gateway/GatewaySettingsStore.swift -Sources/Gateway/KeychainStore.swift +Sources/Calendar/CalendarService.swift Sources/Camera/CameraController.swift +Sources/Capabilities/NodeCapabilityRouter.swift +Sources/Chat/ChatSheet.swift +Sources/Chat/IOSGatewayChatTransport.swift +Sources/Contacts/ContactsService.swift Sources/Device/DeviceInfoHelper.swift Sources/Device/DeviceStatusService.swift Sources/Device/NetworkStatusService.swift -Sources/Chat/ChatSheet.swift -Sources/Chat/IOSGatewayChatTransport.swift -Sources/OpenClawApp.swift +Sources/Device/NodeDisplayName.swift +Sources/EventKit/EventKitAuthorization.swift +Sources/Gateway/DeepLinkAgentPromptAlert.swift +Sources/Gateway/ExecApprovalPromptDialog.swift +Sources/Gateway/GatewayConnectConfig.swift +Sources/Gateway/GatewayConnectionController.swift +Sources/Gateway/GatewayConnectionIssue.swift +Sources/Gateway/GatewayDiscoveryDebugLogView.swift +Sources/Gateway/GatewayDiscoveryModel.swift +Sources/Gateway/GatewayHealthMonitor.swift +Sources/Gateway/GatewayProblemView.swift +Sources/Gateway/GatewayQuickSetupSheet.swift +Sources/Gateway/GatewayServiceResolver.swift +Sources/Gateway/GatewaySettingsStore.swift +Sources/Gateway/GatewaySetupCode.swift +Sources/Gateway/GatewayTrustPromptAlert.swift +Sources/Gateway/KeychainStore.swift +Sources/Gateway/TCPProbe.swift +Sources/HomeToolbar.swift +Sources/LiveActivity/LiveActivityManager.swift +Sources/LiveActivity/OpenClawActivityAttributes.swift Sources/Location/LocationService.swift -Sources/Model/NodeAppModel.swift +Sources/Location/SignificantLocationMonitor.swift +Sources/Media/PhotoLibraryService.swift Sources/Model/NodeAppModel+Canvas.swift +Sources/Model/NodeAppModel+WatchNotifyNormalization.swift +Sources/Model/NodeAppModel.swift Sources/Model/WatchReplyCoordinator.swift +Sources/Motion/MotionService.swift +Sources/Onboarding/GatewayOnboardingView.swift +Sources/Onboarding/OnboardingStateStore.swift +Sources/Onboarding/OnboardingWizardView.swift +Sources/Onboarding/QRScannerView.swift +Sources/OpenClawApp.swift +Sources/Push/ExecApprovalNotificationBridge.swift +Sources/Push/PushBuildConfig.swift +Sources/Push/PushRegistrationManager.swift +Sources/Push/PushRelayClient.swift +Sources/Push/PushRelayKeychainStore.swift +Sources/Reminders/RemindersService.swift Sources/RootCanvas.swift Sources/RootTabs.swift +Sources/RootView.swift Sources/Screen/ScreenController.swift Sources/Screen/ScreenRecordService.swift Sources/Screen/ScreenTab.swift Sources/Screen/ScreenWebView.swift +Sources/Services/NodeServiceProtocols.swift +Sources/Services/NotificationService.swift +Sources/Services/WatchConnectivityTransport.swift +Sources/Services/WatchMessagingPayloadCodec.swift +Sources/Services/WatchMessagingService.swift Sources/SessionKey.swift Sources/Settings/SettingsNetworkingHelpers.swift Sources/Settings/SettingsTab.swift Sources/Settings/VoiceWakeWordsSettingsView.swift +Sources/Status/GatewayActionsDialog.swift +Sources/Status/GatewayStatusBuilder.swift +Sources/Status/StatusActivityBuilder.swift +Sources/Status/StatusGlassCard.swift Sources/Status/StatusPill.swift Sources/Status/VoiceWakeToast.swift +Sources/Voice/TalkDefaults.swift +Sources/Voice/TalkModeGatewayConfig.swift +Sources/Voice/TalkModeManager.swift +Sources/Voice/TalkOrbOverlay.swift +Sources/Voice/TalkSpeechLocale.swift Sources/Voice/VoiceTab.swift Sources/Voice/VoiceWakeManager.swift Sources/Voice/VoiceWakePreferences.swift +ShareExtension/ShareViewController.swift +ActivityWidget/OpenClawActivityWidgetBundle.swift +ActivityWidget/OpenClawLiveActivity.swift +WatchExtension/Sources/OpenClawWatchApp.swift +WatchExtension/Sources/WatchConnectivityReceiver.swift +WatchExtension/Sources/WatchInboxStore.swift +WatchExtension/Sources/WatchInboxView.swift ../shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift ../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift ../shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift @@ -61,9 +116,3 @@ Sources/Voice/VoiceWakePreferences.swift ../shared/OpenClawKit/Sources/OpenClawKit/SystemCommands.swift ../shared/OpenClawKit/Sources/OpenClawKit/TalkDirective.swift ../../Swabble/Sources/SwabbleKit/WakeWordGate.swift -Sources/Voice/TalkModeManager.swift -Sources/Voice/TalkOrbOverlay.swift -Sources/LiveActivity/OpenClawActivityAttributes.swift -Sources/LiveActivity/LiveActivityManager.swift -ActivityWidget/OpenClawActivityWidgetBundle.swift -ActivityWidget/OpenClawLiveActivity.swift diff --git a/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift b/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift index dae03c76b69..92aa15fb8ab 100644 --- a/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift +++ b/apps/ios/WatchExtension/Sources/WatchConnectivityReceiver.swift @@ -1,7 +1,7 @@ import Foundation import WatchConnectivity -struct WatchReplyDraft: Sendable { +struct WatchReplyDraft { var replyId: String var promptId: String var actionId: String @@ -11,7 +11,7 @@ struct WatchReplyDraft: Sendable { var sentAtMs: Int } -struct WatchReplySendResult: Sendable, Equatable { +struct WatchReplySendResult: Equatable { var deliveredImmediately: Bool var queuedForDelivery: Bool var transport: String @@ -61,14 +61,18 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { let payload = Self.encodeSnapshotRequestPayload(request) if session.isReachable { do { - try await withCheckedThrowingContinuation(isolation: nil) { - (continuation: CheckedContinuation) in + // swiftlint:disable multiline_arguments + try await withCheckedThrowingContinuation(isolation: nil) { (continuation: CheckedContinuation< + Void, + Error, + >) in session.sendMessage(payload, replyHandler: { _ in continuation.resume(returning: ()) }, errorHandler: { error in continuation.resume(throwing: error) }) } + // swiftlint:enable multiline_arguments return } catch { // Fall through to queued delivery. @@ -136,14 +140,18 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { private func sendPayload(_ payload: [String: Any], session: WCSession) async -> WatchReplySendResult { if session.isReachable { do { - try await withCheckedThrowingContinuation(isolation: nil) { - (continuation: CheckedContinuation) in + // swiftlint:disable multiline_arguments + try await withCheckedThrowingContinuation(isolation: nil) { (continuation: CheckedContinuation< + Void, + Error, + >) in session.sendMessage(payload, replyHandler: { _ in continuation.resume(returning: ()) }, errorHandler: { error in continuation.resume(throwing: error) }) } + // swiftlint:enable multiline_arguments return WatchReplySendResult( deliveredImmediately: true, queuedForDelivery: false, @@ -254,7 +262,7 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { } private static func parseExecApprovalItem(_ value: Any?) -> WatchExecApprovalItem? { - guard let payload = value.flatMap(Self.normalizeObject) else { + guard let payload = value.flatMap(normalizeObject) else { return nil } let id = (payload["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" @@ -291,7 +299,7 @@ final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { { guard let type = payload["type"] as? String, type == WatchPayloadType.execApprovalPrompt.rawValue, - let approval = Self.parseExecApprovalItem(payload["approval"]) + let approval = parseExecApprovalItem(payload["approval"]) else { return nil } diff --git a/apps/ios/WatchExtension/Sources/WatchInboxStore.swift b/apps/ios/WatchExtension/Sources/WatchInboxStore.swift index e6280a835f3..f592c881244 100644 --- a/apps/ios/WatchExtension/Sources/WatchInboxStore.swift +++ b/apps/ios/WatchExtension/Sources/WatchInboxStore.swift @@ -3,7 +3,7 @@ import Observation import UserNotifications import WatchKit -enum WatchPayloadType: String, Codable, Sendable, Equatable { +enum WatchPayloadType: String, Codable, Equatable { case notify = "watch.notify" case reply = "watch.reply" case execApprovalPrompt = "watch.execApproval.prompt" @@ -14,18 +14,18 @@ enum WatchPayloadType: String, Codable, Sendable, Equatable { case execApprovalSnapshotRequest = "watch.execApproval.snapshotRequest" } -enum WatchRiskLevel: String, Codable, Sendable, Equatable { +enum WatchRiskLevel: String, Codable, Equatable { case low case medium case high } -enum WatchExecApprovalDecision: String, Codable, Sendable, Equatable { +enum WatchExecApprovalDecision: String, Codable, Equatable { case allowOnce = "allow-once" case deny } -enum WatchExecApprovalCloseReason: String, Codable, Sendable, Equatable { +enum WatchExecApprovalCloseReason: String, Codable, Equatable { case expired case notFound = "not-found" case unavailable @@ -33,7 +33,7 @@ enum WatchExecApprovalCloseReason: String, Codable, Sendable, Equatable { case resolved } -struct WatchExecApprovalItem: Codable, Sendable, Equatable, Identifiable { +struct WatchExecApprovalItem: Codable, Equatable, Identifiable { var id: String var commandText: String var commandPreview: String? @@ -45,51 +45,51 @@ struct WatchExecApprovalItem: Codable, Sendable, Equatable, Identifiable { var risk: WatchRiskLevel? } -struct WatchExecApprovalPromptMessage: Codable, Sendable, Equatable { +struct WatchExecApprovalPromptMessage: Codable, Equatable { var approval: WatchExecApprovalItem var sentAtMs: Int? var deliveryId: String? var resetResolvingState: Bool? } -struct WatchExecApprovalResolvedMessage: Codable, Sendable, Equatable { +struct WatchExecApprovalResolvedMessage: Codable, Equatable { var approvalId: String var decision: WatchExecApprovalDecision? var resolvedAtMs: Int? var source: String? } -struct WatchExecApprovalExpiredMessage: Codable, Sendable, Equatable { +struct WatchExecApprovalExpiredMessage: Codable, Equatable { var approvalId: String var reason: WatchExecApprovalCloseReason var expiredAtMs: Int? } -struct WatchExecApprovalSnapshotMessage: Codable, Sendable, Equatable { +struct WatchExecApprovalSnapshotMessage: Codable, Equatable { var approvals: [WatchExecApprovalItem] var sentAtMs: Int? var snapshotId: String? } -struct WatchExecApprovalSnapshotRequestMessage: Codable, Sendable, Equatable { +struct WatchExecApprovalSnapshotRequestMessage: Codable, Equatable { var requestId: String var sentAtMs: Int? } -struct WatchExecApprovalResolveMessage: Codable, Sendable, Equatable { +struct WatchExecApprovalResolveMessage: Codable, Equatable { var approvalId: String var decision: WatchExecApprovalDecision var replyId: String var sentAtMs: Int? } -struct WatchPromptAction: Codable, Sendable, Equatable, Identifiable { +struct WatchPromptAction: Codable, Equatable, Identifiable { var id: String var label: String var style: String? } -struct WatchNotifyMessage: Sendable { +struct WatchNotifyMessage { var id: String? var title: String var body: String @@ -103,7 +103,7 @@ struct WatchNotifyMessage: Sendable { var actions: [WatchPromptAction] } -struct WatchExecApprovalRecord: Codable, Sendable, Equatable, Identifiable { +struct WatchExecApprovalRecord: Codable, Equatable, Identifiable { var approval: WatchExecApprovalItem var transport: String var updatedAt: Date @@ -112,7 +112,9 @@ struct WatchExecApprovalRecord: Codable, Sendable, Equatable, Identifiable { var statusText: String? var statusAt: Date? - var id: String { self.approval.id } + var id: String { + self.approval.id + } } @MainActor @Observable final class WatchInboxStore { @@ -333,14 +335,13 @@ struct WatchExecApprovalRecord: Codable, Sendable, Equatable, Identifiable { func consume(execApprovalResolved message: WatchExecApprovalResolvedMessage) { self.removeExecApproval(id: message.approvalId) - let statusText: String - switch message.decision { + let statusText = switch message.decision { case .allowOnce: - statusText = "Allowed once" + "Allowed once" case .deny: - statusText = "Denied" + "Denied" case nil: - statusText = "Approval resolved" + "Approval resolved" } self.lastExecApprovalOutcomeText = statusText self.lastExecApprovalOutcomeAt = Date() @@ -349,18 +350,17 @@ struct WatchExecApprovalRecord: Codable, Sendable, Equatable, Identifiable { func consume(execApprovalExpired message: WatchExecApprovalExpiredMessage) { self.removeExecApproval(id: message.approvalId) - let statusText: String - switch message.reason { + let statusText = switch message.reason { case .expired: - statusText = "Approval expired" + "Approval expired" case .notFound: - statusText = "Approval no longer available" + "Approval no longer available" case .resolved: - statusText = "Approval resolved elsewhere" + "Approval resolved elsewhere" case .replaced: - statusText = "Approval replaced" + "Approval replaced" case .unavailable: - statusText = "Approval unavailable" + "Approval unavailable" } self.lastExecApprovalOutcomeText = statusText self.lastExecApprovalOutcomeAt = Date() @@ -482,7 +482,7 @@ struct WatchExecApprovalRecord: Codable, Sendable, Equatable, Identifiable { private func restorePersistedState() { guard let data = self.defaults.data(forKey: Self.persistedStateKey), - let state = try? JSONDecoder().decode(PersistedState.self, from: data) + let state = try? JSONDecoder().decode(PersistedState.self, from: data) else { return } @@ -555,11 +555,11 @@ struct WatchExecApprovalRecord: Codable, Sendable, Equatable, Identifiable { private func mapHapticRisk(_ risk: String?) -> WKHapticType { switch risk?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { case "high": - return .failure + .failure case "medium": - return .notification + .notification default: - return .click + .click } } diff --git a/apps/ios/WatchExtension/Sources/WatchInboxView.swift b/apps/ios/WatchExtension/Sources/WatchInboxView.swift index 251861504b5..305557adaaf 100644 --- a/apps/ios/WatchExtension/Sources/WatchInboxView.swift +++ b/apps/ios/WatchExtension/Sources/WatchInboxView.swift @@ -219,13 +219,13 @@ private struct WatchExecApprovalDetailView: View { private func riskText(_ risk: WatchRiskLevel?) -> String? { switch risk { case .high: - return "High" + "High" case .medium: - return "Medium" + "Medium" case .low: - return "Low" + "Low" case nil: - return nil + nil } } @@ -246,11 +246,11 @@ private struct WatchGenericInboxView: View { private func role(for action: WatchPromptAction) -> ButtonRole? { switch action.style?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { case "destructive": - return .destructive + .destructive case "cancel": - return .cancel + .cancel default: - return nil + nil } } diff --git a/apps/ios/fastlane/metadata/en-US/release_notes.txt b/apps/ios/fastlane/metadata/en-US/release_notes.txt index be0f56c798d..e59b8748c7a 100644 --- a/apps/ios/fastlane/metadata/en-US/release_notes.txt +++ b/apps/ios/fastlane/metadata/en-US/release_notes.txt @@ -1 +1,3 @@ Maintenance update for the current OpenClaw development release. + +- Refreshed build hygiene for the iOS app, Share extension, Activity widget, Watch app, and curated shared Swift sources; relay registration now uses StoreKit app transaction JWS data instead of deprecated receipt APIs. diff --git a/apps/ios/project.yml b/apps/ios/project.yml index ea6ad0d09c5..036fe0868cf 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -71,6 +71,7 @@ targets: exit 1 fi swiftformat --lint --config "$SRCROOT/../../.swiftformat" \ + --unexclude "$SRCROOT/Sources,$SRCROOT/ShareExtension,$SRCROOT/ActivityWidget,$SRCROOT/WatchExtension,$SRCROOT/../shared/OpenClawKit,$SRCROOT/../../Swabble" \ --filelist "$SRCROOT/SwiftSources.input.xcfilelist" - name: SwiftLint basedOnDependencyAnalysis: false @@ -344,6 +345,7 @@ targets: DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)" PRODUCT_BUNDLE_IDENTIFIER: ai.openclaw.ios.logic-tests ENABLE_APP_INTENTS_METADATA_GENERATION: NO + SWIFT_EMIT_CONST_VALUE_PROTOCOLS: "" SWIFT_VERSION: "6.0" SWIFT_STRICT_CONCURRENCY: complete info: diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift index 9c2dc0acd56..a85f922defe 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift @@ -281,9 +281,9 @@ struct OpenClawChatComposer: View { onPasteImageAttachment: { data, fileName, mimeType in self.viewModel.addImageAttachment(data: data, fileName: fileName, mimeType: mimeType) }) - .frame(minHeight: self.textMinHeight, idealHeight: self.textMinHeight, maxHeight: self.textMaxHeight) - .padding(.horizontal, 4) - .padding(.vertical, 3) + .frame(minHeight: self.textMinHeight, idealHeight: self.textMinHeight, maxHeight: self.textMaxHeight) + .padding(.horizontal, 4) + .padding(.vertical, 3) #else TextEditor(text: self.$viewModel.input) .font(.system(size: 15)) @@ -441,7 +441,9 @@ private struct ChatComposerTextView: NSViewRepresentable { var onSend: () -> Void var onPasteImageAttachment: (_ data: Data, _ fileName: String, _ mimeType: String) -> Void - func makeCoordinator() -> Coordinator { Coordinator(self) } + func makeCoordinator() -> Coordinator { + Coordinator(self) + } func makeNSView(context: Context) -> NSScrollView { let textView = ChatComposerTextViewFactory.makeConfiguredTextView() @@ -495,7 +497,9 @@ private struct ChatComposerTextView: NSViewRepresentable { var parent: ChatComposerTextView var isProgrammaticUpdate = false - init(_ parent: ChatComposerTextView) { self.parent = parent } + init(_ parent: ChatComposerTextView) { + self.parent = parent + } func textDidChange(_ notification: Notification) { guard !self.isProgrammaticUpdate else { return } @@ -507,7 +511,7 @@ private struct ChatComposerTextView: NSViewRepresentable { } enum ChatComposerTextViewFactory { - // Internal for @testable import coverage of composer text view defaults. + /// Internal for @testable import coverage of composer text view defaults. @MainActor static func makeConfiguredTextView() -> NSTextView { let textView = ChatComposerNSTextView() @@ -751,7 +755,10 @@ enum ChatComposerPasteSupport { (NSPasteboard.PasteboardType("public.heif"), "heif", "image/heif"), ] - private static func matches(_ preferredType: NSPasteboard.PasteboardType?, candidate: NSPasteboard.PasteboardType) -> Bool { + private static func matches( + _ preferredType: NSPasteboard.PasteboardType?, + candidate: NSPasteboard.PasteboardType) -> Bool + { guard let preferredType else { return true } return preferredType == candidate } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift index 29466a8fcf9..bd008fd2c2f 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownPreprocessor.swift @@ -1,9 +1,9 @@ import Foundation enum ChatMarkdownPreprocessor { - // Keep in sync with `src/auto-reply/reply/strip-inbound-meta.ts` - // (`INBOUND_META_SENTINELS`), and extend parser expectations in - // `ChatMarkdownPreprocessorTests` when sentinels change. + /// Keep in sync with `src/auto-reply/reply/strip-inbound-meta.ts` + /// (`INBOUND_META_SENTINELS`), and extend parser expectations in + /// `ChatMarkdownPreprocessorTests` when sentinels change. private static let inboundContextHeaders = [ "Conversation info (untrusted metadata):", "Sender (untrusted metadata):", @@ -152,11 +152,13 @@ enum ChatMarkdownPreprocessor { for index in lines.indices { let currentLine = lines[index] - if !inMetaBlock && self.shouldStripTrailingUntrustedContext(lines: lines, index: index) { + if !inMetaBlock, self.shouldStripTrailingUntrustedContext(lines: lines, index: index) { break } - if !inMetaBlock && self.inboundContextHeaders.contains(currentLine.trimmingCharacters(in: .whitespacesAndNewlines)) { + if !inMetaBlock, + self.inboundContextHeaders.contains(currentLine.trimmingCharacters(in: .whitespacesAndNewlines)) + { let nextLine = index + 1 < lines.count ? lines[index + 1] : nil if nextLine?.trimmingCharacters(in: .whitespacesAndNewlines) != "```json" { outputLines.append(currentLine) @@ -168,7 +170,7 @@ enum ChatMarkdownPreprocessor { } if inMetaBlock { - if !inFencedJson && currentLine.trimmingCharacters(in: .whitespacesAndNewlines) == "```json" { + if !inFencedJson, currentLine.trimmingCharacters(in: .whitespacesAndNewlines) == "```json" { inFencedJson = true continue } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift index e68c8591bcf..0ebf3cdce60 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift @@ -70,7 +70,7 @@ private struct InlineImageList: View { let images: [ChatMarkdownPreprocessor.InlineImage] var body: some View { - ForEach(images, id: \.id) { item in + ForEach(self.images, id: \.id) { item in if let img = item.image { OpenClawPlatformImageFactory.image(img) .resizable() diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift index bc93eefc87e..d24f92bd42c 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift @@ -1,5 +1,5 @@ -import OpenClawKit import Foundation +import OpenClawKit import SwiftUI private enum ChatUIConstants { @@ -70,7 +70,12 @@ private struct ChatBubbleShape: InsettableShape { to: baseBottom, control1: CGPoint(x: bubbleMaxX + self.tailWidth * 0.95, y: midY + baseH * 0.15), control2: CGPoint(x: bubbleMaxX + self.tailWidth * 0.2, y: baseBottomY - baseH * 0.05)) - self.addBottomEdge(path: &path, bubbleMinX: bubbleMinX, bubbleMaxX: bubbleMaxX, bubbleMaxY: bubbleMaxY, radius: r) + self.addBottomEdge( + path: &path, + bubbleMinX: bubbleMinX, + bubbleMaxX: bubbleMaxX, + bubbleMaxY: bubbleMaxY, + radius: r) path.addLine(to: CGPoint(x: bubbleMinX, y: bubbleMinY + r)) path.addQuadCurve( to: CGPoint(x: bubbleMinX + r, y: bubbleMinY), @@ -102,7 +107,12 @@ private struct ChatBubbleShape: InsettableShape { to: CGPoint(x: bubbleMaxX, y: bubbleMinY + r), control: CGPoint(x: bubbleMaxX, y: bubbleMinY)) path.addLine(to: CGPoint(x: bubbleMaxX, y: bubbleMaxY - r)) - self.addBottomEdge(path: &path, bubbleMinX: bubbleMinX, bubbleMaxX: bubbleMaxX, bubbleMaxY: bubbleMaxY, radius: r) + self.addBottomEdge( + path: &path, + bubbleMinX: bubbleMinX, + bubbleMaxX: bubbleMaxX, + bubbleMaxY: bubbleMaxY, + radius: r) path.addLine(to: baseBottom) path.addCurve( to: tip, @@ -158,7 +168,9 @@ struct ChatMessageBubble: View { .padding(.horizontal, 2) } - private var isUser: Bool { self.message.role.lowercased() == "user" } + private var isUser: Bool { + self.message.role.lowercased() == "user" + } } @MainActor @@ -498,8 +510,8 @@ extension ChatTypingIndicatorBubble: @MainActor Equatable { } } -private extension View { - func assistantBubbleContainerStyle() -> some View { +extension View { + fileprivate func assistantBubbleContainerStyle() -> some View { self .background( RoundedRectangle(cornerRadius: 16, style: .continuous) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift index c58f2d702e4..1b2155c8e5f 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatModels.swift @@ -1,5 +1,5 @@ -import OpenClawKit import Foundation +import OpenClawKit // NOTE: keep this file lightweight; decode must be resilient to varying transcript formats. @@ -270,7 +270,10 @@ public struct OpenClawChatEventPayload: Codable, Sendable { } public struct OpenClawAgentEventPayload: Codable, Sendable, Identifiable { - public var id: String { "\(self.runId)-\(self.seq ?? -1)" } + public var id: String { + "\(self.runId)-\(self.seq ?? -1)" + } + public let runId: String public let seq: Int? public let stream: String @@ -279,7 +282,10 @@ public struct OpenClawAgentEventPayload: Codable, Sendable, Identifiable { } public struct OpenClawChatPendingToolCall: Identifiable, Hashable, Sendable { - public var id: String { self.toolCallId } + public var id: String { + self.toolCallId + } + public let toolCallId: String public let name: String public let args: AnyCodable? diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatPayloadDecoding.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatPayloadDecoding.swift index 02636696d21..d3249a7a4a7 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatPayloadDecoding.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatPayloadDecoding.swift @@ -1,5 +1,5 @@ -import OpenClawKit import Foundation +import OpenClawKit enum ChatPayloadDecoding { static func decode(_ payload: AnyCodable, as _: T.Type = T.self) throws -> T { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift index c5a74c9a9aa..381829f428f 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift @@ -1,7 +1,9 @@ import Foundation public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable { - public var id: String { self.selectionID } + public var id: String { + self.selectionID + } public let modelID: String public let name: String @@ -44,7 +46,9 @@ public struct OpenClawChatSessionsDefaults: Codable, Sendable { } public struct OpenClawChatSessionEntry: Codable, Identifiable, Sendable, Hashable { - public var id: String { self.key } + public var id: String { + self.key + } public let key: String public let kind: String? diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTheme.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTheme.swift index c06ed4f46af..0675ffc155f 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTheme.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatTheme.swift @@ -128,7 +128,9 @@ enum OpenClawChatTheme { #endif } - static var userText: Color { .white } + static var userText: Color { + .white + } static var assistantText: Color { #if os(macOS) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift index c760fad30d5..4faeac05870 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift @@ -86,8 +86,6 @@ public struct OpenClawChatView: View { .sheet(isPresented: self.$showSessions) { if self.showsSessionSwitcher { ChatSessionsSheet(viewModel: self.viewModel) - } else { - EmptyView() } } } @@ -99,11 +97,11 @@ public struct OpenClawChatView: View { self.messageListRows Color.clear - #if os(macOS) + #if os(macOS) .frame(height: Layout.messageListPaddingBottom) - #else + #else .frame(height: Layout.messageListPaddingBottom + 1) - #endif + #endif .id(self.scrollerBottomID) } // Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches. @@ -115,11 +113,11 @@ public struct OpenClawChatView: View { .scrollDismissesKeyboard(.interactively) #endif // Keep the scroll pinned to the bottom for new messages. - .scrollPosition(id: self.$scrollPosition, anchor: .bottom) - .onChange(of: self.scrollPosition) { _, position in - guard let position else { return } - self.isPinnedToBottom = position == self.scrollerBottomID - } + .scrollPosition(id: self.$scrollPosition, anchor: .bottom) + .onChange(of: self.scrollPosition) { _, position in + guard let position else { return } + self.isPinnedToBottom = position == self.scrollerBottomID + } if self.viewModel.isLoading { ProgressView() @@ -158,7 +156,8 @@ public struct OpenClawChatView: View { guard self.hasPerformedInitialScroll else { return } if let lastMessage = self.viewModel.messages.last, lastMessage.role.lowercased() == "user", - lastMessage.id != self.lastUserMessageID { + lastMessage.id != self.lastUserMessageID + { self.lastUserMessageID = lastMessage.id self.isPinnedToBottom = true withAnimation(.snappy(duration: 0.22)) { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift index 762fda73f68..64beb1bac88 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift @@ -1,6 +1,6 @@ -import OpenClawKit import Foundation import Observation +import OpenClawKit import OSLog import UniformTypeIdentifiers @@ -14,6 +14,7 @@ private let chatUILogger = Logger(subsystem: "ai.openclaw", category: "OpenClawC @MainActor @Observable +// swiftlint:disable:next type_body_length public final class OpenClawChatViewModel { public static let defaultModelSelectionID = "__default__" @@ -659,8 +660,8 @@ public final class OpenClawChatViewModel { self.errorText = "Unable to compact the session. Please try again." let nsError = error as NSError chatUILogger.error( - "session compact failed domain=\(nsError.domain, privacy: .public) code=\(nsError.code, privacy: .public) details=\(String(describing: error), privacy: .private)" - ) + // swiftlint:disable:next line_length + "session compact failed domain=\(nsError.domain, privacy: .public) code=\(nsError.code, privacy: .public) details=\(String(describing: error), privacy: .private)") return } @@ -733,7 +734,10 @@ public final class OpenClawChatViewModel { self.latestModelSelectionRequestIDsBySession.removeValue(forKey: sessionKey) } if self.lastSuccessfulModelSelectionIDsBySession[sessionKey] == previous { - self.applySuccessfulModelSelection(previous, sessionKey: sessionKey, syncSelection: sessionKey == self.sessionKey) + self.applySuccessfulModelSelection( + previous, + sessionKey: sessionKey, + syncSelection: sessionKey == self.sessionKey) } guard sessionKey == self.sessionKey else { return } self.modelSelectionID = previous @@ -856,7 +860,8 @@ public final class OpenClawChatViewModel { syncSelection: syncSelection) } - private func resolvedSessionModelIdentity(forSelectionID selectionID: String) -> (modelID: String?, modelProvider: String?) { + private func resolvedSessionModelIdentity(forSelectionID selectionID: String) + -> (modelID: String?, modelProvider: String?) { guard let modelRef = self.modelRef(forSelectionID: selectionID) else { return (nil, nil) } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable+Helpers.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable+Helpers.swift index ee0d9c78769..b3059b9a6eb 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable+Helpers.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable+Helpers.swift @@ -1,11 +1,11 @@ import Foundation -public extension AnyCodable { - var stringValue: String? { +extension AnyCodable { + public var stringValue: String? { self.value as? String } - var boolValue: Bool? { + public var boolValue: Bool? { if let value = self.value as? Bool { return value } @@ -15,7 +15,7 @@ public extension AnyCodable { return nil } - var intValue: Int? { + public var intValue: Int? { if let value = self.value as? Int { return value } @@ -28,7 +28,7 @@ public extension AnyCodable { return nil } - var doubleValue: Double? { + public var doubleValue: Double? { if let value = self.value as? Double { return value } @@ -41,7 +41,7 @@ public extension AnyCodable { return nil } - var dictionaryValue: [String: AnyCodable]? { + public var dictionaryValue: [String: AnyCodable]? { if let value = self.value as? [String: AnyCodable] { return value } @@ -58,7 +58,7 @@ public extension AnyCodable { return nil } - var arrayValue: [AnyCodable]? { + public var arrayValue: [AnyCodable]? { if let value = self.value as? [AnyCodable] { return value } @@ -71,7 +71,7 @@ public extension AnyCodable { return nil } - var foundationValue: Any { + public var foundationValue: Any { switch self.value { case let dict as [String: AnyCodable]: dict.mapValues(\.foundationValue) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift index 02b53e3c392..72669f541ea 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/AnyCodable.swift @@ -1,4 +1,3 @@ import OpenClawProtocol public typealias AnyCodable = OpenClawProtocol.AnyCodable - diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/BonjourTypes.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/BonjourTypes.swift index 5c3c50ca482..c5160d671f4 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/BonjourTypes.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/BonjourTypes.swift @@ -20,8 +20,8 @@ public enum OpenClawBonjour { private static func resolveWideAreaDomain(_ raw: String?) -> String? { let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return nil } - let normalized = normalizeServiceDomain(trimmed) - return normalized == gatewayServiceDomain ? nil : normalized + let normalized = self.normalizeServiceDomain(trimmed) + return normalized == self.gatewayServiceDomain ? nil : normalized } public static func normalizeServiceDomain(_ raw: String?) -> String { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/CaptureRateLimits.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/CaptureRateLimits.swift index 5b95bf6bf04..b175e9e140e 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/CaptureRateLimits.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/CaptureRateLimits.swift @@ -3,9 +3,9 @@ import Foundation public enum CaptureRateLimits { public static func clampDurationMs( _ ms: Int?, - defaultMs: Int = 10_000, + defaultMs: Int = 10000, minMs: Int = 250, - maxMs: Int = 60_000) -> Int + maxMs: Int = 60000) -> Int { let value = ms ?? defaultMs return min(maxMs, max(minMs, value)) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift index 5f1440ccb1a..7d7e099e6fe 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeepLinks.swift @@ -29,7 +29,7 @@ public struct GatewayConnectDeepLink: Codable, Sendable, Equatable { /// Parse a device-pair setup code (base64url-encoded JSON: `{url, bootstrapToken?, token?, password?}`). public static func fromSetupCode(_ code: String) -> GatewayConnectDeepLink? { - guard let data = Self.decodeBase64Url(code) else { return nil } + guard let data = decodeBase64Url(code) else { return nil } guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } guard let urlString = json["url"] as? String, let parsed = URLComponents(string: urlString), diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift index 9b8e4c2673b..7e8b4aa00b4 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthPayload.swift @@ -16,8 +16,8 @@ public enum GatewayDeviceAuthPayload { { let scopeString = scopes.joined(separator: ",") let authToken = token ?? "" - let normalizedPlatform = normalizeMetadataField(platform) - let normalizedDeviceFamily = normalizeMetadataField(deviceFamily) + let normalizedPlatform = self.normalizeMetadataField(platform) + let normalizedDeviceFamily = self.normalizeMetadataField(deviceFamily) return [ "v3", deviceId, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthStore.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthStore.swift index 80ff20c3f35..5ba934490af 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthStore.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthStore.swift @@ -25,7 +25,7 @@ public enum DeviceAuthStore { public static func loadToken(deviceId: String, role: String) -> DeviceAuthEntry? { guard let store = readStore(), store.deviceId == deviceId else { return nil } - let role = normalizeRole(role) + let role = self.normalizeRole(role) return store.tokens[role] } @@ -33,10 +33,10 @@ public enum DeviceAuthStore { deviceId: String, role: String, token: String, - scopes: [String] = [] - ) -> DeviceAuthEntry { - let normalizedRole = normalizeRole(role) - var next = readStore() + scopes: [String] = []) -> DeviceAuthEntry + { + let normalizedRole = self.normalizeRole(role) + var next = self.readStore() if next?.deviceId != deviceId { next = DeviceAuthStoreFile(version: 1, deviceId: deviceId, tokens: [:]) } @@ -44,24 +44,23 @@ public enum DeviceAuthStore { token: token, role: normalizedRole, scopes: normalizeScopes(scopes), - updatedAtMs: Int(Date().timeIntervalSince1970 * 1000) - ) + updatedAtMs: Int(Date().timeIntervalSince1970 * 1000)) if next == nil { next = DeviceAuthStoreFile(version: 1, deviceId: deviceId, tokens: [:]) } next?.tokens[normalizedRole] = entry if let store = next { - writeStore(store) + self.writeStore(store) } return entry } public static func clearToken(deviceId: String, role: String) { guard var store = readStore(), store.deviceId == deviceId else { return } - let normalizedRole = normalizeRole(role) + let normalizedRole = self.normalizeRole(role) guard store.tokens[normalizedRole] != nil else { return } store.tokens.removeValue(forKey: normalizedRole) - writeStore(store) + self.writeStore(store) } private static func normalizeRole(_ role: String) -> String { @@ -78,11 +77,11 @@ public enum DeviceAuthStore { private static func fileURL() -> URL { DeviceIdentityPaths.stateDirURL() .appendingPathComponent("identity", isDirectory: true) - .appendingPathComponent(fileName, isDirectory: false) + .appendingPathComponent(self.fileName, isDirectory: false) } private static func readStore() -> DeviceAuthStoreFile? { - let url = fileURL() + let url = self.fileURL() guard let data = try? Data(contentsOf: url) else { return nil } guard let decoded = try? JSONDecoder().decode(DeviceAuthStoreFile.self, from: data) else { return nil @@ -92,7 +91,7 @@ public enum DeviceAuthStore { } private static func writeStore(_ store: DeviceAuthStoreFile) { - let url = fileURL() + let url = self.fileURL() do { try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceIdentity.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceIdentity.swift index a992bc58f29..6b8945f9df4 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceIdentity.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceIdentity.swift @@ -45,7 +45,8 @@ public enum DeviceIdentityStore { let decoded = try? JSONDecoder().decode(DeviceIdentity.self, from: data), !decoded.deviceId.isEmpty, !decoded.publicKey.isEmpty, - !decoded.privateKey.isEmpty { + !decoded.privateKey.isEmpty + { return decoded } let identity = self.generate() @@ -107,6 +108,6 @@ public enum DeviceIdentityStore { let base = DeviceIdentityPaths.stateDirURL() return base .appendingPathComponent("identity", isDirectory: true) - .appendingPathComponent(fileName, isDirectory: false) + .appendingPathComponent(self.fileName, isDirectory: false) } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift index 457aed5c573..c9e455cc49e 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol import OSLog public protocol WebSocketTasking: AnyObject { @@ -20,9 +20,13 @@ public struct WebSocketTaskBox: @unchecked Sendable { self.task = task } - public var state: URLSessionTask.State { self.task.state } + public var state: URLSessionTask.State { + self.task.state + } - public func resume() { self.task.resume() } + public func resume() { + self.task.resume() + } public func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { self.task.cancel(with: closeCode, reason: reason) @@ -81,9 +85,9 @@ public struct GatewayConnectOptions: Sendable { public var clientId: String public var clientMode: String public var clientDisplayName: String? - // When false, the connection omits the signed device identity payload and cannot use - // device-scoped auth (role/scope upgrades will require pairing). Keep this true for - // role/scoped sessions such as operator UI clients. + /// When false, the connection omits the signed device identity payload and cannot use + /// device-scoped auth (role/scope upgrades will require pairing). Keep this true for + /// role/scoped sessions such as operator UI clients. public var includeDeviceIdentity: Bool public init( @@ -113,11 +117,11 @@ public enum GatewayAuthSource: String, Sendable { case deviceToken = "device-token" case sharedToken = "shared-token" case bootstrapToken = "bootstrap-token" - case password = "password" - case none = "none" + case password + case none } -// Avoid ambiguity with the app's own AnyCodable type. +/// Avoid ambiguity with the app's own AnyCodable type. private typealias ProtoAnyCodable = OpenClawProtocol.AnyCodable private enum ConnectChallengeError: Error { @@ -132,13 +136,13 @@ private let defaultOperatorConnectScopes: [String] = [ "operator.pairing", ] -private extension String { - var nilIfEmpty: String? { +extension String { + fileprivate var nilIfEmpty: String? { self.isEmpty ? nil : self } } -private struct SelectedConnectAuth: Sendable { +private struct SelectedConnectAuth { let authToken: String? let authBootstrapToken: String? let authDeviceToken: String? @@ -223,7 +227,9 @@ public actor GatewayChannelActor { } } - public func authSource() -> GatewayAuthSource { self.lastAuthSource } + public func authSource() -> GatewayAuthSource { + self.lastAuthSource + } public func shutdown() async { self.shouldReconnect = false @@ -277,8 +283,7 @@ public actor GatewayChannelActor { if self.shouldPauseReconnectAfterAuthFailure(error) { self.reconnectPausedForAuthFailure = true self.logger.error( - "gateway watchdog reconnect paused for non-recoverable auth failure \(error.localizedDescription, privacy: .public)" - ) + "gateway watchdog reconnect paused for non-recoverable auth failure \(error.localizedDescription, privacy: .public)") continue } let wrapped = self.wrap(error, context: "gateway watchdog reconnect") @@ -312,11 +317,10 @@ public actor GatewayChannelActor { }, operation: { try await self.sendConnect() }) } catch { - let wrapped: Error - if let authError = error as? GatewayConnectAuthError { - wrapped = authError + let wrapped: Error = if let authError = error as? GatewayConnectAuthError { + authError } else { - wrapped = self.wrap(error, context: "connect to gateway @ \(self.url.absoluteString)") + self.wrap(error, context: "connect to gateway @ \(self.url.absoluteString)") } self.connected = false self.task?.cancel(with: .goingAway, reason: nil) @@ -422,7 +426,7 @@ public actor GatewayChannelActor { role: role, includeDeviceIdentity: includeDeviceIdentity, deviceId: identity?.deviceId) - if selectedAuth.authDeviceToken != nil && self.pendingDeviceTokenRetry { + if selectedAuth.authDeviceToken != nil, self.pendingDeviceTokenRetry { self.pendingDeviceTokenRetry = false } self.lastAuthSource = selectedAuth.authSource @@ -485,8 +489,8 @@ public actor GatewayChannelActor { self.deviceTokenRetryBudgetUsed = true self.backoffMs = min(self.backoffMs, 250) } else if selectedAuth.authDeviceToken != nil, - let identity, - self.shouldClearStoredDeviceTokenAfterRetry(error) + let identity, + self.shouldClearStoredDeviceTokenAfterRetry(error) { // Retry failed with an explicit device-token mismatch; clear stale local token. DeviceAuthStore.clearToken(deviceId: identity.deviceId, role: role) @@ -498,39 +502,38 @@ public actor GatewayChannelActor { private func selectConnectAuth( role: String, includeDeviceIdentity: Bool, - deviceId: String? - ) -> SelectedConnectAuth { + deviceId: String?) -> SelectedConnectAuth + { let explicitToken = self.token?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty let explicitBootstrapToken = self.bootstrapToken?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty let explicitPassword = self.password?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty let storedToken = (includeDeviceIdentity && deviceId != nil) - ? DeviceAuthStore.loadToken(deviceId: deviceId!, role: role)?.token - : nil + ? DeviceAuthStore.loadToken(deviceId: deviceId!, role: role)?.token + : nil let shouldUseDeviceRetryToken = includeDeviceIdentity && self.pendingDeviceTokenRetry && storedToken != nil && explicitToken != nil && self.isTrustedDeviceRetryEndpoint() let authToken = explicitToken ?? - // A freshly scanned setup code should force the bootstrap pairing path instead of - // silently reusing an older stored device token. - (includeDeviceIdentity && explicitPassword == nil && explicitBootstrapToken == nil - ? storedToken - : nil) + // A freshly scanned setup code should force the bootstrap pairing path instead of + // silently reusing an older stored device token. + (includeDeviceIdentity && explicitPassword == nil && explicitBootstrapToken == nil + ? storedToken + : nil) let authBootstrapToken = authToken == nil ? explicitBootstrapToken : nil let authDeviceToken = shouldUseDeviceRetryToken ? storedToken : nil - let authSource: GatewayAuthSource - if authDeviceToken != nil || (explicitToken == nil && authToken != nil) { - authSource = .deviceToken + let authSource: GatewayAuthSource = if authDeviceToken != nil || (explicitToken == nil && authToken != nil) { + .deviceToken } else if authToken != nil { - authSource = .sharedToken + .sharedToken } else if authBootstrapToken != nil { - authSource = .bootstrapToken + .bootstrapToken } else if explicitPassword != nil { - authSource = .password + .password } else { - authSource = .none + .none } return SelectedConnectAuth( authToken: authToken, @@ -560,7 +563,7 @@ public actor GatewayChannelActor { case "node": return [] case "operator": - let allowedOperatorScopes: Set = [ + let allowedOperatorScopes: Set = [ "operator.approvals", "operator.read", "operator.talk.secrets", @@ -576,8 +579,8 @@ public actor GatewayChannelActor { deviceId: String, role: String, token: String, - scopes: [String] - ) { + scopes: [String]) + { guard let filteredScopes = self.filteredBootstrapHandoffScopes(role: role, scopes: scopes) else { return } @@ -593,8 +596,8 @@ public actor GatewayChannelActor { deviceId: String, role: String, token: String, - scopes: [String] - ) { + scopes: [String]) + { if authSource == .bootstrapToken { guard self.shouldPersistBootstrapHandoffTokens() else { return @@ -616,8 +619,8 @@ public actor GatewayChannelActor { private func handleConnectResponse( _ res: ResponseFrame, identity: DeviceIdentity?, - role: String - ) async throws { + role: String) async throws + { if res.ok == false { let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed" let details = res.error?["details"]?.value as? [String: ProtoAnyCodable] @@ -809,12 +812,11 @@ public actor GatewayChannelActor { } private nonisolated func decodeMessageData(_ msg: URLSessionWebSocketTask.Message) -> Data? { - let data: Data? = switch msg { + return switch msg { case let .data(data): data case let .string(text): text.data(using: .utf8) @unknown default: nil } - return data } private func watchTicks() async { @@ -853,8 +855,7 @@ public actor GatewayChannelActor { if self.shouldPauseReconnectAfterAuthFailure(error) { self.reconnectPausedForAuthFailure = true self.logger.error( - "gateway reconnect paused for non-recoverable auth failure \(error.localizedDescription, privacy: .public)" - ) + "gateway reconnect paused for non-recoverable auth failure \(error.localizedDescription, privacy: .public)") return } let wrapped = self.wrap(error, context: "gateway reconnect") @@ -867,8 +868,8 @@ public actor GatewayChannelActor { error: Error, explicitGatewayToken: String?, storedToken: String?, - attemptedDeviceTokenRetry: Bool - ) -> Bool { + attemptedDeviceTokenRetry: Bool) -> Bool + { if self.deviceTokenRetryBudgetUsed { return false } @@ -895,8 +896,8 @@ public actor GatewayChannelActor { if authError.isNonRecoverable { return true } - if authError.detail == .authTokenMismatch && - self.deviceTokenRetryBudgetUsed && !self.pendingDeviceTokenRetry + if authError.detail == .authTokenMismatch, + self.deviceTokenRetryBudgetUsed, !self.pendingDeviceTokenRetry { return true } @@ -1007,7 +1008,7 @@ public actor GatewayChannelActor { } } - // Wrap low-level URLSession/WebSocket errors with context so UI can surface them. + /// Wrap low-level URLSession/WebSocket errors with context so UI can surface them. private func wrap(_ error: Error, context: String) -> Error { if error is GatewayConnectAuthError || error is GatewayResponseError || error is GatewayDecodingError { return error @@ -1055,8 +1056,7 @@ public actor GatewayChannelActor { return (id: id, data: data) } catch { self.logger.error( - "gateway \(kind) encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)" - ) + "gateway \(kind) encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)") throw error } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectChallengeSupport.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectChallengeSupport.swift index f2ad187bc46..0b88c3b71b3 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectChallengeSupport.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectChallengeSupport.swift @@ -9,9 +9,9 @@ public enum GatewayConnectChallengeSupport { return trimmed } - public static func waitForNonce( + public static func waitForNonce( timeoutSeconds: Double, - onTimeout: @escaping @Sendable () -> E, + onTimeout: @escaping @Sendable () -> some Error, receiveNonce: @escaping @Sendable () async throws -> String?) async throws -> String { try await AsyncTimeout.withTimeout( diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectionProblem.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectionProblem.swift index fb015613d1c..32f3a1dbef9 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectionProblem.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectionProblem.swift @@ -81,32 +81,34 @@ public struct GatewayConnectionProblem: Equatable, Sendable { public var needsPairingApproval: Bool { switch self.kind { - case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired, .pairingMetadataUpgradeRequired: - return true + case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired, + .pairingMetadataUpgradeRequired: + true default: - return false + false } } public var needsCredentialUpdate: Bool { switch self.kind { case .gatewayAuthTokenMissing, - .gatewayAuthTokenMismatch, - .gatewayAuthTokenNotConfigured, - .gatewayAuthPasswordMissing, - .gatewayAuthPasswordMismatch, - .gatewayAuthPasswordNotConfigured, - .bootstrapTokenInvalid, - .deviceTokenMismatch: - return true + .gatewayAuthTokenMismatch, + .gatewayAuthTokenNotConfigured, + .gatewayAuthPasswordMissing, + .gatewayAuthPasswordMismatch, + .gatewayAuthPasswordNotConfigured, + .bootstrapTokenInvalid, + .deviceTokenMismatch: + true default: - return false + false } } public var statusText: String { switch self.kind { - case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired, .pairingMetadataUpgradeRequired: + case .pairingRequired, .pairingRoleUpgradeRequired, .pairingScopeUpgradeRequired, + .pairingMetadataUpgradeRequired: if let requestId { return "\(self.title) (request ID: \(requestId))" } @@ -123,7 +125,10 @@ public struct GatewayConnectionProblem: Equatable, Sendable { } public enum GatewayConnectionProblemMapper { - public static func map(error: Error, preserving previousProblem: GatewayConnectionProblem? = nil) -> GatewayConnectionProblem? { + public static func map( + error: Error, + preserving previousProblem: GatewayConnectionProblem? = nil) -> GatewayConnectionProblem? + { guard let nextProblem = self.rawMap(error) else { return nil } @@ -136,14 +141,20 @@ public enum GatewayConnectionProblemMapper { return nextProblem } - public static func shouldPreserve(previousProblem: GatewayConnectionProblem, over nextProblem: GatewayConnectionProblem) -> Bool { + public static func shouldPreserve( + previousProblem: GatewayConnectionProblem, + over nextProblem: GatewayConnectionProblem) -> Bool + { if nextProblem.kind == .websocketCancelled { return previousProblem.pauseReconnect || previousProblem.requestId != nil } return false } - public static func shouldPreserve(previousProblem: GatewayConnectionProblem, overDisconnectReason reason: String) -> Bool { + public static func shouldPreserve( + previousProblem: GatewayConnectionProblem, + overDisconnectReason reason: String) -> Bool + { let normalized = reason.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() guard !normalized.isEmpty else { return false } if normalized.contains("cancelled") || normalized.contains("canceled") { @@ -175,7 +186,9 @@ public enum GatewayConnectionProblemMapper { ?? "This gateway requires an auth token, but this iPhone did not send one.", actionLabel: authError.actionLabel ?? "Open Settings", actionCommand: authError.actionCommand, - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/authentication"), requestId: authError.requestId, retryable: false, pauseReconnect: true, @@ -187,9 +200,12 @@ public enum GatewayConnectionProblemMapper { title: authError.titleOverride ?? "Gateway token is out of date", message: authError.userMessageOverride ?? "The token on this iPhone does not match the gateway token.", - actionLabel: authError.actionLabel ?? (authError.canRetryWithDeviceToken ? "Retry once" : "Update gateway token"), + actionLabel: authError + .actionLabel ?? (authError.canRetryWithDeviceToken ? "Retry once" : "Update gateway token"), actionCommand: authError.actionCommand, - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/authentication"), requestId: authError.requestId, retryable: authError.retryableOverride ?? authError.canRetryWithDeviceToken, pauseReconnect: authError.pauseReconnectOverride ?? !authError.canRetryWithDeviceToken, @@ -203,7 +219,9 @@ public enum GatewayConnectionProblemMapper { ?? "This gateway is set to token auth, but no gateway token is configured on the gateway.", actionLabel: authError.actionLabel ?? "Fix on gateway", actionCommand: authError.actionCommand ?? "openclaw config set gateway.auth.token ", - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/authentication"), requestId: authError.requestId, retryable: false, pauseReconnect: true, @@ -217,7 +235,9 @@ public enum GatewayConnectionProblemMapper { ?? "This gateway requires a password, but this iPhone did not send one.", actionLabel: authError.actionLabel ?? "Open Settings", actionCommand: authError.actionCommand, - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/authentication"), requestId: authError.requestId, retryable: false, pauseReconnect: true, @@ -231,7 +251,9 @@ public enum GatewayConnectionProblemMapper { ?? "The saved password on this iPhone does not match the gateway password.", actionLabel: authError.actionLabel ?? "Update password", actionCommand: authError.actionCommand, - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/authentication"), requestId: authError.requestId, retryable: false, pauseReconnect: true, @@ -245,7 +267,9 @@ public enum GatewayConnectionProblemMapper { ?? "This gateway is set to password auth, but no gateway password is configured on the gateway.", actionLabel: authError.actionLabel ?? "Fix on gateway", actionCommand: authError.actionCommand ?? "openclaw config set gateway.auth.password ", - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/authentication"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/authentication"), requestId: authError.requestId, retryable: false, pauseReconnect: true, @@ -286,7 +310,8 @@ public enum GatewayConnectionProblemMapper { owner: .iphone, title: authError.titleOverride ?? "Secure device identity is required", message: authError.userMessageOverride - ?? "This connection must include a signed device identity before the gateway can bind permissions to this iPhone.", + ?? + "This connection must include a signed device identity before the gateway can bind permissions to this iPhone.", actionLabel: authError.actionLabel ?? "Retry from the app", actionCommand: authError.actionCommand, docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/platforms/ios"), @@ -302,7 +327,9 @@ public enum GatewayConnectionProblemMapper { message: authError.userMessageOverride ?? "The device signature is too old to use.", actionLabel: authError.actionLabel ?? "Check iPhone time", actionCommand: authError.actionCommand, - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), requestId: authError.requestId, retryable: true, pauseReconnect: true, @@ -316,7 +343,9 @@ public enum GatewayConnectionProblemMapper { ?? "The gateway expected a one-time challenge response, but the nonce was missing.", actionLabel: authError.actionLabel ?? "Retry", actionCommand: authError.actionCommand, - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), requestId: authError.requestId, retryable: true, pauseReconnect: true, @@ -329,7 +358,9 @@ public enum GatewayConnectionProblemMapper { message: authError.userMessageOverride ?? "The challenge response was stale or mismatched.", actionLabel: authError.actionLabel ?? "Retry", actionCommand: authError.actionCommand, - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), requestId: authError.requestId, retryable: true, pauseReconnect: true, @@ -441,7 +472,9 @@ public enum GatewayConnectionProblemMapper { ?? "The gateway is temporarily refusing new auth attempts after repeated failures.", actionLabel: authError.actionLabel ?? "Wait and retry", actionCommand: authError.actionCommand, - docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), + docsURL: self.docsURL( + authError.docsURLString, + fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), requestId: authError.requestId, retryable: false, pauseReconnect: true, @@ -520,7 +553,8 @@ public enum GatewayConnectionProblemMapper { retryable: true, pauseReconnect: false, technicalDetails: rawMessage) - case .cannotFindHost, .dnsLookupFailed, .notConnectedToInternet, .networkConnectionLost, .internationalRoamingOff, .callIsActive, .dataNotAllowed: + case .cannotFindHost, .dnsLookupFailed, .notConnectedToInternet, .networkConnectionLost, + .internationalRoamingOff, .callIsActive, .dataNotAllowed: return GatewayConnectionProblem( kind: .reachabilityFailed, owner: .network, @@ -575,7 +609,9 @@ public enum GatewayConnectionProblemMapper { pauseReconnect: false, technicalDetails: rawMessage) } - if lower.contains("cannot find host") || lower.contains("could not connect") || lower.contains("network is unreachable") { + if lower.contains("cannot find host") || lower.contains("could not connect") || lower + .contains("network is unreachable") + { return GatewayConnectionProblem( kind: .reachabilityFailed, owner: .network, @@ -615,7 +651,8 @@ public enum GatewayConnectionProblemMapper { owner: .gateway, title: authError.titleOverride ?? "Additional approval required", message: authError.userMessageOverride - ?? "This iPhone is already paired, but it is requesting a new role that was not previously approved.", + ?? + "This iPhone is already paired, but it is requesting a new role that was not previously approved.", actionLabel: authError.actionLabel ?? "Approve on gateway", actionCommand: authError.actionCommand ?? pairingCommand, docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"), @@ -643,7 +680,8 @@ public enum GatewayConnectionProblemMapper { owner: .gateway, title: authError.titleOverride ?? "Device approval needs refresh", message: authError.userMessageOverride - ?? "The gateway detected a change in this device's approved identity metadata and requires re-approval.", + ?? + "The gateway detected a change in this device's approved identity metadata and requires re-approval.", actionLabel: authError.actionLabel ?? "Approve on gateway", actionCommand: authError.actionCommand ?? pairingCommand, docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"), @@ -736,17 +774,17 @@ public enum GatewayConnectionProblemMapper { private static func owner(from raw: String) -> GatewayConnectionProblem.Owner? { switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { case "gateway": - return .gateway + .gateway case "iphone", "ios", "device": - return .iphone + .iphone case "both": - return .both + .both case "network": - return .network + .network case "unknown", "": - return .unknown + .unknown default: - return nil + nil } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayDiscoveryStatusText.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayDiscoveryStatusText.swift index e15baf17fdb..4c1aa430eaa 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayDiscoveryStatusText.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayDiscoveryStatusText.swift @@ -36,4 +36,3 @@ public enum GatewayDiscoveryStatusText { return "Searching…" } } - diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayErrors.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayErrors.swift index 7009f85a70c..7908e1146b8 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayErrors.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayErrors.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol public enum GatewayConnectAuthDetailCode: String, Sendable { case authRequired = "AUTH_REQUIRED" @@ -129,9 +129,13 @@ public struct GatewayConnectAuthError: LocalizedError, Sendable { return trimmed.isEmpty ? nil : trimmed } - public var detailCode: String? { self.detailCodeRaw } + public var detailCode: String? { + self.detailCodeRaw + } - public var recommendedNextStepCode: String? { self.recommendedNextStepRaw } + public var recommendedNextStepCode: String? { + self.recommendedNextStepRaw + } public var detail: GatewayConnectAuthDetailCode? { guard let detailCodeRaw else { return nil } @@ -143,23 +147,25 @@ public struct GatewayConnectAuthError: LocalizedError, Sendable { return GatewayConnectRecoveryNextStep(rawValue: recommendedNextStepRaw) } - public var errorDescription: String? { self.message } + public var errorDescription: String? { + self.message + } public var isNonRecoverable: Bool { switch self.detail { case .authTokenMissing, - .authBootstrapTokenInvalid, - .authTokenNotConfigured, - .authPasswordMissing, - .authPasswordMismatch, - .authPasswordNotConfigured, - .authRateLimited, - .pairingRequired, - .controlUiDeviceIdentityRequired, - .deviceIdentityRequired: - return true + .authBootstrapTokenInvalid, + .authTokenNotConfigured, + .authPasswordMissing, + .authPasswordMismatch, + .authPasswordNotConfigured, + .authRateLimited, + .pairingRequired, + .controlUiDeviceIdentityRequired, + .deviceIdentityRequired: + true default: - return false + false } } } @@ -203,5 +209,7 @@ public struct GatewayDecodingError: LocalizedError, Sendable { self.message = message } - public var errorDescription: String? { "\(self.method): \(self.message)" } + public var errorDescription: String? { + "\(self.method): \(self.message)" + } } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift index 945e482bbbf..58d437ce1bf 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift @@ -1,8 +1,8 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol import OSLog -private struct NodeInvokeRequestPayload: Codable, Sendable { +private struct NodeInvokeRequestPayload: Codable { var id: String var nodeId: String var command: String @@ -19,7 +19,7 @@ private func replaceCanvasCapabilityInScopedHostUrl(scopedUrl: String, capabilit let nextSlash = suffix.firstIndex(of: "/") let nextQuery = suffix.firstIndex(of: "?") let nextFragment = suffix.firstIndex(of: "#") - let capabilityEnd = [nextSlash, nextQuery, nextFragment].compactMap { $0 }.min() ?? scopedUrl.endIndex + let capabilityEnd = [nextSlash, nextQuery, nextFragment].compactMap(\.self).min() ?? scopedUrl.endIndex guard capabilityStart < capabilityEnd else { return nil } return String(scopedUrl[.. String? { return parsed.string ?? trimmed } - public actor GatewayNodeSession { private let logger = Logger(subsystem: "ai.openclaw", category: "node.gateway") private let decoder = JSONDecoder() private let encoder = JSONEncoder() - private static let defaultInvokeTimeoutMs = 30_000 + private static let defaultInvokeTimeoutMs = 30000 private var channel: GatewayChannelActor? private var activeURL: URL? private var activeToken: String? @@ -79,8 +78,8 @@ public actor GatewayNodeSession { static func invokeWithTimeout( request: BridgeInvokeRequest, timeoutMs: Int?, - onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse - ) async -> BridgeInvokeResponse { + onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse) async -> BridgeInvokeResponse + { let timeoutLogger = Logger(subsystem: "ai.openclaw", category: "node.gateway") let timeout: Int = { if let timeoutMs { return max(0, timeoutMs) } @@ -144,13 +143,14 @@ public actor GatewayNodeSession { ok: false, error: OpenClawNodeError( code: .unavailable, - message: "node invoke timed out") - )) + message: "node invoke timed out"))) } } - timeoutLogger.info("node invoke race resolved id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)") + timeoutLogger + .info("node invoke race resolved id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)") return response } + private var serverEventSubscribers: [UUID: AsyncStream.Continuation] = [:] private var canvasHostUrl: String? @@ -201,8 +201,8 @@ public actor GatewayNodeSession { sessionBox: WebSocketSessionBox?, onConnected: @escaping @Sendable () async -> Void, onDisconnected: @escaping @Sendable (String) async -> Void, - onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse - ) async throws { + onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse) async throws + { let nextOptionsKey = self.connectOptionsKey(connectOptions) let shouldReconnect = self.activeURL != url || self.activeToken != token || @@ -273,7 +273,7 @@ public actor GatewayNodeSession { self.canvasHostUrl } - public func refreshNodeCanvasCapability(timeoutMs: Int = 8_000) async -> Bool { + public func refreshNodeCanvasCapability(timeoutMs: Int = 8000) async -> Bool { guard let channel = self.channel else { return false } do { let data = try await channel.request( @@ -455,8 +455,7 @@ public actor GatewayNodeSession { let response = await Self.invokeWithTimeout( request: req, timeoutMs: request.timeoutMs, - onInvoke: onInvoke - ) + onInvoke: onInvoke) self.logger.info( "node invoke completed id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)") await self.sendInvokeResult(request: request, response: response) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift index 139aa7d2942..c4eb59cb138 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayPayloadDecoding.swift @@ -1,5 +1,5 @@ -import OpenClawProtocol import Foundation +import OpenClawProtocol public enum GatewayPayloadDecoding { public static func decode( diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift index 001c84e29c4..9dae32bdb50 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift @@ -66,7 +66,8 @@ public enum GatewayTLSStore { !existing.isEmpty else { return } if GenericPasswordKeychainStore.loadString(service: self.keychainService, account: stableID) == nil { - guard GenericPasswordKeychainStore.saveString(existing, service: self.keychainService, account: stableID) else { + guard GenericPasswordKeychainStore.saveString(existing, service: self.keychainService, account: stableID) + else { return } } @@ -108,8 +109,8 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS public func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let trust = challenge.protectionSpace.serverTrust else { @@ -117,7 +118,7 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS return } - let expected = params.expectedFingerprint.map(normalizeFingerprint) + let expected = self.params.expectedFingerprint.map(normalizeFingerprint) if let fingerprint = certificateFingerprint(trust) { if let expected { if fingerprint == expected { @@ -127,7 +128,7 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS } return } - if params.allowTOFU { + if self.params.allowTOFU { if let storeKey = params.storeKey { GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey) } @@ -137,7 +138,7 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS } let ok = SecTrustEvaluateWithError(trust, nil) - if ok || !params.required { + if ok || !self.params.required { completionHandler(.useCredential, URLCredential(trust: trust)) } else { completionHandler(.cancelAuthenticationChallenge, nil) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GenericPasswordKeychainStore.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GenericPasswordKeychainStore.swift index 01603f7848b..8575ffcef33 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GenericPasswordKeychainStore.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GenericPasswordKeychainStore.swift @@ -12,8 +12,8 @@ public enum GenericPasswordKeychainStore { _ value: String, service: String, account: String, - accessible: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly - ) -> Bool { + accessible: CFString = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) -> Bool + { self.saveData(Data(value.utf8), service: service, account: account, accessible: accessible) } @@ -40,8 +40,8 @@ public enum GenericPasswordKeychainStore { _ data: Data, service: String, account: String, - accessible: CFString - ) -> Bool { + accessible: CFString) -> Bool + { let query = self.baseQuery(service: service, account: account) let previousData = self.loadData(service: service, account: account) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/InstanceIdentity.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/InstanceIdentity.swift index d18fa4e9fbf..8e04c6ac1b7 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/InstanceIdentity.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/InstanceIdentity.swift @@ -12,7 +12,7 @@ public enum InstanceIdentity { UserDefaults(suiteName: suiteName) ?? .standard } -#if canImport(UIKit) + #if canImport(UIKit) private static func readMainActor(_ body: @MainActor () -> T) -> T { if Thread.isMainThread { return MainActor.assumeIsolated { body() } @@ -21,7 +21,7 @@ public enum InstanceIdentity { MainActor.assumeIsolated { body() } } } -#endif + #endif public static let instanceId: String = { let defaults = Self.defaults @@ -38,23 +38,23 @@ public enum InstanceIdentity { }() public static let displayName: String = { -#if canImport(UIKit) + #if canImport(UIKit) let name = Self.readMainActor { UIDevice.current.name.trimmingCharacters(in: .whitespacesAndNewlines) } return name.isEmpty ? "openclaw" : name -#else + #else if let name = Host.current().localizedName?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty { return name } return "openclaw" -#endif + #endif }() public static let modelIdentifier: String? = { -#if canImport(UIKit) + #if canImport(UIKit) var systemInfo = utsname() uname(&systemInfo) let machine = withUnsafeBytes(of: &systemInfo.machine) { ptr in @@ -62,7 +62,7 @@ public enum InstanceIdentity { } let trimmed = machine?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return trimmed.isEmpty ? nil : trimmed -#else + #else var size = 0 guard sysctlbyname("hw.model", nil, &size, nil, 0) == 0, size > 1 else { return nil } @@ -73,36 +73,36 @@ public enum InstanceIdentity { guard let raw = String(bytes: bytes, encoding: .utf8) else { return nil } let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? nil : trimmed -#endif + #endif }() public static let deviceFamily: String = { -#if canImport(UIKit) + #if canImport(UIKit) return Self.readMainActor { switch UIDevice.current.userInterfaceIdiom { - case .pad: return "iPad" - case .phone: return "iPhone" - default: return "iOS" + case .pad: "iPad" + case .phone: "iPhone" + default: "iOS" } } -#else + #else return "Mac" -#endif + #endif }() public static let platformString: String = { let v = ProcessInfo.processInfo.operatingSystemVersion -#if canImport(UIKit) + #if canImport(UIKit) let name = Self.readMainActor { switch UIDevice.current.userInterfaceIdiom { - case .pad: return "iPadOS" - case .phone: return "iOS" - default: return "iOS" + case .pad: "iPadOS" + case .phone: "iOS" + default: "iOS" } } return "\(name) \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" -#else + #else return "macOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" -#endif + #endif }() } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationCurrentRequest.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationCurrentRequest.swift index 80038d6016c..0bb23f43fa0 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationCurrentRequest.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationCurrentRequest.swift @@ -4,8 +4,7 @@ import Foundation public enum LocationCurrentRequest { public typealias TimeoutRunner = @Sendable ( _ timeoutMs: Int, - _ operation: @escaping @Sendable () async throws -> CLLocation - ) async throws -> CLLocation + _ operation: @escaping @Sendable () async throws -> CLLocation) async throws -> CLLocation @MainActor public static func resolve( diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationServiceSupport.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationServiceSupport.swift index 1a818c6c262..db628452a73 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationServiceSupport.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/LocationServiceSupport.swift @@ -7,21 +7,21 @@ public protocol LocationServiceCommon: AnyObject, CLLocationManagerDelegate { var locationRequestContinuation: CheckedContinuation? { get set } } -public extension LocationServiceCommon { - func configureLocationManager() { +extension LocationServiceCommon { + public func configureLocationManager() { self.locationManager.delegate = self self.locationManager.desiredAccuracy = kCLLocationAccuracyBest } - func authorizationStatus() -> CLAuthorizationStatus { + public func authorizationStatus() -> CLAuthorizationStatus { self.locationManager.authorizationStatus } - func accuracyAuthorization() -> CLAccuracyAuthorization { + public func accuracyAuthorization() -> CLAccuracyAuthorization { LocationServiceSupport.accuracyAuthorization(manager: self.locationManager) } - func requestLocationOnce() async throws -> CLLocation { + public func requestLocationOnce() async throws -> CLLocation { try await LocationServiceSupport.requestLocation(manager: self.locationManager) { continuation in self.locationRequestContinuation = continuation } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift index 5af33d1d35c..38398e5a769 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/OpenClawKitResources.swift @@ -18,7 +18,7 @@ public enum OpenClawKitResources { private static func locateBundle() -> Bundle { // 1. Check inside Bundle.main (packaged apps copy resources here) if let mainResourceURL = Bundle.main.resourceURL { - let bundleURL = mainResourceURL.appendingPathComponent("\(bundleName).bundle") + let bundleURL = mainResourceURL.appendingPathComponent("\(self.bundleName).bundle") if let bundle = Bundle(url: bundleURL) { return bundle } @@ -60,7 +60,7 @@ public enum OpenClawKitResources { roots.append(baseURL.appendingPathComponent("Contents/Resources")) var current = baseURL - for _ in 0 ..< 5 { + for _ in 0..<5 { current = current.deletingLastPathComponent() roots.append(current) roots.append(current.appendingPathComponent("Resources")) @@ -68,7 +68,7 @@ public enum OpenClawKitResources { } for root in roots { - let bundleURL = root.appendingPathComponent("\(bundleName).bundle") + let bundleURL = root.appendingPathComponent("\(self.bundleName).bundle") if let bundle = Bundle(url: bundleURL) { return bundle } @@ -79,5 +79,5 @@ public enum OpenClawKitResources { } } -// Helper class for bundle lookup via Bundle(for:) +/// Helper class for bundle lookup via Bundle(for:) private final class BundleLocator {} diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/PhotoCapture.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/PhotoCapture.swift index b5f00d34751..e5657826f0c 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/PhotoCapture.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/PhotoCapture.swift @@ -5,8 +5,8 @@ public enum PhotoCapture { rawData: Data, maxWidthPx: Int, quality: Double, - maxPayloadBytes: Int = 5 * 1024 * 1024 - ) throws -> (data: Data, widthPx: Int, heightPx: Int) { + maxPayloadBytes: Int = 5 * 1024 * 1024) throws -> (data: Data, widthPx: Int, heightPx: Int) + { // Base64 inflates payloads by ~4/3; cap encoded bytes so the payload stays under maxPayloadBytes (API limit). let maxEncodedBytes = (maxPayloadBytes / 4) * 3 return try JPEGTranscoder.transcodeToJPEG( @@ -16,4 +16,3 @@ public enum PhotoCapture { maxBytes: maxEncodedBytes) } } - diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentDeepLink.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentDeepLink.swift index 08f06234334..8b44aa0c0dd 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentDeepLink.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/ShareToAgentDeepLink.swift @@ -33,7 +33,7 @@ public enum ShareToAgentDeepLink { let urlText = payload.url?.absoluteString.trimmingCharacters(in: .whitespacesAndNewlines) let resolvedInstruction = self.clean(instruction) ?? ShareToAgentSettings.loadDefaultInstruction() - var lines: [String] = ["Shared from iOS."] + var lines = ["Shared from iOS."] if let title, !title.isEmpty { lines.append("Title: \(title)") } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkConfigParsing.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkConfigParsing.swift index 38efece3e1d..77dd7ffa635 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkConfigParsing.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkConfigParsing.swift @@ -20,8 +20,8 @@ public enum TalkConfigParsing { public static func selectProviderConfig( _ talk: [String: AnyCodable]?, defaultProvider: String, - allowLegacyFallback: Bool = true, - ) -> TalkProviderConfigSelection? { + allowLegacyFallback: Bool = true) -> TalkProviderConfigSelection? + { guard let talk else { return nil } if let resolvedSelection = self.resolvedProviderConfig(talk) { return resolvedSelection @@ -63,16 +63,16 @@ public enum TalkConfigParsing { public static func resolvedSpeechLocaleID( _ talk: [String: AnyCodable]?, - fallback: String? = nil - ) -> String? { + fallback: String? = nil) -> String? + { self.normalizedSpeechLocaleID(talk?["speechLocale"]?.stringValue) ?? self.normalizedSpeechLocaleID(fallback) } public static func normalizedExplicitSpeechLocaleID( _ value: String?, - automaticID: String = "auto" - ) -> String? { + automaticID: String = "auto") -> String? + { let normalized = self.normalizedSpeechLocaleID(value) return normalized == automaticID ? nil : normalized } @@ -80,8 +80,8 @@ public enum TalkConfigParsing { public static func resolvedSpeechRecognitionLocaleID( preferredLocaleIDs: [String?], fallbackLocaleID: String = "en-US", - supportedLocaleIDs: Set - ) -> String? { + supportedLocaleIDs: Set) -> String? + { let supported = Set(supportedLocaleIDs.compactMap(self.normalizedSpeechLocaleID)) var seen = Set() let candidates = (preferredLocaleIDs + [fallbackLocaleID]) @@ -102,8 +102,8 @@ public enum TalkConfigParsing { } private static func resolvedProviderConfig( - _ talk: [String: AnyCodable] - ) -> TalkProviderConfigSelection? { + _ talk: [String: AnyCodable]) -> TalkProviderConfigSelection? + { guard let resolved = talk["resolved"]?.dictionaryValue, let providerID = self.normalizedTalkProviderID(resolved["provider"]?.stringValue) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift index 2a2e39d68cf..5c12c7dd0e3 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkPromptBuilder.swift @@ -2,16 +2,15 @@ public enum TalkPromptBuilder: Sendable { public static func build( transcript: String, interruptedAtSeconds: Double?, - includeVoiceDirectiveHint: Bool = true - ) -> String { + includeVoiceDirectiveHint: Bool = true) -> String + { var lines: [String] = [ "Talk Mode active. Reply in a concise, spoken tone.", ] if includeVoiceDirectiveHint { lines.append( - "You may optionally prefix the response with JSON (first line) to set ElevenLabs voice (id or alias), e.g. {\"voice\":\"\",\"once\":true}." - ) + "You may optionally prefix the response with JSON (first line) to set ElevenLabs voice (id or alias), e.g. {\"voice\":\"\",\"once\":true}.") } if let interruptedAtSeconds { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift index d9518e6db94..585a1d8ea3d 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/TalkSystemSpeechSynthesizer.swift @@ -16,7 +16,9 @@ public final class TalkSystemSpeechSynthesizer: NSObject { private var currentToken = UUID() private var watchdog: Task? - public var isSpeaking: Bool { self.synth.isSpeaking } + public var isSpeaking: Bool { + self.synth.isSpeaking + } override private init() { super.init() @@ -35,8 +37,8 @@ public final class TalkSystemSpeechSynthesizer: NSObject { public func speak( text: String, language: String? = nil, - onStart: (() -> Void)? = nil - ) async throws { + onStart: (() -> Void)? = nil) async throws + { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } @@ -51,7 +53,9 @@ public final class TalkSystemSpeechSynthesizer: NSObject { } self.currentUtterance = utterance - let watchdogTimeout = Self.watchdogTimeoutSeconds(text: trimmed, language: language ?? utterance.voice?.language) + let watchdogTimeout = Self.watchdogTimeoutSeconds( + text: trimmed, + language: language ?? utterance.voice?.language) self.watchdog?.cancel() self.watchdog = Task { @MainActor [weak self] in guard let self else { return } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift index 4315bb073ef..90ef6574451 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/AnyCodable.swift @@ -6,7 +6,9 @@ import Foundation public struct AnyCodable: Codable, @unchecked Sendable, Hashable { public let value: Any - public init(_ value: Any) { self.value = Self.normalize(value) } + public init(_ value: Any) { + self.value = Self.normalize(value) + } public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WizardHelpers.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WizardHelpers.swift index d410914bfa5..2580f2b8878 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WizardHelpers.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/WizardHelpers.swift @@ -74,11 +74,11 @@ public func anyCodableBool(_ value: AnyCodable?) -> Bool { public func anyCodableArray(_ value: AnyCodable?) -> [AnyCodable] { switch value?.value { case let arr as [AnyCodable]: - return arr + arr case let arr as [Any]: - return arr.map { AnyCodable($0) } + arr.map { AnyCodable($0) } default: - return [] + [] } } diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index 0346e5a9466..eebf35a3d04 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -93,7 +93,7 @@ Gateway-side requirement: How the flow works: -- The iOS app registers with the relay using App Attest and the app receipt. +- The iOS app registers with the relay using App Attest and a StoreKit app transaction JWS. - The relay returns an opaque relay handle plus a registration-scoped send grant. - The iOS app fetches the paired gateway identity and includes it in relay registration, so the relay-backed registration is delegated to that specific gateway. - The app forwards that relay-backed registration to the paired gateway with `push.apns.register`. @@ -136,8 +136,8 @@ Hop by hop: 2. `iOS app -> relay` - The app calls the relay registration endpoints over HTTPS. - - Registration includes App Attest proof plus the app receipt. - - The relay validates the bundle ID, App Attest proof, and Apple receipt, and requires the + - Registration includes App Attest proof plus a StoreKit app transaction JWS. + - The relay validates the bundle ID, App Attest proof, and Apple distribution proof, and requires the official/production distribution path. - This is what blocks local Xcode/dev builds from using the hosted relay. A local build may be signed, but it does not satisfy the official Apple distribution proof the relay expects. @@ -227,6 +227,18 @@ Notes: - The iOS node auto-navigates to A2UI on connect when a canvas host URL is advertised. - Return to the built-in scaffold with `canvas.navigate` and `{"url":""}`. +## Computer Use relationship + +The iOS app is a mobile node surface, not a Codex Computer Use backend. Codex +Computer Use and `cua-driver mcp` control a local macOS desktop through MCP +tools; the iOS app exposes iPhone capabilities through OpenClaw node commands +such as `canvas.*`, `camera.*`, `screen.*`, `location.*`, and `talk.*`. + +Agents can still operate the iOS app through OpenClaw by invoking node +commands, but those calls go through the gateway node protocol and follow iOS +foreground/background limits. Use [Codex Computer Use](/plugins/codex-computer-use) +for local desktop control and this page for iOS node capabilities. + ### Canvas eval / snapshot ```bash diff --git a/docs/plugins/codex-computer-use.md b/docs/plugins/codex-computer-use.md index 7a0c35399d6..8ca1f29533b 100644 --- a/docs/plugins/codex-computer-use.md +++ b/docs/plugins/codex-computer-use.md @@ -32,6 +32,18 @@ a permission-aware host for Peekaboo CLI automation. Use this page when a Codex-mode OpenClaw agent should have Codex's native `computer-use` MCP plugin available before the turn starts. +## iOS app + +The iOS app is separate from Codex Computer Use. It does not install or proxy +the Codex `computer-use` MCP server and it is not a desktop-control backend. +Instead, the iOS app connects as an OpenClaw node and exposes mobile +capabilities through node commands such as `canvas.*`, `camera.*`, `screen.*`, +`location.*`, and `talk.*`. + +Use [iOS](/platforms/ios) when you want an agent to drive an iPhone node through +the gateway. Use this page when a Codex-mode agent should control the local +macOS desktop through Codex's native Computer Use plugin. + ## Direct cua-driver MCP Codex Computer Use is not the only way to expose desktop control. If you want