import ClawdbotProtocol import Foundation extension ChannelsStore { func loadConfigSchema() async { guard !self.configSchemaLoading else { return } self.configSchemaLoading = true defer { self.configSchemaLoading = false } do { let res: ConfigSchemaResponse = try await GatewayConnection.shared.requestDecoded( method: .configSchema, params: nil, timeoutMs: 8000) let schemaValue = res.schema.foundationValue self.configSchema = ConfigSchemaNode(raw: schemaValue) let hintValues = res.uihints.mapValues { $0.foundationValue } self.configUiHints = decodeUiHints(hintValues) } catch { self.configStatus = error.localizedDescription } } func loadConfig() async { do { let snap: ConfigSnapshot = try await GatewayConnection.shared.requestDecoded( method: .configGet, params: nil, timeoutMs: 10000) self.configStatus = snap.valid == false ? "Config invalid; fix it in ~/.clawdbot/clawdbot.json." : nil self.configRoot = snap.config?.mapValues { $0.foundationValue } ?? [:] self.configDraft = cloneConfigValue(self.configRoot) as? [String: Any] ?? self.configRoot self.configDirty = false self.configLoaded = true self.applyUIConfig(snap) } catch { self.configStatus = error.localizedDescription } } private func applyUIConfig(_ snap: ConfigSnapshot) { let ui = snap.config?["ui"]?.dictionaryValue let rawSeam = ui?["seamColor"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" AppStateStore.shared.seamColorHex = rawSeam.isEmpty ? nil : rawSeam } func channelConfigSchema(for channelId: String) -> ConfigSchemaNode? { guard let root = self.configSchema else { return nil } return root.node(at: [.key("channels"), .key(channelId)]) } func configValue(at path: ConfigPath) -> Any? { if let value = valueAtPath(self.configDraft, path: path) { return value } guard path.count >= 2 else { return nil } if case .key("channels") = path[0], case .key = path[1] { let fallbackPath = Array(path.dropFirst()) return valueAtPath(self.configDraft, path: fallbackPath) } return nil } func updateConfigValue(path: ConfigPath, value: Any?) { var root: Any = self.configDraft setValue(&root, path: path, value: value) self.configDraft = root as? [String: Any] ?? self.configDraft self.configDirty = true } func saveConfigDraft() async { guard !self.isSavingConfig else { return } self.isSavingConfig = true defer { self.isSavingConfig = false } do { try await ConfigStore.save(self.configDraft) await self.loadConfig() } catch { self.configStatus = error.localizedDescription } } func reloadConfigDraft() async { await self.loadConfig() } } private func valueAtPath(_ root: Any, path: ConfigPath) -> Any? { var current: Any? = root for segment in path { switch segment { case let .key(key): guard let dict = current as? [String: Any] else { return nil } current = dict[key] case let .index(index): guard let array = current as? [Any], array.indices.contains(index) else { return nil } current = array[index] } } return current } private func setValue(_ root: inout Any, path: ConfigPath, value: Any?) { guard let segment = path.first else { return } switch segment { case let .key(key): var dict = root as? [String: Any] ?? [:] if path.count == 1 { if let value { dict[key] = value } else { dict.removeValue(forKey: key) } root = dict return } var child = dict[key] ?? [:] setValue(&child, path: Array(path.dropFirst()), value: value) dict[key] = child root = dict case let .index(index): var array = root as? [Any] ?? [] if index >= array.count { array.append(contentsOf: repeatElement(NSNull() as Any, count: index - array.count + 1)) } if path.count == 1 { if let value { array[index] = value } else if array.indices.contains(index) { array.remove(at: index) } root = array return } var child = array[index] setValue(&child, path: Array(path.dropFirst()), value: value) array[index] = child root = array } } private func cloneConfigValue(_ value: Any) -> Any { guard JSONSerialization.isValidJSONObject(value) else { return value } do { let data = try JSONSerialization.data(withJSONObject: value, options: []) return try JSONSerialization.jsonObject(with: data, options: []) } catch { return value } }