From 2407d5a82982d4371240dac6560d6b3c23092d75 Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Sun, 31 May 2026 11:01:51 -0700 Subject: [PATCH] Add 2-4 word length preset and re-bound the short tier to 4-7 Splits the previous 3-7 length tier into a new 2-4 option (for users who want shorter, higher-precision completions) and a 4-7 option (the former 3-7, lower bound bumped so it doesn't overlap with 2-4 and the tier chain stays aligned with 7-12 and 12-20). Existing users whose saved preset is '3-7' migrate to '4-7' on launch instead of silently falling back to the default. Quick onboarding template also tracks the rename. Closes #475. --- Cotabby/Models/OnboardingTemplate.swift | 2 +- Cotabby/Models/SuggestionModels.swift | 15 ++++++++++----- Cotabby/Models/SuggestionSettingsModel.swift | 18 ++++++++++++++---- .../ModelAndPresentationValueTests.swift | 7 +++++-- .../OnboardingTemplateRecommenderTests.swift | 2 +- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Cotabby/Models/OnboardingTemplate.swift b/Cotabby/Models/OnboardingTemplate.swift index aad0b6c5..8c4d07eb 100644 --- a/Cotabby/Models/OnboardingTemplate.swift +++ b/Cotabby/Models/OnboardingTemplate.swift @@ -70,7 +70,7 @@ enum OnboardingTemplate: String, CaseIterable, Identifiable, Equatable, Sendable var wordCountPreset: SuggestionWordCountPreset { switch self { case .quick: - return .threeToSeven + return .fourToSeven case .everyday: return .sevenToTwelve case .powerful: diff --git a/Cotabby/Models/SuggestionModels.swift b/Cotabby/Models/SuggestionModels.swift index e7273bac..9daa148c 100644 --- a/Cotabby/Models/SuggestionModels.swift +++ b/Cotabby/Models/SuggestionModels.swift @@ -12,7 +12,8 @@ import Foundation /// User-facing presets that bound how long one inline suggestion may be. /// Treating this as an enum keeps the UI and prompt policy in one source of truth. enum SuggestionWordCountPreset: String, CaseIterable, Equatable, Hashable, Sendable, Identifiable { - case threeToSeven = "3-7" + case twoToFour = "2-4" + case fourToSeven = "4-7" case sevenToTwelve = "7-12" case twelveToTwenty = "12-20" @@ -30,8 +31,10 @@ enum SuggestionWordCountPreset: String, CaseIterable, Equatable, Hashable, Senda var promptInstruction: String { switch self { - case .threeToSeven: - return "Return only the next 3 to 7 words." + case .twoToFour: + return "Return only the next 2 to 4 words." + case .fourToSeven: + return "Return only the next 4 to 7 words." case .sevenToTwelve: return "Return only the next 7 to 12 words." case .twelveToTwenty: @@ -43,10 +46,12 @@ enum SuggestionWordCountPreset: String, CaseIterable, Equatable, Hashable, Senda /// word-range cue was removed), so it must track the upper word bound closely. Sized at /// ~1.5x the upper word count to leave headroom for multi-token words (contractions, proper /// nouns, punctuation) without overrunning the preset. The earlier 50% bump (17/27/45) let - /// completions blow past the setting — e.g. ~12 words on the 3-7 preset (#271). + /// completions blow past the setting, e.g. ~12 words on the shortest preset (#271). var suggestedPredictionTokenBudget: Int { switch self { - case .threeToSeven: + case .twoToFour: + return 6 + case .fourToSeven: return 11 case .sevenToTwelve: return 18 diff --git a/Cotabby/Models/SuggestionSettingsModel.swift b/Cotabby/Models/SuggestionSettingsModel.swift index d22b102e..c773e947 100644 --- a/Cotabby/Models/SuggestionSettingsModel.swift +++ b/Cotabby/Models/SuggestionSettingsModel.swift @@ -88,6 +88,9 @@ final class SuggestionSettingsModel: ObservableObject { private static let ghostTextOpacityDefaultsKey = "cotabbyGhostTextOpacity" private static let selectedEngineDefaultsKey = "cotabbySelectedEngine" private static let selectedWordCountPresetDefaultsKey = "cotabbySelectedWordCountPreset" + /// Pre-#475 raw value for the shortest length tier. Kept here only so the read path can + /// rewrite it to `.fourToSeven` on launch; never re-emitted to UserDefaults. + private static let legacyShortPresetRawValue = "3-7" private static let clipboardContextEnabledDefaultsKey = "cotabbyClipboardContextEnabled" private static let fastModeEnabledDefaultsKey = "cotabbyFastModeEnabled" private static let performanceTrackingEnabledDefaultsKey = "cotabbyPerformanceTrackingEnabled" @@ -170,10 +173,17 @@ final class SuggestionSettingsModel: ObservableObject { .string(forKey: Self.selectedEngineDefaultsKey) .flatMap(SuggestionEngineKind.init(rawValue:)) ?? .llamaOpenSource - let resolvedWordCountPreset = userDefaults - .string(forKey: Self.selectedWordCountPresetDefaultsKey) - .flatMap(SuggestionWordCountPreset.init(rawValue:)) - ?? configuration.defaultWordCountPreset + let resolvedWordCountPreset: SuggestionWordCountPreset = { + let storedRaw = userDefaults.string(forKey: Self.selectedWordCountPresetDefaultsKey) + // Migrate the retired "3-7" raw value to its replacement "4-7" so users who picked + // the short preset don't silently jump to the default after #475 split the short + // tier into 2-4 and 4-7. + if storedRaw == Self.legacyShortPresetRawValue { + return .fourToSeven + } + return storedRaw.flatMap(SuggestionWordCountPreset.init(rawValue:)) + ?? configuration.defaultWordCountPreset + }() let resolvedClipboardContextEnabled = userDefaults.object(forKey: Self.clipboardContextEnabledDefaultsKey) as? Bool ?? false // Defaults to false so the visual-context pipeline keeps running for existing users; opting diff --git a/CotabbyTests/ModelAndPresentationValueTests.swift b/CotabbyTests/ModelAndPresentationValueTests.swift index b9c636da..77db6069 100644 --- a/CotabbyTests/ModelAndPresentationValueTests.swift +++ b/CotabbyTests/ModelAndPresentationValueTests.swift @@ -39,8 +39,11 @@ final class SuggestionTextColorCodecTests: XCTestCase { final class SuggestionModelValueTests: XCTestCase { func test_wordCountPresetsExposeMatchingPromptInstructionsAndTokenBudgets() { - XCTAssertEqual(SuggestionWordCountPreset.threeToSeven.promptInstruction, "Return only the next 3 to 7 words.") - XCTAssertEqual(SuggestionWordCountPreset.threeToSeven.suggestedPredictionTokenBudget, 11) + XCTAssertEqual(SuggestionWordCountPreset.twoToFour.promptInstruction, "Return only the next 2 to 4 words.") + XCTAssertEqual(SuggestionWordCountPreset.twoToFour.suggestedPredictionTokenBudget, 6) + + XCTAssertEqual(SuggestionWordCountPreset.fourToSeven.promptInstruction, "Return only the next 4 to 7 words.") + XCTAssertEqual(SuggestionWordCountPreset.fourToSeven.suggestedPredictionTokenBudget, 11) XCTAssertEqual(SuggestionWordCountPreset.sevenToTwelve.promptInstruction, "Return only the next 7 to 12 words.") XCTAssertEqual(SuggestionWordCountPreset.sevenToTwelve.suggestedPredictionTokenBudget, 18) diff --git a/CotabbyTests/OnboardingTemplateRecommenderTests.swift b/CotabbyTests/OnboardingTemplateRecommenderTests.swift index b493e241..2385debb 100644 --- a/CotabbyTests/OnboardingTemplateRecommenderTests.swift +++ b/CotabbyTests/OnboardingTemplateRecommenderTests.swift @@ -26,7 +26,7 @@ final class OnboardingTemplateRecommenderTests: XCTestCase { func testAppleIntelligenceStillCarriesTierBehaviorFlags() { let quick = OnboardingTemplateRecommender.resolvePlan(for: .quick, engine: .appleIntelligence) - XCTAssertEqual(quick.wordCountPreset, .threeToSeven) + XCTAssertEqual(quick.wordCountPreset, .fourToSeven) XCTAssertTrue(quick.enablesFastMode) XCTAssertFalse(quick.enablesMultiLine) XCTAssertFalse(quick.enablesClipboardContext)