import Foundation // Thinking-level normalization and option resolution. Session entries, // session defaults, and free-form user aliases all feed the picker; this // extension owns collapsing them into the canonical option list. extension OpenClawChatViewModel { func syncThinkingLevelOptions() { let currentSession = self.sessions.first(where: { $0.key == self.sessionKey }) var options = self.resolvedThinkingLevelOptions(for: currentSession) if let current = Self.normalizedThinkingLevel(thinkingLevel) { options = Self.withCurrentThinkingOption(options, current: current) } self.thinkingLevelOptions = options } private func resolvedThinkingLevelOptions( for currentSession: OpenClawChatSessionEntry?) -> [OpenClawChatThinkingLevelOption] { if let levels = Self.normalizedThinkingLevelOptions(currentSession?.thinkingLevels), !levels.isEmpty { return levels } let defaultsMatch = currentSession.map { Self.sessionModelMatchesDefaults($0, defaults: self.sessionDefaults) } ?? true if defaultsMatch, let levels = Self.normalizedThinkingLevelOptions(sessionDefaults?.thinkingLevels), !levels.isEmpty { return levels } if let options = Self.thinkingOptions(from: currentSession?.thinkingOptions), !options.isEmpty { return options } if defaultsMatch, let options = Self.thinkingOptions(from: sessionDefaults?.thinkingOptions), !options.isEmpty { return options } return Self.baseThinkingLevelOptions } private static func sessionModelMatchesDefaults( _ session: OpenClawChatSessionEntry, defaults: OpenClawChatSessionsDefaults?) -> Bool { let providerMatches = session.modelProvider == nil || session.modelProvider == defaults?.modelProvider let modelMatches = session.model == nil || session.model == defaults?.model return providerMatches && modelMatches } private static func normalizedThinkingLevelOptions( _ levels: [OpenClawChatThinkingLevelOption]?) -> [OpenClawChatThinkingLevelOption]? { guard let levels else { return nil } return Self.dedupedThinkingOptions( levels.compactMap { level in guard let id = Self.normalizedThinkingLevel(level.id) else { return nil } let label = level.label.trimmingCharacters(in: .whitespacesAndNewlines) return OpenClawChatThinkingLevelOption(id: id, label: label.isEmpty ? id : label) }) } private static func thinkingOptions(from labels: [String]?) -> [OpenClawChatThinkingLevelOption]? { guard let labels else { return nil } return Self.dedupedThinkingOptions( labels.compactMap { label in guard let id = Self.normalizedThinkingLevel(label) else { return nil } let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines) return OpenClawChatThinkingLevelOption(id: id, label: trimmed.isEmpty ? id : trimmed) }) } static func withCurrentThinkingOption( _ options: [OpenClawChatThinkingLevelOption], current: String) -> [OpenClawChatThinkingLevelOption] { guard !options.contains(where: { $0.id == current }) else { return options } return options + [OpenClawChatThinkingLevelOption(id: current, label: current)] } private static func dedupedThinkingOptions( _ options: [OpenClawChatThinkingLevelOption]) -> [OpenClawChatThinkingLevelOption] { var result: [OpenClawChatThinkingLevelOption] = [] var seen = Set() for option in options { guard !option.id.isEmpty, !seen.contains(option.id) else { continue } seen.insert(option.id) result.append(option) } return result } static func normalizedThinkingLevel(_ level: String?) -> String? { guard let level else { return nil } let trimmed = level.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() guard !trimmed.isEmpty else { return nil } let collapsed = trimmed.replacingOccurrences( of: "[\\s_-]+", with: "", options: .regularExpression) switch collapsed { case "adaptive", "auto": return "adaptive" case "max": return "max" case "xhigh", "extrahigh": return "xhigh" case "off", "none": return "off" case "on", "enable", "enabled": return "low" case "min", "minimal", "think": return "minimal" case "low", "thinkhard": return "low" case "mid", "med", "medium", "thinkharder", "harder": return "medium" case "high", "ultra", "ultrathink", "thinkhardest", "highest": return "high" default: return trimmed } } }