diff --git a/.swiftlint.yml b/.swiftlint.yml index a664b9e84b..180daba71a 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -108,6 +108,10 @@ function_body_length: warning: 150 error: 300 +function_parameter_count: + warning: 7 + error: 10 + file_length: warning: 1500 error: 2500 diff --git a/apps/macos/Sources/Clawdbot/AboutSettings.swift b/apps/macos/Sources/Clawdbot/AboutSettings.swift index a19c53b441..cbbdda0e2c 100644 --- a/apps/macos/Sources/Clawdbot/AboutSettings.swift +++ b/apps/macos/Sources/Clawdbot/AboutSettings.swift @@ -108,7 +108,8 @@ struct AboutSettings: View { } private var buildTimestamp: String? { - guard let raw = Bundle.main.object(forInfoDictionaryKey: "ClawdbotBuildTimestamp") as? String else { return nil } + guard let raw = Bundle.main.object(forInfoDictionaryKey: "ClawdbotBuildTimestamp") as? String + else { return nil } let parser = ISO8601DateFormatter() parser.formatOptions = [.withInternetDateTime] guard let date = parser.date(from: raw) else { return raw } diff --git a/apps/macos/Sources/Clawdbot/AppState.swift b/apps/macos/Sources/Clawdbot/AppState.swift index f99f0afea2..e46bc82de2 100644 --- a/apps/macos/Sources/Clawdbot/AppState.swift +++ b/apps/macos/Sources/Clawdbot/AppState.swift @@ -9,7 +9,7 @@ import SwiftUI final class AppState { private let isPreview: Bool private var isInitializing = true - private var configWatcher: ConfigFileWatcher? + private nonisolated var configWatcher: ConfigFileWatcher? private var suppressVoiceWakeGlobalSync = false private var voiceWakeGlobalSyncTask: Task? @@ -254,34 +254,33 @@ final class AppState { let configRoot = ClawdbotConfigFile.loadDict() let configGateway = configRoot["gateway"] as? [String: Any] let configModeRaw = (configGateway?["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) - let configMode: ConnectionMode? = { - switch configModeRaw { - case "local": - return .local - case "remote": - return .remote - default: - return nil - } - }() + let configMode: ConnectionMode? = switch configModeRaw { + case "local": + .local + case "remote": + .remote + default: + nil + } let configRemoteUrl = (configGateway?["remote"] as? [String: Any])?["url"] as? String let configHasRemoteUrl = !(configRemoteUrl? .trimmingCharacters(in: .whitespacesAndNewlines) .isEmpty ?? true) let storedMode = UserDefaults.standard.string(forKey: connectionModeKey) - if let configMode { - self.connectionMode = configMode + let resolvedConnectionMode: ConnectionMode = if let configMode { + configMode } else if configHasRemoteUrl { - self.connectionMode = .remote + .remote } else if let storedMode { - self.connectionMode = ConnectionMode(rawValue: storedMode) ?? .local + ConnectionMode(rawValue: storedMode) ?? .local } else { - self.connectionMode = onboardingSeen ? .local : .unconfigured + onboardingSeen ? .local : .unconfigured } + self.connectionMode = resolvedConnectionMode let storedRemoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? "" - if self.connectionMode == .remote, + if resolvedConnectionMode == .remote, storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, let host = AppState.remoteHost(from: configRemoteUrl) { @@ -361,18 +360,16 @@ final class AppState { .trimmingCharacters(in: .whitespacesAndNewlines) .isEmpty ?? true) - let desiredMode: ConnectionMode? = { - switch modeRaw { - case "local": - return .local - case "remote": - return .remote - case "unconfigured": - return .unconfigured - default: - return nil - } - }() + let desiredMode: ConnectionMode? = switch modeRaw { + case "local": + .local + case "remote": + .remote + case "unconfigured": + .unconfigured + default: + nil + } if let desiredMode { if desiredMode != self.connectionMode { @@ -407,14 +404,13 @@ final class AppState { var gateway = root["gateway"] as? [String: Any] ?? [:] var changed = false - let desiredMode: String? - switch self.connectionMode { + let desiredMode: String? = switch self.connectionMode { case .local: - desiredMode = "local" + "local" case .remote: - desiredMode = "remote" + "remote" case .unconfigured: - desiredMode = nil + nil } let currentMode = (gateway["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/apps/macos/Sources/Clawdbot/CameraCaptureService.swift b/apps/macos/Sources/Clawdbot/CameraCaptureService.swift index a364f5d72c..b7a7de3085 100644 --- a/apps/macos/Sources/Clawdbot/CameraCaptureService.swift +++ b/apps/macos/Sources/Clawdbot/CameraCaptureService.swift @@ -244,7 +244,7 @@ actor CameraCaptureService { deviceId: String?) -> AVCaptureDevice? { if let deviceId, !deviceId.isEmpty { - if let match = Self.availableCameras().first(where: { $0.uniqueID == deviceId }) { + if let match = availableCameras().first(where: { $0.uniqueID == deviceId }) { return match } } @@ -331,7 +331,7 @@ actor CameraCaptureService { private func sleepDelayMs(_ delayMs: Int) async { guard delayMs > 0 else { return } - let ns = UInt64(min(delayMs, 10_000)) * 1_000_000 + let ns = UInt64(min(delayMs, 10000)) * 1_000_000 try? await Task.sleep(nanoseconds: ns) } diff --git a/apps/macos/Sources/Clawdbot/ClawdbotConfigFile.swift b/apps/macos/Sources/Clawdbot/ClawdbotConfigFile.swift index 899c556aed..55b62a9fcd 100644 --- a/apps/macos/Sources/Clawdbot/ClawdbotConfigFile.swift +++ b/apps/macos/Sources/Clawdbot/ClawdbotConfigFile.swift @@ -100,7 +100,8 @@ enum ClawdbotConfigFile { static func gatewayPassword() -> String? { let root = self.loadDict() guard let gateway = root["gateway"] as? [String: Any], - let remote = gateway["remote"] as? [String: Any] else { + let remote = gateway["remote"] as? [String: Any] + else { return nil } return remote["password"] as? String @@ -121,5 +122,4 @@ enum ClawdbotConfigFile { } return nil } - } diff --git a/apps/macos/Sources/Clawdbot/CommandResolver.swift b/apps/macos/Sources/Clawdbot/CommandResolver.swift index 745c1e25b0..9c88cf5da6 100644 --- a/apps/macos/Sources/Clawdbot/CommandResolver.swift +++ b/apps/macos/Sources/Clawdbot/CommandResolver.swift @@ -157,7 +157,7 @@ enum CommandResolver { } static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? { - for dir in (searchPaths ?? self.preferredPaths()) { + for dir in searchPaths ?? self.preferredPaths() { let candidate = (dir as NSString).appendingPathComponent(name) if FileManager.default.isExecutableFile(atPath: candidate) { return candidate diff --git a/apps/macos/Sources/Clawdbot/ConfigFileWatcher.swift b/apps/macos/Sources/Clawdbot/ConfigFileWatcher.swift index 43f7e97e9f..c21b002a74 100644 --- a/apps/macos/Sources/Clawdbot/ConfigFileWatcher.swift +++ b/apps/macos/Sources/Clawdbot/ConfigFileWatcher.swift @@ -90,8 +90,8 @@ extension ConfigFileWatcher { private func handleEvents( numEvents: Int, eventPaths: UnsafeMutableRawPointer?, - eventFlags: UnsafePointer? - ) { + eventFlags: UnsafePointer?) + { guard numEvents > 0 else { return } guard eventFlags != nil else { return } guard self.matchesTarget(eventPaths: eventPaths) else { return } diff --git a/apps/macos/Sources/Clawdbot/ConfigStore.swift b/apps/macos/Sources/Clawdbot/ConfigStore.swift index 0b5064fc07..be05aec9f1 100644 --- a/apps/macos/Sources/Clawdbot/ConfigStore.swift +++ b/apps/macos/Sources/Clawdbot/ConfigStore.swift @@ -78,7 +78,7 @@ enum ConfigStore { let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) guard let raw = String(data: data, encoding: .utf8) else { throw NSError(domain: "ConfigStore", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Failed to encode config." + NSLocalizedDescriptionKey: "Failed to encode config.", ]) } let params: [String: AnyCodable] = ["raw": AnyCodable(raw)] @@ -88,7 +88,7 @@ enum ConfigStore { timeoutMs: 10000) } -#if DEBUG + #if DEBUG static func _testSetOverrides(_ overrides: Overrides) async { await self.overrideStore.setOverride(overrides) } @@ -96,5 +96,5 @@ enum ConfigStore { static func _testClearOverrides() async { await self.overrideStore.setOverride(.init()) } -#endif + #endif } diff --git a/apps/macos/Sources/Clawdbot/ContextUsageBar.swift b/apps/macos/Sources/Clawdbot/ContextUsageBar.swift index 6ec34207b7..f5bfa0530b 100644 --- a/apps/macos/Sources/Clawdbot/ContextUsageBar.swift +++ b/apps/macos/Sources/Clawdbot/ContextUsageBar.swift @@ -12,11 +12,13 @@ struct ContextUsageBar: View { if match == .darkAqua { return base } return base.blended(withFraction: 0.24, of: .black) ?? base } + private static let trackFill: NSColor = .init(name: nil) { appearance in let match = appearance.bestMatch(from: [.aqua, .darkAqua]) if match == .darkAqua { return NSColor.white.withAlphaComponent(0.14) } return NSColor.black.withAlphaComponent(0.12) } + private static let trackStroke: NSColor = .init(name: nil) { appearance in let match = appearance.bestMatch(from: [.aqua, .darkAqua]) if match == .darkAqua { return NSColor.white.withAlphaComponent(0.22) } diff --git a/apps/macos/Sources/Clawdbot/ControlChannel.swift b/apps/macos/Sources/Clawdbot/ControlChannel.swift index 09d7aa4a66..9634a2d31a 100644 --- a/apps/macos/Sources/Clawdbot/ControlChannel.swift +++ b/apps/macos/Sources/Clawdbot/ControlChannel.swift @@ -55,8 +55,8 @@ final class ControlChannel { private(set) var state: ConnectionState = .disconnected { didSet { CanvasManager.shared.refreshDebugStatus() - guard oldValue != state else { return } - switch state { + guard oldValue != self.state else { return } + switch self.state { case .connected: self.logger.info("control channel state -> connected") case .connecting: @@ -71,6 +71,7 @@ final class ControlChannel { } } } + private(set) var lastPingMs: Double? private let logger = Logger(subsystem: "com.clawdbot", category: "control") @@ -105,7 +106,8 @@ final class ControlChannel { _ = (target, identity) let idSet = !identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty self.logger.info( - "control channel configure mode=remote target=\(target, privacy: .public) identitySet=\(idSet, privacy: .public)") + "control channel configure mode=remote " + + "target=\(target, privacy: .public) identitySet=\(idSet, privacy: .public)") _ = try await GatewayEndpointStore.shared.ensureRemoteControlTunnel() await self.configure() } catch { @@ -261,7 +263,9 @@ final class ControlChannel { let trimmedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines) let reasonText = trimmedReason.isEmpty ? "unknown" : trimmedReason self.logger.info( - "control channel recovery starting mode=\(String(describing: mode), privacy: .public) reason=\(reasonText, privacy: .public)") + "control channel recovery starting " + + "mode=\(String(describing: mode), privacy: .public) " + + "reason=\(reasonText, privacy: .public)") if mode == .local { GatewayProcessManager.shared.setActive(true) } diff --git a/apps/macos/Sources/Clawdbot/DeviceModelCatalog.swift b/apps/macos/Sources/Clawdbot/DeviceModelCatalog.swift index aa8216b34d..ce6dd10c93 100644 --- a/apps/macos/Sources/Clawdbot/DeviceModelCatalog.swift +++ b/apps/macos/Sources/Clawdbot/DeviceModelCatalog.swift @@ -138,17 +138,20 @@ enum DeviceModelCatalog { if bundle.url( forResource: "ios-device-identifiers", withExtension: "json", - subdirectory: self.resourceSubdirectory) != nil { + subdirectory: self.resourceSubdirectory) != nil + { return bundle } if bundle.url( forResource: "mac-device-identifiers", withExtension: "json", - subdirectory: self.resourceSubdirectory) != nil { + subdirectory: self.resourceSubdirectory) != nil + { return bundle } return nil } + private enum NameValue: Decodable { case string(String) case stringArray([String]) diff --git a/apps/macos/Sources/Clawdbot/GatewayConnection.swift b/apps/macos/Sources/Clawdbot/GatewayConnection.swift index 1286052b18..ac8e6f1ff6 100644 --- a/apps/macos/Sources/Clawdbot/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdbot/GatewayConnection.swift @@ -237,8 +237,9 @@ actor GatewayConnection { guard let snapshot = self.lastSnapshot else { return (nil, nil) } let configPath = snapshot.snapshot.configpath?.trimmingCharacters(in: .whitespacesAndNewlines) let stateDir = snapshot.snapshot.statedir?.trimmingCharacters(in: .whitespacesAndNewlines) - return (configPath?.isEmpty == false ? configPath : nil, - stateDir?.isEmpty == false ? stateDir : nil) + return ( + configPath?.isEmpty == false ? configPath : nil, + stateDir?.isEmpty == false ? stateDir : nil) } func subscribe(bufferingNewest: Int = 100) -> AsyncStream { @@ -270,7 +271,9 @@ actor GatewayConnection { } private func configure(url: URL, token: String?, password: String?) async { - if self.client != nil, self.configuredURL == url, self.configuredToken == token, self.configuredPassword == password { + if self.client != nil, self.configuredURL == url, self.configuredToken == token, + self.configuredPassword == password + { return } if let client { diff --git a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift index 0e3ac7f3e0..f201544b70 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift @@ -40,8 +40,8 @@ actor GatewayEndpointStore { private static func resolveGatewayPassword( isRemote: Bool, root: [String: Any], - env: [String: String] - ) -> String? { + env: [String: String]) -> String? + { let raw = env["CLAWDBOT_GATEWAY_PASSWORD"] ?? "" let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { @@ -93,7 +93,11 @@ actor GatewayEndpointStore { let password = deps.password() switch initialMode { case .local: - self.state = .ready(mode: .local, url: URL(string: "ws://127.0.0.1:\(port)")!, token: token, password: password) + self.state = .ready( + mode: .local, + url: URL(string: "ws://127.0.0.1:\(port)")!, + token: token, + password: password) case .remote: self.state = .unavailable(mode: .remote, reason: "Remote mode enabled but no active control tunnel") case .unconfigured: @@ -125,14 +129,22 @@ actor GatewayEndpointStore { switch mode { case .local: let port = self.deps.localPort() - self.setState(.ready(mode: .local, url: URL(string: "ws://127.0.0.1:\(port)")!, token: token, password: password)) + self.setState(.ready( + mode: .local, + url: URL(string: "ws://127.0.0.1:\(port)")!, + token: token, + password: password)) case .remote: let port = await self.deps.remotePortIfRunning() guard let port else { self.setState(.unavailable(mode: .remote, reason: "Remote mode enabled but no active control tunnel")) return } - self.setState(.ready(mode: .remote, url: URL(string: "ws://127.0.0.1:\(Int(port))")!, token: token, password: password)) + self.setState(.ready( + mode: .remote, + url: URL(string: "ws://127.0.0.1:\(Int(port))")!, + token: token, + password: password)) case .unconfigured: self.setState(.unavailable(mode: .unconfigured, reason: "Gateway not configured")) } @@ -213,8 +225,8 @@ extension GatewayEndpointStore { static func _testResolveGatewayPassword( isRemote: Bool, root: [String: Any], - env: [String: String] - ) -> String? { + env: [String: String]) -> String? + { self.resolveGatewayPassword(isRemote: isRemote, root: root, env: env) } } diff --git a/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift index b0a55b6824..ee6b2e8e11 100644 --- a/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift @@ -24,7 +24,7 @@ enum GatewayLaunchAgentManager { } private static func gatewayProgramArguments(bundlePath: String, port: Int, bind: String) -> [String] { -#if DEBUG + #if DEBUG let projectRoot = CommandResolver.projectRoot() if let localBin = CommandResolver.projectClawdbotExecutable(projectRoot: projectRoot) { return [localBin, "gateway", "--port", "\(port)", "--bind", bind] @@ -38,7 +38,7 @@ enum GatewayLaunchAgentManager { subcommand: "gateway", extraArgs: ["--port", "\(port)", "--bind", bind]) } -#endif + #endif let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath) return [gatewayBin, "gateway-daemon", "--port", "\(port)", "--bind", bind] } @@ -51,7 +51,7 @@ enum GatewayLaunchAgentManager { static func set(enabled: Bool, bundlePath: String, port: Int) async -> String? { if enabled { - _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(legacyGatewayLaunchdLabel)"]) + _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyGatewayLaunchdLabel)"]) try? FileManager.default.removeItem(at: self.legacyPlistURL) let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath) guard FileManager.default.isExecutableFile(atPath: gatewayBin) else { diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index b53ba63de6..0f7d99b2a8 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -150,21 +150,12 @@ struct GeneralSettings: View { private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool { guard mode != .off else { return true } - let status = CLLocationManager.authorizationStatus() - if status == .authorizedAlways || status == .authorizedWhenInUse { - if mode == .always && status != .authorizedAlways { - let updated = await LocationPermissionRequester.shared.request(always: true) - return updated == .authorizedAlways || updated == .authorizedWhenInUse - } + let status = CLLocationManager().authorizationStatus + if status == .authorizedAlways { return true } let updated = await LocationPermissionRequester.shared.request(always: mode == .always) - switch updated { - case .authorizedAlways, .authorizedWhenInUse: - return true - default: - return false - } + return updated == .authorizedAlways } private var connectionSection: some View { diff --git a/apps/macos/Sources/Clawdbot/HealthStore.swift b/apps/macos/Sources/Clawdbot/HealthStore.swift index d0d8ac145c..c2974be8a8 100644 --- a/apps/macos/Sources/Clawdbot/HealthStore.swift +++ b/apps/macos/Sources/Clawdbot/HealthStore.swift @@ -189,7 +189,8 @@ final class HealthStore { let lower = error.lowercased() if lower.contains("connection refused") { let port = GatewayEnvironment.gatewayPort() - return "The gateway control port (127.0.0.1:\(port)) isn’t listening — restart Clawdbot to bring it back." + return "The gateway control port (127.0.0.1:\(port)) isn’t listening — " + + "restart Clawdbot to bring it back." } if lower.contains("timeout") { return "Timed out waiting for the control server; the gateway may be crashed or still starting." diff --git a/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift b/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift index 3768a22ec4..8c525129a7 100644 --- a/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift @@ -6,6 +6,7 @@ enum LaunchAgentManager { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist") } + private static var legacyPlistURL: URL { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist") @@ -19,7 +20,7 @@ enum LaunchAgentManager { static func set(enabled: Bool, bundlePath: String) async { if enabled { - _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(legacyLaunchdLabel)"]) + _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyLaunchdLabel)"]) try? FileManager.default.removeItem(at: self.legacyPlistURL) self.writePlist(bundlePath: bundlePath) _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"]) diff --git a/apps/macos/Sources/Clawdbot/Logging/ClawdbotLogging.swift b/apps/macos/Sources/Clawdbot/Logging/ClawdbotLogging.swift index c9f66d2b90..4a19165c14 100644 --- a/apps/macos/Sources/Clawdbot/Logging/ClawdbotLogging.swift +++ b/apps/macos/Sources/Clawdbot/Logging/ClawdbotLogging.swift @@ -1,7 +1,7 @@ -@_exported import Logging import Foundation -import OSLog +@_exported import Logging import os +import OSLog typealias Logger = Logging.Logger @@ -65,15 +65,15 @@ enum ClawdbotLogging { }() static func bootstrapIfNeeded() { - _ = Self.didBootstrap + _ = self.didBootstrap } static func makeLabel(subsystem: String, category: String) -> String { - "\(subsystem)\(Self.labelSeparator)\(category)" + "\(subsystem)\(self.labelSeparator)\(category)" } static func parseLabel(_ label: String) -> (String, String) { - guard let range = label.range(of: Self.labelSeparator) else { + guard let range = label.range(of: labelSeparator) else { return ("com.clawdbot", label) } let subsystem = String(label[..(_ value: T, privacy: OSLogPrivacy) { + mutating func appendInterpolation(_ value: some Any, privacy: OSLogPrivacy) { self.appendInterpolation(String(describing: value)) } } @@ -114,7 +114,6 @@ struct ClawdbotOSLogHandler: LogHandler { set { self.metadata[key] = newValue } } - // swiftlint:disable:next function_parameter_count func log( level: Logger.Level, message: Logger.Message, @@ -132,15 +131,15 @@ struct ClawdbotOSLogHandler: LogHandler { private static func osLogType(for level: Logger.Level) -> OSLogType { switch level { case .trace, .debug: - return .debug + .debug case .info, .notice: - return .info + .info case .warning: - return .default + .default case .error: - return .error + .error case .critical: - return .fault + .fault } } @@ -156,7 +155,7 @@ struct ClawdbotOSLogHandler: LogHandler { guard !metadata.isEmpty else { return message.description } let meta = metadata .sorted(by: { $0.key < $1.key }) - .map { "\($0.key)=\(stringify($0.value))" } + .map { "\($0.key)=\(self.stringify($0.value))" } .joined(separator: " ") return "\(message.description) [\(meta)]" } @@ -168,9 +167,9 @@ struct ClawdbotOSLogHandler: LogHandler { case let .stringConvertible(value): String(describing: value) case let .array(values): - "[" + values.map { stringify($0) }.joined(separator: ",") + "]" + "[" + values.map { self.stringify($0) }.joined(separator: ",") + "]" case let .dictionary(entries): - "{" + entries.map { "\($0.key)=\(stringify($0.value))" }.joined(separator: ",") + "}" + "{" + entries.map { "\($0.key)=\(self.stringify($0.value))" }.joined(separator: ",") + "}" } } } @@ -189,7 +188,6 @@ struct ClawdbotFileLogHandler: LogHandler { set { self.metadata[key] = newValue } } - // swiftlint:disable:next function_parameter_count func log( level: Logger.Level, message: Logger.Message, @@ -224,9 +222,9 @@ struct ClawdbotFileLogHandler: LogHandler { case let .stringConvertible(value): String(describing: value) case let .array(values): - "[" + values.map { stringify($0) }.joined(separator: ",") + "]" + "[" + values.map { self.stringify($0) }.joined(separator: ",") + "]" case let .dictionary(entries): - "{" + entries.map { "\($0.key)=\(stringify($0.value))" }.joined(separator: ",") + "}" + "{" + entries.map { "\($0.key)=\(self.stringify($0.value))" }.joined(separator: ",") + "}" } } } diff --git a/apps/macos/Sources/Clawdbot/MenuContentView.swift b/apps/macos/Sources/Clawdbot/MenuContentView.swift index 8549a71b82..83cd5b5b7b 100644 --- a/apps/macos/Sources/Clawdbot/MenuContentView.swift +++ b/apps/macos/Sources/Clawdbot/MenuContentView.swift @@ -172,7 +172,7 @@ struct MenuContent: View { } @MainActor - private static func buildAndSaveBrowserEnabled(_ enabled: Bool) async -> (Bool,()) { + private static func buildAndSaveBrowserEnabled(_ enabled: Bool) async -> (Bool, ()) { var root = await ConfigStore.load() var browser = root["browser"] as? [String: Any] ?? [:] browser["enabled"] = enabled diff --git a/apps/macos/Sources/Clawdbot/MenuHighlightedHostView.swift b/apps/macos/Sources/Clawdbot/MenuHighlightedHostView.swift index 4f0d5f03da..f1e85cba15 100644 --- a/apps/macos/Sources/Clawdbot/MenuHighlightedHostView.swift +++ b/apps/macos/Sources/Clawdbot/MenuHighlightedHostView.swift @@ -86,7 +86,6 @@ final class HighlightedMenuItemHostView: NSView { self.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height)) self.invalidateIntrinsicContentSize() } - } struct MenuHostedHighlightedItem: NSViewRepresentable { diff --git a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift index 7f3a22b8d8..347c56bccc 100644 --- a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift +++ b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift @@ -435,7 +435,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { compact.representedObject = row.key menu.addItem(compact) - if row.key != "main" && row.key != "global" { + if row.key != "main", row.key != "global" { let del = NSMenuItem(title: "Delete Session", action: #selector(self.deleteSession(_:)), keyEquivalent: "") del.target = self del.representedObject = row.key @@ -541,12 +541,14 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { menu.addItem(self.makeNodeDetailItem(label: "Paired", value: entry.isPaired ? "Yes" : "No")) if let caps = entry.caps?.filter({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }), - !caps.isEmpty { + !caps.isEmpty + { menu.addItem(self.makeNodeCopyItem(label: "Caps", value: caps.joined(separator: ", "))) } if let commands = entry.commands?.filter({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }), - !commands.isEmpty { + !commands.isEmpty + { menu.addItem(self.makeNodeMultilineItem( label: "Commands", value: commands.joined(separator: ", "), @@ -589,6 +591,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { } return trimmed } + @objc private func patchThinking(_ sender: NSMenuItem) { guard let dict = sender.representedObject as? [String: Any], @@ -770,7 +773,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { } private func sortedNodeEntries() -> [NodeInfo] { - let entries = self.nodesStore.nodes.filter { $0.isConnected } + let entries = self.nodesStore.nodes.filter(\.isConnected) return entries.sorted { lhs, rhs in if lhs.isConnected != rhs.isConnected { return lhs.isConnected } if lhs.isPaired != rhs.isPaired { return lhs.isPaired } @@ -781,8 +784,6 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { } } - - // MARK: - Views private func makeHostedView(rootView: AnyView, width: CGFloat, highlighted: Bool) -> NSView { diff --git a/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift b/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift index f8f9d23fe7..17532ec8ed 100644 --- a/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift +++ b/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift @@ -611,7 +611,7 @@ final class NodePairingApprovalPrompter { private func updatePendingCounts() { // Keep a cheap observable summary for the menu bar status line. self.pendingCount = self.queue.count - self.pendingRepairCount = self.queue.filter { $0.isRepair == true }.count + self.pendingRepairCount = self.queue.count(where: { $0.isRepair == true }) } private func reconcileOnce(timeoutMs: Double) async { diff --git a/apps/macos/Sources/Clawdbot/NodesMenu.swift b/apps/macos/Sources/Clawdbot/NodesMenu.swift index 05fb85f3b2..3ba560aafe 100644 --- a/apps/macos/Sources/Clawdbot/NodesMenu.swift +++ b/apps/macos/Sources/Clawdbot/NodesMenu.swift @@ -110,8 +110,8 @@ struct NodeMenuEntryFormatter { guard !trimmed.isEmpty else { return trimmed } if let range = trimmed.range( of: #"\s*\([^)]*\d[^)]*\)$"#, - options: .regularExpression - ) { + options: .regularExpression) + { return String(trimmed[.. (wizardLockIndex ?? 0) + let isLocked = wizardLockIndex != nil && !self.onboardingWizard + .isComplete && index > (wizardLockIndex ?? 0) Button { withAnimation { self.currentPage = index } } label: { diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift index d1a950e0e9..9ee1d266ab 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift @@ -198,7 +198,9 @@ extension OnboardingView { Text("CLI path") .font(.callout.weight(.semibold)) .frame(width: labelWidth, alignment: .leading) - TextField("/Applications/Clawdbot.app/.../clawdbot", text: self.$state.remoteCliPath) + TextField( + "/Applications/Clawdbot.app/.../clawdbot", + text: self.$state.remoteCliPath) .textFieldStyle(.roundedBorder) .frame(width: fieldWidth) } @@ -444,7 +446,7 @@ extension OnboardingView { } func permissionsPage() -> some View { - return self.onboardingPage { + self.onboardingPage { Text("Grant permissions") .font(.largeTitle.weight(.semibold)) Text("These macOS permissions let Clawdbot automate apps and capture context on this Mac.") diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Wizard.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Wizard.swift index a43cb74c61..e2776945d7 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Wizard.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Wizard.swift @@ -44,15 +44,15 @@ private struct OnboardingWizardCardContent: View { private var state: CardState { if let error = wizard.errorMessage { return .error(error) } - if wizard.isStarting { return .starting } + if self.wizard.isStarting { return .starting } if let step = wizard.currentStep { return .step(step) } - if wizard.isComplete { return .complete } + if self.wizard.isComplete { return .complete } return .waiting } var body: some View { - switch state { - case .error(let error): + switch self.state { + case let .error(error): Text("Wizard error") .font(.headline) Text(error) @@ -60,11 +60,11 @@ private struct OnboardingWizardCardContent: View { .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) Button("Retry") { - wizard.reset() + self.wizard.reset() Task { - await wizard.startIfNeeded( - mode: mode, - workspace: workspacePath.isEmpty ? nil : workspacePath) + await self.wizard.startIfNeeded( + mode: self.mode, + workspace: self.workspacePath.isEmpty ? nil : self.workspacePath) } } .buttonStyle(.borderedProminent) @@ -74,12 +74,12 @@ private struct OnboardingWizardCardContent: View { Text("Starting wizard…") .foregroundStyle(.secondary) } - case .step(let step): + case let .step(step): OnboardingWizardStepView( step: step, - isSubmitting: wizard.isSubmitting) + isSubmitting: self.wizard.isSubmitting) { value in - Task { await wizard.submit(step: step, value: value) } + Task { await self.wizard.submit(step: step, value: value) } } .id(step.id) case .complete: diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Workspace.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Workspace.swift index 6e602b9daf..fa35a0af20 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Workspace.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Workspace.swift @@ -101,7 +101,7 @@ extension OnboardingView { do { try await ConfigStore.save(root) return (true, nil) - } catch let error { + } catch { let errorMessage = "Failed to save config: \(error.localizedDescription)" return (false, errorMessage) } diff --git a/apps/macos/Sources/Clawdbot/OnboardingWizard.swift b/apps/macos/Sources/Clawdbot/OnboardingWizard.swift index 4b3fcd049e..845ea05310 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingWizard.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingWizard.swift @@ -7,6 +7,7 @@ import SwiftUI private let onboardingWizardLogger = Logger(subsystem: "com.clawdbot", category: "onboarding.wizard") // MARK: - Swift 6 AnyCodable Bridging Helpers + // Bridge between ClawdbotProtocol.AnyCodable and the local module to avoid // Swift 6 strict concurrency type conflicts. @@ -62,7 +63,7 @@ final class OnboardingWizardModel { let res: WizardStartResult = try await GatewayConnection.shared.requestDecoded( method: .wizardStart, params: params) - applyStartResult(res) + self.applyStartResult(res) } catch { self.status = "error" self.errorMessage = error.localizedDescription @@ -86,7 +87,7 @@ final class OnboardingWizardModel { let res: WizardNextResult = try await GatewayConnection.shared.requestDecoded( method: .wizardNext, params: params) - applyNextResult(res) + self.applyNextResult(res) } catch { self.status = "error" self.errorMessage = error.localizedDescription @@ -100,7 +101,7 @@ final class OnboardingWizardModel { let res: WizardStatusResult = try await GatewayConnection.shared.requestDecoded( method: .wizardCancel, params: ["sessionId": AnyCodable(sessionId)]) - applyStatusResult(res) + self.applyStatusResult(res) } catch { self.status = "error" self.errorMessage = error.localizedDescription @@ -122,7 +123,8 @@ final class OnboardingWizardModel { self.currentStep = decodeWizardStep(res.step) if res.done { self.currentStep = nil } if res.done || anyCodableStringValue(res.status) == "done" || anyCodableStringValue(res.status) == "cancelled" - || anyCodableStringValue(res.status) == "error" { + || anyCodableStringValue(res.status) == "error" + { self.sessionId = nil } } @@ -161,8 +163,7 @@ struct OnboardingWizardStepView: View { let initialMulti = Set( options.filter { option in anyCodableArray(step.initialvalue).contains { anyCodableEqual($0, option.option.value) } - }.map { $0.index } - ) + }.map(\.index)) _textValue = State(initialValue: initialText) _confirmValue = State(initialValue: initialConfirm) @@ -183,18 +184,18 @@ struct OnboardingWizardStepView: View { .fixedSize(horizontal: false, vertical: true) } - switch wizardStepType(step) { + switch wizardStepType(self.step) { case "note": EmptyView() case "text": - textField + self.textField case "confirm": - Toggle("", isOn: $confirmValue) + Toggle("", isOn: self.$confirmValue) .toggleStyle(.switch) case "select": - selectOptions + self.selectOptions case "multiselect": - multiselectOptions + self.multiselectOptions case "progress": ProgressView() .controlSize(.small) @@ -205,25 +206,25 @@ struct OnboardingWizardStepView: View { .foregroundStyle(.secondary) } - Button(action: submit) { - Text(wizardStepType(step) == "action" ? "Run" : "Continue") + Button(action: self.submit) { + Text(wizardStepType(self.step) == "action" ? "Run" : "Continue") .frame(minWidth: 120) } .buttonStyle(.borderedProminent) - .disabled(isSubmitting || isBlocked) + .disabled(self.isSubmitting || self.isBlocked) } .frame(maxWidth: .infinity, alignment: .leading) } @ViewBuilder private var textField: some View { - let isSensitive = step.sensitive == true + let isSensitive = self.step.sensitive == true if isSensitive { - SecureField(step.placeholder ?? "", text: $textValue) + SecureField(self.step.placeholder ?? "", text: self.$textValue) .textFieldStyle(.roundedBorder) .frame(maxWidth: 360) } else { - TextField(step.placeholder ?? "", text: $textValue) + TextField(self.step.placeholder ?? "", text: self.$textValue) .textFieldStyle(.roundedBorder) .frame(maxWidth: 360) } @@ -231,12 +232,12 @@ struct OnboardingWizardStepView: View { private var selectOptions: some View { VStack(alignment: .leading, spacing: 8) { - ForEach(optionItems) { item in + ForEach(self.optionItems) { item in Button { - selectedIndex = item.index + self.selectedIndex = item.index } label: { HStack(alignment: .top, spacing: 8) { - Image(systemName: selectedIndex == item.index ? "largecircle.fill.circle" : "circle") + Image(systemName: self.selectedIndex == item.index ? "largecircle.fill.circle" : "circle") .foregroundStyle(.accent) VStack(alignment: .leading, spacing: 2) { Text(item.option.label) @@ -256,8 +257,8 @@ struct OnboardingWizardStepView: View { private var multiselectOptions: some View { VStack(alignment: .leading, spacing: 8) { - ForEach(optionItems) { item in - Toggle(isOn: bindingForOption(item)) { + ForEach(self.optionItems) { item in + Toggle(isOn: self.bindingForOption(item)) { VStack(alignment: .leading, spacing: 2) { Text(item.option.label) if let hint = item.option.hint, !hint.isEmpty { @@ -273,47 +274,47 @@ struct OnboardingWizardStepView: View { private func bindingForOption(_ item: WizardOptionItem) -> Binding { Binding(get: { - selectedIndices.contains(item.index) + self.selectedIndices.contains(item.index) }, set: { newValue in if newValue { - selectedIndices.insert(item.index) + self.selectedIndices.insert(item.index) } else { - selectedIndices.remove(item.index) + self.selectedIndices.remove(item.index) } }) } private var isBlocked: Bool { let type = wizardStepType(step) - if type == "select" { return optionItems.isEmpty } - if type == "multiselect" { return optionItems.isEmpty } + if type == "select" { return self.optionItems.isEmpty } + if type == "multiselect" { return self.optionItems.isEmpty } return false } private func submit() { - switch wizardStepType(step) { + switch wizardStepType(self.step) { case "note", "progress": - onSubmit(nil) + self.onSubmit(nil) case "text": - onSubmit(AnyCodable(textValue)) + self.onSubmit(AnyCodable(self.textValue)) case "confirm": - onSubmit(AnyCodable(confirmValue)) + self.onSubmit(AnyCodable(self.confirmValue)) case "select": - guard optionItems.indices.contains(selectedIndex) else { - onSubmit(nil) + guard self.optionItems.indices.contains(self.selectedIndex) else { + self.onSubmit(nil) return } - let option = optionItems[selectedIndex].option - onSubmit(bridgeToLocal(option.value) ?? AnyCodable(option.label)) + let option = self.optionItems[self.selectedIndex].option + self.onSubmit(bridgeToLocal(option.value) ?? AnyCodable(option.label)) case "multiselect": - let values = optionItems - .filter { selectedIndices.contains($0.index) } + let values = self.optionItems + .filter { self.selectedIndices.contains($0.index) } .map { bridgeToLocal($0.option.value) ?? AnyCodable($0.option.label) } - onSubmit(AnyCodable(values)) + self.onSubmit(AnyCodable(values)) case "action": - onSubmit(AnyCodable(true)) + self.onSubmit(AnyCodable(true)) default: - onSubmit(nil) + self.onSubmit(nil) } } } @@ -322,7 +323,7 @@ private struct WizardOptionItem: Identifiable { let index: Int let option: WizardOption - var id: Int { index } + var id: Int { self.index } } private struct WizardOption { @@ -359,15 +360,15 @@ private func wizardStepType(_ step: WizardStep) -> String { private func anyCodableString(_ value: ProtocolAnyCodable?) -> String { switch value?.value { case let string as String: - return string + string case let int as Int: - return String(int) + String(int) case let double as Double: - return String(double) + String(double) case let bool as Bool: - return bool ? "true" : "false" + bool ? "true" : "false" default: - return "" + "" } } @@ -378,44 +379,44 @@ private func anyCodableStringValue(_ value: ProtocolAnyCodable?) -> String? { private func anyCodableBool(_ value: ProtocolAnyCodable?) -> Bool { switch value?.value { case let bool as Bool: - return bool + bool case let string as String: - return string.lowercased() == "true" + string.lowercased() == "true" default: - return false + false } } private func anyCodableArray(_ value: ProtocolAnyCodable?) -> [ProtocolAnyCodable] { switch value?.value { case let arr as [ProtocolAnyCodable]: - return arr + arr case let arr as [Any]: - return arr.map { ProtocolAnyCodable($0) } + arr.map { ProtocolAnyCodable($0) } default: - return [] + [] } } private func anyCodableEqual(_ lhs: ProtocolAnyCodable?, _ rhs: ProtocolAnyCodable?) -> Bool { switch (lhs?.value, rhs?.value) { case let (l as String, r as String): - return l == r + l == r case let (l as Int, r as Int): - return l == r + l == r case let (l as Double, r as Double): - return l == r + l == r case let (l as Bool, r as Bool): - return l == r + l == r case let (l as String, r as Int): - return l == String(r) + l == String(r) case let (l as Int, r as String): - return String(l) == r + String(l) == r case let (l as String, r as Double): - return l == String(r) + l == String(r) case let (l as Double, r as String): - return String(l) == r + String(l) == r default: - return false + false } } diff --git a/apps/macos/Sources/Clawdbot/PeekabooBridgeHostCoordinator.swift b/apps/macos/Sources/Clawdbot/PeekabooBridgeHostCoordinator.swift index 397cf71c32..dbdb22be45 100644 --- a/apps/macos/Sources/Clawdbot/PeekabooBridgeHostCoordinator.swift +++ b/apps/macos/Sources/Clawdbot/PeekabooBridgeHostCoordinator.swift @@ -1,9 +1,9 @@ import Foundation -import Security import os import PeekabooAutomationKit import PeekabooBridge import PeekabooFoundation +import Security @MainActor final class PeekabooBridgeHostCoordinator { @@ -80,7 +80,7 @@ final class PeekabooBridgeHostCoordinator { staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &infoCF) == errSecSuccess, - let info = infoCF as? [String: Any] + let info = infoCF as? [String: Any] else { return nil } diff --git a/apps/macos/Sources/Clawdbot/PermissionManager.swift b/apps/macos/Sources/Clawdbot/PermissionManager.swift index 1fe1d5523b..a89b2d596a 100644 --- a/apps/macos/Sources/Clawdbot/PermissionManager.swift +++ b/apps/macos/Sources/Clawdbot/PermissionManager.swift @@ -138,14 +138,14 @@ enum PermissionManager { } private static func ensureLocation(interactive: Bool) async -> Bool { - let status = CLLocationManager.authorizationStatus() + let status = CLLocationManager().authorizationStatus switch status { - case .authorizedAlways, .authorizedWhenInUse: + case .authorizedAlways: return true case .notDetermined: guard interactive else { return false } let updated = await LocationPermissionRequester.shared.request(always: false) - return updated == .authorizedAlways || updated == .authorizedWhenInUse + return updated == .authorizedAlways case .denied, .restricted: if interactive { LocationPermissionHelper.openSettings() @@ -198,9 +198,10 @@ enum PermissionManager { case .camera: results[cap] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized + case .location: - let status = CLLocationManager.authorizationStatus() - results[cap] = status == .authorizedAlways || status == .authorizedWhenInUse + let status = CLLocationManager().authorizationStatus + results[cap] = status == .authorizedAlways } } return results diff --git a/apps/macos/Sources/Clawdbot/PermissionsSettings.swift b/apps/macos/Sources/Clawdbot/PermissionsSettings.swift index 749ebd2be0..1994ba4064 100644 --- a/apps/macos/Sources/Clawdbot/PermissionsSettings.swift +++ b/apps/macos/Sources/Clawdbot/PermissionsSettings.swift @@ -48,7 +48,8 @@ struct PermissionStatusList: View { if (self.status[.accessibility] ?? false) == false || (self.status[.screenRecording] ?? false) == false { VStack(alignment: .leading, spacing: 8) { - Text("Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.") + Text( + "Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.") .font(.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) diff --git a/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift b/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift index ba8d4ef440..d03a446ef3 100644 --- a/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift +++ b/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift @@ -53,10 +53,12 @@ final class RemotePortTunnel { let resolvedRemotePort = remotePortOverride ?? remotePort if let override = remotePortOverride { Self.logger.info( - "ssh tunnel remote port override host=\(sshHost, privacy: .public) port=\(override, privacy: .public)") + "ssh tunnel remote port override " + + "host=\(sshHost, privacy: .public) port=\(override, privacy: .public)") } else { Self.logger.debug( - "ssh tunnel using default remote port host=\(sshHost, privacy: .public) port=\(remotePort, privacy: .public)") + "ssh tunnel using default remote port " + + "host=\(sshHost, privacy: .public) port=\(remotePort, privacy: .public)") } var args: [String] = [ "-o", "BatchMode=yes", diff --git a/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift b/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift index be41d21f98..9330e9c27b 100644 --- a/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift +++ b/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift @@ -29,7 +29,9 @@ actor RemoteTunnelManager { { if await self.isTunnelHealthy(port: desiredPort) { self.logger.info( - "reusing existing SSH tunnel listener localPort=\(desiredPort, privacy: .public) pid=\(desc.pid, privacy: .public)") + "reusing existing SSH tunnel listener " + + "localPort=\(desiredPort, privacy: .public) " + + "pid=\(desc.pid, privacy: .public)") return desiredPort } await self.cleanupStaleTunnel(desc: desc, port: desiredPort) @@ -50,7 +52,8 @@ actor RemoteTunnelManager { let identitySet = !settings.identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty self.logger.info( - "ensure SSH tunnel target=\(settings.target, privacy: .public) identitySet=\(identitySet, privacy: .public)") + "ensure SSH tunnel target=\(settings.target, privacy: .public) " + + "identitySet=\(identitySet, privacy: .public)") if let local = await self.controlTunnelPortIfRunning() { return local } diff --git a/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift b/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift index df98f68b30..76bf271fd3 100644 --- a/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift +++ b/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift @@ -174,12 +174,13 @@ struct SessionMenuPreviewView: View { let timeoutMs = Int(Self.previewTimeoutSeconds * 1000) let payload = try await AsyncTimeout.withTimeout( seconds: Self.previewTimeoutSeconds, - onTimeout: { PreviewTimeoutError() }) { + onTimeout: { PreviewTimeoutError() }, + operation: { try await GatewayConnection.shared.chatHistory( sessionKey: self.sessionKey, limit: self.previewLimit, timeoutMs: timeoutMs) - } + }) let built = Self.previewItems(from: payload, maxItems: self.maxItems) await SessionPreviewCache.shared.store(items: built, for: self.sessionKey) await MainActor.run { @@ -198,7 +199,10 @@ struct SessionMenuPreviewView: View { self.status = .error("Preview unavailable") } } - Self.logger.warning("Session preview failed session=\(self.sessionKey, privacy: .public) error=\(String(describing: error), privacy: .public)") + let errorDescription = String(describing: error) + Self.logger.warning( + "Session preview failed session=\(self.sessionKey, privacy: .public) " + + "error=\(errorDescription, privacy: .public)") } } diff --git a/apps/macos/Sources/Clawdbot/TailscaleIntegrationSection.swift b/apps/macos/Sources/Clawdbot/TailscaleIntegrationSection.swift index edb601d79d..f523660f64 100644 --- a/apps/macos/Sources/Clawdbot/TailscaleIntegrationSection.swift +++ b/apps/macos/Sources/Clawdbot/TailscaleIntegrationSection.swift @@ -285,8 +285,7 @@ struct TailscaleIntegrationSection: View { requireCredentialsForServe: self.requireCredentialsForServe, password: trimmedPassword, connectionMode: self.connectionMode, - isPaused: self.isPaused - ) + isPaused: self.isPaused) if !success, let errorMessage { self.statusMessage = errorMessage @@ -307,8 +306,8 @@ struct TailscaleIntegrationSection: View { requireCredentialsForServe: Bool, password: String, connectionMode: AppState.ConnectionMode, - isPaused: Bool - ) async -> (Bool, String?) { + isPaused: Bool) async -> (Bool, String?) + { var root = await ConfigStore.load() var gateway = root["gateway"] as? [String: Any] ?? [:] var tailscale = gateway["tailscale"] as? [String: Any] ?? [:] @@ -349,7 +348,7 @@ struct TailscaleIntegrationSection: View { do { try await ConfigStore.save(root) return (true, nil) - } catch let error { + } catch { return (false, error.localizedDescription) } } diff --git a/apps/macos/Sources/Clawdbot/TalkModeRuntime.swift b/apps/macos/Sources/Clawdbot/TalkModeRuntime.swift index 92532fcdb0..ee964628b2 100644 --- a/apps/macos/Sources/Clawdbot/TalkModeRuntime.swift +++ b/apps/macos/Sources/Clawdbot/TalkModeRuntime.swift @@ -436,14 +436,49 @@ actor TalkModeRuntime { } } - // swiftlint:disable:next cyclomatic_complexity function_body_length private func playAssistant(text: String) async { + guard let input = await self.preparePlaybackInput(text: text) else { return } + do { + if let apiKey = input.apiKey, !apiKey.isEmpty, let voiceId = input.voiceId { + try await self.playElevenLabs(input: input, apiKey: apiKey, voiceId: voiceId) + } else { + try await self.playSystemVoice(input: input) + } + } catch { + self.ttsLogger + .error( + "talk TTS failed: \(error.localizedDescription, privacy: .public); " + + "falling back to system voice") + do { + try await self.playSystemVoice(input: input) + } catch { + self.ttsLogger.error("talk system voice failed: \(error.localizedDescription, privacy: .public)") + } + } + + if self.phase == .speaking { + self.phase = .thinking + await MainActor.run { TalkModeController.shared.updatePhase(.thinking) } + } + } + + private struct TalkPlaybackInput { + let generation: Int + let cleanedText: String + let directive: TalkDirective? + let apiKey: String? + let voiceId: String? + let language: String? + let synthTimeoutSeconds: Double + } + + private func preparePlaybackInput(text: String) async -> TalkPlaybackInput? { let gen = self.lifecycleGeneration let parse = TalkDirectiveParser.parse(text) let directive = parse.directive let cleaned = parse.stripped.trimmingCharacters(in: .whitespacesAndNewlines) - guard !cleaned.isEmpty else { return } - guard self.isCurrent(gen) else { return } + guard !cleaned.isEmpty else { return nil } + guard self.isCurrent(gen) else { return nil } if !parse.unknownKeys.isEmpty { self.logger @@ -504,116 +539,123 @@ actor TalkModeRuntime { let synthTimeoutSeconds = max(20.0, min(90.0, Double(cleaned.count) * 0.12)) - do { - if let apiKey, !apiKey.isEmpty, let voiceId { - let desiredOutputFormat = directive?.outputFormat ?? self.defaultOutputFormat ?? "pcm_44100" - let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(desiredOutputFormat) - if outputFormat == nil, !desiredOutputFormat.isEmpty { - self.logger - .warning( - "talk output_format unsupported for local playback: " + - "\(desiredOutputFormat, privacy: .public)") - } + guard self.isCurrent(gen) else { return nil } - let modelId = directive?.modelId ?? self.currentModelId ?? self.defaultModelId - func makeRequest(outputFormat: String?) -> ElevenLabsTTSRequest { - ElevenLabsTTSRequest( - text: cleaned, - modelId: modelId, - outputFormat: outputFormat, - speed: TalkTTSValidation.resolveSpeed(speed: directive?.speed, rateWPM: directive?.rateWPM), - stability: TalkTTSValidation.validatedStability(directive?.stability, modelId: modelId), - similarity: TalkTTSValidation.validatedUnit(directive?.similarity), - style: TalkTTSValidation.validatedUnit(directive?.style), - speakerBoost: directive?.speakerBoost, - seed: TalkTTSValidation.validatedSeed(directive?.seed), - normalize: ElevenLabsTTSClient.validatedNormalize(directive?.normalize), - language: language, - latencyTier: TalkTTSValidation.validatedLatencyTier(directive?.latencyTier)) - } + return TalkPlaybackInput( + generation: gen, + cleanedText: cleaned, + directive: directive, + apiKey: apiKey, + voiceId: voiceId, + language: language, + synthTimeoutSeconds: synthTimeoutSeconds) + } - let request = makeRequest(outputFormat: outputFormat) - - self.ttsLogger.info("talk TTS synth timeout=\(synthTimeoutSeconds, privacy: .public)s") - let client = ElevenLabsTTSClient(apiKey: apiKey) - let stream = client.streamSynthesize(voiceId: voiceId, request: request) - guard self.isCurrent(gen) else { return } - - if self.interruptOnSpeech { - await self.startRecognition() - guard self.isCurrent(gen) else { return } - } - - await MainActor.run { TalkModeController.shared.updatePhase(.speaking) } - self.phase = .speaking - - let sampleRate = TalkTTSValidation.pcmSampleRate(from: outputFormat) - var result: StreamingPlaybackResult - if let sampleRate { - self.lastPlaybackWasPCM = true - result = await self.playPCM(stream: stream, sampleRate: sampleRate) - if !result.finished, result.interruptedAt == nil { - let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100") - self.ttsLogger.warning("talk pcm playback failed; retrying mp3") - self.lastPlaybackWasPCM = false - let mp3Stream = client.streamSynthesize( - voiceId: voiceId, - request: makeRequest(outputFormat: mp3Format)) - result = await self.playMP3(stream: mp3Stream) - } - } else { - self.lastPlaybackWasPCM = false - result = await self.playMP3(stream: stream) - } - self.ttsLogger - .info( - "talk audio result finished=\(result.finished, privacy: .public) " + - "interruptedAt=\(String(describing: result.interruptedAt), privacy: .public)") - if !result.finished, result.interruptedAt == nil { - throw NSError(domain: "StreamingAudioPlayer", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "audio playback failed", - ]) - } - if !result.finished, let interruptedAt = result.interruptedAt, self.phase == .speaking { - if self.interruptOnSpeech { - self.lastInterruptedAtSeconds = interruptedAt - } - } - } else { - self.ttsLogger.info("talk system voice start chars=\(cleaned.count, privacy: .public)") - if self.interruptOnSpeech { - await self.startRecognition() - guard self.isCurrent(gen) else { return } - } - await MainActor.run { TalkModeController.shared.updatePhase(.speaking) } - self.phase = .speaking - await TalkSystemSpeechSynthesizer.shared.stop() - try await TalkSystemSpeechSynthesizer.shared.speak(text: cleaned, language: language) - self.ttsLogger.info("talk system voice done") - } - } catch { - self.ttsLogger - .error( - "talk TTS failed: \(error.localizedDescription, privacy: .public); " + - "falling back to system voice") - do { - if self.interruptOnSpeech { - await self.startRecognition() - guard self.isCurrent(gen) else { return } - } - await MainActor.run { TalkModeController.shared.updatePhase(.speaking) } - self.phase = .speaking - await TalkSystemSpeechSynthesizer.shared.stop() - try await TalkSystemSpeechSynthesizer.shared.speak(text: cleaned, language: language) - } catch { - self.ttsLogger.error("talk system voice failed: \(error.localizedDescription, privacy: .public)") - } + private func playElevenLabs(input: TalkPlaybackInput, apiKey: String, voiceId: String) async throws { + let desiredOutputFormat = input.directive?.outputFormat ?? self.defaultOutputFormat ?? "pcm_44100" + let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(desiredOutputFormat) + if outputFormat == nil, !desiredOutputFormat.isEmpty { + self.logger + .warning( + "talk output_format unsupported for local playback: " + + "\(desiredOutputFormat, privacy: .public)") } - if self.phase == .speaking { - self.phase = .thinking - await MainActor.run { TalkModeController.shared.updatePhase(.thinking) } + let modelId = input.directive?.modelId ?? self.currentModelId ?? self.defaultModelId + func makeRequest(outputFormat: String?) -> ElevenLabsTTSRequest { + ElevenLabsTTSRequest( + text: input.cleanedText, + modelId: modelId, + outputFormat: outputFormat, + speed: TalkTTSValidation.resolveSpeed( + speed: input.directive?.speed, + rateWPM: input.directive?.rateWPM), + stability: TalkTTSValidation.validatedStability( + input.directive?.stability, + modelId: modelId), + similarity: TalkTTSValidation.validatedUnit(input.directive?.similarity), + style: TalkTTSValidation.validatedUnit(input.directive?.style), + speakerBoost: input.directive?.speakerBoost, + seed: TalkTTSValidation.validatedSeed(input.directive?.seed), + normalize: ElevenLabsTTSClient.validatedNormalize(input.directive?.normalize), + language: input.language, + latencyTier: TalkTTSValidation.validatedLatencyTier(input.directive?.latencyTier)) } + + let request = makeRequest(outputFormat: outputFormat) + self.ttsLogger.info("talk TTS synth timeout=\(input.synthTimeoutSeconds, privacy: .public)s") + let client = ElevenLabsTTSClient(apiKey: apiKey) + let stream = client.streamSynthesize(voiceId: voiceId, request: request) + guard self.isCurrent(input.generation) else { return } + + if self.interruptOnSpeech, ! await self.prepareForPlayback(generation: input.generation) { return } + + await MainActor.run { TalkModeController.shared.updatePhase(.speaking) } + self.phase = .speaking + + let result = await self.playRemoteStream( + client: client, + voiceId: voiceId, + outputFormat: outputFormat, + makeRequest: makeRequest, + stream: stream) + self.ttsLogger + .info( + "talk audio result finished=\(result.finished, privacy: .public) " + + "interruptedAt=\(String(describing: result.interruptedAt), privacy: .public)") + if !result.finished, result.interruptedAt == nil { + throw NSError(domain: "StreamingAudioPlayer", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "audio playback failed", + ]) + } + if !result.finished, let interruptedAt = result.interruptedAt, self.phase == .speaking { + if self.interruptOnSpeech { + self.lastInterruptedAtSeconds = interruptedAt + } + } + } + + private func playRemoteStream( + client: ElevenLabsTTSClient, + voiceId: String, + outputFormat: String?, + makeRequest: (String?) -> ElevenLabsTTSRequest, + stream: AsyncThrowingStream) async -> StreamingPlaybackResult + { + let sampleRate = TalkTTSValidation.pcmSampleRate(from: outputFormat) + if let sampleRate { + self.lastPlaybackWasPCM = true + let result = await self.playPCM(stream: stream, sampleRate: sampleRate) + if result.finished || result.interruptedAt != nil { + return result + } + let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100") + self.ttsLogger.warning("talk pcm playback failed; retrying mp3") + self.lastPlaybackWasPCM = false + let mp3Stream = client.streamSynthesize( + voiceId: voiceId, + request: makeRequest(mp3Format)) + return await self.playMP3(stream: mp3Stream) + } + self.lastPlaybackWasPCM = false + return await self.playMP3(stream: stream) + } + + private func playSystemVoice(input: TalkPlaybackInput) async throws { + self.ttsLogger.info("talk system voice start chars=\(input.cleanedText.count, privacy: .public)") + if self.interruptOnSpeech, ! await self.prepareForPlayback(generation: input.generation) { return } + await MainActor.run { TalkModeController.shared.updatePhase(.speaking) } + self.phase = .speaking + await TalkSystemSpeechSynthesizer.shared.stop() + try await TalkSystemSpeechSynthesizer.shared.speak( + text: input.cleanedText, + language: input.language) + self.ttsLogger.info("talk system voice done") + } + + private func prepareForPlayback(generation: Int) async -> Bool { + await self.startRecognition() + return self.isCurrent(generation) } private func resolveVoiceId(preferred: String?, apiKey: String) async -> String? { @@ -682,6 +724,9 @@ actor TalkModeRuntime { self.phase = .thinking await MainActor.run { TalkModeController.shared.updatePhase(.thinking) } } +} + +extension TalkModeRuntime { // MARK: - Audio playback (MainActor helpers) diff --git a/apps/macos/Sources/Clawdbot/TalkOverlayView.swift b/apps/macos/Sources/Clawdbot/TalkOverlayView.swift index 154a948a79..a24ba17437 100644 --- a/apps/macos/Sources/Clawdbot/TalkOverlayView.swift +++ b/apps/macos/Sources/Clawdbot/TalkOverlayView.swift @@ -35,14 +35,14 @@ struct TalkOverlayView: View { .frame(width: 18, height: 18) .background(Color.black.opacity(0.4)) .clipShape(Circle()) + } + .buttonStyle(.plain) + .contentShape(Circle()) + .offset(x: -2, y: -2) + .opacity(self.hoveringWindow ? 1 : 0) + .animation(.easeOut(duration: 0.12), value: self.hoveringWindow) } - .buttonStyle(.plain) - .contentShape(Circle()) - .offset(x: -2, y: -2) - .opacity(self.hoveringWindow ? 1 : 0) - .animation(.easeOut(duration: 0.12), value: self.hoveringWindow) - } - .onHover { self.hoveringWindow = $0 } + .onHover { self.hoveringWindow = $0 } } .frame( width: TalkOverlayController.overlaySize, @@ -124,7 +124,7 @@ private final class OrbInteractionNSView: NSView { } override func mouseUp(with event: NSEvent) { - if !self.didDrag && !self.suppressSingleClick { + if !self.didDrag, !self.suppressSingleClick { self.onSingleClick?() } self.mouseDownEvent = nil @@ -148,8 +148,8 @@ private struct TalkOrbView: View { } else { TimelineView(.animation) { context in let t = context.date.timeIntervalSinceReferenceDate - let listenScale = phase == .listening ? (1 + CGFloat(self.level) * 0.12) : 1 - let pulse = phase == .speaking ? (1 + 0.06 * sin(t * 6)) : 1 + let listenScale = self.phase == .listening ? (1 + CGFloat(self.level) * 0.12) : 1 + let pulse = self.phase == .speaking ? (1 + 0.06 * sin(t * 6)) : 1 ZStack { Circle() @@ -158,9 +158,9 @@ private struct TalkOrbView: View { .shadow(color: Color.black.opacity(0.22), radius: 10, x: 0, y: 5) .scaleEffect(pulse * listenScale) - TalkWaveRings(phase: phase, level: level, time: t, accent: self.accent) + TalkWaveRings(phase: self.phase, level: self.level, time: t, accent: self.accent) - if phase == .thinking { + if self.phase == .thinking { TalkOrbitArcs(time: t) } } @@ -186,11 +186,12 @@ private struct TalkWaveRings: View { var body: some View { ZStack { ForEach(0..<3, id: \.self) { idx in - let speed = phase == .speaking ? 1.4 : phase == .listening ? 0.9 : 0.6 + let speed = self.phase == .speaking ? 1.4 : self.phase == .listening ? 0.9 : 0.6 let progress = (time * speed + Double(idx) * 0.28).truncatingRemainder(dividingBy: 1) - let amplitude = phase == .speaking ? 0.95 : phase == .listening ? 0.5 + level * 0.7 : 0.35 - let scale = 0.75 + progress * amplitude + (phase == .listening ? level * 0.15 : 0) - let alpha = phase == .speaking ? 0.72 : phase == .listening ? 0.58 + level * 0.28 : 0.4 + let amplitude = self.phase == .speaking ? 0.95 : self.phase == .listening ? 0.5 + self + .level * 0.7 : 0.35 + let scale = 0.75 + progress * amplitude + (self.phase == .listening ? self.level * 0.15 : 0) + let alpha = self.phase == .speaking ? 0.72 : self.phase == .listening ? 0.58 + self.level * 0.28 : 0.4 Circle() .stroke(self.accent.opacity(alpha - progress * 0.3), lineWidth: 1.6) .scaleEffect(scale) @@ -208,11 +209,11 @@ private struct TalkOrbitArcs: View { Circle() .trim(from: 0.08, to: 0.26) .stroke(Color.white.opacity(0.88), style: StrokeStyle(lineWidth: 1.6, lineCap: .round)) - .rotationEffect(.degrees(time * 42)) + .rotationEffect(.degrees(self.time * 42)) Circle() .trim(from: 0.62, to: 0.86) .stroke(Color.white.opacity(0.7), style: StrokeStyle(lineWidth: 1.4, lineCap: .round)) - .rotationEffect(.degrees(-time * 35)) + .rotationEffect(.degrees(-self.time * 35)) } .scaleEffect(1.08) } diff --git a/apps/macos/Sources/Clawdbot/WorkActivityStore.swift b/apps/macos/Sources/Clawdbot/WorkActivityStore.swift index dd1aaa187d..47d241acec 100644 --- a/apps/macos/Sources/Clawdbot/WorkActivityStore.swift +++ b/apps/macos/Sources/Clawdbot/WorkActivityStore.swift @@ -213,7 +213,7 @@ final class WorkActivityStore { meta: String?, args: [String: AnyCodable]?) -> String { - let wrappedArgs = wrapToolArgs(args) + let wrappedArgs = self.wrapToolArgs(args) let display = ToolDisplayRegistry.resolve(name: name ?? "tool", args: wrappedArgs, meta: meta) if let detail = display.detailLine, !detail.isEmpty { return "\(display.label): \(detail)" @@ -223,22 +223,22 @@ final class WorkActivityStore { private static func wrapToolArgs(_ args: [String: AnyCodable]?) -> ClawdbotKit.AnyCodable? { guard let args else { return nil } - let converted: [String: Any] = args.mapValues { unwrapJSONValue($0.value) } + let converted: [String: Any] = args.mapValues { self.unwrapJSONValue($0.value) } return ClawdbotKit.AnyCodable(converted) } private static func unwrapJSONValue(_ value: Any) -> Any { if let dict = value as? [String: AnyCodable] { - return dict.mapValues { unwrapJSONValue($0.value) } + return dict.mapValues { self.unwrapJSONValue($0.value) } } if let array = value as? [AnyCodable] { - return array.map { unwrapJSONValue($0.value) } + return array.map { self.unwrapJSONValue($0.value) } } if let dict = value as? [String: Any] { - return dict.mapValues { unwrapJSONValue($0) } + return dict.mapValues { self.unwrapJSONValue($0) } } if let array = value as? [Any] { - return array.map { unwrapJSONValue($0) } + return array.map { self.unwrapJSONValue($0) } } return value } diff --git a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift index 9fc796c63a..cfb8d18f9f 100644 --- a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift +++ b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift @@ -26,8 +26,8 @@ public struct ConnectParams: Codable { caps: [String]?, auth: [String: AnyCodable]?, locale: String?, - useragent: String? - ) { + useragent: String?) + { self.minprotocol = minprotocol self.maxprotocol = maxprotocol self.client = client @@ -36,6 +36,7 @@ public struct ConnectParams: Codable { self.locale = locale self.useragent = useragent } + private enum CodingKeys: String, CodingKey { case minprotocol = "minProtocol" case maxprotocol = "maxProtocol" @@ -63,8 +64,8 @@ public struct HelloOk: Codable { features: [String: AnyCodable], snapshot: Snapshot, canvashosturl: String?, - policy: [String: AnyCodable] - ) { + policy: [String: AnyCodable]) + { self.type = type self._protocol = _protocol self.server = server @@ -73,6 +74,7 @@ public struct HelloOk: Codable { self.canvashosturl = canvashosturl self.policy = policy } + private enum CodingKeys: String, CodingKey { case type case _protocol = "protocol" @@ -94,13 +96,14 @@ public struct RequestFrame: Codable { type: String, id: String, method: String, - params: AnyCodable? - ) { + params: AnyCodable?) + { self.type = type self.id = id self.method = method self.params = params } + private enum CodingKeys: String, CodingKey { case type case id @@ -121,14 +124,15 @@ public struct ResponseFrame: Codable { id: String, ok: Bool, payload: AnyCodable?, - error: [String: AnyCodable]? - ) { + error: [String: AnyCodable]?) + { self.type = type self.id = id self.ok = ok self.payload = payload self.error = error } + private enum CodingKeys: String, CodingKey { case type case id @@ -150,14 +154,15 @@ public struct EventFrame: Codable { event: String, payload: AnyCodable?, seq: Int?, - stateversion: [String: AnyCodable]? - ) { + stateversion: [String: AnyCodable]?) + { self.type = type self.event = event self.payload = payload self.seq = seq self.stateversion = stateversion } + private enum CodingKeys: String, CodingKey { case type case event @@ -195,8 +200,8 @@ public struct PresenceEntry: Codable { tags: [String]?, text: String?, ts: Int, - instanceid: String? - ) { + instanceid: String?) + { self.host = host self.ip = ip self.version = version @@ -211,6 +216,7 @@ public struct PresenceEntry: Codable { self.ts = ts self.instanceid = instanceid } + private enum CodingKeys: String, CodingKey { case host case ip @@ -234,11 +240,12 @@ public struct StateVersion: Codable { public init( presence: Int, - health: Int - ) { + health: Int) + { self.presence = presence self.health = health } + private enum CodingKeys: String, CodingKey { case presence case health @@ -259,8 +266,8 @@ public struct Snapshot: Codable { stateversion: StateVersion, uptimems: Int, configpath: String?, - statedir: String? - ) { + statedir: String?) + { self.presence = presence self.health = health self.stateversion = stateversion @@ -268,6 +275,7 @@ public struct Snapshot: Codable { self.configpath = configpath self.statedir = statedir } + private enum CodingKeys: String, CodingKey { case presence case health @@ -290,14 +298,15 @@ public struct ErrorShape: Codable { message: String, details: AnyCodable?, retryable: Bool?, - retryafterms: Int? - ) { + retryafterms: Int?) + { self.code = code self.message = message self.details = details self.retryable = retryable self.retryafterms = retryafterms } + private enum CodingKeys: String, CodingKey { case code case message @@ -319,14 +328,15 @@ public struct AgentEvent: Codable { seq: Int, stream: String, ts: Int, - data: [String: AnyCodable] - ) { + data: [String: AnyCodable]) + { self.runid = runid self.seq = seq self.stream = stream self.ts = ts self.data = data } + private enum CodingKeys: String, CodingKey { case runid = "runId" case seq @@ -350,8 +360,8 @@ public struct SendParams: Codable { mediaurl: String?, gifplayback: Bool?, provider: String?, - idempotencykey: String - ) { + idempotencykey: String) + { self.to = to self.message = message self.mediaurl = mediaurl @@ -359,6 +369,7 @@ public struct SendParams: Codable { self.provider = provider self.idempotencykey = idempotencykey } + private enum CodingKeys: String, CodingKey { case to case message @@ -393,8 +404,8 @@ public struct AgentParams: Codable { timeout: Int?, lane: String?, extrasystemprompt: String?, - idempotencykey: String - ) { + idempotencykey: String) + { self.message = message self.to = to self.sessionid = sessionid @@ -407,6 +418,7 @@ public struct AgentParams: Codable { self.extrasystemprompt = extrasystemprompt self.idempotencykey = idempotencykey } + private enum CodingKeys: String, CodingKey { case message case to @@ -430,12 +442,13 @@ public struct AgentWaitParams: Codable { public init( runid: String, afterms: Int?, - timeoutms: Int? - ) { + timeoutms: Int?) + { self.runid = runid self.afterms = afterms self.timeoutms = timeoutms } + private enum CodingKeys: String, CodingKey { case runid = "runId" case afterms = "afterMs" @@ -449,11 +462,12 @@ public struct WakeParams: Codable { public init( mode: AnyCodable, - text: String - ) { + text: String) + { self.mode = mode self.text = text } + private enum CodingKeys: String, CodingKey { case mode case text @@ -482,8 +496,8 @@ public struct NodePairRequestParams: Codable { caps: [String]?, commands: [String]?, remoteip: String?, - silent: Bool? - ) { + silent: Bool?) + { self.nodeid = nodeid self.displayname = displayname self.platform = platform @@ -495,6 +509,7 @@ public struct NodePairRequestParams: Codable { self.remoteip = remoteip self.silent = silent } + private enum CodingKeys: String, CodingKey { case nodeid = "nodeId" case displayname = "displayName" @@ -509,17 +524,17 @@ public struct NodePairRequestParams: Codable { } } -public struct NodePairListParams: Codable { -} +public struct NodePairListParams: Codable {} public struct NodePairApproveParams: Codable { public let requestid: String public init( - requestid: String - ) { + requestid: String) + { self.requestid = requestid } + private enum CodingKeys: String, CodingKey { case requestid = "requestId" } @@ -529,10 +544,11 @@ public struct NodePairRejectParams: Codable { public let requestid: String public init( - requestid: String - ) { + requestid: String) + { self.requestid = requestid } + private enum CodingKeys: String, CodingKey { case requestid = "requestId" } @@ -544,11 +560,12 @@ public struct NodePairVerifyParams: Codable { public init( nodeid: String, - token: String - ) { + token: String) + { self.nodeid = nodeid self.token = token } + private enum CodingKeys: String, CodingKey { case nodeid = "nodeId" case token @@ -561,28 +578,29 @@ public struct NodeRenameParams: Codable { public init( nodeid: String, - displayname: String - ) { + displayname: String) + { self.nodeid = nodeid self.displayname = displayname } + private enum CodingKeys: String, CodingKey { case nodeid = "nodeId" case displayname = "displayName" } } -public struct NodeListParams: Codable { -} +public struct NodeListParams: Codable {} public struct NodeDescribeParams: Codable { public let nodeid: String public init( - nodeid: String - ) { + nodeid: String) + { self.nodeid = nodeid } + private enum CodingKeys: String, CodingKey { case nodeid = "nodeId" } @@ -600,14 +618,15 @@ public struct NodeInvokeParams: Codable { command: String, params: AnyCodable?, timeoutms: Int?, - idempotencykey: String - ) { + idempotencykey: String) + { self.nodeid = nodeid self.command = command self.params = params self.timeoutms = timeoutms self.idempotencykey = idempotencykey } + private enum CodingKeys: String, CodingKey { case nodeid = "nodeId" case command @@ -627,13 +646,14 @@ public struct SessionsListParams: Codable { limit: Int?, activeminutes: Int?, includeglobal: Bool?, - includeunknown: Bool? - ) { + includeunknown: Bool?) + { self.limit = limit self.activeminutes = activeminutes self.includeglobal = includeglobal self.includeunknown = includeunknown } + private enum CodingKeys: String, CodingKey { case limit case activeminutes = "activeMinutes" @@ -658,8 +678,8 @@ public struct SessionsPatchParams: Codable { elevatedlevel: AnyCodable?, model: AnyCodable?, sendpolicy: AnyCodable?, - groupactivation: AnyCodable? - ) { + groupactivation: AnyCodable?) + { self.key = key self.thinkinglevel = thinkinglevel self.verboselevel = verboselevel @@ -668,6 +688,7 @@ public struct SessionsPatchParams: Codable { self.sendpolicy = sendpolicy self.groupactivation = groupactivation } + private enum CodingKeys: String, CodingKey { case key case thinkinglevel = "thinkingLevel" @@ -683,10 +704,11 @@ public struct SessionsResetParams: Codable { public let key: String public init( - key: String - ) { + key: String) + { self.key = key } + private enum CodingKeys: String, CodingKey { case key } @@ -698,11 +720,12 @@ public struct SessionsDeleteParams: Codable { public init( key: String, - deletetranscript: Bool? - ) { + deletetranscript: Bool?) + { self.key = key self.deletetranscript = deletetranscript } + private enum CodingKeys: String, CodingKey { case key case deletetranscript = "deleteTranscript" @@ -715,35 +738,35 @@ public struct SessionsCompactParams: Codable { public init( key: String, - maxlines: Int? - ) { + maxlines: Int?) + { self.key = key self.maxlines = maxlines } + private enum CodingKeys: String, CodingKey { case key case maxlines = "maxLines" } } -public struct ConfigGetParams: Codable { -} +public struct ConfigGetParams: Codable {} public struct ConfigSetParams: Codable { public let raw: String public init( - raw: String - ) { + raw: String) + { self.raw = raw } + private enum CodingKeys: String, CodingKey { case raw } } -public struct ConfigSchemaParams: Codable { -} +public struct ConfigSchemaParams: Codable {} public struct ConfigSchemaResponse: Codable { public let schema: AnyCodable @@ -755,13 +778,14 @@ public struct ConfigSchemaResponse: Codable { schema: AnyCodable, uihints: [String: AnyCodable], version: String, - generatedat: String - ) { + generatedat: String) + { self.schema = schema self.uihints = uihints self.version = version self.generatedat = generatedat } + private enum CodingKeys: String, CodingKey { case schema case uihints = "uiHints" @@ -776,11 +800,12 @@ public struct WizardStartParams: Codable { public init( mode: AnyCodable?, - workspace: String? - ) { + workspace: String?) + { self.mode = mode self.workspace = workspace } + private enum CodingKeys: String, CodingKey { case mode case workspace @@ -793,11 +818,12 @@ public struct WizardNextParams: Codable { public init( sessionid: String, - answer: [String: AnyCodable]? - ) { + answer: [String: AnyCodable]?) + { self.sessionid = sessionid self.answer = answer } + private enum CodingKeys: String, CodingKey { case sessionid = "sessionId" case answer @@ -808,10 +834,11 @@ public struct WizardCancelParams: Codable { public let sessionid: String public init( - sessionid: String - ) { + sessionid: String) + { self.sessionid = sessionid } + private enum CodingKeys: String, CodingKey { case sessionid = "sessionId" } @@ -821,10 +848,11 @@ public struct WizardStatusParams: Codable { public let sessionid: String public init( - sessionid: String - ) { + sessionid: String) + { self.sessionid = sessionid } + private enum CodingKeys: String, CodingKey { case sessionid = "sessionId" } @@ -850,8 +878,8 @@ public struct WizardStep: Codable { initialvalue: AnyCodable?, placeholder: String?, sensitive: Bool?, - executor: AnyCodable? - ) { + executor: AnyCodable?) + { self.id = id self.type = type self.title = title @@ -862,6 +890,7 @@ public struct WizardStep: Codable { self.sensitive = sensitive self.executor = executor } + private enum CodingKeys: String, CodingKey { case id case type @@ -885,13 +914,14 @@ public struct WizardNextResult: Codable { done: Bool, step: [String: AnyCodable]?, status: AnyCodable?, - error: String? - ) { + error: String?) + { self.done = done self.step = step self.status = status self.error = error } + private enum CodingKeys: String, CodingKey { case done case step @@ -912,14 +942,15 @@ public struct WizardStartResult: Codable { done: Bool, step: [String: AnyCodable]?, status: AnyCodable?, - error: String? - ) { + error: String?) + { self.sessionid = sessionid self.done = done self.step = step self.status = status self.error = error } + private enum CodingKeys: String, CodingKey { case sessionid = "sessionId" case done @@ -935,11 +966,12 @@ public struct WizardStatusResult: Codable { public init( status: AnyCodable, - error: String? - ) { + error: String?) + { self.status = status self.error = error } + private enum CodingKeys: String, CodingKey { case status case error @@ -952,11 +984,12 @@ public struct TalkModeParams: Codable { public init( enabled: Bool, - phase: String? - ) { + phase: String?) + { self.enabled = enabled self.phase = phase } + private enum CodingKeys: String, CodingKey { case enabled case phase @@ -969,11 +1002,12 @@ public struct ProvidersStatusParams: Codable { public init( probe: Bool?, - timeoutms: Int? - ) { + timeoutms: Int?) + { self.probe = probe self.timeoutms = timeoutms } + private enum CodingKeys: String, CodingKey { case probe case timeoutms = "timeoutMs" @@ -988,12 +1022,13 @@ public struct WebLoginStartParams: Codable { public init( force: Bool?, timeoutms: Int?, - verbose: Bool? - ) { + verbose: Bool?) + { self.force = force self.timeoutms = timeoutms self.verbose = verbose } + private enum CodingKeys: String, CodingKey { case force case timeoutms = "timeoutMs" @@ -1005,10 +1040,11 @@ public struct WebLoginWaitParams: Codable { public let timeoutms: Int? public init( - timeoutms: Int? - ) { + timeoutms: Int?) + { self.timeoutms = timeoutms } + private enum CodingKeys: String, CodingKey { case timeoutms = "timeoutMs" } @@ -1026,14 +1062,15 @@ public struct ModelChoice: Codable { name: String, provider: String, contextwindow: Int?, - reasoning: Bool? - ) { + reasoning: Bool?) + { self.id = id self.name = name self.provider = provider self.contextwindow = contextwindow self.reasoning = reasoning } + private enum CodingKeys: String, CodingKey { case id case name @@ -1043,24 +1080,23 @@ public struct ModelChoice: Codable { } } -public struct ModelsListParams: Codable { -} +public struct ModelsListParams: Codable {} public struct ModelsListResult: Codable { public let models: [ModelChoice] public init( - models: [ModelChoice] - ) { + models: [ModelChoice]) + { self.models = models } + private enum CodingKeys: String, CodingKey { case models } } -public struct SkillsStatusParams: Codable { -} +public struct SkillsStatusParams: Codable {} public struct SkillsInstallParams: Codable { public let name: String @@ -1070,12 +1106,13 @@ public struct SkillsInstallParams: Codable { public init( name: String, installid: String, - timeoutms: Int? - ) { + timeoutms: Int?) + { self.name = name self.installid = installid self.timeoutms = timeoutms } + private enum CodingKeys: String, CodingKey { case name case installid = "installId" @@ -1093,13 +1130,14 @@ public struct SkillsUpdateParams: Codable { skillkey: String, enabled: Bool?, apikey: String?, - env: [String: AnyCodable]? - ) { + env: [String: AnyCodable]?) + { self.skillkey = skillkey self.enabled = enabled self.apikey = apikey self.env = env } + private enum CodingKeys: String, CodingKey { case skillkey = "skillKey" case enabled @@ -1134,8 +1172,8 @@ public struct CronJob: Codable { wakemode: AnyCodable, payload: AnyCodable, isolation: [String: AnyCodable]?, - state: [String: AnyCodable] - ) { + state: [String: AnyCodable]) + { self.id = id self.name = name self.description = description @@ -1149,6 +1187,7 @@ public struct CronJob: Codable { self.isolation = isolation self.state = state } + private enum CodingKeys: String, CodingKey { case id case name @@ -1169,17 +1208,17 @@ public struct CronListParams: Codable { public let includedisabled: Bool? public init( - includedisabled: Bool? - ) { + includedisabled: Bool?) + { self.includedisabled = includedisabled } + private enum CodingKeys: String, CodingKey { case includedisabled = "includeDisabled" } } -public struct CronStatusParams: Codable { -} +public struct CronStatusParams: Codable {} public struct CronAddParams: Codable { public let name: String @@ -1199,8 +1238,8 @@ public struct CronAddParams: Codable { sessiontarget: AnyCodable, wakemode: AnyCodable, payload: AnyCodable, - isolation: [String: AnyCodable]? - ) { + isolation: [String: AnyCodable]?) + { self.name = name self.description = description self.enabled = enabled @@ -1210,6 +1249,7 @@ public struct CronAddParams: Codable { self.payload = payload self.isolation = isolation } + private enum CodingKeys: String, CodingKey { case name case description @@ -1228,11 +1268,12 @@ public struct CronUpdateParams: Codable { public init( id: String, - patch: [String: AnyCodable] - ) { + patch: [String: AnyCodable]) + { self.id = id self.patch = patch } + private enum CodingKeys: String, CodingKey { case id case patch @@ -1243,10 +1284,11 @@ public struct CronRemoveParams: Codable { public let id: String public init( - id: String - ) { + id: String) + { self.id = id } + private enum CodingKeys: String, CodingKey { case id } @@ -1258,11 +1300,12 @@ public struct CronRunParams: Codable { public init( id: String, - mode: AnyCodable? - ) { + mode: AnyCodable?) + { self.id = id self.mode = mode } + private enum CodingKeys: String, CodingKey { case id case mode @@ -1275,11 +1318,12 @@ public struct CronRunsParams: Codable { public init( id: String, - limit: Int? - ) { + limit: Int?) + { self.id = id self.limit = limit } + private enum CodingKeys: String, CodingKey { case id case limit @@ -1306,8 +1350,8 @@ public struct CronRunLogEntry: Codable { summary: String?, runatms: Int?, durationms: Int?, - nextrunatms: Int? - ) { + nextrunatms: Int?) + { self.ts = ts self.jobid = jobid self.action = action @@ -1318,6 +1362,7 @@ public struct CronRunLogEntry: Codable { self.durationms = durationms self.nextrunatms = nextrunatms } + private enum CodingKeys: String, CodingKey { case ts case jobid = "jobId" @@ -1337,11 +1382,12 @@ public struct ChatHistoryParams: Codable { public init( sessionkey: String, - limit: Int? - ) { + limit: Int?) + { self.sessionkey = sessionkey self.limit = limit } + private enum CodingKeys: String, CodingKey { case sessionkey = "sessionKey" case limit @@ -1364,8 +1410,8 @@ public struct ChatSendParams: Codable { deliver: Bool?, attachments: [AnyCodable]?, timeoutms: Int?, - idempotencykey: String - ) { + idempotencykey: String) + { self.sessionkey = sessionkey self.message = message self.thinking = thinking @@ -1374,6 +1420,7 @@ public struct ChatSendParams: Codable { self.timeoutms = timeoutms self.idempotencykey = idempotencykey } + private enum CodingKeys: String, CodingKey { case sessionkey = "sessionKey" case message @@ -1391,11 +1438,12 @@ public struct ChatAbortParams: Codable { public init( sessionkey: String, - runid: String - ) { + runid: String) + { self.sessionkey = sessionkey self.runid = runid } + private enum CodingKeys: String, CodingKey { case sessionkey = "sessionKey" case runid = "runId" @@ -1420,8 +1468,8 @@ public struct ChatEvent: Codable { message: AnyCodable?, errormessage: String?, usage: AnyCodable?, - stopreason: String? - ) { + stopreason: String?) + { self.runid = runid self.sessionkey = sessionkey self.seq = seq @@ -1431,6 +1479,7 @@ public struct ChatEvent: Codable { self.usage = usage self.stopreason = stopreason } + private enum CodingKeys: String, CodingKey { case runid = "runId" case sessionkey = "sessionKey" @@ -1447,10 +1496,11 @@ public struct TickEvent: Codable { public let ts: Int public init( - ts: Int - ) { + ts: Int) + { self.ts = ts } + private enum CodingKeys: String, CodingKey { case ts } @@ -1462,11 +1512,12 @@ public struct ShutdownEvent: Codable { public init( reason: String, - restartexpectedms: Int? - ) { + restartexpectedms: Int?) + { self.reason = reason self.restartexpectedms = restartexpectedms } + private enum CodingKeys: String, CodingKey { case reason case restartexpectedms = "restartExpectedMs" @@ -1488,11 +1539,11 @@ public enum GatewayFrame: Codable { let type = try typeContainer.decode(String.self, forKey: .type) switch type { case "req": - self = .req(try RequestFrame(from: decoder)) + self = try .req(RequestFrame(from: decoder)) case "res": - self = .res(try ResponseFrame(from: decoder)) + self = try .res(ResponseFrame(from: decoder)) case "event": - self = .event(try EventFrame(from: decoder)) + self = try .event(EventFrame(from: decoder)) default: let container = try decoder.singleValueContainer() let raw = try container.decode([String: AnyCodable].self) @@ -1502,13 +1553,12 @@ public enum GatewayFrame: Codable { public func encode(to encoder: Encoder) throws { switch self { - case .req(let v): try v.encode(to: encoder) - case .res(let v): try v.encode(to: encoder) - case .event(let v): try v.encode(to: encoder) - case .unknown(_, let raw): + case let .req(v): try v.encode(to: encoder) + case let .res(v): try v.encode(to: encoder) + case let .event(v): try v.encode(to: encoder) + case let .unknown(_, raw): var container = encoder.singleValueContainer() try container.encode(raw) } } - } diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/AssistantTextParser.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/AssistantTextParser.swift index ffa672eba7..c4395adfae 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/AssistantTextParser.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/AssistantTextParser.swift @@ -86,7 +86,7 @@ enum AssistantTextParser { self.findTagStart(tag: "final", closing: true, in: text, from: start).map { TagMatch(kind: .final, closing: true, range: $0) }, - ].compactMap { $0 } + ].compactMap(\.self) return candidates.min { $0.range.lowerBound < $1.range.lowerBound } } diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/TalkHistoryTimestamp.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/TalkHistoryTimestamp.swift index 087384dfba..75f14ef85b 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/TalkHistoryTimestamp.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/TalkHistoryTimestamp.swift @@ -10,4 +10,3 @@ public enum TalkHistoryTimestamp: Sendable { return timestamp >= sinceSeconds - 0.5 } } - diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/TalkSystemSpeechSynthesizer.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/TalkSystemSpeechSynthesizer.swift index d1d4eeb39d..4cfc536da8 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/TalkSystemSpeechSynthesizer.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/TalkSystemSpeechSynthesizer.swift @@ -17,7 +17,7 @@ public final class TalkSystemSpeechSynthesizer: NSObject { public var isSpeaking: Bool { self.synth.isSpeaking } - private override init() { + override private init() { super.init() self.synth.delegate = self } @@ -96,13 +96,19 @@ public final class TalkSystemSpeechSynthesizer: NSObject { } extension TalkSystemSpeechSynthesizer: AVSpeechSynthesizerDelegate { - public nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { + public nonisolated func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, + didFinish utterance: AVSpeechUtterance) + { Task { @MainActor in self.handleFinish(error: nil) } } - public nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) { + public nonisolated func speechSynthesizer( + _ synthesizer: AVSpeechSynthesizer, + didCancel utterance: AVSpeechUtterance) + { Task { @MainActor in self.handleFinish(error: SpeakError.canceled) } diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/ToolDisplay.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/ToolDisplay.swift index 1c397ad39f..1f7a881471 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/ToolDisplay.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/ToolDisplay.swift @@ -17,9 +17,9 @@ public struct ToolDisplaySummary: Sendable, Equatable { public var summaryLine: String { if let detailLine { - return "\(emoji) \(label): \(detailLine)" + return "\(self.emoji) \(self.label): \(detailLine)" } - return "\(emoji) \(label)" + return "\(self.emoji) \(self.label)" } } @@ -48,28 +48,28 @@ public enum ToolDisplayRegistry { public static func resolve(name: String?, args: AnyCodable?, meta: String? = nil) -> ToolDisplaySummary { let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "tool" let key = trimmedName.lowercased() - let spec = config.tools?[key] - let fallback = config.fallback + let spec = self.config.tools?[key] + let fallback = self.config.fallback let emoji = spec?.emoji ?? fallback?.emoji ?? "🧩" - let title = spec?.title ?? titleFromName(trimmedName) + let title = spec?.title ?? self.titleFromName(trimmedName) let label = spec?.label ?? trimmedName - let actionRaw = valueForKeyPath(args, path: "action") as? String + let actionRaw = self.valueForKeyPath(args, path: "action") as? String let action = actionRaw?.trimmingCharacters(in: .whitespacesAndNewlines) let actionSpec = action.flatMap { spec?.actions?[$0] } - let verb = normalizeVerb(actionSpec?.label ?? action) + let verb = self.normalizeVerb(actionSpec?.label ?? action) var detail: String? if key == "read" { - detail = readDetail(args) + detail = self.readDetail(args) } else if key == "write" || key == "edit" || key == "attach" { - detail = pathDetail(args) + detail = self.pathDetail(args) } let detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? fallback?.detailKeys ?? [] if detail == nil { - detail = firstValue(args, keys: detailKeys) + detail = self.firstValue(args, keys: detailKeys) } if detail == nil { @@ -77,7 +77,7 @@ public enum ToolDisplayRegistry { } if let detailValue = detail { - detail = shortenHomeInString(detailValue) + detail = self.shortenHomeInString(detailValue) } return ToolDisplaySummary( @@ -108,7 +108,7 @@ public enum ToolDisplayRegistry { .split(separator: " ") .map { part in let upper = part.uppercased() - if part.count <= 2 && part == upper { return String(part) } + if part.count <= 2, part == upper { return String(part) } return String(upper.prefix(1)) + String(part.lowercased().dropFirst()) } .joined(separator: " ") @@ -122,8 +122,8 @@ public enum ToolDisplayRegistry { private static func readDetail(_ args: AnyCodable?) -> String? { guard let path = valueForKeyPath(args, path: "path") as? String else { return nil } - let offset = valueForKeyPath(args, path: "offset") as? Double - let limit = valueForKeyPath(args, path: "limit") as? Double + let offset = self.valueForKeyPath(args, path: "offset") as? Double + let limit = self.valueForKeyPath(args, path: "limit") as? Double if let offset, let limit { let end = offset + limit return "\(path):\(Int(offset))-\(Int(end))" @@ -132,7 +132,7 @@ public enum ToolDisplayRegistry { } private static func pathDetail(_ args: AnyCodable?) -> String? { - return valueForKeyPath(args, path: "path") as? String + self.valueForKeyPath(args, path: "path") as? String } private static func firstValue(_ args: AnyCodable?, keys: [String]) -> String? { @@ -158,7 +158,7 @@ public enum ToolDisplayRegistry { if let num = value as? Double { return String(num) } if let bool = value as? Bool { return bool ? "true" : "false" } if let array = value as? [Any] { - let items = array.compactMap { renderValue($0) } + let items = array.compactMap { self.renderValue($0) } guard !items.isEmpty else { return nil } let preview = items.prefix(3).joined(separator: ", ") return items.count > 3 ? "\(preview)…" : preview diff --git a/src/canvas-host/a2ui/.bundle.hash b/src/canvas-host/a2ui/.bundle.hash index 126c648bc6..a1c6a37d56 100644 --- a/src/canvas-host/a2ui/.bundle.hash +++ b/src/canvas-host/a2ui/.bundle.hash @@ -1 +1 @@ -01110b37b0ec6481527fbc894303ed127520716d76108535874567d8331973bc +7daf1cbf58ef395b74c2690c439ac7b3cb536e8eb124baf72ad41da4f542204d