diff --git a/clients/macos/vellum-assistant/Features/Settings/InferenceProfile.swift b/clients/macos/vellum-assistant/Features/Settings/InferenceProfile.swift index 03c1ef107fa..73f2f1c26a4 100644 --- a/clients/macos/vellum-assistant/Features/Settings/InferenceProfile.swift +++ b/clients/macos/vellum-assistant/Features/Settings/InferenceProfile.swift @@ -95,8 +95,41 @@ public struct InferenceProfile: Hashable, Identifiable { public var isDisabled: Bool { status == "disabled" } /// Label for pickers and list rows. Prefers the explicit `label` - /// (e.g. "Quality") and falls back to `name`. - public var displayName: String { label ?? name } + /// (e.g. "Quality") and falls back to a title-cased rendering of + /// `name` when `label` is missing or blank. + /// + /// Treating empty / whitespace-only `label` as missing — not just `nil` + /// — keeps the fallback robust against any code path that might set + /// `label = ""` without going through the JSON decoder's normalization. + /// The title-case step makes the fallback presentable since `name` is a + /// stable identifier and is typically kebab- or snake-cased + /// (e.g. `"quality-optimized"` → `"Quality Optimized"`). + public var displayName: String { + if let label, !label.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return label + } + return Self.titleCased(name) + } + + /// Renders a profile identifier as a human-readable label by splitting + /// on common separators (`-`, `_`, whitespace) and capitalising the + /// first character of each part. Returns the input unchanged when no + /// non-separator parts remain (e.g. an empty string or `"---"`). + /// + /// Examples: + /// - `"balanced"` → `"Balanced"` + /// - `"quality-optimized"` → `"Quality Optimized"` + /// - `"custom_balanced"` → `"Custom Balanced"` + static func titleCased(_ identifier: String) -> String { + let separators = CharacterSet(charactersIn: "-_").union(.whitespacesAndNewlines) + let parts = identifier + .components(separatedBy: separators) + .filter { !$0.isEmpty } + guard !parts.isEmpty else { return identifier } + return parts + .map { $0.prefix(1).uppercased() + $0.dropFirst() } + .joined(separator: " ") + } /// Optional secondary text for list row subtitles. public var subtitle: String? { profileDescription } diff --git a/clients/macos/vellum-assistantTests/Features/Chat/ChatProfilePickerTests.swift b/clients/macos/vellum-assistantTests/Features/Chat/ChatProfilePickerTests.swift index c41de298867..220c76c35dc 100644 --- a/clients/macos/vellum-assistantTests/Features/Chat/ChatProfilePickerTests.swift +++ b/clients/macos/vellum-assistantTests/Features/Chat/ChatProfilePickerTests.swift @@ -12,7 +12,7 @@ final class ChatProfilePickerTests: XCTestCase { let profiles = [InferenceProfile(name: "balanced")] XCTAssertEqual( ChatProfilePicker.label(current: nil, profiles: profiles, activeProfile: "balanced"), - "Default (balanced)" + "Default (Balanced)" ) } @@ -23,7 +23,7 @@ final class ChatProfilePickerTests: XCTestCase { ] XCTAssertEqual( ChatProfilePicker.label(current: "quality-optimized", profiles: profiles, activeProfile: "balanced"), - "quality-optimized" + "Quality Optimized" ) } @@ -31,7 +31,7 @@ final class ChatProfilePickerTests: XCTestCase { let profiles = [InferenceProfile(name: "cost-optimized")] XCTAssertEqual( ChatProfilePicker.label(current: nil, profiles: profiles, activeProfile: "cost-optimized"), - "Default (cost-optimized)" + "Default (Cost Optimized)" ) } diff --git a/clients/macos/vellum-assistantTests/Features/Settings/InferenceProfileTests.swift b/clients/macos/vellum-assistantTests/Features/Settings/InferenceProfileTests.swift index c885ea96c74..cffb80fa557 100644 --- a/clients/macos/vellum-assistantTests/Features/Settings/InferenceProfileTests.swift +++ b/clients/macos/vellum-assistantTests/Features/Settings/InferenceProfileTests.swift @@ -222,9 +222,29 @@ final class InferenceProfileTests: XCTestCase { XCTAssertFalse(profile.isManaged) } - func testDisplayNameFallsBackToNameWhenLabelIsNil() { + func testDisplayNameFallsBackToTitleCasedNameWhenLabelIsNil() { let profile = InferenceProfile(name: "my-profile") - XCTAssertEqual(profile.displayName, "my-profile") + XCTAssertEqual(profile.displayName, "My Profile") + } + + func testDisplayNameFallsBackToTitleCasedNameWhenLabelIsEmptyString() { + let profile = InferenceProfile(name: "quality-optimized", label: "") + XCTAssertEqual(profile.displayName, "Quality Optimized") + } + + func testDisplayNameFallsBackToTitleCasedNameWhenLabelIsWhitespace() { + let profile = InferenceProfile(name: "custom-balanced", label: " ") + XCTAssertEqual(profile.displayName, "Custom Balanced") + } + + func testTitleCasedHandlesCommonIdentifierShapes() { + XCTAssertEqual(InferenceProfile.titleCased("balanced"), "Balanced") + XCTAssertEqual(InferenceProfile.titleCased("quality-optimized"), "Quality Optimized") + XCTAssertEqual(InferenceProfile.titleCased("custom_balanced"), "Custom Balanced") + XCTAssertEqual(InferenceProfile.titleCased("custom-quality-optimized"), "Custom Quality Optimized") + XCTAssertEqual(InferenceProfile.titleCased(" spaced name "), "Spaced Name") + XCTAssertEqual(InferenceProfile.titleCased("---"), "---") + XCTAssertEqual(InferenceProfile.titleCased(""), "") } func testSubtitleIsNilWhenDescriptionIsNil() {