chore: lint and format cleanup

This commit is contained in:
Peter Steinberger 2026-01-04 16:24:10 +01:00
parent fd95ededaa
commit 026a25d164
45 changed files with 627 additions and 496 deletions

View file

@ -108,6 +108,10 @@ function_body_length:
warning: 150 warning: 150
error: 300 error: 300
function_parameter_count:
warning: 7
error: 10
file_length: file_length:
warning: 1500 warning: 1500
error: 2500 error: 2500

View file

@ -108,7 +108,8 @@ struct AboutSettings: View {
} }
private var buildTimestamp: String? { 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() let parser = ISO8601DateFormatter()
parser.formatOptions = [.withInternetDateTime] parser.formatOptions = [.withInternetDateTime]
guard let date = parser.date(from: raw) else { return raw } guard let date = parser.date(from: raw) else { return raw }

View file

@ -9,7 +9,7 @@ import SwiftUI
final class AppState { final class AppState {
private let isPreview: Bool private let isPreview: Bool
private var isInitializing = true private var isInitializing = true
private var configWatcher: ConfigFileWatcher? private nonisolated var configWatcher: ConfigFileWatcher?
private var suppressVoiceWakeGlobalSync = false private var suppressVoiceWakeGlobalSync = false
private var voiceWakeGlobalSyncTask: Task<Void, Never>? private var voiceWakeGlobalSyncTask: Task<Void, Never>?
@ -254,34 +254,33 @@ final class AppState {
let configRoot = ClawdbotConfigFile.loadDict() let configRoot = ClawdbotConfigFile.loadDict()
let configGateway = configRoot["gateway"] as? [String: Any] let configGateway = configRoot["gateway"] as? [String: Any]
let configModeRaw = (configGateway?["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let configModeRaw = (configGateway?["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
let configMode: ConnectionMode? = { let configMode: ConnectionMode? = switch configModeRaw {
switch configModeRaw { case "local":
case "local": .local
return .local case "remote":
case "remote": .remote
return .remote default:
default: nil
return nil }
}
}()
let configRemoteUrl = (configGateway?["remote"] as? [String: Any])?["url"] as? String let configRemoteUrl = (configGateway?["remote"] as? [String: Any])?["url"] as? String
let configHasRemoteUrl = !(configRemoteUrl? let configHasRemoteUrl = !(configRemoteUrl?
.trimmingCharacters(in: .whitespacesAndNewlines) .trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty ?? true) .isEmpty ?? true)
let storedMode = UserDefaults.standard.string(forKey: connectionModeKey) let storedMode = UserDefaults.standard.string(forKey: connectionModeKey)
if let configMode { let resolvedConnectionMode: ConnectionMode = if let configMode {
self.connectionMode = configMode configMode
} else if configHasRemoteUrl { } else if configHasRemoteUrl {
self.connectionMode = .remote .remote
} else if let storedMode { } else if let storedMode {
self.connectionMode = ConnectionMode(rawValue: storedMode) ?? .local ConnectionMode(rawValue: storedMode) ?? .local
} else { } else {
self.connectionMode = onboardingSeen ? .local : .unconfigured onboardingSeen ? .local : .unconfigured
} }
self.connectionMode = resolvedConnectionMode
let storedRemoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? "" let storedRemoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? ""
if self.connectionMode == .remote, if resolvedConnectionMode == .remote,
storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, storedRemoteTarget.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
let host = AppState.remoteHost(from: configRemoteUrl) let host = AppState.remoteHost(from: configRemoteUrl)
{ {
@ -361,18 +360,16 @@ final class AppState {
.trimmingCharacters(in: .whitespacesAndNewlines) .trimmingCharacters(in: .whitespacesAndNewlines)
.isEmpty ?? true) .isEmpty ?? true)
let desiredMode: ConnectionMode? = { let desiredMode: ConnectionMode? = switch modeRaw {
switch modeRaw { case "local":
case "local": .local
return .local case "remote":
case "remote": .remote
return .remote case "unconfigured":
case "unconfigured": .unconfigured
return .unconfigured default:
default: nil
return nil }
}
}()
if let desiredMode { if let desiredMode {
if desiredMode != self.connectionMode { if desiredMode != self.connectionMode {
@ -407,14 +404,13 @@ final class AppState {
var gateway = root["gateway"] as? [String: Any] ?? [:] var gateway = root["gateway"] as? [String: Any] ?? [:]
var changed = false var changed = false
let desiredMode: String? let desiredMode: String? = switch self.connectionMode {
switch self.connectionMode {
case .local: case .local:
desiredMode = "local" "local"
case .remote: case .remote:
desiredMode = "remote" "remote"
case .unconfigured: case .unconfigured:
desiredMode = nil nil
} }
let currentMode = (gateway["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let currentMode = (gateway["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)

View file

@ -244,7 +244,7 @@ actor CameraCaptureService {
deviceId: String?) -> AVCaptureDevice? deviceId: String?) -> AVCaptureDevice?
{ {
if let deviceId, !deviceId.isEmpty { 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 return match
} }
} }
@ -331,7 +331,7 @@ actor CameraCaptureService {
private func sleepDelayMs(_ delayMs: Int) async { private func sleepDelayMs(_ delayMs: Int) async {
guard delayMs > 0 else { return } 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) try? await Task.sleep(nanoseconds: ns)
} }

View file

@ -100,7 +100,8 @@ enum ClawdbotConfigFile {
static func gatewayPassword() -> String? { static func gatewayPassword() -> String? {
let root = self.loadDict() let root = self.loadDict()
guard let gateway = root["gateway"] as? [String: Any], 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 nil
} }
return remote["password"] as? String return remote["password"] as? String
@ -121,5 +122,4 @@ enum ClawdbotConfigFile {
} }
return nil return nil
} }
} }

View file

@ -157,7 +157,7 @@ enum CommandResolver {
} }
static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? { 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) let candidate = (dir as NSString).appendingPathComponent(name)
if FileManager.default.isExecutableFile(atPath: candidate) { if FileManager.default.isExecutableFile(atPath: candidate) {
return candidate return candidate

View file

@ -90,8 +90,8 @@ extension ConfigFileWatcher {
private func handleEvents( private func handleEvents(
numEvents: Int, numEvents: Int,
eventPaths: UnsafeMutableRawPointer?, eventPaths: UnsafeMutableRawPointer?,
eventFlags: UnsafePointer<FSEventStreamEventFlags>? eventFlags: UnsafePointer<FSEventStreamEventFlags>?)
) { {
guard numEvents > 0 else { return } guard numEvents > 0 else { return }
guard eventFlags != nil else { return } guard eventFlags != nil else { return }
guard self.matchesTarget(eventPaths: eventPaths) else { return } guard self.matchesTarget(eventPaths: eventPaths) else { return }

View file

@ -78,7 +78,7 @@ enum ConfigStore {
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])
guard let raw = String(data: data, encoding: .utf8) else { guard let raw = String(data: data, encoding: .utf8) else {
throw NSError(domain: "ConfigStore", code: 1, userInfo: [ throw NSError(domain: "ConfigStore", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode config." NSLocalizedDescriptionKey: "Failed to encode config.",
]) ])
} }
let params: [String: AnyCodable] = ["raw": AnyCodable(raw)] let params: [String: AnyCodable] = ["raw": AnyCodable(raw)]
@ -88,7 +88,7 @@ enum ConfigStore {
timeoutMs: 10000) timeoutMs: 10000)
} }
#if DEBUG #if DEBUG
static func _testSetOverrides(_ overrides: Overrides) async { static func _testSetOverrides(_ overrides: Overrides) async {
await self.overrideStore.setOverride(overrides) await self.overrideStore.setOverride(overrides)
} }
@ -96,5 +96,5 @@ enum ConfigStore {
static func _testClearOverrides() async { static func _testClearOverrides() async {
await self.overrideStore.setOverride(.init()) await self.overrideStore.setOverride(.init())
} }
#endif #endif
} }

View file

@ -12,11 +12,13 @@ struct ContextUsageBar: View {
if match == .darkAqua { return base } if match == .darkAqua { return base }
return base.blended(withFraction: 0.24, of: .black) ?? base return base.blended(withFraction: 0.24, of: .black) ?? base
} }
private static let trackFill: NSColor = .init(name: nil) { appearance in private static let trackFill: NSColor = .init(name: nil) { appearance in
let match = appearance.bestMatch(from: [.aqua, .darkAqua]) let match = appearance.bestMatch(from: [.aqua, .darkAqua])
if match == .darkAqua { return NSColor.white.withAlphaComponent(0.14) } if match == .darkAqua { return NSColor.white.withAlphaComponent(0.14) }
return NSColor.black.withAlphaComponent(0.12) return NSColor.black.withAlphaComponent(0.12)
} }
private static let trackStroke: NSColor = .init(name: nil) { appearance in private static let trackStroke: NSColor = .init(name: nil) { appearance in
let match = appearance.bestMatch(from: [.aqua, .darkAqua]) let match = appearance.bestMatch(from: [.aqua, .darkAqua])
if match == .darkAqua { return NSColor.white.withAlphaComponent(0.22) } if match == .darkAqua { return NSColor.white.withAlphaComponent(0.22) }

View file

@ -55,8 +55,8 @@ final class ControlChannel {
private(set) var state: ConnectionState = .disconnected { private(set) var state: ConnectionState = .disconnected {
didSet { didSet {
CanvasManager.shared.refreshDebugStatus() CanvasManager.shared.refreshDebugStatus()
guard oldValue != state else { return } guard oldValue != self.state else { return }
switch state { switch self.state {
case .connected: case .connected:
self.logger.info("control channel state -> connected") self.logger.info("control channel state -> connected")
case .connecting: case .connecting:
@ -71,6 +71,7 @@ final class ControlChannel {
} }
} }
} }
private(set) var lastPingMs: Double? private(set) var lastPingMs: Double?
private let logger = Logger(subsystem: "com.clawdbot", category: "control") private let logger = Logger(subsystem: "com.clawdbot", category: "control")
@ -105,7 +106,8 @@ final class ControlChannel {
_ = (target, identity) _ = (target, identity)
let idSet = !identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty let idSet = !identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
self.logger.info( 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() _ = try await GatewayEndpointStore.shared.ensureRemoteControlTunnel()
await self.configure() await self.configure()
} catch { } catch {
@ -261,7 +263,9 @@ final class ControlChannel {
let trimmedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedReason = reason.trimmingCharacters(in: .whitespacesAndNewlines)
let reasonText = trimmedReason.isEmpty ? "unknown" : trimmedReason let reasonText = trimmedReason.isEmpty ? "unknown" : trimmedReason
self.logger.info( 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 { if mode == .local {
GatewayProcessManager.shared.setActive(true) GatewayProcessManager.shared.setActive(true)
} }

View file

@ -138,17 +138,20 @@ enum DeviceModelCatalog {
if bundle.url( if bundle.url(
forResource: "ios-device-identifiers", forResource: "ios-device-identifiers",
withExtension: "json", withExtension: "json",
subdirectory: self.resourceSubdirectory) != nil { subdirectory: self.resourceSubdirectory) != nil
{
return bundle return bundle
} }
if bundle.url( if bundle.url(
forResource: "mac-device-identifiers", forResource: "mac-device-identifiers",
withExtension: "json", withExtension: "json",
subdirectory: self.resourceSubdirectory) != nil { subdirectory: self.resourceSubdirectory) != nil
{
return bundle return bundle
} }
return nil return nil
} }
private enum NameValue: Decodable { private enum NameValue: Decodable {
case string(String) case string(String)
case stringArray([String]) case stringArray([String])

View file

@ -237,8 +237,9 @@ actor GatewayConnection {
guard let snapshot = self.lastSnapshot else { return (nil, nil) } guard let snapshot = self.lastSnapshot else { return (nil, nil) }
let configPath = snapshot.snapshot.configpath?.trimmingCharacters(in: .whitespacesAndNewlines) let configPath = snapshot.snapshot.configpath?.trimmingCharacters(in: .whitespacesAndNewlines)
let stateDir = snapshot.snapshot.statedir?.trimmingCharacters(in: .whitespacesAndNewlines) let stateDir = snapshot.snapshot.statedir?.trimmingCharacters(in: .whitespacesAndNewlines)
return (configPath?.isEmpty == false ? configPath : nil, return (
stateDir?.isEmpty == false ? stateDir : nil) configPath?.isEmpty == false ? configPath : nil,
stateDir?.isEmpty == false ? stateDir : nil)
} }
func subscribe(bufferingNewest: Int = 100) -> AsyncStream<GatewayPush> { func subscribe(bufferingNewest: Int = 100) -> AsyncStream<GatewayPush> {
@ -270,7 +271,9 @@ actor GatewayConnection {
} }
private func configure(url: URL, token: String?, password: String?) async { 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 return
} }
if let client { if let client {

View file

@ -40,8 +40,8 @@ actor GatewayEndpointStore {
private static func resolveGatewayPassword( private static func resolveGatewayPassword(
isRemote: Bool, isRemote: Bool,
root: [String: Any], root: [String: Any],
env: [String: String] env: [String: String]) -> String?
) -> String? { {
let raw = env["CLAWDBOT_GATEWAY_PASSWORD"] ?? "" let raw = env["CLAWDBOT_GATEWAY_PASSWORD"] ?? ""
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty { if !trimmed.isEmpty {
@ -93,7 +93,11 @@ actor GatewayEndpointStore {
let password = deps.password() let password = deps.password()
switch initialMode { switch initialMode {
case .local: 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: case .remote:
self.state = .unavailable(mode: .remote, reason: "Remote mode enabled but no active control tunnel") self.state = .unavailable(mode: .remote, reason: "Remote mode enabled but no active control tunnel")
case .unconfigured: case .unconfigured:
@ -125,14 +129,22 @@ actor GatewayEndpointStore {
switch mode { switch mode {
case .local: case .local:
let port = self.deps.localPort() 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: case .remote:
let port = await self.deps.remotePortIfRunning() let port = await self.deps.remotePortIfRunning()
guard let port else { guard let port else {
self.setState(.unavailable(mode: .remote, reason: "Remote mode enabled but no active control tunnel")) self.setState(.unavailable(mode: .remote, reason: "Remote mode enabled but no active control tunnel"))
return 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: case .unconfigured:
self.setState(.unavailable(mode: .unconfigured, reason: "Gateway not configured")) self.setState(.unavailable(mode: .unconfigured, reason: "Gateway not configured"))
} }
@ -213,8 +225,8 @@ extension GatewayEndpointStore {
static func _testResolveGatewayPassword( static func _testResolveGatewayPassword(
isRemote: Bool, isRemote: Bool,
root: [String: Any], root: [String: Any],
env: [String: String] env: [String: String]) -> String?
) -> String? { {
self.resolveGatewayPassword(isRemote: isRemote, root: root, env: env) self.resolveGatewayPassword(isRemote: isRemote, root: root, env: env)
} }
} }

View file

@ -24,7 +24,7 @@ enum GatewayLaunchAgentManager {
} }
private static func gatewayProgramArguments(bundlePath: String, port: Int, bind: String) -> [String] { private static func gatewayProgramArguments(bundlePath: String, port: Int, bind: String) -> [String] {
#if DEBUG #if DEBUG
let projectRoot = CommandResolver.projectRoot() let projectRoot = CommandResolver.projectRoot()
if let localBin = CommandResolver.projectClawdbotExecutable(projectRoot: projectRoot) { if let localBin = CommandResolver.projectClawdbotExecutable(projectRoot: projectRoot) {
return [localBin, "gateway", "--port", "\(port)", "--bind", bind] return [localBin, "gateway", "--port", "\(port)", "--bind", bind]
@ -38,7 +38,7 @@ enum GatewayLaunchAgentManager {
subcommand: "gateway", subcommand: "gateway",
extraArgs: ["--port", "\(port)", "--bind", bind]) extraArgs: ["--port", "\(port)", "--bind", bind])
} }
#endif #endif
let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath) let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath)
return [gatewayBin, "gateway-daemon", "--port", "\(port)", "--bind", bind] 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? { static func set(enabled: Bool, bundlePath: String, port: Int) async -> String? {
if enabled { if enabled {
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(legacyGatewayLaunchdLabel)"]) _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyGatewayLaunchdLabel)"])
try? FileManager.default.removeItem(at: self.legacyPlistURL) try? FileManager.default.removeItem(at: self.legacyPlistURL)
let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath) let gatewayBin = self.gatewayExecutablePath(bundlePath: bundlePath)
guard FileManager.default.isExecutableFile(atPath: gatewayBin) else { guard FileManager.default.isExecutableFile(atPath: gatewayBin) else {

View file

@ -150,21 +150,12 @@ struct GeneralSettings: View {
private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool { private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool {
guard mode != .off else { return true } guard mode != .off else { return true }
let status = CLLocationManager.authorizationStatus() let status = CLLocationManager().authorizationStatus
if status == .authorizedAlways || status == .authorizedWhenInUse { if status == .authorizedAlways {
if mode == .always && status != .authorizedAlways {
let updated = await LocationPermissionRequester.shared.request(always: true)
return updated == .authorizedAlways || updated == .authorizedWhenInUse
}
return true return true
} }
let updated = await LocationPermissionRequester.shared.request(always: mode == .always) let updated = await LocationPermissionRequester.shared.request(always: mode == .always)
switch updated { return updated == .authorizedAlways
case .authorizedAlways, .authorizedWhenInUse:
return true
default:
return false
}
} }
private var connectionSection: some View { private var connectionSection: some View {

View file

@ -189,7 +189,8 @@ final class HealthStore {
let lower = error.lowercased() let lower = error.lowercased()
if lower.contains("connection refused") { if lower.contains("connection refused") {
let port = GatewayEnvironment.gatewayPort() let port = GatewayEnvironment.gatewayPort()
return "The gateway control port (127.0.0.1:\(port)) isnt listening — restart Clawdbot to bring it back." return "The gateway control port (127.0.0.1:\(port)) isnt listening — " +
"restart Clawdbot to bring it back."
} }
if lower.contains("timeout") { if lower.contains("timeout") {
return "Timed out waiting for the control server; the gateway may be crashed or still starting." return "Timed out waiting for the control server; the gateway may be crashed or still starting."

View file

@ -6,6 +6,7 @@ enum LaunchAgentManager {
FileManager.default.homeDirectoryForCurrentUser FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist") .appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist")
} }
private static var legacyPlistURL: URL { private static var legacyPlistURL: URL {
FileManager.default.homeDirectoryForCurrentUser FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist") .appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist")
@ -19,7 +20,7 @@ enum LaunchAgentManager {
static func set(enabled: Bool, bundlePath: String) async { static func set(enabled: Bool, bundlePath: String) async {
if enabled { if enabled {
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(legacyLaunchdLabel)"]) _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyLaunchdLabel)"])
try? FileManager.default.removeItem(at: self.legacyPlistURL) try? FileManager.default.removeItem(at: self.legacyPlistURL)
self.writePlist(bundlePath: bundlePath) self.writePlist(bundlePath: bundlePath)
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"]) _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])

View file

@ -1,7 +1,7 @@
@_exported import Logging
import Foundation import Foundation
import OSLog @_exported import Logging
import os import os
import OSLog
typealias Logger = Logging.Logger typealias Logger = Logging.Logger
@ -65,15 +65,15 @@ enum ClawdbotLogging {
}() }()
static func bootstrapIfNeeded() { static func bootstrapIfNeeded() {
_ = Self.didBootstrap _ = self.didBootstrap
} }
static func makeLabel(subsystem: String, category: String) -> String { static func makeLabel(subsystem: String, category: String) -> String {
"\(subsystem)\(Self.labelSeparator)\(category)" "\(subsystem)\(self.labelSeparator)\(category)"
} }
static func parseLabel(_ label: String) -> (String, String) { 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) return ("com.clawdbot", label)
} }
let subsystem = String(label[..<range.lowerBound]) let subsystem = String(label[..<range.lowerBound])
@ -91,7 +91,7 @@ extension Logging.Logger {
} }
extension Logger.Message.StringInterpolation { extension Logger.Message.StringInterpolation {
mutating func appendInterpolation<T>(_ value: T, privacy: OSLogPrivacy) { mutating func appendInterpolation(_ value: some Any, privacy: OSLogPrivacy) {
self.appendInterpolation(String(describing: value)) self.appendInterpolation(String(describing: value))
} }
} }
@ -114,7 +114,6 @@ struct ClawdbotOSLogHandler: LogHandler {
set { self.metadata[key] = newValue } set { self.metadata[key] = newValue }
} }
// swiftlint:disable:next function_parameter_count
func log( func log(
level: Logger.Level, level: Logger.Level,
message: Logger.Message, message: Logger.Message,
@ -132,15 +131,15 @@ struct ClawdbotOSLogHandler: LogHandler {
private static func osLogType(for level: Logger.Level) -> OSLogType { private static func osLogType(for level: Logger.Level) -> OSLogType {
switch level { switch level {
case .trace, .debug: case .trace, .debug:
return .debug .debug
case .info, .notice: case .info, .notice:
return .info .info
case .warning: case .warning:
return .default .default
case .error: case .error:
return .error .error
case .critical: case .critical:
return .fault .fault
} }
} }
@ -156,7 +155,7 @@ struct ClawdbotOSLogHandler: LogHandler {
guard !metadata.isEmpty else { return message.description } guard !metadata.isEmpty else { return message.description }
let meta = metadata let meta = metadata
.sorted(by: { $0.key < $1.key }) .sorted(by: { $0.key < $1.key })
.map { "\($0.key)=\(stringify($0.value))" } .map { "\($0.key)=\(self.stringify($0.value))" }
.joined(separator: " ") .joined(separator: " ")
return "\(message.description) [\(meta)]" return "\(message.description) [\(meta)]"
} }
@ -168,9 +167,9 @@ struct ClawdbotOSLogHandler: LogHandler {
case let .stringConvertible(value): case let .stringConvertible(value):
String(describing: value) String(describing: value)
case let .array(values): case let .array(values):
"[" + values.map { stringify($0) }.joined(separator: ",") + "]" "[" + values.map { self.stringify($0) }.joined(separator: ",") + "]"
case let .dictionary(entries): 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 } set { self.metadata[key] = newValue }
} }
// swiftlint:disable:next function_parameter_count
func log( func log(
level: Logger.Level, level: Logger.Level,
message: Logger.Message, message: Logger.Message,
@ -224,9 +222,9 @@ struct ClawdbotFileLogHandler: LogHandler {
case let .stringConvertible(value): case let .stringConvertible(value):
String(describing: value) String(describing: value)
case let .array(values): case let .array(values):
"[" + values.map { stringify($0) }.joined(separator: ",") + "]" "[" + values.map { self.stringify($0) }.joined(separator: ",") + "]"
case let .dictionary(entries): case let .dictionary(entries):
"{" + entries.map { "\($0.key)=\(stringify($0.value))" }.joined(separator: ",") + "}" "{" + entries.map { "\($0.key)=\(self.stringify($0.value))" }.joined(separator: ",") + "}"
} }
} }
} }

View file

@ -172,7 +172,7 @@ struct MenuContent: View {
} }
@MainActor @MainActor
private static func buildAndSaveBrowserEnabled(_ enabled: Bool) async -> (Bool,()) { private static func buildAndSaveBrowserEnabled(_ enabled: Bool) async -> (Bool, ()) {
var root = await ConfigStore.load() var root = await ConfigStore.load()
var browser = root["browser"] as? [String: Any] ?? [:] var browser = root["browser"] as? [String: Any] ?? [:]
browser["enabled"] = enabled browser["enabled"] = enabled

View file

@ -86,7 +86,6 @@ final class HighlightedMenuItemHostView: NSView {
self.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height)) self.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
self.invalidateIntrinsicContentSize() self.invalidateIntrinsicContentSize()
} }
} }
struct MenuHostedHighlightedItem: NSViewRepresentable { struct MenuHostedHighlightedItem: NSViewRepresentable {

View file

@ -435,7 +435,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
compact.representedObject = row.key compact.representedObject = row.key
menu.addItem(compact) 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: "") let del = NSMenuItem(title: "Delete Session", action: #selector(self.deleteSession(_:)), keyEquivalent: "")
del.target = self del.target = self
del.representedObject = row.key del.representedObject = row.key
@ -541,12 +541,14 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
menu.addItem(self.makeNodeDetailItem(label: "Paired", value: entry.isPaired ? "Yes" : "No")) menu.addItem(self.makeNodeDetailItem(label: "Paired", value: entry.isPaired ? "Yes" : "No"))
if let caps = entry.caps?.filter({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }), 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: ", "))) menu.addItem(self.makeNodeCopyItem(label: "Caps", value: caps.joined(separator: ", ")))
} }
if let commands = entry.commands?.filter({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }), if let commands = entry.commands?.filter({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }),
!commands.isEmpty { !commands.isEmpty
{
menu.addItem(self.makeNodeMultilineItem( menu.addItem(self.makeNodeMultilineItem(
label: "Commands", label: "Commands",
value: commands.joined(separator: ", "), value: commands.joined(separator: ", "),
@ -589,6 +591,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
} }
return trimmed return trimmed
} }
@objc @objc
private func patchThinking(_ sender: NSMenuItem) { private func patchThinking(_ sender: NSMenuItem) {
guard let dict = sender.representedObject as? [String: Any], guard let dict = sender.representedObject as? [String: Any],
@ -770,7 +773,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
} }
private func sortedNodeEntries() -> [NodeInfo] { 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 return entries.sorted { lhs, rhs in
if lhs.isConnected != rhs.isConnected { return lhs.isConnected } if lhs.isConnected != rhs.isConnected { return lhs.isConnected }
if lhs.isPaired != rhs.isPaired { return lhs.isPaired } if lhs.isPaired != rhs.isPaired { return lhs.isPaired }
@ -781,8 +784,6 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
} }
} }
// MARK: - Views // MARK: - Views
private func makeHostedView(rootView: AnyView, width: CGFloat, highlighted: Bool) -> NSView { private func makeHostedView(rootView: AnyView, width: CGFloat, highlighted: Bool) -> NSView {

View file

@ -611,7 +611,7 @@ final class NodePairingApprovalPrompter {
private func updatePendingCounts() { private func updatePendingCounts() {
// Keep a cheap observable summary for the menu bar status line. // Keep a cheap observable summary for the menu bar status line.
self.pendingCount = self.queue.count 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 { private func reconcileOnce(timeoutMs: Double) async {

View file

@ -110,8 +110,8 @@ struct NodeMenuEntryFormatter {
guard !trimmed.isEmpty else { return trimmed } guard !trimmed.isEmpty else { return trimmed }
if let range = trimmed.range( if let range = trimmed.range(
of: #"\s*\([^)]*\d[^)]*\)$"#, of: #"\s*\([^)]*\d[^)]*\)$"#,
options: .regularExpression options: .regularExpression)
) { {
return String(trimmed[..<range.lowerBound]) return String(trimmed[..<range.lowerBound])
} }
return trimmed return trimmed
@ -227,7 +227,6 @@ struct NodeMenuRowView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
.padding(.vertical, 8) .padding(.vertical, 8)
.padding(.leading, 18) .padding(.leading, 18)

View file

@ -139,6 +139,7 @@ struct OnboardingView: View {
var isWizardBlocking: Bool { var isWizardBlocking: Bool {
self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete
} }
var canAdvance: Bool { !self.isWizardBlocking } var canAdvance: Bool { !self.isWizardBlocking }
var devLinkCommand: String { var devLinkCommand: String {
let bundlePath = Bundle.main.bundlePath let bundlePath = Bundle.main.bundlePath

View file

@ -112,7 +112,8 @@ extension OnboardingView {
HStack(spacing: 8) { HStack(spacing: 8) {
ForEach(0..<self.pageCount, id: \.self) { index in ForEach(0..<self.pageCount, id: \.self) { index in
let isLocked = wizardLockIndex != nil && !self.onboardingWizard.isComplete && index > (wizardLockIndex ?? 0) let isLocked = wizardLockIndex != nil && !self.onboardingWizard
.isComplete && index > (wizardLockIndex ?? 0)
Button { Button {
withAnimation { self.currentPage = index } withAnimation { self.currentPage = index }
} label: { } label: {

View file

@ -198,7 +198,9 @@ extension OnboardingView {
Text("CLI path") Text("CLI path")
.font(.callout.weight(.semibold)) .font(.callout.weight(.semibold))
.frame(width: labelWidth, alignment: .leading) .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) .textFieldStyle(.roundedBorder)
.frame(width: fieldWidth) .frame(width: fieldWidth)
} }
@ -444,7 +446,7 @@ extension OnboardingView {
} }
func permissionsPage() -> some View { func permissionsPage() -> some View {
return self.onboardingPage { self.onboardingPage {
Text("Grant permissions") Text("Grant permissions")
.font(.largeTitle.weight(.semibold)) .font(.largeTitle.weight(.semibold))
Text("These macOS permissions let Clawdbot automate apps and capture context on this Mac.") Text("These macOS permissions let Clawdbot automate apps and capture context on this Mac.")

View file

@ -44,15 +44,15 @@ private struct OnboardingWizardCardContent: View {
private var state: CardState { private var state: CardState {
if let error = wizard.errorMessage { return .error(error) } 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 let step = wizard.currentStep { return .step(step) }
if wizard.isComplete { return .complete } if self.wizard.isComplete { return .complete }
return .waiting return .waiting
} }
var body: some View { var body: some View {
switch state { switch self.state {
case .error(let error): case let .error(error):
Text("Wizard error") Text("Wizard error")
.font(.headline) .font(.headline)
Text(error) Text(error)
@ -60,11 +60,11 @@ private struct OnboardingWizardCardContent: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
Button("Retry") { Button("Retry") {
wizard.reset() self.wizard.reset()
Task { Task {
await wizard.startIfNeeded( await self.wizard.startIfNeeded(
mode: mode, mode: self.mode,
workspace: workspacePath.isEmpty ? nil : workspacePath) workspace: self.workspacePath.isEmpty ? nil : self.workspacePath)
} }
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
@ -74,12 +74,12 @@ private struct OnboardingWizardCardContent: View {
Text("Starting wizard…") Text("Starting wizard…")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
case .step(let step): case let .step(step):
OnboardingWizardStepView( OnboardingWizardStepView(
step: step, step: step,
isSubmitting: wizard.isSubmitting) isSubmitting: self.wizard.isSubmitting)
{ value in { value in
Task { await wizard.submit(step: step, value: value) } Task { await self.wizard.submit(step: step, value: value) }
} }
.id(step.id) .id(step.id)
case .complete: case .complete:

View file

@ -101,7 +101,7 @@ extension OnboardingView {
do { do {
try await ConfigStore.save(root) try await ConfigStore.save(root)
return (true, nil) return (true, nil)
} catch let error { } catch {
let errorMessage = "Failed to save config: \(error.localizedDescription)" let errorMessage = "Failed to save config: \(error.localizedDescription)"
return (false, errorMessage) return (false, errorMessage)
} }

View file

@ -7,6 +7,7 @@ import SwiftUI
private let onboardingWizardLogger = Logger(subsystem: "com.clawdbot", category: "onboarding.wizard") private let onboardingWizardLogger = Logger(subsystem: "com.clawdbot", category: "onboarding.wizard")
// MARK: - Swift 6 AnyCodable Bridging Helpers // MARK: - Swift 6 AnyCodable Bridging Helpers
// Bridge between ClawdbotProtocol.AnyCodable and the local module to avoid // Bridge between ClawdbotProtocol.AnyCodable and the local module to avoid
// Swift 6 strict concurrency type conflicts. // Swift 6 strict concurrency type conflicts.
@ -62,7 +63,7 @@ final class OnboardingWizardModel {
let res: WizardStartResult = try await GatewayConnection.shared.requestDecoded( let res: WizardStartResult = try await GatewayConnection.shared.requestDecoded(
method: .wizardStart, method: .wizardStart,
params: params) params: params)
applyStartResult(res) self.applyStartResult(res)
} catch { } catch {
self.status = "error" self.status = "error"
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
@ -86,7 +87,7 @@ final class OnboardingWizardModel {
let res: WizardNextResult = try await GatewayConnection.shared.requestDecoded( let res: WizardNextResult = try await GatewayConnection.shared.requestDecoded(
method: .wizardNext, method: .wizardNext,
params: params) params: params)
applyNextResult(res) self.applyNextResult(res)
} catch { } catch {
self.status = "error" self.status = "error"
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
@ -100,7 +101,7 @@ final class OnboardingWizardModel {
let res: WizardStatusResult = try await GatewayConnection.shared.requestDecoded( let res: WizardStatusResult = try await GatewayConnection.shared.requestDecoded(
method: .wizardCancel, method: .wizardCancel,
params: ["sessionId": AnyCodable(sessionId)]) params: ["sessionId": AnyCodable(sessionId)])
applyStatusResult(res) self.applyStatusResult(res)
} catch { } catch {
self.status = "error" self.status = "error"
self.errorMessage = error.localizedDescription self.errorMessage = error.localizedDescription
@ -122,7 +123,8 @@ final class OnboardingWizardModel {
self.currentStep = decodeWizardStep(res.step) self.currentStep = decodeWizardStep(res.step)
if res.done { self.currentStep = nil } if res.done { self.currentStep = nil }
if res.done || anyCodableStringValue(res.status) == "done" || anyCodableStringValue(res.status) == "cancelled" if res.done || anyCodableStringValue(res.status) == "done" || anyCodableStringValue(res.status) == "cancelled"
|| anyCodableStringValue(res.status) == "error" { || anyCodableStringValue(res.status) == "error"
{
self.sessionId = nil self.sessionId = nil
} }
} }
@ -161,8 +163,7 @@ struct OnboardingWizardStepView: View {
let initialMulti = Set( let initialMulti = Set(
options.filter { option in options.filter { option in
anyCodableArray(step.initialvalue).contains { anyCodableEqual($0, option.option.value) } anyCodableArray(step.initialvalue).contains { anyCodableEqual($0, option.option.value) }
}.map { $0.index } }.map(\.index))
)
_textValue = State(initialValue: initialText) _textValue = State(initialValue: initialText)
_confirmValue = State(initialValue: initialConfirm) _confirmValue = State(initialValue: initialConfirm)
@ -183,18 +184,18 @@ struct OnboardingWizardStepView: View {
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
switch wizardStepType(step) { switch wizardStepType(self.step) {
case "note": case "note":
EmptyView() EmptyView()
case "text": case "text":
textField self.textField
case "confirm": case "confirm":
Toggle("", isOn: $confirmValue) Toggle("", isOn: self.$confirmValue)
.toggleStyle(.switch) .toggleStyle(.switch)
case "select": case "select":
selectOptions self.selectOptions
case "multiselect": case "multiselect":
multiselectOptions self.multiselectOptions
case "progress": case "progress":
ProgressView() ProgressView()
.controlSize(.small) .controlSize(.small)
@ -205,25 +206,25 @@ struct OnboardingWizardStepView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
Button(action: submit) { Button(action: self.submit) {
Text(wizardStepType(step) == "action" ? "Run" : "Continue") Text(wizardStepType(self.step) == "action" ? "Run" : "Continue")
.frame(minWidth: 120) .frame(minWidth: 120)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.disabled(isSubmitting || isBlocked) .disabled(self.isSubmitting || self.isBlocked)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
@ViewBuilder @ViewBuilder
private var textField: some View { private var textField: some View {
let isSensitive = step.sensitive == true let isSensitive = self.step.sensitive == true
if isSensitive { if isSensitive {
SecureField(step.placeholder ?? "", text: $textValue) SecureField(self.step.placeholder ?? "", text: self.$textValue)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(maxWidth: 360) .frame(maxWidth: 360)
} else { } else {
TextField(step.placeholder ?? "", text: $textValue) TextField(self.step.placeholder ?? "", text: self.$textValue)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(maxWidth: 360) .frame(maxWidth: 360)
} }
@ -231,12 +232,12 @@ struct OnboardingWizardStepView: View {
private var selectOptions: some View { private var selectOptions: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
ForEach(optionItems) { item in ForEach(self.optionItems) { item in
Button { Button {
selectedIndex = item.index self.selectedIndex = item.index
} label: { } label: {
HStack(alignment: .top, spacing: 8) { 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) .foregroundStyle(.accent)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(item.option.label) Text(item.option.label)
@ -256,8 +257,8 @@ struct OnboardingWizardStepView: View {
private var multiselectOptions: some View { private var multiselectOptions: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
ForEach(optionItems) { item in ForEach(self.optionItems) { item in
Toggle(isOn: bindingForOption(item)) { Toggle(isOn: self.bindingForOption(item)) {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(item.option.label) Text(item.option.label)
if let hint = item.option.hint, !hint.isEmpty { if let hint = item.option.hint, !hint.isEmpty {
@ -273,47 +274,47 @@ struct OnboardingWizardStepView: View {
private func bindingForOption(_ item: WizardOptionItem) -> Binding<Bool> { private func bindingForOption(_ item: WizardOptionItem) -> Binding<Bool> {
Binding(get: { Binding(get: {
selectedIndices.contains(item.index) self.selectedIndices.contains(item.index)
}, set: { newValue in }, set: { newValue in
if newValue { if newValue {
selectedIndices.insert(item.index) self.selectedIndices.insert(item.index)
} else { } else {
selectedIndices.remove(item.index) self.selectedIndices.remove(item.index)
} }
}) })
} }
private var isBlocked: Bool { private var isBlocked: Bool {
let type = wizardStepType(step) let type = wizardStepType(step)
if type == "select" { return optionItems.isEmpty } if type == "select" { return self.optionItems.isEmpty }
if type == "multiselect" { return optionItems.isEmpty } if type == "multiselect" { return self.optionItems.isEmpty }
return false return false
} }
private func submit() { private func submit() {
switch wizardStepType(step) { switch wizardStepType(self.step) {
case "note", "progress": case "note", "progress":
onSubmit(nil) self.onSubmit(nil)
case "text": case "text":
onSubmit(AnyCodable(textValue)) self.onSubmit(AnyCodable(self.textValue))
case "confirm": case "confirm":
onSubmit(AnyCodable(confirmValue)) self.onSubmit(AnyCodable(self.confirmValue))
case "select": case "select":
guard optionItems.indices.contains(selectedIndex) else { guard self.optionItems.indices.contains(self.selectedIndex) else {
onSubmit(nil) self.onSubmit(nil)
return return
} }
let option = optionItems[selectedIndex].option let option = self.optionItems[self.selectedIndex].option
onSubmit(bridgeToLocal(option.value) ?? AnyCodable(option.label)) self.onSubmit(bridgeToLocal(option.value) ?? AnyCodable(option.label))
case "multiselect": case "multiselect":
let values = optionItems let values = self.optionItems
.filter { selectedIndices.contains($0.index) } .filter { self.selectedIndices.contains($0.index) }
.map { bridgeToLocal($0.option.value) ?? AnyCodable($0.option.label) } .map { bridgeToLocal($0.option.value) ?? AnyCodable($0.option.label) }
onSubmit(AnyCodable(values)) self.onSubmit(AnyCodable(values))
case "action": case "action":
onSubmit(AnyCodable(true)) self.onSubmit(AnyCodable(true))
default: default:
onSubmit(nil) self.onSubmit(nil)
} }
} }
} }
@ -322,7 +323,7 @@ private struct WizardOptionItem: Identifiable {
let index: Int let index: Int
let option: WizardOption let option: WizardOption
var id: Int { index } var id: Int { self.index }
} }
private struct WizardOption { private struct WizardOption {
@ -359,15 +360,15 @@ private func wizardStepType(_ step: WizardStep) -> String {
private func anyCodableString(_ value: ProtocolAnyCodable?) -> String { private func anyCodableString(_ value: ProtocolAnyCodable?) -> String {
switch value?.value { switch value?.value {
case let string as String: case let string as String:
return string string
case let int as Int: case let int as Int:
return String(int) String(int)
case let double as Double: case let double as Double:
return String(double) String(double)
case let bool as Bool: case let bool as Bool:
return bool ? "true" : "false" bool ? "true" : "false"
default: default:
return "" ""
} }
} }
@ -378,44 +379,44 @@ private func anyCodableStringValue(_ value: ProtocolAnyCodable?) -> String? {
private func anyCodableBool(_ value: ProtocolAnyCodable?) -> Bool { private func anyCodableBool(_ value: ProtocolAnyCodable?) -> Bool {
switch value?.value { switch value?.value {
case let bool as Bool: case let bool as Bool:
return bool bool
case let string as String: case let string as String:
return string.lowercased() == "true" string.lowercased() == "true"
default: default:
return false false
} }
} }
private func anyCodableArray(_ value: ProtocolAnyCodable?) -> [ProtocolAnyCodable] { private func anyCodableArray(_ value: ProtocolAnyCodable?) -> [ProtocolAnyCodable] {
switch value?.value { switch value?.value {
case let arr as [ProtocolAnyCodable]: case let arr as [ProtocolAnyCodable]:
return arr arr
case let arr as [Any]: case let arr as [Any]:
return arr.map { ProtocolAnyCodable($0) } arr.map { ProtocolAnyCodable($0) }
default: default:
return [] []
} }
} }
private func anyCodableEqual(_ lhs: ProtocolAnyCodable?, _ rhs: ProtocolAnyCodable?) -> Bool { private func anyCodableEqual(_ lhs: ProtocolAnyCodable?, _ rhs: ProtocolAnyCodable?) -> Bool {
switch (lhs?.value, rhs?.value) { switch (lhs?.value, rhs?.value) {
case let (l as String, r as String): case let (l as String, r as String):
return l == r l == r
case let (l as Int, r as Int): case let (l as Int, r as Int):
return l == r l == r
case let (l as Double, r as Double): case let (l as Double, r as Double):
return l == r l == r
case let (l as Bool, r as Bool): case let (l as Bool, r as Bool):
return l == r l == r
case let (l as String, r as Int): case let (l as String, r as Int):
return l == String(r) l == String(r)
case let (l as Int, r as String): case let (l as Int, r as String):
return String(l) == r String(l) == r
case let (l as String, r as Double): case let (l as String, r as Double):
return l == String(r) l == String(r)
case let (l as Double, r as String): case let (l as Double, r as String):
return String(l) == r String(l) == r
default: default:
return false false
} }
} }

View file

@ -1,9 +1,9 @@
import Foundation import Foundation
import Security
import os import os
import PeekabooAutomationKit import PeekabooAutomationKit
import PeekabooBridge import PeekabooBridge
import PeekabooFoundation import PeekabooFoundation
import Security
@MainActor @MainActor
final class PeekabooBridgeHostCoordinator { final class PeekabooBridgeHostCoordinator {
@ -80,7 +80,7 @@ final class PeekabooBridgeHostCoordinator {
staticCode, staticCode,
SecCSFlags(rawValue: kSecCSSigningInformation), SecCSFlags(rawValue: kSecCSSigningInformation),
&infoCF) == errSecSuccess, &infoCF) == errSecSuccess,
let info = infoCF as? [String: Any] let info = infoCF as? [String: Any]
else { else {
return nil return nil
} }

View file

@ -138,14 +138,14 @@ enum PermissionManager {
} }
private static func ensureLocation(interactive: Bool) async -> Bool { private static func ensureLocation(interactive: Bool) async -> Bool {
let status = CLLocationManager.authorizationStatus() let status = CLLocationManager().authorizationStatus
switch status { switch status {
case .authorizedAlways, .authorizedWhenInUse: case .authorizedAlways:
return true return true
case .notDetermined: case .notDetermined:
guard interactive else { return false } guard interactive else { return false }
let updated = await LocationPermissionRequester.shared.request(always: false) let updated = await LocationPermissionRequester.shared.request(always: false)
return updated == .authorizedAlways || updated == .authorizedWhenInUse return updated == .authorizedAlways
case .denied, .restricted: case .denied, .restricted:
if interactive { if interactive {
LocationPermissionHelper.openSettings() LocationPermissionHelper.openSettings()
@ -198,9 +198,10 @@ enum PermissionManager {
case .camera: case .camera:
results[cap] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized results[cap] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
case .location: case .location:
let status = CLLocationManager.authorizationStatus() let status = CLLocationManager().authorizationStatus
results[cap] = status == .authorizedAlways || status == .authorizedWhenInUse results[cap] = status == .authorizedAlways
} }
} }
return results return results

View file

@ -48,7 +48,8 @@ struct PermissionStatusList: View {
if (self.status[.accessibility] ?? false) == false || (self.status[.screenRecording] ?? false) == false { if (self.status[.accessibility] ?? false) == false || (self.status[.screenRecording] ?? false) == false {
VStack(alignment: .leading, spacing: 8) { 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) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)

View file

@ -53,10 +53,12 @@ final class RemotePortTunnel {
let resolvedRemotePort = remotePortOverride ?? remotePort let resolvedRemotePort = remotePortOverride ?? remotePort
if let override = remotePortOverride { if let override = remotePortOverride {
Self.logger.info( 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 { } else {
Self.logger.debug( 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] = [ var args: [String] = [
"-o", "BatchMode=yes", "-o", "BatchMode=yes",

View file

@ -29,7 +29,9 @@ actor RemoteTunnelManager {
{ {
if await self.isTunnelHealthy(port: desiredPort) { if await self.isTunnelHealthy(port: desiredPort) {
self.logger.info( 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 return desiredPort
} }
await self.cleanupStaleTunnel(desc: desc, port: desiredPort) await self.cleanupStaleTunnel(desc: desc, port: desiredPort)
@ -50,7 +52,8 @@ actor RemoteTunnelManager {
let identitySet = !settings.identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty let identitySet = !settings.identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
self.logger.info( 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 } if let local = await self.controlTunnelPortIfRunning() { return local }

View file

@ -174,12 +174,13 @@ struct SessionMenuPreviewView: View {
let timeoutMs = Int(Self.previewTimeoutSeconds * 1000) let timeoutMs = Int(Self.previewTimeoutSeconds * 1000)
let payload = try await AsyncTimeout.withTimeout( let payload = try await AsyncTimeout.withTimeout(
seconds: Self.previewTimeoutSeconds, seconds: Self.previewTimeoutSeconds,
onTimeout: { PreviewTimeoutError() }) { onTimeout: { PreviewTimeoutError() },
operation: {
try await GatewayConnection.shared.chatHistory( try await GatewayConnection.shared.chatHistory(
sessionKey: self.sessionKey, sessionKey: self.sessionKey,
limit: self.previewLimit, limit: self.previewLimit,
timeoutMs: timeoutMs) timeoutMs: timeoutMs)
} })
let built = Self.previewItems(from: payload, maxItems: self.maxItems) let built = Self.previewItems(from: payload, maxItems: self.maxItems)
await SessionPreviewCache.shared.store(items: built, for: self.sessionKey) await SessionPreviewCache.shared.store(items: built, for: self.sessionKey)
await MainActor.run { await MainActor.run {
@ -198,7 +199,10 @@ struct SessionMenuPreviewView: View {
self.status = .error("Preview unavailable") 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)")
} }
} }

View file

@ -285,8 +285,7 @@ struct TailscaleIntegrationSection: View {
requireCredentialsForServe: self.requireCredentialsForServe, requireCredentialsForServe: self.requireCredentialsForServe,
password: trimmedPassword, password: trimmedPassword,
connectionMode: self.connectionMode, connectionMode: self.connectionMode,
isPaused: self.isPaused isPaused: self.isPaused)
)
if !success, let errorMessage { if !success, let errorMessage {
self.statusMessage = errorMessage self.statusMessage = errorMessage
@ -307,8 +306,8 @@ struct TailscaleIntegrationSection: View {
requireCredentialsForServe: Bool, requireCredentialsForServe: Bool,
password: String, password: String,
connectionMode: AppState.ConnectionMode, connectionMode: AppState.ConnectionMode,
isPaused: Bool isPaused: Bool) async -> (Bool, String?)
) async -> (Bool, String?) { {
var root = await ConfigStore.load() var root = await ConfigStore.load()
var gateway = root["gateway"] as? [String: Any] ?? [:] var gateway = root["gateway"] as? [String: Any] ?? [:]
var tailscale = gateway["tailscale"] as? [String: Any] ?? [:] var tailscale = gateway["tailscale"] as? [String: Any] ?? [:]
@ -349,7 +348,7 @@ struct TailscaleIntegrationSection: View {
do { do {
try await ConfigStore.save(root) try await ConfigStore.save(root)
return (true, nil) return (true, nil)
} catch let error { } catch {
return (false, error.localizedDescription) return (false, error.localizedDescription)
} }
} }

View file

@ -436,14 +436,49 @@ actor TalkModeRuntime {
} }
} }
// swiftlint:disable:next cyclomatic_complexity function_body_length
private func playAssistant(text: String) async { 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 gen = self.lifecycleGeneration
let parse = TalkDirectiveParser.parse(text) let parse = TalkDirectiveParser.parse(text)
let directive = parse.directive let directive = parse.directive
let cleaned = parse.stripped.trimmingCharacters(in: .whitespacesAndNewlines) let cleaned = parse.stripped.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else { return } guard !cleaned.isEmpty else { return nil }
guard self.isCurrent(gen) else { return } guard self.isCurrent(gen) else { return nil }
if !parse.unknownKeys.isEmpty { if !parse.unknownKeys.isEmpty {
self.logger self.logger
@ -504,116 +539,123 @@ actor TalkModeRuntime {
let synthTimeoutSeconds = max(20.0, min(90.0, Double(cleaned.count) * 0.12)) let synthTimeoutSeconds = max(20.0, min(90.0, Double(cleaned.count) * 0.12))
do { guard self.isCurrent(gen) else { return nil }
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)")
}
let modelId = directive?.modelId ?? self.currentModelId ?? self.defaultModelId return TalkPlaybackInput(
func makeRequest(outputFormat: String?) -> ElevenLabsTTSRequest { generation: gen,
ElevenLabsTTSRequest( cleanedText: cleaned,
text: cleaned, directive: directive,
modelId: modelId, apiKey: apiKey,
outputFormat: outputFormat, voiceId: voiceId,
speed: TalkTTSValidation.resolveSpeed(speed: directive?.speed, rateWPM: directive?.rateWPM), language: language,
stability: TalkTTSValidation.validatedStability(directive?.stability, modelId: modelId), synthTimeoutSeconds: synthTimeoutSeconds)
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))
}
let request = makeRequest(outputFormat: outputFormat) private func playElevenLabs(input: TalkPlaybackInput, apiKey: String, voiceId: String) async throws {
let desiredOutputFormat = input.directive?.outputFormat ?? self.defaultOutputFormat ?? "pcm_44100"
self.ttsLogger.info("talk TTS synth timeout=\(synthTimeoutSeconds, privacy: .public)s") let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(desiredOutputFormat)
let client = ElevenLabsTTSClient(apiKey: apiKey) if outputFormat == nil, !desiredOutputFormat.isEmpty {
let stream = client.streamSynthesize(voiceId: voiceId, request: request) self.logger
guard self.isCurrent(gen) else { return } .warning(
"talk output_format unsupported for local playback: " +
if self.interruptOnSpeech { "\(desiredOutputFormat, privacy: .public)")
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)")
}
} }
if self.phase == .speaking { let modelId = input.directive?.modelId ?? self.currentModelId ?? self.defaultModelId
self.phase = .thinking func makeRequest(outputFormat: String?) -> ElevenLabsTTSRequest {
await MainActor.run { TalkModeController.shared.updatePhase(.thinking) } 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<Data, Error>) 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? { private func resolveVoiceId(preferred: String?, apiKey: String) async -> String? {
@ -682,6 +724,9 @@ actor TalkModeRuntime {
self.phase = .thinking self.phase = .thinking
await MainActor.run { TalkModeController.shared.updatePhase(.thinking) } await MainActor.run { TalkModeController.shared.updatePhase(.thinking) }
} }
}
extension TalkModeRuntime {
// MARK: - Audio playback (MainActor helpers) // MARK: - Audio playback (MainActor helpers)

View file

@ -35,14 +35,14 @@ struct TalkOverlayView: View {
.frame(width: 18, height: 18) .frame(width: 18, height: 18)
.background(Color.black.opacity(0.4)) .background(Color.black.opacity(0.4))
.clipShape(Circle()) .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) .onHover { self.hoveringWindow = $0 }
.contentShape(Circle())
.offset(x: -2, y: -2)
.opacity(self.hoveringWindow ? 1 : 0)
.animation(.easeOut(duration: 0.12), value: self.hoveringWindow)
}
.onHover { self.hoveringWindow = $0 }
} }
.frame( .frame(
width: TalkOverlayController.overlaySize, width: TalkOverlayController.overlaySize,
@ -124,7 +124,7 @@ private final class OrbInteractionNSView: NSView {
} }
override func mouseUp(with event: NSEvent) { override func mouseUp(with event: NSEvent) {
if !self.didDrag && !self.suppressSingleClick { if !self.didDrag, !self.suppressSingleClick {
self.onSingleClick?() self.onSingleClick?()
} }
self.mouseDownEvent = nil self.mouseDownEvent = nil
@ -148,8 +148,8 @@ private struct TalkOrbView: View {
} else { } else {
TimelineView(.animation) { context in TimelineView(.animation) { context in
let t = context.date.timeIntervalSinceReferenceDate let t = context.date.timeIntervalSinceReferenceDate
let listenScale = phase == .listening ? (1 + CGFloat(self.level) * 0.12) : 1 let listenScale = self.phase == .listening ? (1 + CGFloat(self.level) * 0.12) : 1
let pulse = phase == .speaking ? (1 + 0.06 * sin(t * 6)) : 1 let pulse = self.phase == .speaking ? (1 + 0.06 * sin(t * 6)) : 1
ZStack { ZStack {
Circle() Circle()
@ -158,9 +158,9 @@ private struct TalkOrbView: View {
.shadow(color: Color.black.opacity(0.22), radius: 10, x: 0, y: 5) .shadow(color: Color.black.opacity(0.22), radius: 10, x: 0, y: 5)
.scaleEffect(pulse * listenScale) .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) TalkOrbitArcs(time: t)
} }
} }
@ -186,11 +186,12 @@ private struct TalkWaveRings: View {
var body: some View { var body: some View {
ZStack { ZStack {
ForEach(0..<3, id: \.self) { idx in 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 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 amplitude = self.phase == .speaking ? 0.95 : self.phase == .listening ? 0.5 + self
let scale = 0.75 + progress * amplitude + (phase == .listening ? level * 0.15 : 0) .level * 0.7 : 0.35
let alpha = phase == .speaking ? 0.72 : phase == .listening ? 0.58 + level * 0.28 : 0.4 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() Circle()
.stroke(self.accent.opacity(alpha - progress * 0.3), lineWidth: 1.6) .stroke(self.accent.opacity(alpha - progress * 0.3), lineWidth: 1.6)
.scaleEffect(scale) .scaleEffect(scale)
@ -208,11 +209,11 @@ private struct TalkOrbitArcs: View {
Circle() Circle()
.trim(from: 0.08, to: 0.26) .trim(from: 0.08, to: 0.26)
.stroke(Color.white.opacity(0.88), style: StrokeStyle(lineWidth: 1.6, lineCap: .round)) .stroke(Color.white.opacity(0.88), style: StrokeStyle(lineWidth: 1.6, lineCap: .round))
.rotationEffect(.degrees(time * 42)) .rotationEffect(.degrees(self.time * 42))
Circle() Circle()
.trim(from: 0.62, to: 0.86) .trim(from: 0.62, to: 0.86)
.stroke(Color.white.opacity(0.7), style: StrokeStyle(lineWidth: 1.4, lineCap: .round)) .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) .scaleEffect(1.08)
} }

View file

@ -213,7 +213,7 @@ final class WorkActivityStore {
meta: String?, meta: String?,
args: [String: AnyCodable]?) -> 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) let display = ToolDisplayRegistry.resolve(name: name ?? "tool", args: wrappedArgs, meta: meta)
if let detail = display.detailLine, !detail.isEmpty { if let detail = display.detailLine, !detail.isEmpty {
return "\(display.label): \(detail)" return "\(display.label): \(detail)"
@ -223,22 +223,22 @@ final class WorkActivityStore {
private static func wrapToolArgs(_ args: [String: AnyCodable]?) -> ClawdbotKit.AnyCodable? { private static func wrapToolArgs(_ args: [String: AnyCodable]?) -> ClawdbotKit.AnyCodable? {
guard let args else { return nil } 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) return ClawdbotKit.AnyCodable(converted)
} }
private static func unwrapJSONValue(_ value: Any) -> Any { private static func unwrapJSONValue(_ value: Any) -> Any {
if let dict = value as? [String: AnyCodable] { 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] { 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] { if let dict = value as? [String: Any] {
return dict.mapValues { unwrapJSONValue($0) } return dict.mapValues { self.unwrapJSONValue($0) }
} }
if let array = value as? [Any] { if let array = value as? [Any] {
return array.map { unwrapJSONValue($0) } return array.map { self.unwrapJSONValue($0) }
} }
return value return value
} }

View file

@ -26,8 +26,8 @@ public struct ConnectParams: Codable {
caps: [String]?, caps: [String]?,
auth: [String: AnyCodable]?, auth: [String: AnyCodable]?,
locale: String?, locale: String?,
useragent: String? useragent: String?)
) { {
self.minprotocol = minprotocol self.minprotocol = minprotocol
self.maxprotocol = maxprotocol self.maxprotocol = maxprotocol
self.client = client self.client = client
@ -36,6 +36,7 @@ public struct ConnectParams: Codable {
self.locale = locale self.locale = locale
self.useragent = useragent self.useragent = useragent
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case minprotocol = "minProtocol" case minprotocol = "minProtocol"
case maxprotocol = "maxProtocol" case maxprotocol = "maxProtocol"
@ -63,8 +64,8 @@ public struct HelloOk: Codable {
features: [String: AnyCodable], features: [String: AnyCodable],
snapshot: Snapshot, snapshot: Snapshot,
canvashosturl: String?, canvashosturl: String?,
policy: [String: AnyCodable] policy: [String: AnyCodable])
) { {
self.type = type self.type = type
self._protocol = _protocol self._protocol = _protocol
self.server = server self.server = server
@ -73,6 +74,7 @@ public struct HelloOk: Codable {
self.canvashosturl = canvashosturl self.canvashosturl = canvashosturl
self.policy = policy self.policy = policy
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case _protocol = "protocol" case _protocol = "protocol"
@ -94,13 +96,14 @@ public struct RequestFrame: Codable {
type: String, type: String,
id: String, id: String,
method: String, method: String,
params: AnyCodable? params: AnyCodable?)
) { {
self.type = type self.type = type
self.id = id self.id = id
self.method = method self.method = method
self.params = params self.params = params
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case id case id
@ -121,14 +124,15 @@ public struct ResponseFrame: Codable {
id: String, id: String,
ok: Bool, ok: Bool,
payload: AnyCodable?, payload: AnyCodable?,
error: [String: AnyCodable]? error: [String: AnyCodable]?)
) { {
self.type = type self.type = type
self.id = id self.id = id
self.ok = ok self.ok = ok
self.payload = payload self.payload = payload
self.error = error self.error = error
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case id case id
@ -150,14 +154,15 @@ public struct EventFrame: Codable {
event: String, event: String,
payload: AnyCodable?, payload: AnyCodable?,
seq: Int?, seq: Int?,
stateversion: [String: AnyCodable]? stateversion: [String: AnyCodable]?)
) { {
self.type = type self.type = type
self.event = event self.event = event
self.payload = payload self.payload = payload
self.seq = seq self.seq = seq
self.stateversion = stateversion self.stateversion = stateversion
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type case type
case event case event
@ -195,8 +200,8 @@ public struct PresenceEntry: Codable {
tags: [String]?, tags: [String]?,
text: String?, text: String?,
ts: Int, ts: Int,
instanceid: String? instanceid: String?)
) { {
self.host = host self.host = host
self.ip = ip self.ip = ip
self.version = version self.version = version
@ -211,6 +216,7 @@ public struct PresenceEntry: Codable {
self.ts = ts self.ts = ts
self.instanceid = instanceid self.instanceid = instanceid
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case host case host
case ip case ip
@ -234,11 +240,12 @@ public struct StateVersion: Codable {
public init( public init(
presence: Int, presence: Int,
health: Int health: Int)
) { {
self.presence = presence self.presence = presence
self.health = health self.health = health
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case presence case presence
case health case health
@ -259,8 +266,8 @@ public struct Snapshot: Codable {
stateversion: StateVersion, stateversion: StateVersion,
uptimems: Int, uptimems: Int,
configpath: String?, configpath: String?,
statedir: String? statedir: String?)
) { {
self.presence = presence self.presence = presence
self.health = health self.health = health
self.stateversion = stateversion self.stateversion = stateversion
@ -268,6 +275,7 @@ public struct Snapshot: Codable {
self.configpath = configpath self.configpath = configpath
self.statedir = statedir self.statedir = statedir
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case presence case presence
case health case health
@ -290,14 +298,15 @@ public struct ErrorShape: Codable {
message: String, message: String,
details: AnyCodable?, details: AnyCodable?,
retryable: Bool?, retryable: Bool?,
retryafterms: Int? retryafterms: Int?)
) { {
self.code = code self.code = code
self.message = message self.message = message
self.details = details self.details = details
self.retryable = retryable self.retryable = retryable
self.retryafterms = retryafterms self.retryafterms = retryafterms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case code case code
case message case message
@ -319,14 +328,15 @@ public struct AgentEvent: Codable {
seq: Int, seq: Int,
stream: String, stream: String,
ts: Int, ts: Int,
data: [String: AnyCodable] data: [String: AnyCodable])
) { {
self.runid = runid self.runid = runid
self.seq = seq self.seq = seq
self.stream = stream self.stream = stream
self.ts = ts self.ts = ts
self.data = data self.data = data
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case runid = "runId" case runid = "runId"
case seq case seq
@ -350,8 +360,8 @@ public struct SendParams: Codable {
mediaurl: String?, mediaurl: String?,
gifplayback: Bool?, gifplayback: Bool?,
provider: String?, provider: String?,
idempotencykey: String idempotencykey: String)
) { {
self.to = to self.to = to
self.message = message self.message = message
self.mediaurl = mediaurl self.mediaurl = mediaurl
@ -359,6 +369,7 @@ public struct SendParams: Codable {
self.provider = provider self.provider = provider
self.idempotencykey = idempotencykey self.idempotencykey = idempotencykey
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case to case to
case message case message
@ -393,8 +404,8 @@ public struct AgentParams: Codable {
timeout: Int?, timeout: Int?,
lane: String?, lane: String?,
extrasystemprompt: String?, extrasystemprompt: String?,
idempotencykey: String idempotencykey: String)
) { {
self.message = message self.message = message
self.to = to self.to = to
self.sessionid = sessionid self.sessionid = sessionid
@ -407,6 +418,7 @@ public struct AgentParams: Codable {
self.extrasystemprompt = extrasystemprompt self.extrasystemprompt = extrasystemprompt
self.idempotencykey = idempotencykey self.idempotencykey = idempotencykey
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case message case message
case to case to
@ -430,12 +442,13 @@ public struct AgentWaitParams: Codable {
public init( public init(
runid: String, runid: String,
afterms: Int?, afterms: Int?,
timeoutms: Int? timeoutms: Int?)
) { {
self.runid = runid self.runid = runid
self.afterms = afterms self.afterms = afterms
self.timeoutms = timeoutms self.timeoutms = timeoutms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case runid = "runId" case runid = "runId"
case afterms = "afterMs" case afterms = "afterMs"
@ -449,11 +462,12 @@ public struct WakeParams: Codable {
public init( public init(
mode: AnyCodable, mode: AnyCodable,
text: String text: String)
) { {
self.mode = mode self.mode = mode
self.text = text self.text = text
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case mode case mode
case text case text
@ -482,8 +496,8 @@ public struct NodePairRequestParams: Codable {
caps: [String]?, caps: [String]?,
commands: [String]?, commands: [String]?,
remoteip: String?, remoteip: String?,
silent: Bool? silent: Bool?)
) { {
self.nodeid = nodeid self.nodeid = nodeid
self.displayname = displayname self.displayname = displayname
self.platform = platform self.platform = platform
@ -495,6 +509,7 @@ public struct NodePairRequestParams: Codable {
self.remoteip = remoteip self.remoteip = remoteip
self.silent = silent self.silent = silent
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case nodeid = "nodeId" case nodeid = "nodeId"
case displayname = "displayName" case displayname = "displayName"
@ -509,17 +524,17 @@ public struct NodePairRequestParams: Codable {
} }
} }
public struct NodePairListParams: Codable { public struct NodePairListParams: Codable {}
}
public struct NodePairApproveParams: Codable { public struct NodePairApproveParams: Codable {
public let requestid: String public let requestid: String
public init( public init(
requestid: String requestid: String)
) { {
self.requestid = requestid self.requestid = requestid
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case requestid = "requestId" case requestid = "requestId"
} }
@ -529,10 +544,11 @@ public struct NodePairRejectParams: Codable {
public let requestid: String public let requestid: String
public init( public init(
requestid: String requestid: String)
) { {
self.requestid = requestid self.requestid = requestid
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case requestid = "requestId" case requestid = "requestId"
} }
@ -544,11 +560,12 @@ public struct NodePairVerifyParams: Codable {
public init( public init(
nodeid: String, nodeid: String,
token: String token: String)
) { {
self.nodeid = nodeid self.nodeid = nodeid
self.token = token self.token = token
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case nodeid = "nodeId" case nodeid = "nodeId"
case token case token
@ -561,28 +578,29 @@ public struct NodeRenameParams: Codable {
public init( public init(
nodeid: String, nodeid: String,
displayname: String displayname: String)
) { {
self.nodeid = nodeid self.nodeid = nodeid
self.displayname = displayname self.displayname = displayname
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case nodeid = "nodeId" case nodeid = "nodeId"
case displayname = "displayName" case displayname = "displayName"
} }
} }
public struct NodeListParams: Codable { public struct NodeListParams: Codable {}
}
public struct NodeDescribeParams: Codable { public struct NodeDescribeParams: Codable {
public let nodeid: String public let nodeid: String
public init( public init(
nodeid: String nodeid: String)
) { {
self.nodeid = nodeid self.nodeid = nodeid
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case nodeid = "nodeId" case nodeid = "nodeId"
} }
@ -600,14 +618,15 @@ public struct NodeInvokeParams: Codable {
command: String, command: String,
params: AnyCodable?, params: AnyCodable?,
timeoutms: Int?, timeoutms: Int?,
idempotencykey: String idempotencykey: String)
) { {
self.nodeid = nodeid self.nodeid = nodeid
self.command = command self.command = command
self.params = params self.params = params
self.timeoutms = timeoutms self.timeoutms = timeoutms
self.idempotencykey = idempotencykey self.idempotencykey = idempotencykey
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case nodeid = "nodeId" case nodeid = "nodeId"
case command case command
@ -627,13 +646,14 @@ public struct SessionsListParams: Codable {
limit: Int?, limit: Int?,
activeminutes: Int?, activeminutes: Int?,
includeglobal: Bool?, includeglobal: Bool?,
includeunknown: Bool? includeunknown: Bool?)
) { {
self.limit = limit self.limit = limit
self.activeminutes = activeminutes self.activeminutes = activeminutes
self.includeglobal = includeglobal self.includeglobal = includeglobal
self.includeunknown = includeunknown self.includeunknown = includeunknown
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case limit case limit
case activeminutes = "activeMinutes" case activeminutes = "activeMinutes"
@ -658,8 +678,8 @@ public struct SessionsPatchParams: Codable {
elevatedlevel: AnyCodable?, elevatedlevel: AnyCodable?,
model: AnyCodable?, model: AnyCodable?,
sendpolicy: AnyCodable?, sendpolicy: AnyCodable?,
groupactivation: AnyCodable? groupactivation: AnyCodable?)
) { {
self.key = key self.key = key
self.thinkinglevel = thinkinglevel self.thinkinglevel = thinkinglevel
self.verboselevel = verboselevel self.verboselevel = verboselevel
@ -668,6 +688,7 @@ public struct SessionsPatchParams: Codable {
self.sendpolicy = sendpolicy self.sendpolicy = sendpolicy
self.groupactivation = groupactivation self.groupactivation = groupactivation
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case key case key
case thinkinglevel = "thinkingLevel" case thinkinglevel = "thinkingLevel"
@ -683,10 +704,11 @@ public struct SessionsResetParams: Codable {
public let key: String public let key: String
public init( public init(
key: String key: String)
) { {
self.key = key self.key = key
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case key case key
} }
@ -698,11 +720,12 @@ public struct SessionsDeleteParams: Codable {
public init( public init(
key: String, key: String,
deletetranscript: Bool? deletetranscript: Bool?)
) { {
self.key = key self.key = key
self.deletetranscript = deletetranscript self.deletetranscript = deletetranscript
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case key case key
case deletetranscript = "deleteTranscript" case deletetranscript = "deleteTranscript"
@ -715,35 +738,35 @@ public struct SessionsCompactParams: Codable {
public init( public init(
key: String, key: String,
maxlines: Int? maxlines: Int?)
) { {
self.key = key self.key = key
self.maxlines = maxlines self.maxlines = maxlines
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case key case key
case maxlines = "maxLines" case maxlines = "maxLines"
} }
} }
public struct ConfigGetParams: Codable { public struct ConfigGetParams: Codable {}
}
public struct ConfigSetParams: Codable { public struct ConfigSetParams: Codable {
public let raw: String public let raw: String
public init( public init(
raw: String raw: String)
) { {
self.raw = raw self.raw = raw
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case raw case raw
} }
} }
public struct ConfigSchemaParams: Codable { public struct ConfigSchemaParams: Codable {}
}
public struct ConfigSchemaResponse: Codable { public struct ConfigSchemaResponse: Codable {
public let schema: AnyCodable public let schema: AnyCodable
@ -755,13 +778,14 @@ public struct ConfigSchemaResponse: Codable {
schema: AnyCodable, schema: AnyCodable,
uihints: [String: AnyCodable], uihints: [String: AnyCodable],
version: String, version: String,
generatedat: String generatedat: String)
) { {
self.schema = schema self.schema = schema
self.uihints = uihints self.uihints = uihints
self.version = version self.version = version
self.generatedat = generatedat self.generatedat = generatedat
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case schema case schema
case uihints = "uiHints" case uihints = "uiHints"
@ -776,11 +800,12 @@ public struct WizardStartParams: Codable {
public init( public init(
mode: AnyCodable?, mode: AnyCodable?,
workspace: String? workspace: String?)
) { {
self.mode = mode self.mode = mode
self.workspace = workspace self.workspace = workspace
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case mode case mode
case workspace case workspace
@ -793,11 +818,12 @@ public struct WizardNextParams: Codable {
public init( public init(
sessionid: String, sessionid: String,
answer: [String: AnyCodable]? answer: [String: AnyCodable]?)
) { {
self.sessionid = sessionid self.sessionid = sessionid
self.answer = answer self.answer = answer
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId" case sessionid = "sessionId"
case answer case answer
@ -808,10 +834,11 @@ public struct WizardCancelParams: Codable {
public let sessionid: String public let sessionid: String
public init( public init(
sessionid: String sessionid: String)
) { {
self.sessionid = sessionid self.sessionid = sessionid
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId" case sessionid = "sessionId"
} }
@ -821,10 +848,11 @@ public struct WizardStatusParams: Codable {
public let sessionid: String public let sessionid: String
public init( public init(
sessionid: String sessionid: String)
) { {
self.sessionid = sessionid self.sessionid = sessionid
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId" case sessionid = "sessionId"
} }
@ -850,8 +878,8 @@ public struct WizardStep: Codable {
initialvalue: AnyCodable?, initialvalue: AnyCodable?,
placeholder: String?, placeholder: String?,
sensitive: Bool?, sensitive: Bool?,
executor: AnyCodable? executor: AnyCodable?)
) { {
self.id = id self.id = id
self.type = type self.type = type
self.title = title self.title = title
@ -862,6 +890,7 @@ public struct WizardStep: Codable {
self.sensitive = sensitive self.sensitive = sensitive
self.executor = executor self.executor = executor
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id case id
case type case type
@ -885,13 +914,14 @@ public struct WizardNextResult: Codable {
done: Bool, done: Bool,
step: [String: AnyCodable]?, step: [String: AnyCodable]?,
status: AnyCodable?, status: AnyCodable?,
error: String? error: String?)
) { {
self.done = done self.done = done
self.step = step self.step = step
self.status = status self.status = status
self.error = error self.error = error
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case done case done
case step case step
@ -912,14 +942,15 @@ public struct WizardStartResult: Codable {
done: Bool, done: Bool,
step: [String: AnyCodable]?, step: [String: AnyCodable]?,
status: AnyCodable?, status: AnyCodable?,
error: String? error: String?)
) { {
self.sessionid = sessionid self.sessionid = sessionid
self.done = done self.done = done
self.step = step self.step = step
self.status = status self.status = status
self.error = error self.error = error
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case sessionid = "sessionId" case sessionid = "sessionId"
case done case done
@ -935,11 +966,12 @@ public struct WizardStatusResult: Codable {
public init( public init(
status: AnyCodable, status: AnyCodable,
error: String? error: String?)
) { {
self.status = status self.status = status
self.error = error self.error = error
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case status case status
case error case error
@ -952,11 +984,12 @@ public struct TalkModeParams: Codable {
public init( public init(
enabled: Bool, enabled: Bool,
phase: String? phase: String?)
) { {
self.enabled = enabled self.enabled = enabled
self.phase = phase self.phase = phase
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case enabled case enabled
case phase case phase
@ -969,11 +1002,12 @@ public struct ProvidersStatusParams: Codable {
public init( public init(
probe: Bool?, probe: Bool?,
timeoutms: Int? timeoutms: Int?)
) { {
self.probe = probe self.probe = probe
self.timeoutms = timeoutms self.timeoutms = timeoutms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case probe case probe
case timeoutms = "timeoutMs" case timeoutms = "timeoutMs"
@ -988,12 +1022,13 @@ public struct WebLoginStartParams: Codable {
public init( public init(
force: Bool?, force: Bool?,
timeoutms: Int?, timeoutms: Int?,
verbose: Bool? verbose: Bool?)
) { {
self.force = force self.force = force
self.timeoutms = timeoutms self.timeoutms = timeoutms
self.verbose = verbose self.verbose = verbose
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case force case force
case timeoutms = "timeoutMs" case timeoutms = "timeoutMs"
@ -1005,10 +1040,11 @@ public struct WebLoginWaitParams: Codable {
public let timeoutms: Int? public let timeoutms: Int?
public init( public init(
timeoutms: Int? timeoutms: Int?)
) { {
self.timeoutms = timeoutms self.timeoutms = timeoutms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case timeoutms = "timeoutMs" case timeoutms = "timeoutMs"
} }
@ -1026,14 +1062,15 @@ public struct ModelChoice: Codable {
name: String, name: String,
provider: String, provider: String,
contextwindow: Int?, contextwindow: Int?,
reasoning: Bool? reasoning: Bool?)
) { {
self.id = id self.id = id
self.name = name self.name = name
self.provider = provider self.provider = provider
self.contextwindow = contextwindow self.contextwindow = contextwindow
self.reasoning = reasoning self.reasoning = reasoning
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id case id
case name case name
@ -1043,24 +1080,23 @@ public struct ModelChoice: Codable {
} }
} }
public struct ModelsListParams: Codable { public struct ModelsListParams: Codable {}
}
public struct ModelsListResult: Codable { public struct ModelsListResult: Codable {
public let models: [ModelChoice] public let models: [ModelChoice]
public init( public init(
models: [ModelChoice] models: [ModelChoice])
) { {
self.models = models self.models = models
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case models case models
} }
} }
public struct SkillsStatusParams: Codable { public struct SkillsStatusParams: Codable {}
}
public struct SkillsInstallParams: Codable { public struct SkillsInstallParams: Codable {
public let name: String public let name: String
@ -1070,12 +1106,13 @@ public struct SkillsInstallParams: Codable {
public init( public init(
name: String, name: String,
installid: String, installid: String,
timeoutms: Int? timeoutms: Int?)
) { {
self.name = name self.name = name
self.installid = installid self.installid = installid
self.timeoutms = timeoutms self.timeoutms = timeoutms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case name case name
case installid = "installId" case installid = "installId"
@ -1093,13 +1130,14 @@ public struct SkillsUpdateParams: Codable {
skillkey: String, skillkey: String,
enabled: Bool?, enabled: Bool?,
apikey: String?, apikey: String?,
env: [String: AnyCodable]? env: [String: AnyCodable]?)
) { {
self.skillkey = skillkey self.skillkey = skillkey
self.enabled = enabled self.enabled = enabled
self.apikey = apikey self.apikey = apikey
self.env = env self.env = env
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case skillkey = "skillKey" case skillkey = "skillKey"
case enabled case enabled
@ -1134,8 +1172,8 @@ public struct CronJob: Codable {
wakemode: AnyCodable, wakemode: AnyCodable,
payload: AnyCodable, payload: AnyCodable,
isolation: [String: AnyCodable]?, isolation: [String: AnyCodable]?,
state: [String: AnyCodable] state: [String: AnyCodable])
) { {
self.id = id self.id = id
self.name = name self.name = name
self.description = description self.description = description
@ -1149,6 +1187,7 @@ public struct CronJob: Codable {
self.isolation = isolation self.isolation = isolation
self.state = state self.state = state
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id case id
case name case name
@ -1169,17 +1208,17 @@ public struct CronListParams: Codable {
public let includedisabled: Bool? public let includedisabled: Bool?
public init( public init(
includedisabled: Bool? includedisabled: Bool?)
) { {
self.includedisabled = includedisabled self.includedisabled = includedisabled
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case includedisabled = "includeDisabled" case includedisabled = "includeDisabled"
} }
} }
public struct CronStatusParams: Codable { public struct CronStatusParams: Codable {}
}
public struct CronAddParams: Codable { public struct CronAddParams: Codable {
public let name: String public let name: String
@ -1199,8 +1238,8 @@ public struct CronAddParams: Codable {
sessiontarget: AnyCodable, sessiontarget: AnyCodable,
wakemode: AnyCodable, wakemode: AnyCodable,
payload: AnyCodable, payload: AnyCodable,
isolation: [String: AnyCodable]? isolation: [String: AnyCodable]?)
) { {
self.name = name self.name = name
self.description = description self.description = description
self.enabled = enabled self.enabled = enabled
@ -1210,6 +1249,7 @@ public struct CronAddParams: Codable {
self.payload = payload self.payload = payload
self.isolation = isolation self.isolation = isolation
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case name case name
case description case description
@ -1228,11 +1268,12 @@ public struct CronUpdateParams: Codable {
public init( public init(
id: String, id: String,
patch: [String: AnyCodable] patch: [String: AnyCodable])
) { {
self.id = id self.id = id
self.patch = patch self.patch = patch
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id case id
case patch case patch
@ -1243,10 +1284,11 @@ public struct CronRemoveParams: Codable {
public let id: String public let id: String
public init( public init(
id: String id: String)
) { {
self.id = id self.id = id
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id case id
} }
@ -1258,11 +1300,12 @@ public struct CronRunParams: Codable {
public init( public init(
id: String, id: String,
mode: AnyCodable? mode: AnyCodable?)
) { {
self.id = id self.id = id
self.mode = mode self.mode = mode
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id case id
case mode case mode
@ -1275,11 +1318,12 @@ public struct CronRunsParams: Codable {
public init( public init(
id: String, id: String,
limit: Int? limit: Int?)
) { {
self.id = id self.id = id
self.limit = limit self.limit = limit
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case id case id
case limit case limit
@ -1306,8 +1350,8 @@ public struct CronRunLogEntry: Codable {
summary: String?, summary: String?,
runatms: Int?, runatms: Int?,
durationms: Int?, durationms: Int?,
nextrunatms: Int? nextrunatms: Int?)
) { {
self.ts = ts self.ts = ts
self.jobid = jobid self.jobid = jobid
self.action = action self.action = action
@ -1318,6 +1362,7 @@ public struct CronRunLogEntry: Codable {
self.durationms = durationms self.durationms = durationms
self.nextrunatms = nextrunatms self.nextrunatms = nextrunatms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case ts case ts
case jobid = "jobId" case jobid = "jobId"
@ -1337,11 +1382,12 @@ public struct ChatHistoryParams: Codable {
public init( public init(
sessionkey: String, sessionkey: String,
limit: Int? limit: Int?)
) { {
self.sessionkey = sessionkey self.sessionkey = sessionkey
self.limit = limit self.limit = limit
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey" case sessionkey = "sessionKey"
case limit case limit
@ -1364,8 +1410,8 @@ public struct ChatSendParams: Codable {
deliver: Bool?, deliver: Bool?,
attachments: [AnyCodable]?, attachments: [AnyCodable]?,
timeoutms: Int?, timeoutms: Int?,
idempotencykey: String idempotencykey: String)
) { {
self.sessionkey = sessionkey self.sessionkey = sessionkey
self.message = message self.message = message
self.thinking = thinking self.thinking = thinking
@ -1374,6 +1420,7 @@ public struct ChatSendParams: Codable {
self.timeoutms = timeoutms self.timeoutms = timeoutms
self.idempotencykey = idempotencykey self.idempotencykey = idempotencykey
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey" case sessionkey = "sessionKey"
case message case message
@ -1391,11 +1438,12 @@ public struct ChatAbortParams: Codable {
public init( public init(
sessionkey: String, sessionkey: String,
runid: String runid: String)
) { {
self.sessionkey = sessionkey self.sessionkey = sessionkey
self.runid = runid self.runid = runid
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey" case sessionkey = "sessionKey"
case runid = "runId" case runid = "runId"
@ -1420,8 +1468,8 @@ public struct ChatEvent: Codable {
message: AnyCodable?, message: AnyCodable?,
errormessage: String?, errormessage: String?,
usage: AnyCodable?, usage: AnyCodable?,
stopreason: String? stopreason: String?)
) { {
self.runid = runid self.runid = runid
self.sessionkey = sessionkey self.sessionkey = sessionkey
self.seq = seq self.seq = seq
@ -1431,6 +1479,7 @@ public struct ChatEvent: Codable {
self.usage = usage self.usage = usage
self.stopreason = stopreason self.stopreason = stopreason
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case runid = "runId" case runid = "runId"
case sessionkey = "sessionKey" case sessionkey = "sessionKey"
@ -1447,10 +1496,11 @@ public struct TickEvent: Codable {
public let ts: Int public let ts: Int
public init( public init(
ts: Int ts: Int)
) { {
self.ts = ts self.ts = ts
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case ts case ts
} }
@ -1462,11 +1512,12 @@ public struct ShutdownEvent: Codable {
public init( public init(
reason: String, reason: String,
restartexpectedms: Int? restartexpectedms: Int?)
) { {
self.reason = reason self.reason = reason
self.restartexpectedms = restartexpectedms self.restartexpectedms = restartexpectedms
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case reason case reason
case restartexpectedms = "restartExpectedMs" case restartexpectedms = "restartExpectedMs"
@ -1488,11 +1539,11 @@ public enum GatewayFrame: Codable {
let type = try typeContainer.decode(String.self, forKey: .type) let type = try typeContainer.decode(String.self, forKey: .type)
switch type { switch type {
case "req": case "req":
self = .req(try RequestFrame(from: decoder)) self = try .req(RequestFrame(from: decoder))
case "res": case "res":
self = .res(try ResponseFrame(from: decoder)) self = try .res(ResponseFrame(from: decoder))
case "event": case "event":
self = .event(try EventFrame(from: decoder)) self = try .event(EventFrame(from: decoder))
default: default:
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let raw = try container.decode([String: AnyCodable].self) let raw = try container.decode([String: AnyCodable].self)
@ -1502,13 +1553,12 @@ public enum GatewayFrame: Codable {
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
switch self { switch self {
case .req(let v): try v.encode(to: encoder) case let .req(v): try v.encode(to: encoder)
case .res(let v): try v.encode(to: encoder) case let .res(v): try v.encode(to: encoder)
case .event(let v): try v.encode(to: encoder) case let .event(v): try v.encode(to: encoder)
case .unknown(_, let raw): case let .unknown(_, raw):
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
try container.encode(raw) try container.encode(raw)
} }
} }
} }

View file

@ -86,7 +86,7 @@ enum AssistantTextParser {
self.findTagStart(tag: "final", closing: true, in: text, from: start).map { self.findTagStart(tag: "final", closing: true, in: text, from: start).map {
TagMatch(kind: .final, closing: true, range: $0) TagMatch(kind: .final, closing: true, range: $0)
}, },
].compactMap { $0 } ].compactMap(\.self)
return candidates.min { $0.range.lowerBound < $1.range.lowerBound } return candidates.min { $0.range.lowerBound < $1.range.lowerBound }
} }

View file

@ -10,4 +10,3 @@ public enum TalkHistoryTimestamp: Sendable {
return timestamp >= sinceSeconds - 0.5 return timestamp >= sinceSeconds - 0.5
} }
} }

View file

@ -17,7 +17,7 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
public var isSpeaking: Bool { self.synth.isSpeaking } public var isSpeaking: Bool { self.synth.isSpeaking }
private override init() { override private init() {
super.init() super.init()
self.synth.delegate = self self.synth.delegate = self
} }
@ -96,13 +96,19 @@ public final class TalkSystemSpeechSynthesizer: NSObject {
} }
extension TalkSystemSpeechSynthesizer: AVSpeechSynthesizerDelegate { extension TalkSystemSpeechSynthesizer: AVSpeechSynthesizerDelegate {
public nonisolated func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { public nonisolated func speechSynthesizer(
_ synthesizer: AVSpeechSynthesizer,
didFinish utterance: AVSpeechUtterance)
{
Task { @MainActor in Task { @MainActor in
self.handleFinish(error: nil) 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 Task { @MainActor in
self.handleFinish(error: SpeakError.canceled) self.handleFinish(error: SpeakError.canceled)
} }

View file

@ -17,9 +17,9 @@ public struct ToolDisplaySummary: Sendable, Equatable {
public var summaryLine: String { public var summaryLine: String {
if let detailLine { 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 { public static func resolve(name: String?, args: AnyCodable?, meta: String? = nil) -> ToolDisplaySummary {
let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "tool" let trimmedName = name?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "tool"
let key = trimmedName.lowercased() let key = trimmedName.lowercased()
let spec = config.tools?[key] let spec = self.config.tools?[key]
let fallback = config.fallback let fallback = self.config.fallback
let emoji = spec?.emoji ?? fallback?.emoji ?? "🧩" 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 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 action = actionRaw?.trimmingCharacters(in: .whitespacesAndNewlines)
let actionSpec = action.flatMap { spec?.actions?[$0] } let actionSpec = action.flatMap { spec?.actions?[$0] }
let verb = normalizeVerb(actionSpec?.label ?? action) let verb = self.normalizeVerb(actionSpec?.label ?? action)
var detail: String? var detail: String?
if key == "read" { if key == "read" {
detail = readDetail(args) detail = self.readDetail(args)
} else if key == "write" || key == "edit" || key == "attach" { } else if key == "write" || key == "edit" || key == "attach" {
detail = pathDetail(args) detail = self.pathDetail(args)
} }
let detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? fallback?.detailKeys ?? [] let detailKeys = actionSpec?.detailKeys ?? spec?.detailKeys ?? fallback?.detailKeys ?? []
if detail == nil { if detail == nil {
detail = firstValue(args, keys: detailKeys) detail = self.firstValue(args, keys: detailKeys)
} }
if detail == nil { if detail == nil {
@ -77,7 +77,7 @@ public enum ToolDisplayRegistry {
} }
if let detailValue = detail { if let detailValue = detail {
detail = shortenHomeInString(detailValue) detail = self.shortenHomeInString(detailValue)
} }
return ToolDisplaySummary( return ToolDisplaySummary(
@ -108,7 +108,7 @@ public enum ToolDisplayRegistry {
.split(separator: " ") .split(separator: " ")
.map { part in .map { part in
let upper = part.uppercased() 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()) return String(upper.prefix(1)) + String(part.lowercased().dropFirst())
} }
.joined(separator: " ") .joined(separator: " ")
@ -122,8 +122,8 @@ public enum ToolDisplayRegistry {
private static func readDetail(_ args: AnyCodable?) -> String? { private static func readDetail(_ args: AnyCodable?) -> String? {
guard let path = valueForKeyPath(args, path: "path") as? String else { return nil } guard let path = valueForKeyPath(args, path: "path") as? String else { return nil }
let offset = valueForKeyPath(args, path: "offset") as? Double let offset = self.valueForKeyPath(args, path: "offset") as? Double
let limit = valueForKeyPath(args, path: "limit") as? Double let limit = self.valueForKeyPath(args, path: "limit") as? Double
if let offset, let limit { if let offset, let limit {
let end = offset + limit let end = offset + limit
return "\(path):\(Int(offset))-\(Int(end))" return "\(path):\(Int(offset))-\(Int(end))"
@ -132,7 +132,7 @@ public enum ToolDisplayRegistry {
} }
private static func pathDetail(_ args: AnyCodable?) -> String? { 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? { 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 num = value as? Double { return String(num) }
if let bool = value as? Bool { return bool ? "true" : "false" } if let bool = value as? Bool { return bool ? "true" : "false" }
if let array = value as? [Any] { 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 } guard !items.isEmpty else { return nil }
let preview = items.prefix(3).joined(separator: ", ") let preview = items.prefix(3).joined(separator: ", ")
return items.count > 3 ? "\(preview)" : preview return items.count > 3 ? "\(preview)" : preview

View file

@ -1 +1 @@
01110b37b0ec6481527fbc894303ed127520716d76108535874567d8331973bc 7daf1cbf58ef395b74c2690c439ac7b3cb536e8eb124baf72ad41da4f542204d