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

View File

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

View File

@@ -244,7 +244,7 @@ actor CameraCaptureService {
deviceId: String?) -> AVCaptureDevice?
{
if let deviceId, !deviceId.isEmpty {
if let match = Self.availableCameras().first(where: { $0.uniqueID == deviceId }) {
if let match = availableCameras().first(where: { $0.uniqueID == deviceId }) {
return match
}
}
@@ -331,7 +331,7 @@ actor CameraCaptureService {
private func sleepDelayMs(_ delayMs: Int) async {
guard delayMs > 0 else { return }
let ns = UInt64(min(delayMs, 10_000)) * 1_000_000
let ns = UInt64(min(delayMs, 10000)) * 1_000_000
try? await Task.sleep(nanoseconds: ns)
}

View File

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

View File

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

View File

@@ -90,8 +90,8 @@ extension ConfigFileWatcher {
private func handleEvents(
numEvents: Int,
eventPaths: UnsafeMutableRawPointer?,
eventFlags: UnsafePointer<FSEventStreamEventFlags>?
) {
eventFlags: UnsafePointer<FSEventStreamEventFlags>?)
{
guard numEvents > 0 else { return }
guard eventFlags != nil 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])
guard let raw = String(data: data, encoding: .utf8) else {
throw NSError(domain: "ConfigStore", code: 1, userInfo: [
NSLocalizedDescriptionKey: "Failed to encode config."
NSLocalizedDescriptionKey: "Failed to encode config.",
])
}
let params: [String: AnyCodable] = ["raw": AnyCodable(raw)]
@@ -88,7 +88,7 @@ enum ConfigStore {
timeoutMs: 10000)
}
#if DEBUG
#if DEBUG
static func _testSetOverrides(_ overrides: Overrides) async {
await self.overrideStore.setOverride(overrides)
}
@@ -96,5 +96,5 @@ enum ConfigStore {
static func _testClearOverrides() async {
await self.overrideStore.setOverride(.init())
}
#endif
#endif
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -189,7 +189,8 @@ final class HealthStore {
let lower = error.lowercased()
if lower.contains("connection refused") {
let port = GatewayEnvironment.gatewayPort()
return "The gateway control port (127.0.0.1:\(port)) 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") {
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
.appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist")
}
private static var legacyPlistURL: URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist")
@@ -19,7 +20,7 @@ enum LaunchAgentManager {
static func set(enabled: Bool, bundlePath: String) async {
if enabled {
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(legacyLaunchdLabel)"])
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyLaunchdLabel)"])
try? FileManager.default.removeItem(at: self.legacyPlistURL)
self.writePlist(bundlePath: bundlePath)
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])

View File

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

View File

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

View File

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

View File

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

View File

@@ -611,7 +611,7 @@ final class NodePairingApprovalPrompter {
private func updatePendingCounts() {
// Keep a cheap observable summary for the menu bar status line.
self.pendingCount = self.queue.count
self.pendingRepairCount = self.queue.filter { $0.isRepair == true }.count
self.pendingRepairCount = self.queue.count(where: { $0.isRepair == true })
}
private func reconcileOnce(timeoutMs: Double) async {

View File

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

View File

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

View File

@@ -112,7 +112,8 @@ extension OnboardingView {
HStack(spacing: 8) {
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 {
withAnimation { self.currentPage = index }
} label: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,8 @@ struct PermissionStatusList: View {
if (self.status[.accessibility] ?? false) == false || (self.status[.screenRecording] ?? false) == false {
VStack(alignment: .leading, spacing: 8) {
Text("Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.")
Text(
"Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)

View File

@@ -53,10 +53,12 @@ final class RemotePortTunnel {
let resolvedRemotePort = remotePortOverride ?? remotePort
if let override = remotePortOverride {
Self.logger.info(
"ssh tunnel remote port override host=\(sshHost, privacy: .public) port=\(override, privacy: .public)")
"ssh tunnel remote port override " +
"host=\(sshHost, privacy: .public) port=\(override, privacy: .public)")
} else {
Self.logger.debug(
"ssh tunnel using default remote port host=\(sshHost, privacy: .public) port=\(remotePort, privacy: .public)")
"ssh tunnel using default remote port " +
"host=\(sshHost, privacy: .public) port=\(remotePort, privacy: .public)")
}
var args: [String] = [
"-o", "BatchMode=yes",

View File

@@ -29,7 +29,9 @@ actor RemoteTunnelManager {
{
if await self.isTunnelHealthy(port: desiredPort) {
self.logger.info(
"reusing existing SSH tunnel listener localPort=\(desiredPort, privacy: .public) pid=\(desc.pid, privacy: .public)")
"reusing existing SSH tunnel listener " +
"localPort=\(desiredPort, privacy: .public) " +
"pid=\(desc.pid, privacy: .public)")
return desiredPort
}
await self.cleanupStaleTunnel(desc: desc, port: desiredPort)
@@ -50,7 +52,8 @@ actor RemoteTunnelManager {
let identitySet = !settings.identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
self.logger.info(
"ensure SSH tunnel target=\(settings.target, privacy: .public) identitySet=\(identitySet, privacy: .public)")
"ensure SSH tunnel target=\(settings.target, privacy: .public) " +
"identitySet=\(identitySet, privacy: .public)")
if let local = await self.controlTunnelPortIfRunning() { return local }

View File

@@ -174,12 +174,13 @@ struct SessionMenuPreviewView: View {
let timeoutMs = Int(Self.previewTimeoutSeconds * 1000)
let payload = try await AsyncTimeout.withTimeout(
seconds: Self.previewTimeoutSeconds,
onTimeout: { PreviewTimeoutError() }) {
onTimeout: { PreviewTimeoutError() },
operation: {
try await GatewayConnection.shared.chatHistory(
sessionKey: self.sessionKey,
limit: self.previewLimit,
timeoutMs: timeoutMs)
}
})
let built = Self.previewItems(from: payload, maxItems: self.maxItems)
await SessionPreviewCache.shared.store(items: built, for: self.sessionKey)
await MainActor.run {
@@ -198,7 +199,10 @@ struct SessionMenuPreviewView: View {
self.status = .error("Preview unavailable")
}
}
Self.logger.warning("Session preview failed session=\(self.sessionKey, privacy: .public) error=\(String(describing: error), privacy: .public)")
let errorDescription = String(describing: error)
Self.logger.warning(
"Session preview failed session=\(self.sessionKey, privacy: .public) " +
"error=\(errorDescription, privacy: .public)")
}
}

View File

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

View File

@@ -436,14 +436,49 @@ actor TalkModeRuntime {
}
}
// swiftlint:disable:next cyclomatic_complexity function_body_length
private func playAssistant(text: String) async {
guard let input = await self.preparePlaybackInput(text: text) else { return }
do {
if let apiKey = input.apiKey, !apiKey.isEmpty, let voiceId = input.voiceId {
try await self.playElevenLabs(input: input, apiKey: apiKey, voiceId: voiceId)
} else {
try await self.playSystemVoice(input: input)
}
} catch {
self.ttsLogger
.error(
"talk TTS failed: \(error.localizedDescription, privacy: .public); " +
"falling back to system voice")
do {
try await self.playSystemVoice(input: input)
} catch {
self.ttsLogger.error("talk system voice failed: \(error.localizedDescription, privacy: .public)")
}
}
if self.phase == .speaking {
self.phase = .thinking
await MainActor.run { TalkModeController.shared.updatePhase(.thinking) }
}
}
private struct TalkPlaybackInput {
let generation: Int
let cleanedText: String
let directive: TalkDirective?
let apiKey: String?
let voiceId: String?
let language: String?
let synthTimeoutSeconds: Double
}
private func preparePlaybackInput(text: String) async -> TalkPlaybackInput? {
let gen = self.lifecycleGeneration
let parse = TalkDirectiveParser.parse(text)
let directive = parse.directive
let cleaned = parse.stripped.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else { return }
guard self.isCurrent(gen) else { return }
guard !cleaned.isEmpty else { return nil }
guard self.isCurrent(gen) else { return nil }
if !parse.unknownKeys.isEmpty {
self.logger
@@ -504,116 +539,123 @@ actor TalkModeRuntime {
let synthTimeoutSeconds = max(20.0, min(90.0, Double(cleaned.count) * 0.12))
do {
if let apiKey, !apiKey.isEmpty, let voiceId {
let desiredOutputFormat = directive?.outputFormat ?? self.defaultOutputFormat ?? "pcm_44100"
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(desiredOutputFormat)
if outputFormat == nil, !desiredOutputFormat.isEmpty {
self.logger
.warning(
"talk output_format unsupported for local playback: " +
"\(desiredOutputFormat, privacy: .public)")
}
guard self.isCurrent(gen) else { return nil }
let modelId = directive?.modelId ?? self.currentModelId ?? self.defaultModelId
func makeRequest(outputFormat: String?) -> ElevenLabsTTSRequest {
ElevenLabsTTSRequest(
text: cleaned,
modelId: modelId,
outputFormat: outputFormat,
speed: TalkTTSValidation.resolveSpeed(speed: directive?.speed, rateWPM: directive?.rateWPM),
stability: TalkTTSValidation.validatedStability(directive?.stability, modelId: modelId),
similarity: TalkTTSValidation.validatedUnit(directive?.similarity),
style: TalkTTSValidation.validatedUnit(directive?.style),
speakerBoost: directive?.speakerBoost,
seed: TalkTTSValidation.validatedSeed(directive?.seed),
normalize: ElevenLabsTTSClient.validatedNormalize(directive?.normalize),
language: language,
latencyTier: TalkTTSValidation.validatedLatencyTier(directive?.latencyTier))
}
return TalkPlaybackInput(
generation: gen,
cleanedText: cleaned,
directive: directive,
apiKey: apiKey,
voiceId: voiceId,
language: language,
synthTimeoutSeconds: synthTimeoutSeconds)
}
let request = makeRequest(outputFormat: outputFormat)
self.ttsLogger.info("talk TTS synth timeout=\(synthTimeoutSeconds, privacy: .public)s")
let client = ElevenLabsTTSClient(apiKey: apiKey)
let stream = client.streamSynthesize(voiceId: voiceId, request: request)
guard self.isCurrent(gen) else { return }
if self.interruptOnSpeech {
await self.startRecognition()
guard self.isCurrent(gen) else { return }
}
await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking
let sampleRate = TalkTTSValidation.pcmSampleRate(from: outputFormat)
var result: StreamingPlaybackResult
if let sampleRate {
self.lastPlaybackWasPCM = true
result = await self.playPCM(stream: stream, sampleRate: sampleRate)
if !result.finished, result.interruptedAt == nil {
let mp3Format = ElevenLabsTTSClient.validatedOutputFormat("mp3_44100")
self.ttsLogger.warning("talk pcm playback failed; retrying mp3")
self.lastPlaybackWasPCM = false
let mp3Stream = client.streamSynthesize(
voiceId: voiceId,
request: makeRequest(outputFormat: mp3Format))
result = await self.playMP3(stream: mp3Stream)
}
} else {
self.lastPlaybackWasPCM = false
result = await self.playMP3(stream: stream)
}
self.ttsLogger
.info(
"talk audio result finished=\(result.finished, privacy: .public) " +
"interruptedAt=\(String(describing: result.interruptedAt), privacy: .public)")
if !result.finished, result.interruptedAt == nil {
throw NSError(domain: "StreamingAudioPlayer", code: 1, userInfo: [
NSLocalizedDescriptionKey: "audio playback failed",
])
}
if !result.finished, let interruptedAt = result.interruptedAt, self.phase == .speaking {
if self.interruptOnSpeech {
self.lastInterruptedAtSeconds = interruptedAt
}
}
} else {
self.ttsLogger.info("talk system voice start chars=\(cleaned.count, privacy: .public)")
if self.interruptOnSpeech {
await self.startRecognition()
guard self.isCurrent(gen) else { return }
}
await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking
await TalkSystemSpeechSynthesizer.shared.stop()
try await TalkSystemSpeechSynthesizer.shared.speak(text: cleaned, language: language)
self.ttsLogger.info("talk system voice done")
}
} catch {
self.ttsLogger
.error(
"talk TTS failed: \(error.localizedDescription, privacy: .public); " +
"falling back to system voice")
do {
if self.interruptOnSpeech {
await self.startRecognition()
guard self.isCurrent(gen) else { return }
}
await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking
await TalkSystemSpeechSynthesizer.shared.stop()
try await TalkSystemSpeechSynthesizer.shared.speak(text: cleaned, language: language)
} catch {
self.ttsLogger.error("talk system voice failed: \(error.localizedDescription, privacy: .public)")
}
private func playElevenLabs(input: TalkPlaybackInput, apiKey: String, voiceId: String) async throws {
let desiredOutputFormat = input.directive?.outputFormat ?? self.defaultOutputFormat ?? "pcm_44100"
let outputFormat = ElevenLabsTTSClient.validatedOutputFormat(desiredOutputFormat)
if outputFormat == nil, !desiredOutputFormat.isEmpty {
self.logger
.warning(
"talk output_format unsupported for local playback: " +
"\(desiredOutputFormat, privacy: .public)")
}
if self.phase == .speaking {
self.phase = .thinking
await MainActor.run { TalkModeController.shared.updatePhase(.thinking) }
let modelId = input.directive?.modelId ?? self.currentModelId ?? self.defaultModelId
func makeRequest(outputFormat: String?) -> ElevenLabsTTSRequest {
ElevenLabsTTSRequest(
text: input.cleanedText,
modelId: modelId,
outputFormat: outputFormat,
speed: TalkTTSValidation.resolveSpeed(
speed: input.directive?.speed,
rateWPM: input.directive?.rateWPM),
stability: TalkTTSValidation.validatedStability(
input.directive?.stability,
modelId: modelId),
similarity: TalkTTSValidation.validatedUnit(input.directive?.similarity),
style: TalkTTSValidation.validatedUnit(input.directive?.style),
speakerBoost: input.directive?.speakerBoost,
seed: TalkTTSValidation.validatedSeed(input.directive?.seed),
normalize: ElevenLabsTTSClient.validatedNormalize(input.directive?.normalize),
language: input.language,
latencyTier: TalkTTSValidation.validatedLatencyTier(input.directive?.latencyTier))
}
let request = makeRequest(outputFormat: outputFormat)
self.ttsLogger.info("talk TTS synth timeout=\(input.synthTimeoutSeconds, privacy: .public)s")
let client = ElevenLabsTTSClient(apiKey: apiKey)
let stream = client.streamSynthesize(voiceId: voiceId, request: request)
guard self.isCurrent(input.generation) else { return }
if self.interruptOnSpeech, ! await self.prepareForPlayback(generation: input.generation) { return }
await MainActor.run { TalkModeController.shared.updatePhase(.speaking) }
self.phase = .speaking
let result = await self.playRemoteStream(
client: client,
voiceId: voiceId,
outputFormat: outputFormat,
makeRequest: makeRequest,
stream: stream)
self.ttsLogger
.info(
"talk audio result finished=\(result.finished, privacy: .public) " +
"interruptedAt=\(String(describing: result.interruptedAt), privacy: .public)")
if !result.finished, result.interruptedAt == nil {
throw NSError(domain: "StreamingAudioPlayer", code: 1, userInfo: [
NSLocalizedDescriptionKey: "audio playback failed",
])
}
if !result.finished, let interruptedAt = result.interruptedAt, self.phase == .speaking {
if self.interruptOnSpeech {
self.lastInterruptedAtSeconds = interruptedAt
}
}
}
private func playRemoteStream(
client: ElevenLabsTTSClient,
voiceId: String,
outputFormat: String?,
makeRequest: (String?) -> ElevenLabsTTSRequest,
stream: AsyncThrowingStream<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? {
@@ -682,6 +724,9 @@ actor TalkModeRuntime {
self.phase = .thinking
await MainActor.run { TalkModeController.shared.updatePhase(.thinking) }
}
}
extension TalkModeRuntime {
// MARK: - Audio playback (MainActor helpers)

View File

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

View File

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

View File

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