From 37734232f57ee21dde652edb8a28aa07c7dd67ce Mon Sep 17 00:00:00 2001 From: Rune Olsen Date: Tue, 28 Jul 2026 14:57:19 +0200 Subject: [PATCH] Decouple merge model picker from the main chat's active provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Screens/CombineConversationsSheet.swift | 73 +++++++++++++++---- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/oAI/Views/Screens/CombineConversationsSheet.swift b/oAI/Views/Screens/CombineConversationsSheet.swift index 5561ae8..3e45b7f 100644 --- a/oAI/Views/Screens/CombineConversationsSheet.swift +++ b/oAI/Views/Screens/CombineConversationsSheet.swift @@ -25,7 +25,6 @@ import SwiftUI struct CombineConversationsSheet: View { @Environment(\.dismiss) var dismiss - @Environment(ChatViewModel.self) private var chatViewModel let conversations: [Conversation] var onCompleted: (Conversation) -> Void @@ -37,6 +36,8 @@ struct CombineConversationsSheet: View { @State private var errorMessage: String? @State private var mergeModel: ModelInfo? @State private var mergeProvider: Settings.Provider + @State private var mergeModels: [ModelInfo] = [] + @State private var isLoadingMergeModels = false @State private var showModelPicker = false private let settings = SettingsService.shared @@ -50,11 +51,8 @@ struct CombineConversationsSheet: View { } private var mergeModelLabel: String? { - if let mergeModel { - return "\(mergeProvider.displayName) / \(mergeModel.name)" - } - guard let model = settings.defaultModel, !model.isEmpty else { return nil } - return "\(settings.defaultProvider.displayName) / \(model)" + guard let mergeModel else { return nil } + return "\(mergeProvider.displayName) / \(mergeModel.name)" } private var isValid: Bool { @@ -125,12 +123,37 @@ struct CombineConversationsSheet: View { Label("No model selected", systemImage: "exclamationmark.triangle.fill") .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 } .buttonStyle(.link) .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) .task { - if chatViewModel.availableModels.count <= 10 { - await chatViewModel.loadAvailableModels() - } - if mergeModel == nil, let defaultModel = settings.defaultModel { - mergeModel = chatViewModel.availableModels.first(where: { $0.id == defaultModel }) + await loadMergeModels() + if let defaultModel = settings.defaultModel { + mergeModel = mergeModels.first(where: { $0.id == defaultModel }) } } .sheet(isPresented: $showModelPicker) { ModelSelectorView( - models: chatViewModel.availableModels, + models: mergeModels, selectedModel: mergeModel, onSelect: { model in mergeModel = model - mergeProvider = chatViewModel.inferProviderPublic(from: model.id) ?? mergeProvider 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() { isProcessing = true errorMessage = nil