chore: lint and format cleanup
This commit is contained in:
parent
fd95ededaa
commit
026a25d164
45 changed files with 627 additions and 496 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)) isn’t listening — restart Clawdbot to bring it back."
|
return "The gateway control port (127.0.0.1:\(port)) isn’t listening — " +
|
||||||
|
"restart Clawdbot to bring it back."
|
||||||
}
|
}
|
||||||
if lower.contains("timeout") {
|
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."
|
||||||
|
|
|
||||||
|
|
@ -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)"])
|
||||||
|
|
|
||||||
|
|
@ -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: ",") + "}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,3 @@ public enum TalkHistoryTimestamp: Sendable {
|
||||||
return timestamp >= sinceSeconds - 0.5
|
return timestamp >= sinceSeconds - 0.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
01110b37b0ec6481527fbc894303ed127520716d76108535874567d8331973bc
|
7daf1cbf58ef395b74c2690c439ac7b3cb536e8eb124baf72ad41da4f542204d
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue