Decouple merge model picker from the main chat's active provider

The merge model picker was reusing chatViewModel.availableModels,
which only ever holds whichever provider the main chat window
currently has active — fine for the main chat's own switcher (where
provider and model change together via the header), wrong for an
independent one-off picker like this. If your active chat was on
Anthropic, that's all you could pick from here regardless of what
other providers you have configured.

Added its own provider menu (mirroring HeaderView's) and an
independent model list fetched via ProviderRegistry for whichever
provider is selected, so OpenRouter, Anthropic, OpenAI, etc. are all
genuinely selectable regardless of what the main chat is doing.
This commit is contained in:
2026-07-28 14:57:19 +02:00
parent e898d4b6db
commit 37734232f5
@@ -25,7 +25,6 @@ import SwiftUI
struct CombineConversationsSheet: View { struct CombineConversationsSheet: View {
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(ChatViewModel.self) private var chatViewModel
let conversations: [Conversation] let conversations: [Conversation]
var onCompleted: (Conversation) -> Void var onCompleted: (Conversation) -> Void
@@ -37,6 +36,8 @@ struct CombineConversationsSheet: View {
@State private var errorMessage: String? @State private var errorMessage: String?
@State private var mergeModel: ModelInfo? @State private var mergeModel: ModelInfo?
@State private var mergeProvider: Settings.Provider @State private var mergeProvider: Settings.Provider
@State private var mergeModels: [ModelInfo] = []
@State private var isLoadingMergeModels = false
@State private var showModelPicker = false @State private var showModelPicker = false
private let settings = SettingsService.shared private let settings = SettingsService.shared
@@ -50,11 +51,8 @@ struct CombineConversationsSheet: View {
} }
private var mergeModelLabel: String? { private var mergeModelLabel: String? {
if let mergeModel { guard let mergeModel else { return nil }
return "\(mergeProvider.displayName) / \(mergeModel.name)" return "\(mergeProvider.displayName) / \(mergeModel.name)"
}
guard let model = settings.defaultModel, !model.isEmpty else { return nil }
return "\(settings.defaultProvider.displayName) / \(model)"
} }
private var isValid: Bool { private var isValid: Bool {
@@ -125,12 +123,37 @@ struct CombineConversationsSheet: View {
Label("No model selected", systemImage: "exclamationmark.triangle.fill") Label("No model selected", systemImage: "exclamationmark.triangle.fill")
.font(.caption).foregroundStyle(.orange) .font(.caption).foregroundStyle(.orange)
} }
Button("Change…") {
Menu {
ForEach(ProviderRegistry.shared.configuredProviders, id: \.self) { p in
Button {
switchMergeProvider(to: p)
} label: {
HStack {
Image(systemName: p.iconName)
Text(p.displayName)
if p == mergeProvider { Image(systemName: "checkmark") }
}
}
}
} label: {
Text(mergeProvider.displayName)
}
.menuStyle(.borderlessButton)
.fixedSize()
.font(.caption)
.disabled(isProcessing || isLoadingMergeModels)
Button("Change Model…") {
showModelPicker = true showModelPicker = true
} }
.buttonStyle(.link) .buttonStyle(.link)
.font(.caption) .font(.caption)
.disabled(isProcessing) .disabled(isProcessing || isLoadingMergeModels || mergeModels.isEmpty)
if isLoadingMergeModels {
ProgressView().controlSize(.mini)
}
} }
} }
} }
@@ -173,26 +196,46 @@ struct CombineConversationsSheet: View {
} }
.frame(minWidth: 520, idealWidth: 560, minHeight: 460, idealHeight: 520) .frame(minWidth: 520, idealWidth: 560, minHeight: 460, idealHeight: 520)
.task { .task {
if chatViewModel.availableModels.count <= 10 { await loadMergeModels()
await chatViewModel.loadAvailableModels() if let defaultModel = settings.defaultModel {
} mergeModel = mergeModels.first(where: { $0.id == defaultModel })
if mergeModel == nil, let defaultModel = settings.defaultModel {
mergeModel = chatViewModel.availableModels.first(where: { $0.id == defaultModel })
} }
} }
.sheet(isPresented: $showModelPicker) { .sheet(isPresented: $showModelPicker) {
ModelSelectorView( ModelSelectorView(
models: chatViewModel.availableModels, models: mergeModels,
selectedModel: mergeModel, selectedModel: mergeModel,
onSelect: { model in onSelect: { model in
mergeModel = model mergeModel = model
mergeProvider = chatViewModel.inferProviderPublic(from: model.id) ?? mergeProvider
showModelPicker = false showModelPicker = false
} }
) )
} }
} }
private func switchMergeProvider(to newProvider: Settings.Provider) {
guard newProvider != mergeProvider else { return }
mergeProvider = newProvider
mergeModel = nil
mergeModels = []
Task { await loadMergeModels() }
}
private func loadMergeModels() async {
guard let provider = ProviderRegistry.shared.getProvider(for: mergeProvider) else {
mergeModels = []
return
}
isLoadingMergeModels = true
defer { isLoadingMergeModels = false }
do {
mergeModels = try await provider.listModels()
} catch {
Log.api.error("Failed to load models for merge provider \(mergeProvider.rawValue): \(error.localizedDescription)")
mergeModels = []
}
}
private func combine() { private func combine() {
isProcessing = true isProcessing = true
errorMessage = nil errorMessage = nil