diff --git a/CHANGELOG.md b/CHANGELOG.md index a62b4ac412d..929772b6f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Native chat: decode gateway-provided thinking metadata for the iOS/macOS picker so provider-specific levels such as `adaptive`, `xhigh`, and `max` appear without leaking unsupported default-model options. Thanks @BunsDev. - Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev. - Agents/models: accept legacy `anthropic-cli/*` model refs as Claude CLI runtime refs instead of failing model resolution with `Unknown model`. Thanks @BunsDev. - Agents/tools: keep restrictive-profile tool-section warnings scoped to the configured sections whose tools are still missing from `alsoAllow`, so already re-allowed filesystem tools do not make exec-only fixes look broader than they are. Thanks @BunsDev. diff --git a/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift b/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift index bf8ebbc7ed4..a47d5a1393f 100644 --- a/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift +++ b/apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift @@ -63,8 +63,12 @@ struct MacGatewayChatTransport: OpenClawChatTransport { let mainSessionKey = await GatewayConnection.shared.cachedMainSessionKey() let defaults = decoded.defaults.map { OpenClawChatSessionsDefaults( + modelProvider: $0.modelProvider, model: $0.model, contextTokens: $0.contextTokens, + thinkingLevels: $0.thinkingLevels, + thinkingOptions: $0.thinkingOptions, + thinkingDefault: $0.thinkingDefault, mainSessionKey: mainSessionKey) } ?? OpenClawChatSessionsDefaults( model: nil, diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift index a85f922defe..28835f02d0e 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift @@ -9,8 +9,6 @@ import UniformTypeIdentifiers @MainActor struct OpenClawChatComposer: View { - private static let menuThinkingLevels = ["off", "low", "medium", "high"] - @Bindable var viewModel: OpenClawChatViewModel let style: OpenClawChatView.Style let showsSessionSwitcher: Bool @@ -95,12 +93,8 @@ struct OpenClawChatComposer: View { get: { self.viewModel.thinkingLevel }, set: { next in self.viewModel.selectThinkingLevel(next) })) { - Text("Off").tag("off") - Text("Low").tag("low") - Text("Medium").tag("medium") - Text("High").tag("high") - if !Self.menuThinkingLevels.contains(self.viewModel.thinkingLevel) { - Text(self.viewModel.thinkingLevel.capitalized).tag(self.viewModel.thinkingLevel) + ForEach(self.viewModel.thinkingLevelOptions) { option in + Text(option.label).tag(option.id) } } .labelsHidden() diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift index 381829f428f..6733a55c757 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessions.swift @@ -1,5 +1,15 @@ import Foundation +public struct OpenClawChatThinkingLevelOption: Codable, Identifiable, Sendable, Hashable { + public let id: String + public let label: String + + public init(id: String, label: String) { + self.id = id + self.label = label + } +} + public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable { public var id: String { self.selectionID @@ -34,13 +44,29 @@ public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable } public struct OpenClawChatSessionsDefaults: Codable, Sendable { + public let modelProvider: String? public let model: String? public let contextTokens: Int? + public let thinkingLevels: [OpenClawChatThinkingLevelOption]? + public let thinkingOptions: [String]? + public let thinkingDefault: String? public let mainSessionKey: String? - public init(model: String?, contextTokens: Int?, mainSessionKey: String? = nil) { + public init( + modelProvider: String? = nil, + model: String?, + contextTokens: Int?, + thinkingLevels: [OpenClawChatThinkingLevelOption]? = nil, + thinkingOptions: [String]? = nil, + thinkingDefault: String? = nil, + mainSessionKey: String? = nil) + { + self.modelProvider = modelProvider self.model = model self.contextTokens = contextTokens + self.thinkingLevels = thinkingLevels + self.thinkingOptions = thinkingOptions + self.thinkingDefault = thinkingDefault self.mainSessionKey = mainSessionKey } } @@ -72,6 +98,57 @@ public struct OpenClawChatSessionEntry: Codable, Identifiable, Sendable, Hashabl public let modelProvider: String? public let model: String? public let contextTokens: Int? + public let thinkingLevels: [OpenClawChatThinkingLevelOption]? + public let thinkingOptions: [String]? + public let thinkingDefault: String? + + public init( + key: String, + kind: String?, + displayName: String?, + surface: String?, + subject: String?, + room: String?, + space: String?, + updatedAt: Double?, + sessionId: String?, + systemSent: Bool?, + abortedLastRun: Bool?, + thinkingLevel: String?, + verboseLevel: String?, + inputTokens: Int?, + outputTokens: Int?, + totalTokens: Int?, + modelProvider: String?, + model: String?, + contextTokens: Int?, + thinkingLevels: [OpenClawChatThinkingLevelOption]? = nil, + thinkingOptions: [String]? = nil, + thinkingDefault: String? = nil) + { + self.key = key + self.kind = kind + self.displayName = displayName + self.surface = surface + self.subject = subject + self.room = room + self.space = space + self.updatedAt = updatedAt + self.sessionId = sessionId + self.systemSent = systemSent + self.abortedLastRun = abortedLastRun + self.thinkingLevel = thinkingLevel + self.verboseLevel = verboseLevel + self.inputTokens = inputTokens + self.outputTokens = outputTokens + self.totalTokens = totalTokens + self.modelProvider = modelProvider + self.model = model + self.contextTokens = contextTokens + self.thinkingLevels = thinkingLevels + self.thinkingOptions = thinkingOptions + self.thinkingDefault = thinkingDefault + } } public struct OpenClawChatSessionsListResponse: Codable, Sendable { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift index e647435008f..d5601c86415 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift @@ -21,6 +21,7 @@ public final class OpenClawChatViewModel { public private(set) var messages: [OpenClawChatMessage] = [] public var input: String = "" public private(set) var thinkingLevel: String + public private(set) var thinkingLevelOptions: [OpenClawChatThinkingLevelOption] public private(set) var modelSelectionID: String = "__default__" public private(set) var modelChoices: [OpenClawChatModelChoice] = [] public private(set) var isLoading = false @@ -83,7 +84,11 @@ public final class OpenClawChatViewModel { self.sessionKey = sessionKey self.transport = transport let normalizedThinkingLevel = Self.normalizedThinkingLevel(initialThinkingLevel) - self.thinkingLevel = normalizedThinkingLevel ?? "off" + let initialResolvedThinkingLevel = normalizedThinkingLevel ?? "off" + self.thinkingLevel = initialResolvedThinkingLevel + self.thinkingLevelOptions = Self.withCurrentThinkingOption( + Self.baseThinkingLevelOptions, + current: initialResolvedThinkingLevel) self.prefersExplicitThinkingLevel = normalizedThinkingLevel != nil self.onThinkingLevelChanged = onThinkingLevelChanged @@ -198,6 +203,14 @@ public final class OpenClawChatViewModel { return "Default: \(self.modelLabel(for: defaultModelID))" } + private static let baseThinkingLevelOptions: [OpenClawChatThinkingLevelOption] = [ + OpenClawChatThinkingLevelOption(id: "off", label: "off"), + OpenClawChatThinkingLevelOption(id: "minimal", label: "minimal"), + OpenClawChatThinkingLevelOption(id: "low", label: "low"), + OpenClawChatThinkingLevelOption(id: "medium", label: "medium"), + OpenClawChatThinkingLevelOption(id: "high", label: "high"), + ] + public func addAttachments(urls: [URL]) { Task { await self.loadAttachments(urls: urls) } } @@ -243,6 +256,7 @@ public final class OpenClawChatViewModel { { self.thinkingLevel = level } + self.syncThinkingLevelOptions() await self.pollHealthIfNeeded(force: true) await self.fetchSessions(limit: 50) await self.fetchModels() @@ -594,6 +608,7 @@ public final class OpenClawChatViewModel { self.sessions = res.sessions self.sessionDefaults = res.defaults self.syncSelectedModel() + self.syncThinkingLevelOptions() } catch { // Best-effort. } @@ -675,6 +690,8 @@ public final class OpenClawChatViewModel { let sessionKey = self.sessionKey self.thinkingLevel = next + self.syncThinkingLevelOptions() + self.updateCurrentSessionThinkingLevel(next, sessionKey: sessionKey) self.onThinkingLevelChanged?(next) self.nextThinkingSelectionRequestID &+= 1 let requestID = self.nextThinkingSelectionRequestID @@ -770,6 +787,99 @@ public final class OpenClawChatViewModel { } } + private func syncThinkingLevelOptions() { + let currentSession = self.sessions.first(where: { $0.key == self.sessionKey }) + var options = self.resolvedThinkingLevelOptions(for: currentSession) + if let current = Self.normalizedThinkingLevel(self.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(self.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: self.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) + }) + } + + private 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 + } + private func placeholderSession(key: String) -> OpenClawChatSessionEntry { OpenClawChatSessionEntry( key: key, @@ -858,6 +968,9 @@ public final class OpenClawChatViewModel { modelProvider: resolved.modelProvider, sessionKey: sessionKey, syncSelection: syncSelection) + if sessionKey == self.sessionKey { + self.syncThinkingLevelOptions() + } } private func resolvedSessionModelIdentity(forSelectionID selectionID: String) @@ -885,6 +998,34 @@ public final class OpenClawChatViewModel { return "\(provider)/\(modelID)" } + private func updateCurrentSessionThinkingLevel(_ thinkingLevel: String?, sessionKey: String) { + guard let index = self.sessions.firstIndex(where: { $0.key == sessionKey }) else { return } + let current = self.sessions[index] + self.sessions[index] = OpenClawChatSessionEntry( + key: current.key, + kind: current.kind, + displayName: current.displayName, + surface: current.surface, + subject: current.subject, + room: current.room, + space: current.space, + updatedAt: current.updatedAt, + sessionId: current.sessionId, + systemSent: current.systemSent, + abortedLastRun: current.abortedLastRun, + thinkingLevel: thinkingLevel, + verboseLevel: current.verboseLevel, + inputTokens: current.inputTokens, + outputTokens: current.outputTokens, + totalTokens: current.totalTokens, + modelProvider: current.modelProvider, + model: current.model, + contextTokens: current.contextTokens, + thinkingLevels: current.thinkingLevels, + thinkingOptions: current.thinkingOptions, + thinkingDefault: current.thinkingDefault) + } + private func updateCurrentSessionModel( modelID: String?, modelProvider: String?, @@ -1084,6 +1225,7 @@ public final class OpenClawChatViewModel { let level = Self.normalizedThinkingLevel(payload.thinkingLevel) { self.thinkingLevel = level + self.syncThinkingLevelOptions() } } catch { chatUILogger.error("refresh history failed \(error.localizedDescription, privacy: .public)") @@ -1195,9 +1337,33 @@ public final class OpenClawChatViewModel { private static func normalizedThinkingLevel(_ level: String?) -> String? { guard let level else { return nil } let trimmed = level.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - guard ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"].contains(trimmed) else { - return nil + 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 } - return trimmed } } diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift index e33c2890c39..278f0a76174 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift @@ -46,6 +46,10 @@ private func sessionEntry(key: String, updatedAt: Double) -> OpenClawChatSession contextTokens: nil) } +private func thinkingOption(_ id: String, label: String? = nil) -> OpenClawChatThinkingLevelOption { + OpenClawChatThinkingLevelOption(id: id, label: label ?? id) +} + private func sessionEntry( key: String, updatedAt: Double, @@ -1632,6 +1636,272 @@ extension TestChatTransportState { } } + @Test func decodesGatewayThinkingMetadataFromSessionList() throws { + let json = """ + { + "defaults": { + "modelProvider": "anthropic", + "model": "claude-opus-4-7", + "thinkingLevels": [ + { "id": "off", "label": "off" }, + { "id": "adaptive", "label": "adaptive" }, + { "id": "max", "label": "maximum" } + ], + "thinkingOptions": ["off", "adaptive", "maximum"], + "thinkingDefault": "adaptive" + }, + "sessions": [ + { + "key": "main", + "modelProvider": "openrouter", + "model": "deepseek/deepseek-v4", + "thinkingLevel": "max", + "thinkingLevels": [ + { "id": "off", "label": "off" }, + { "id": "xhigh", "label": "xhigh" }, + { "id": "max", "label": "max" } + ], + "thinkingOptions": ["off", "xhigh", "max"], + "thinkingDefault": "max" + } + ] + } + """ + + let decoded = try JSONDecoder().decode( + OpenClawChatSessionsListResponse.self, + from: Data(json.utf8)) + + #expect(decoded.defaults?.modelProvider == "anthropic") + #expect(decoded.defaults?.thinkingLevels?.map(\.id) == ["off", "adaptive", "max"]) + #expect(decoded.defaults?.thinkingLevels?.last?.label == "maximum") + #expect(decoded.defaults?.thinkingDefault == "adaptive") + #expect(decoded.sessions.first?.thinkingLevels?.map(\.id) == ["off", "xhigh", "max"]) + #expect(decoded.sessions.first?.thinkingDefault == "max") + } + + @Test func sessionThinkingLevelsDrivePickerOptions() async throws { + let history = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "adaptive") + let sessions = OpenClawChatSessionsListResponse( + ts: 1, + path: nil, + count: 1, + defaults: OpenClawChatSessionsDefaults( + modelProvider: "openai-codex", + model: "gpt-5.5", + contextTokens: nil, + thinkingLevels: [ + thinkingOption("off"), + thinkingOption("low"), + thinkingOption("xhigh"), + thinkingOption("max", label: "maximum"), + ], + thinkingOptions: ["off", "low", "xhigh", "maximum"], + thinkingDefault: "xhigh"), + sessions: [ + OpenClawChatSessionEntry( + key: "main", + kind: nil, + displayName: nil, + surface: nil, + subject: nil, + room: nil, + space: nil, + updatedAt: 1, + sessionId: "sess-main", + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: "adaptive", + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + modelProvider: "anthropic", + model: "claude-opus-4-7", + contextTokens: nil, + thinkingLevels: [ + thinkingOption("off"), + thinkingOption("adaptive"), + thinkingOption("max", label: "maximum"), + ], + thinkingOptions: ["off", "adaptive", "maximum"], + thinkingDefault: "adaptive"), + ]) + + let (_, vm) = await makeViewModel( + historyResponses: [history], + sessionsResponses: [sessions]) + + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + + #expect(await MainActor.run { vm.thinkingLevel } == "adaptive") + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "adaptive", "max"]) + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.label) } == ["off", "adaptive", "maximum"]) + } + + @Test func thinkingOptionsFallbackAndCurrentUnsupportedLevelStayVisible() async throws { + let history = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "xhigh") + let sessions = OpenClawChatSessionsListResponse( + ts: 1, + path: nil, + count: 1, + defaults: nil, + sessions: [ + OpenClawChatSessionEntry( + key: "main", + kind: nil, + displayName: nil, + surface: nil, + subject: nil, + room: nil, + space: nil, + updatedAt: 1, + sessionId: "sess-main", + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: "xhigh", + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + modelProvider: "openrouter", + model: "deepseek/deepseek-v4", + contextTokens: nil, + thinkingLevels: nil, + thinkingOptions: ["off", "max"], + thinkingDefault: "max"), + ]) + + let (_, vm) = await makeViewModel( + historyResponses: [history], + sessionsResponses: [sessions]) + + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + + #expect(await MainActor.run { vm.thinkingLevel } == "xhigh") + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "max", "xhigh"]) + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.label) } == ["off", "max", "xhigh"]) + } + + @Test func matchingDefaultThinkingLevelsBeatLegacyRowThinkingOptions() async throws { + let history = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "adaptive") + let sessions = OpenClawChatSessionsListResponse( + ts: 1, + path: nil, + count: 1, + defaults: OpenClawChatSessionsDefaults( + modelProvider: "anthropic", + model: "claude-opus-4-7", + contextTokens: nil, + thinkingLevels: [ + thinkingOption("off"), + thinkingOption("adaptive"), + thinkingOption("max"), + ], + thinkingOptions: ["off", "adaptive", "max"], + thinkingDefault: "adaptive"), + sessions: [ + OpenClawChatSessionEntry( + key: "main", + kind: nil, + displayName: nil, + surface: nil, + subject: nil, + room: nil, + space: nil, + updatedAt: 1, + sessionId: "sess-main", + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: "adaptive", + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + modelProvider: "anthropic", + model: "claude-opus-4-7", + contextTokens: nil, + thinkingLevels: nil, + thinkingOptions: ["off"], + thinkingDefault: "off"), + ]) + + let (_, vm) = await makeViewModel( + historyResponses: [history], + sessionsResponses: [sessions]) + + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "adaptive", "max"]) + } + + @Test func defaultThinkingLevelsDoNotLeakToDifferentSessionModel() async throws { + let history = OpenClawChatHistoryPayload( + sessionKey: "main", + sessionId: "sess-main", + messages: [], + thinkingLevel: "max") + let sessions = OpenClawChatSessionsListResponse( + ts: 1, + path: nil, + count: 1, + defaults: OpenClawChatSessionsDefaults( + modelProvider: "anthropic", + model: "claude-opus-4-7", + contextTokens: nil, + thinkingLevels: [ + thinkingOption("off"), + thinkingOption("adaptive"), + thinkingOption("max"), + ], + thinkingOptions: ["off", "adaptive", "max"], + thinkingDefault: "adaptive"), + sessions: [ + OpenClawChatSessionEntry( + key: "main", + kind: nil, + displayName: nil, + surface: nil, + subject: nil, + room: nil, + space: nil, + updatedAt: 1, + sessionId: "sess-main", + systemSent: nil, + abortedLastRun: nil, + thinkingLevel: "max", + verboseLevel: nil, + inputTokens: nil, + outputTokens: nil, + totalTokens: nil, + modelProvider: "openai", + model: "gpt-5.4", + contextTokens: nil), + ]) + + let (_, vm) = await makeViewModel( + historyResponses: [history], + sessionsResponses: [sessions]) + + try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main") + + #expect(await MainActor.run { vm.thinkingLevel } == "max") + #expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == + ["off", "minimal", "low", "medium", "high", "max"]) + } + @Test func staleThinkingPatchCompletionReappliesLatestSelection() async throws { let history = OpenClawChatHistoryPayload( sessionKey: "main",