mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 07:07:27 +00:00
macOS: add chat model selector and persist thinking (#42314)
* feat(macos): add chat model selector and thinking persistence UX * Chat UI: carry session model providers * Docs: add macOS model selector changelog * macOS: persist extended thinking levels * Chat UI: keep model picker state in sync * Chat UI tests: cover model selection races --------- Co-authored-by: Ubuntu <ubuntu@vps-90352893.vps.ovh.ca> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -8,6 +8,7 @@ import QuartzCore
|
||||
import SwiftUI
|
||||
|
||||
private let webChatSwiftLogger = Logger(subsystem: "ai.openclaw", category: "WebChatSwiftUI")
|
||||
private let webChatThinkingLevelDefaultsKey = "openclaw.webchat.thinkingLevel"
|
||||
|
||||
private enum WebChatSwiftUILayout {
|
||||
static let windowSize = NSSize(width: 500, height: 840)
|
||||
@@ -21,6 +22,21 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
||||
try await GatewayConnection.shared.chatHistory(sessionKey: sessionKey)
|
||||
}
|
||||
|
||||
func listModels() async throws -> [OpenClawChatModelChoice] {
|
||||
do {
|
||||
let data = try await GatewayConnection.shared.request(
|
||||
method: "models.list",
|
||||
params: [:],
|
||||
timeoutMs: 15000)
|
||||
let result = try JSONDecoder().decode(ModelsListResult.self, from: data)
|
||||
return result.models.map(Self.mapModelChoice)
|
||||
} catch {
|
||||
webChatSwiftLogger.warning(
|
||||
"models.list failed; hiding model picker: \(error.localizedDescription, privacy: .public)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
func abortRun(sessionKey: String, runId: String) async throws {
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "chat.abort",
|
||||
@@ -46,6 +62,28 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
||||
return try JSONDecoder().decode(OpenClawChatSessionsListResponse.self, from: data)
|
||||
}
|
||||
|
||||
func setSessionModel(sessionKey: String, model: String?) async throws {
|
||||
var params: [String: AnyCodable] = [
|
||||
"key": AnyCodable(sessionKey),
|
||||
]
|
||||
params["model"] = model.map(AnyCodable.init) ?? AnyCodable(NSNull())
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "sessions.patch",
|
||||
params: params,
|
||||
timeoutMs: 15000)
|
||||
}
|
||||
|
||||
func setSessionThinking(sessionKey: String, thinkingLevel: String) async throws {
|
||||
let params: [String: AnyCodable] = [
|
||||
"key": AnyCodable(sessionKey),
|
||||
"thinkingLevel": AnyCodable(thinkingLevel),
|
||||
]
|
||||
_ = try await GatewayConnection.shared.request(
|
||||
method: "sessions.patch",
|
||||
params: params,
|
||||
timeoutMs: 15000)
|
||||
}
|
||||
|
||||
func sendMessage(
|
||||
sessionKey: String,
|
||||
message: String,
|
||||
@@ -133,6 +171,14 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
||||
return .seqGap
|
||||
}
|
||||
}
|
||||
|
||||
private static func mapModelChoice(_ model: OpenClawProtocol.ModelChoice) -> OpenClawChatModelChoice {
|
||||
OpenClawChatModelChoice(
|
||||
modelID: model.id,
|
||||
name: model.name,
|
||||
provider: model.provider,
|
||||
contextWindow: model.contextwindow)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Window controller
|
||||
@@ -155,7 +201,13 @@ final class WebChatSwiftUIWindowController {
|
||||
init(sessionKey: String, presentation: WebChatPresentation, transport: any OpenClawChatTransport) {
|
||||
self.sessionKey = sessionKey
|
||||
self.presentation = presentation
|
||||
let vm = OpenClawChatViewModel(sessionKey: sessionKey, transport: transport)
|
||||
let vm = OpenClawChatViewModel(
|
||||
sessionKey: sessionKey,
|
||||
transport: transport,
|
||||
initialThinkingLevel: Self.persistedThinkingLevel(),
|
||||
onThinkingLevelChanged: { level in
|
||||
UserDefaults.standard.set(level, forKey: webChatThinkingLevelDefaultsKey)
|
||||
})
|
||||
let accent = Self.color(fromHex: AppStateStore.shared.seamColorHex)
|
||||
self.hosting = NSHostingController(rootView: OpenClawChatView(
|
||||
viewModel: vm,
|
||||
@@ -254,6 +306,16 @@ final class WebChatSwiftUIWindowController {
|
||||
OverlayPanelFactory.clearGlobalEventMonitor(&self.dismissMonitor)
|
||||
}
|
||||
|
||||
private static func persistedThinkingLevel() -> String? {
|
||||
let stored = UserDefaults.standard.string(forKey: webChatThinkingLevelDefaultsKey)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased()
|
||||
guard let stored, ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"].contains(stored) else {
|
||||
return nil
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
private static func makeWindow(
|
||||
for presentation: WebChatPresentation,
|
||||
contentViewController: NSViewController) -> NSWindow
|
||||
|
||||
Reference in New Issue
Block a user