diff --git a/clients/macos/vellum-assistant/Features/Settings/InferenceServiceCard.swift b/clients/macos/vellum-assistant/Features/Settings/InferenceServiceCard.swift index d1bf8527618..905a2d64147 100644 --- a/clients/macos/vellum-assistant/Features/Settings/InferenceServiceCard.swift +++ b/clients/macos/vellum-assistant/Features/Settings/InferenceServiceCard.swift @@ -1,62 +1,34 @@ import SwiftUI import VellumAssistantShared -/// Card for the inference service provider and profile configuration. +/// Card for the inference service: active profile selector and management +/// buttons for provider connections, profiles, and per-call-site overrides. /// -/// Shows the global provider picker, active profile selector, and -/// management buttons for provider connections, profiles, and per-call-site -/// overrides. Saving persists the provider selection only — model selection -/// lives inside inference profiles. +/// Profiles already carry the provider they dispatch against, so the card +/// does not surface a separate global "default Provider" picker — picking a +/// profile picks its provider transitively. Provider connections themselves +/// are managed through the "Providers" button. @MainActor struct InferenceServiceCard: View { @ObservedObject var store: SettingsStore var showToast: (String, ToastInfo.Style) -> Void - /// Local draft of the provider selection — only persisted on Save. - @State private var draftProvider: String = "anthropic" - /// Snapshot of the provider at card appear — used to detect provider changes. - @State private var initialProvider: String = "" /// Whether the read-only per-call-site overrides sheet is presented. @State private var showOverridesSheet = false /// Whether the inference profiles management sheet is presented. @State private var showProfilesSheet = false /// Whether the provider connections management sheet is presented. @State private var showProvidersSheet = false - /// Whether to show the per-call-site override confirmation dialog. Fires - /// when the user is about to switch the global provider AND has at least - /// one override pinned to the OLD provider — we ask whether to keep those - /// pins or reset them to follow the new default. - @State private var showOverrideConfirmation = false - /// Snapshot of the overrides pinned to the OLD provider at the moment the - /// confirmation dialog is shown. Used both to render the dialog message - /// (count + provider name) and to drive the "Reset" action. - @State private var pendingOverrideClears: [CallSiteOverride] = [] - /// The provider name displayed in the confirmation dialog message. - @State private var pendingOverrideOldProviderName: String = "" /// The most recent in-flight `setActiveProfile` task, retained so a /// subsequent dropdown pick can cancel it and avoid an out-of-order /// PATCH landing the older selection last. @State private var activeProfileTask: Task? - // MARK: - Computed State - - private var hasChanges: Bool { - draftProvider != initialProvider - } - var body: some View { SettingsCard(title: "Language Model", subtitle: "Configure the LLMs that power your assistant") { VStack(alignment: .leading, spacing: VSpacing.sm) { - providerPicker activeProfilePicker secondaryActionsRow - if hasChanges { - ServiceCardActions( - hasChanges: true, - isSaving: false, - onSave: { save() } - ) - } } } .sheet(isPresented: $showOverridesSheet) { @@ -68,48 +40,6 @@ struct InferenceServiceCard: View { .sheet(isPresented: $showProvidersSheet) { ProvidersSheet(store: store, isPresented: $showProvidersSheet) } - .onAppear { - draftProvider = store.selectedInferenceProvider - initialProvider = store.selectedInferenceProvider - } - .onChange(of: store.selectedInferenceProvider) { _, newValue in - draftProvider = newValue - initialProvider = newValue - } - .confirmationDialog( - "Keep per-task overrides?", - isPresented: $showOverrideConfirmation, - titleVisibility: .visible - ) { - Button("Keep overrides") { - performSaveCore(clearingOverrides: []) - } - Button("Reset to follow default") { - performSaveCore(clearingOverrides: pendingOverrideClears) - } - Button("Cancel", role: .cancel) { - pendingOverrideClears = [] - pendingOverrideOldProviderName = "" - } - } message: { - let msg = "\(pendingOverrideClears.count) task(s) are pinned to \(pendingOverrideOldProviderName). Keep them as-is, or update them to follow the new default?" - Text(msg) - } - } - - // MARK: - Provider Picker - - private var providerPicker: some View { - VStack(alignment: .leading, spacing: VSpacing.sm) { - Text("Provider") - .font(VFont.labelDefault) - .foregroundStyle(VColor.contentSecondary) - VDropdown( - placeholder: "Select a provider\u{2026}", - selection: $draftProvider, - options: store.providerCatalog.map { (label: $0.displayName, value: $0.id) } - ) - } } // MARK: - Secondary Actions Row @@ -157,47 +87,4 @@ struct InferenceServiceCard: View { ) } } - - // MARK: - Save - - private func save() { - performSave() - } - - private func performSave() { - let persistProvider = draftProvider - let providerIdChanged = persistProvider != initialProvider - if providerIdChanged { - let overridesPinnedToOldProvider = store.callSiteOverrides.filter { - $0.provider == initialProvider - } - if !overridesPinnedToOldProvider.isEmpty { - pendingOverrideClears = overridesPinnedToOldProvider - pendingOverrideOldProviderName = store.dynamicProviderDisplayName(initialProvider) - showOverrideConfirmation = true - return - } - } - performSaveCore(clearingOverrides: []) - } - - private func performSaveCore(clearingOverrides overridesToClear: [CallSiteOverride]) { - store.apiKeySaveError = nil - - for override in overridesToClear { - _ = store.clearCallSiteOverride(override.id) - } - pendingOverrideClears = [] - pendingOverrideOldProviderName = "" - - let persistProvider = draftProvider - let providerChanged = persistProvider != initialProvider - if providerChanged { - initialProvider = draftProvider - let capturedProvider = persistProvider - Task { - _ = await store.setLLMDefaultProvider(capturedProvider).value - } - } - } } diff --git a/clients/macos/vellum-assistantTests/Features/Settings/InferenceServiceCardTests.swift b/clients/macos/vellum-assistantTests/Features/Settings/InferenceServiceCardTests.swift index c290a820b5f..2959428142e 100644 --- a/clients/macos/vellum-assistantTests/Features/Settings/InferenceServiceCardTests.swift +++ b/clients/macos/vellum-assistantTests/Features/Settings/InferenceServiceCardTests.swift @@ -5,11 +5,10 @@ import XCTest /// Structural tests for `InferenceServiceCard`. Exercises the bindings the /// card surfaces — Active Profile selection routing through -/// `store.setActiveProfile`, the Manage Profiles sheet toggle, and the Save -/// path no longer writing `llm.default.model`. Mirrors the `InferenceProfilesSheetTests` -/// pattern: build the SwiftUI tree without rendering, drive store-backed -/// invariants directly, and assert the patches captured by -/// `MockSettingsClient`. +/// `store.setActiveProfile` and the management sheet toggles. Mirrors the +/// `InferenceProfilesSheetTests` pattern: build the SwiftUI tree without +/// rendering, drive store-backed invariants directly, and assert the +/// patches captured by `MockSettingsClient`. @MainActor final class InferenceServiceCardTests: XCTestCase { @@ -78,7 +77,8 @@ final class InferenceServiceCardTests: XCTestCase { } /// True when any captured `llm.default` patch has touched `model`. Used - /// to assert the card's Save path no longer writes the model leaf. + /// to assert that flows driven from this card never mutate the model + /// leaf — model selection lives inside inference profiles. private func anyPatchWroteLLMDefaultModel() -> Bool { for payload in mockSettingsClient.patchConfigCalls { guard let llm = payload["llm"] as? [String: Any], @@ -124,9 +124,8 @@ final class InferenceServiceCardTests: XCTestCase { XCTAssertEqual(lastActive, "quality-optimized") // The patch must touch `activeProfile` — and nothing else under - // `llm.default`. This is the central invariant of PR 14: the - // active profile setter is its own path, distinct from - // `llm.default.{provider,model}`. + // `llm.default`. The active profile setter is its own path, + // distinct from `llm.default.{provider,model}`. XCTAssertFalse( anyPatchWroteLLMDefaultModel(), "Active Profile selection must not write llm.default.model" @@ -173,34 +172,6 @@ final class InferenceServiceCardTests: XCTestCase { XCTAssertNotNil(sheet.body, "ProvidersSheet must be constructible with the card's store") } - // MARK: - Save flow no longer writes llm.default.model - - /// Persisting a provider change writes `llm.default.provider` only. - /// This is the second central invariant of PR 14: Save no longer - /// touches `llm.default.model`. - func testProviderOnlySetterPatchesProviderWithoutModel() async { - let task = store.setLLMDefaultProvider("openai") - _ = await task.value - - // Find the captured provider patch. - let providerPatches = mockSettingsClient.patchConfigCalls.compactMap { payload -> [String: Any]? in - guard let llm = payload["llm"] as? [String: Any], - let llmDefault = llm["default"] as? [String: Any] else { return nil } - return llmDefault - } - XCTAssertEqual(providerPatches.count, 1, "Provider-only setter must emit exactly one patch") - XCTAssertEqual(providerPatches.first?["provider"] as? String, "openai") - XCTAssertNil( - providerPatches.first?["model"], - "Provider-only setter must not include the model leaf" - ) - - XCTAssertFalse( - anyPatchWroteLLMDefaultModel(), - "PR 14 invariant: the inference card's Save path never writes llm.default.model" - ) - } - // MARK: - Profiles list flows through to dropdown options /// The dropdown options come from `store.profiles.map { $0.name }` —