From 826a8d7ce345fecfcead2fd8d050fb540eb7993a Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Sat, 30 May 2026 19:03:53 -0700 Subject: [PATCH 1/3] Add collapsible 'what's included' disclosure to onboarding template cards --- Cotabby.xcodeproj/project.pbxproj | 8 ++ .../OnboardingTemplateFeatureList.swift | 51 ++++++++ Cotabby/UI/WelcomeTemplateStepView.swift | 121 ++++++++++++++++-- .../OnboardingTemplateFeatureListTests.swift | 30 +++++ 4 files changed, 196 insertions(+), 14 deletions(-) create mode 100644 Cotabby/Support/OnboardingTemplateFeatureList.swift create mode 100644 CotabbyTests/OnboardingTemplateFeatureListTests.swift diff --git a/Cotabby.xcodeproj/project.pbxproj b/Cotabby.xcodeproj/project.pbxproj index 1a5991fc..a82b02c1 100644 --- a/Cotabby.xcodeproj/project.pbxproj +++ b/Cotabby.xcodeproj/project.pbxproj @@ -102,6 +102,7 @@ 61EC9D635D416115E7C96E0F /* PermissionOverlayWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C6EB9FDA48ADF425A116A9 /* PermissionOverlayWindowController.swift */; }; 63054CBDCA87560130BF5ADC /* ExtendedContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54BC85605541E913EE57B258 /* ExtendedContextTests.swift */; }; 644EEF959D07D54CC779BBF6 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3350EDE01ED5125520C79D53 /* SettingsCoordinator.swift */; }; + 64DA031AEAC20AC6C852A24A /* OnboardingTemplateFeatureList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F613F0E2F7046E6532A09C /* OnboardingTemplateFeatureList.swift */; }; 65478B0DABF5460C32D4C458 /* ModelFileValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A829F28F01FAE76CA7244BBC /* ModelFileValidatorTests.swift */; }; 66C23A7C2FCDE0266FF425F8 /* ApplicationBundleMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352AF5B2834FEE1F597394E4 /* ApplicationBundleMetadata.swift */; }; 66D9E37B12A9265D4733E72E /* LlamaRuntimeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944065A858D9BC936CB12B23 /* LlamaRuntimeCore.swift */; }; @@ -184,6 +185,7 @@ D800277BE3296DE2FB8198A8 /* ChromiumAccessibilityEnabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8896D976C7F116EBA0F3969F /* ChromiumAccessibilityEnabler.swift */; }; D90C889A623A928F4F5FDC7B /* HardwareCapabilityProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BBD5A4BA08CABE77860886 /* HardwareCapabilityProbe.swift */; }; D9C51DEDF01033E276A479CE /* AXTextGeometryResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB58035EFFD65B767949BAE6 /* AXTextGeometryResolver.swift */; }; + DA23422A2CF77CFD3B1283C8 /* OnboardingTemplateFeatureListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D814BBA41CF29E8DD9954651 /* OnboardingTemplateFeatureListTests.swift */; }; DD7FA343F1C21C4569F6D181 /* ScreenshotContextGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B84BAE361626891F19DC9DB /* ScreenshotContextGenerator.swift */; }; DDEDCBAA2196303455F6926A /* AcceptanceModePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DAF68AEBFE334F68A65E82 /* AcceptanceModePickerView.swift */; }; DE236C9285635C686D66A2F6 /* TerminalAppDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E37A7E835D3BDE6265843C /* TerminalAppDetectorTests.swift */; }; @@ -254,6 +256,7 @@ 21CB3008986BE7FD2A4D9132 /* WelcomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeCoordinator.swift; sourceTree = ""; }; 220CD4AFA1E96A37BC4514AD /* LaunchAtLoginService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginService.swift; sourceTree = ""; }; 22544F4B756E3E4144497D17 /* SuggestionCoordinator+Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionCoordinator+Input.swift"; sourceTree = ""; }; + 24F613F0E2F7046E6532A09C /* OnboardingTemplateFeatureList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTemplateFeatureList.swift; sourceTree = ""; }; 262BE2F1E97389FE8D7A5FB9 /* Cotabby.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cotabby.app; sourceTree = BUILT_PRODUCTS_DIR; }; 264CA64B2AB1611F82E5B760 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; 273B4DC844F79B4BE2C8910F /* FocusPollBackoffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusPollBackoffTests.swift; sourceTree = ""; }; @@ -401,6 +404,7 @@ D4F6D5F94B238F7B4BE7C247 /* FocusCapabilityResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusCapabilityResolverTests.swift; sourceTree = ""; }; D504BEB224E0C176F5FCFF6E /* CompletionRenderModePolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionRenderModePolicyTests.swift; sourceTree = ""; }; D5D6C2318E405AA717D1C256 /* WelcomePermissionStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePermissionStepView.swift; sourceTree = ""; }; + D814BBA41CF29E8DD9954651 /* OnboardingTemplateFeatureListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTemplateFeatureListTests.swift; sourceTree = ""; }; D84D4528EEC9EFEB8AE8E318 /* ActivationIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivationIndicatorController.swift; sourceTree = ""; }; D9C1C921A1CDA2ADFC39EA01 /* AppsPaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsPaneView.swift; sourceTree = ""; }; DB0CE9AB1286367BA2E82392 /* SettingsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContainerView.swift; sourceTree = ""; }; @@ -669,6 +673,7 @@ FC83D14A7557BC0196E59007 /* MirrorOverlayLayoutTests.swift */, 03766F6253FF17639230C0F6 /* ModelAndPresentationValueTests.swift */, A829F28F01FAE76CA7244BBC /* ModelFileValidatorTests.swift */, + D814BBA41CF29E8DD9954651 /* OnboardingTemplateFeatureListTests.swift */, 01B72736E416910878E8E493 /* OnboardingTemplateRecommenderTests.swift */, E7F42112F14026E6253BB865 /* PermissionAndContextModelTests.swift */, 12DD19BCE610808F1E38702D /* PermissionOverlayTrackerTests.swift */, @@ -806,6 +811,7 @@ 8D610FCA3A97249DCCE7D0B8 /* LLMIOFileHandler.swift */, A863F41C0C03D7B4AC5DC002 /* MarkerSelectionSynthesizer.swift */, 54150A507B03221F137D539B /* MirrorOverlayLayout.swift */, + 24F613F0E2F7046E6532A09C /* OnboardingTemplateFeatureList.swift */, FA878B447441BB4F3E327CC8 /* OnboardingTemplateRecommender.swift */, E6423D6CC8CC371D2DA899DE /* PermissionOverlayTracker.swift */, FA4B45B91D4DEAC979C3113E /* PromptContextSanitizer.swift */, @@ -1030,6 +1036,7 @@ 2F227738D7834B1A7A81D1D6 /* ModelDownloadManager.swift in Sources */, 317883210D1D1D5CD654E562 /* ModelFileValidator.swift in Sources */, FC6B0524B774F20C18BD6889 /* OnboardingTemplate.swift in Sources */, + 64DA031AEAC20AC6C852A24A /* OnboardingTemplateFeatureList.swift in Sources */, DF8794793110A8ED234CBA96 /* OnboardingTemplateRecommender.swift in Sources */, 0DDC0CFF5558A8F4355836B2 /* OverlayController.swift in Sources */, 0187EAA1D37B92DD5B264016 /* PermissionDragSourceView.swift in Sources */, @@ -1131,6 +1138,7 @@ 14D77F0B8A195AC2FA8D24A9 /* MirrorOverlayLayoutTests.swift in Sources */, 25D4FC8D191A50F63E6391F9 /* ModelAndPresentationValueTests.swift in Sources */, 65478B0DABF5460C32D4C458 /* ModelFileValidatorTests.swift in Sources */, + DA23422A2CF77CFD3B1283C8 /* OnboardingTemplateFeatureListTests.swift in Sources */, D648DD70AD847F67B77CE052 /* OnboardingTemplateRecommenderTests.swift in Sources */, 15FA56CEF6FB5FF54C2FBA6F /* PermissionAndContextModelTests.swift in Sources */, 4F38CE1C2602CF4F41323032 /* PermissionOverlayTrackerTests.swift in Sources */, diff --git a/Cotabby/Support/OnboardingTemplateFeatureList.swift b/Cotabby/Support/OnboardingTemplateFeatureList.swift new file mode 100644 index 00000000..0a3db23a --- /dev/null +++ b/Cotabby/Support/OnboardingTemplateFeatureList.swift @@ -0,0 +1,51 @@ +import Foundation + +/// File overview: +/// Pure description of which behaviors each `OnboardingTemplate` turns on or off. The onboarding +/// template card uses this to render a collapsible "what's included" section so the user can see +/// exactly what they're opting into before tapping a card. +/// +/// Keeping this list as a pure helper (instead of inlining it in the SwiftUI view) means the same +/// summary is unit-testable and stays in lock-step with the source-of-truth properties on +/// `OnboardingTemplate`. If a template gains a new behavior flag, the row order here is the only +/// place to extend — the UI walks whatever rows this returns. + +enum OnboardingTemplateFeatureValue: Equatable, Sendable { + /// Toggled on by the template. UI renders a positive affordance (e.g., a check). + case enabled + /// Explicitly off under the template. UI renders a neutral/negative affordance (e.g., a dash). + case disabled + /// A non-boolean setting that takes a value (e.g., suggestion length preset). UI shows the + /// trailing text alongside the row title. + case detail(String) +} + +struct OnboardingTemplateFeatureRow: Equatable, Sendable, Identifiable { + let title: String + let value: OnboardingTemplateFeatureValue + + /// `title` is unique within a template's row list, so it doubles as the SwiftUI identity key. + var id: String { title } +} + +enum OnboardingTemplateFeatureList { + /// Returns the ordered rows shown in the template card's disclosure section. + /// Order matters: the most user-visible knob (suggestion length) comes first, then behavior + /// flags. Adding a new row here automatically surfaces it in the UI. + static func rows(for template: OnboardingTemplate) -> [OnboardingTemplateFeatureRow] { + [ + OnboardingTemplateFeatureRow( + title: "Suggestion length", + value: .detail(template.wordCountPreset.displayLabel) + ), + OnboardingTemplateFeatureRow( + title: "Fast mode (skip screen context)", + value: template.enablesFastMode ? .enabled : .disabled + ), + OnboardingTemplateFeatureRow( + title: "Multi-line completions", + value: template.enablesMultiLine ? .enabled : .disabled + ) + ] + } +} diff --git a/Cotabby/UI/WelcomeTemplateStepView.swift b/Cotabby/UI/WelcomeTemplateStepView.swift index c92b7f73..03ab7315 100644 --- a/Cotabby/UI/WelcomeTemplateStepView.swift +++ b/Cotabby/UI/WelcomeTemplateStepView.swift @@ -106,7 +106,35 @@ private struct TemplateCard: View { let downloadState: ModelDownloadState? let onTap: () -> Void + /// Each card tracks its own disclosure state so opening one doesn't expand the others. + /// Collapsed by default — users who trust the recommendation should never have to see the + /// row list, and short cards preserve the "pick one" feel of this step. + @State private var isFeatureListExpanded = false + var body: some View { + VStack(spacing: 0) { + selectionButton + featureDisclosure + } + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(.regularMaterial) + .shadow(color: .black.opacity(0.06), radius: 2, y: 1) + ) + .overlay( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .stroke( + isSelected && !availability.isDisabled + ? Color.accentColor.opacity(0.45) : Color.white.opacity(0.08), + lineWidth: isSelected && !availability.isDisabled ? 1.5 : 0.5 + ) + ) + .opacity(availability.isDisabled ? 0.55 : 1.0) + } + + /// Main card surface that selects the template. The feature disclosure is rendered as a sibling + /// view below this button so its toggle taps never double-fire selection. + private var selectionButton: some View { Button(action: onTap) { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 14) { @@ -166,25 +194,90 @@ private struct TemplateCard: View { } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill(.regularMaterial) - .shadow(color: .black.opacity(0.06), radius: 2, y: 1) - ) - .overlay( - RoundedRectangle(cornerRadius: 16, style: .continuous) - .stroke( - isSelected && !availability.isDisabled - ? Color.accentColor.opacity(0.45) : Color.white.opacity(0.08), - lineWidth: isSelected && !availability.isDisabled ? 1.5 : 0.5 - ) - ) - .opacity(availability.isDisabled ? 0.55 : 1.0) + .contentShape(Rectangle()) } .buttonStyle(.plain) .disabled(availability.isDisabled) } + /// Collapsible "what's included" section. Rendering it outside the selection button keeps the + /// expand/collapse tap target separate so opening the list doesn't change the selected card. + private var featureDisclosure: some View { + let rows = OnboardingTemplateFeatureList.rows(for: template) + return VStack(alignment: .leading, spacing: 0) { + Divider().opacity(0.4) + + Button { + withAnimation(.easeInOut(duration: 0.18)) { + isFeatureListExpanded.toggle() + } + } label: { + HStack(spacing: 6) { + Text(isFeatureListExpanded ? "Hide details" : "What's included") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(.secondary) + Image(systemName: "chevron.down") + .font(.system(size: 9, weight: .semibold)) + .foregroundStyle(.secondary) + .rotationEffect(.degrees(isFeatureListExpanded ? 0 : -90)) + Spacer(minLength: 0) + } + .padding(.horizontal, 16) + .padding(.vertical, 9) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + + if isFeatureListExpanded { + VStack(alignment: .leading, spacing: 6) { + ForEach(rows) { row in + featureRowView(row) + } + } + .padding(.horizontal, 16) + .padding(.bottom, 12) + } + } + } + + @ViewBuilder + private func featureRowView(_ row: OnboardingTemplateFeatureRow) -> some View { + HStack(spacing: 8) { + featureRowIcon(for: row.value) + .frame(width: 14, alignment: .center) + + Text(row.title) + .font(.system(size: 11)) + .foregroundStyle(.secondary) + + Spacer(minLength: 4) + + if case .detail(let value) = row.value { + Text(value) + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(.secondary) + } + } + } + + @ViewBuilder + private func featureRowIcon(for value: OnboardingTemplateFeatureValue) -> some View { + switch value { + case .enabled: + Image(systemName: "checkmark") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(Color.accentColor) + case .disabled: + Image(systemName: "minus") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.tertiary) + case .detail: + Image(systemName: "slider.horizontal.3") + .font(.system(size: 10, weight: .medium)) + .foregroundStyle(.tertiary) + } + } + private var iconBadge: some View { ZStack { RoundedRectangle(cornerRadius: 10, style: .continuous) diff --git a/CotabbyTests/OnboardingTemplateFeatureListTests.swift b/CotabbyTests/OnboardingTemplateFeatureListTests.swift new file mode 100644 index 00000000..73294e50 --- /dev/null +++ b/CotabbyTests/OnboardingTemplateFeatureListTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import Cotabby + +final class OnboardingTemplateFeatureListTests: XCTestCase { + func testQuickEnablesFastModeOnlyWithShortLength() { + let rows = OnboardingTemplateFeatureList.rows(for: .quick) + XCTAssertEqual(rows.map(\.title), [ + "Suggestion length", + "Fast mode (skip screen context)", + "Multi-line completions" + ]) + XCTAssertEqual(rows[0].value, .detail("3-7 words")) + XCTAssertEqual(rows[1].value, .enabled) + XCTAssertEqual(rows[2].value, .disabled) + } + + func testEverydayLeavesBothFlagsOff() { + let rows = OnboardingTemplateFeatureList.rows(for: .everyday) + XCTAssertEqual(rows[0].value, .detail("7-12 words")) + XCTAssertEqual(rows[1].value, .disabled) + XCTAssertEqual(rows[2].value, .disabled) + } + + func testPowerfulEnablesMultiLineOnlyWithLongLength() { + let rows = OnboardingTemplateFeatureList.rows(for: .powerful) + XCTAssertEqual(rows[0].value, .detail("12-20 words")) + XCTAssertEqual(rows[1].value, .disabled) + XCTAssertEqual(rows[2].value, .enabled) + } +} From d32ee838565d396918c3d3022d0d95d9aeffb7c3 Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Sat, 30 May 2026 19:21:32 -0700 Subject: [PATCH 2/3] Tighten onboarding tier presets: nano model, no multi-line, clipboard knob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tier-shaped changes, plus surface tweaks so the cards reflect them: - Quick switches to tabby-1-nano (SmolLM2-135M, ~0.1 GB) — the previous ~0.4 GB Qwen3 was the wrong fit for a 'fast and lightweight' tier when a smaller model exists in the catalog. - Multi-line completions are now off in every tier. A fresh user should not opt into block completions through tier choice; the General toggle remains the way in. The Multi-line row drops out of the per-card disclosure since it would be identical (off) on all three. - Clipboard context becomes a per-tier knob: off on Quick (skip the per- keystroke pasteboard read and the extra prompt bytes), on for Everyday and Powerful. It takes the slot left by the Multi-line row in the disclosure. - The engine-row footer now names the actual model ('tabby-1-nano · ~0.1 GB download') instead of the generic 'Local model'. - Powerful's detail copy drops the 'multi-line' phrasing now that the flag is off. --- Cotabby/Models/OnboardingTemplate.swift | 16 +++++++++++----- .../Support/OnboardingTemplateFeatureList.swift | 4 ++-- .../Support/OnboardingTemplateRecommender.swift | 3 ++- Cotabby/UI/WelcomeTemplateStepView.swift | 4 ++-- Cotabby/UI/WelcomeView.swift | 1 + .../OnboardingTemplateFeatureListTests.swift | 10 +++++----- .../OnboardingTemplateRecommenderTests.swift | 11 +++++++++-- 7 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Cotabby/Models/OnboardingTemplate.swift b/Cotabby/Models/OnboardingTemplate.swift index 52efd4e1..aad0b6c5 100644 --- a/Cotabby/Models/OnboardingTemplate.swift +++ b/Cotabby/Models/OnboardingTemplate.swift @@ -52,7 +52,7 @@ enum OnboardingTemplate: String, CaseIterable, Identifiable, Equatable, Sendable case .everyday: return "A balance of speed and quality for everyday writing." case .powerful: - return "Longer, multi-line suggestions for the most capable completions." + return "Longer suggestions and higher quality on harder prompts." } } @@ -83,16 +83,21 @@ enum OnboardingTemplate: String, CaseIterable, Identifiable, Equatable, Sendable self == .quick } - /// Powerful opts into multi-line continuations since its model can sustain them. - var enablesMultiLine: Bool { - self == .powerful + /// Multi-line is off in every tier — the onboarding presets stay single-line so a fresh user + /// doesn't get long block completions before they've chosen that tradeoff in General. + var enablesMultiLine: Bool { false } + + /// Quick stays lean by skipping the per-keystroke clipboard read and the extra prompt bytes + /// it adds; Everyday and Powerful pay that small cost for the extra signal. + var enablesClipboardContext: Bool { + self != .quick } /// The local GGUF this template installs when the Open Source engine is selected. var openSourceModelFilename: String { switch self { case .quick: - return "Qwen3-0.6B-Q4_K_M.gguf" + return "SmolLM2-135M-Instruct-q8_0.gguf" case .everyday: return "gemma-4-E2B-it-Q4_K_M.gguf" case .powerful: @@ -110,6 +115,7 @@ struct ResolvedTemplatePlan: Equatable, Sendable { let wordCountPreset: SuggestionWordCountPreset let enablesFastMode: Bool let enablesMultiLine: Bool + let enablesClipboardContext: Bool } /// Whether a template can be offered on the current Mac, plus optional advisory copy. diff --git a/Cotabby/Support/OnboardingTemplateFeatureList.swift b/Cotabby/Support/OnboardingTemplateFeatureList.swift index 0a3db23a..3b97e8b3 100644 --- a/Cotabby/Support/OnboardingTemplateFeatureList.swift +++ b/Cotabby/Support/OnboardingTemplateFeatureList.swift @@ -43,8 +43,8 @@ enum OnboardingTemplateFeatureList { value: template.enablesFastMode ? .enabled : .disabled ), OnboardingTemplateFeatureRow( - title: "Multi-line completions", - value: template.enablesMultiLine ? .enabled : .disabled + title: "Clipboard context", + value: template.enablesClipboardContext ? .enabled : .disabled ) ] } diff --git a/Cotabby/Support/OnboardingTemplateRecommender.swift b/Cotabby/Support/OnboardingTemplateRecommender.swift index 6ad01171..ab112462 100644 --- a/Cotabby/Support/OnboardingTemplateRecommender.swift +++ b/Cotabby/Support/OnboardingTemplateRecommender.swift @@ -36,7 +36,8 @@ enum OnboardingTemplateRecommender { modelToDownload: model, wordCountPreset: template.wordCountPreset, enablesFastMode: template.enablesFastMode, - enablesMultiLine: template.enablesMultiLine + enablesMultiLine: template.enablesMultiLine, + enablesClipboardContext: template.enablesClipboardContext ) } diff --git a/Cotabby/UI/WelcomeTemplateStepView.swift b/Cotabby/UI/WelcomeTemplateStepView.swift index 03ab7315..b8919e1e 100644 --- a/Cotabby/UI/WelcomeTemplateStepView.swift +++ b/Cotabby/UI/WelcomeTemplateStepView.swift @@ -301,8 +301,8 @@ private struct TemplateCard: View { case .appleIntelligence: return "Apple Intelligence · built into macOS" case .llamaOpenSource: - let size = plan.modelToDownload?.approximateSizeLabel ?? "" - return "Local model · \(size) download" + guard let model = plan.modelToDownload else { return "Local model" } + return "\(model.displayName) · \(model.approximateSizeLabel) download" } } diff --git a/Cotabby/UI/WelcomeView.swift b/Cotabby/UI/WelcomeView.swift index fe8f8608..23d63ff1 100644 --- a/Cotabby/UI/WelcomeView.swift +++ b/Cotabby/UI/WelcomeView.swift @@ -660,6 +660,7 @@ extension WelcomeView { suggestionSettings.selectWordCountPreset(plan.wordCountPreset) suggestionSettings.setFastModeEnabled(plan.enablesFastMode) suggestionSettings.setMultiLineEnabled(plan.enablesMultiLine) + suggestionSettings.setClipboardContextEnabled(plan.enablesClipboardContext) guard let model = plan.modelToDownload else { return diff --git a/CotabbyTests/OnboardingTemplateFeatureListTests.swift b/CotabbyTests/OnboardingTemplateFeatureListTests.swift index 73294e50..2143ef2b 100644 --- a/CotabbyTests/OnboardingTemplateFeatureListTests.swift +++ b/CotabbyTests/OnboardingTemplateFeatureListTests.swift @@ -2,26 +2,26 @@ import XCTest @testable import Cotabby final class OnboardingTemplateFeatureListTests: XCTestCase { - func testQuickEnablesFastModeOnlyWithShortLength() { + func testQuickShowsShortLengthAndFastModeOnAndClipboardOff() { let rows = OnboardingTemplateFeatureList.rows(for: .quick) XCTAssertEqual(rows.map(\.title), [ "Suggestion length", "Fast mode (skip screen context)", - "Multi-line completions" + "Clipboard context" ]) XCTAssertEqual(rows[0].value, .detail("3-7 words")) XCTAssertEqual(rows[1].value, .enabled) XCTAssertEqual(rows[2].value, .disabled) } - func testEverydayLeavesBothFlagsOff() { + func testEverydayShowsMediumLengthFastModeOffAndClipboardOn() { let rows = OnboardingTemplateFeatureList.rows(for: .everyday) XCTAssertEqual(rows[0].value, .detail("7-12 words")) XCTAssertEqual(rows[1].value, .disabled) - XCTAssertEqual(rows[2].value, .disabled) + XCTAssertEqual(rows[2].value, .enabled) } - func testPowerfulEnablesMultiLineOnlyWithLongLength() { + func testPowerfulShowsLongLengthFastModeOffAndClipboardOn() { let rows = OnboardingTemplateFeatureList.rows(for: .powerful) XCTAssertEqual(rows[0].value, .detail("12-20 words")) XCTAssertEqual(rows[1].value, .disabled) diff --git a/CotabbyTests/OnboardingTemplateRecommenderTests.swift b/CotabbyTests/OnboardingTemplateRecommenderTests.swift index 82a85192..b493e241 100644 --- a/CotabbyTests/OnboardingTemplateRecommenderTests.swift +++ b/CotabbyTests/OnboardingTemplateRecommenderTests.swift @@ -29,17 +29,24 @@ final class OnboardingTemplateRecommenderTests: XCTestCase { XCTAssertEqual(quick.wordCountPreset, .threeToSeven) XCTAssertTrue(quick.enablesFastMode) XCTAssertFalse(quick.enablesMultiLine) + XCTAssertFalse(quick.enablesClipboardContext) + + let everyday = OnboardingTemplateRecommender.resolvePlan(for: .everyday, engine: .appleIntelligence) + XCTAssertFalse(everyday.enablesFastMode) + XCTAssertFalse(everyday.enablesMultiLine) + XCTAssertTrue(everyday.enablesClipboardContext) let powerful = OnboardingTemplateRecommender.resolvePlan(for: .powerful, engine: .appleIntelligence) XCTAssertEqual(powerful.wordCountPreset, .twelveToTwenty) - XCTAssertTrue(powerful.enablesMultiLine) + XCTAssertFalse(powerful.enablesMultiLine) + XCTAssertTrue(powerful.enablesClipboardContext) } // MARK: - resolvePlan: Open Source engine (each tier maps to its GGUF) func testOpenSourceTiersMapToTheirLocalModels() { let expected: [OnboardingTemplate: String] = [ - .quick: "Qwen3-0.6B-Q4_K_M.gguf", + .quick: "SmolLM2-135M-Instruct-q8_0.gguf", .everyday: "gemma-4-E2B-it-Q4_K_M.gguf", .powerful: "gemma-4-E4B-it-Q4_K_M.gguf" ] From 6e71e783c3cb5513f68f56fe5e094ce99ae6a1d3 Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Sat, 30 May 2026 19:34:37 -0700 Subject: [PATCH 3/3] Address Greptile review: animate disclosure rows; derive preset labels in tests - Add a move(edge: .top) + opacity transition so the "What's included" rows slide open with the chevron instead of popping in. - Assert the suggestion-length detail against OnboardingTemplate..wordCountPreset.displayLabel rather than hardcoded strings, so the test tracks the production label instead of snapshotting it. --- Cotabby/UI/WelcomeTemplateStepView.swift | 1 + CotabbyTests/OnboardingTemplateFeatureListTests.swift | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cotabby/UI/WelcomeTemplateStepView.swift b/Cotabby/UI/WelcomeTemplateStepView.swift index b8919e1e..d431ca33 100644 --- a/Cotabby/UI/WelcomeTemplateStepView.swift +++ b/Cotabby/UI/WelcomeTemplateStepView.swift @@ -236,6 +236,7 @@ private struct TemplateCard: View { } .padding(.horizontal, 16) .padding(.bottom, 12) + .transition(.opacity.combined(with: .move(edge: .top))) } } } diff --git a/CotabbyTests/OnboardingTemplateFeatureListTests.swift b/CotabbyTests/OnboardingTemplateFeatureListTests.swift index 2143ef2b..68d82f41 100644 --- a/CotabbyTests/OnboardingTemplateFeatureListTests.swift +++ b/CotabbyTests/OnboardingTemplateFeatureListTests.swift @@ -9,21 +9,21 @@ final class OnboardingTemplateFeatureListTests: XCTestCase { "Fast mode (skip screen context)", "Clipboard context" ]) - XCTAssertEqual(rows[0].value, .detail("3-7 words")) + XCTAssertEqual(rows[0].value, .detail(OnboardingTemplate.quick.wordCountPreset.displayLabel)) XCTAssertEqual(rows[1].value, .enabled) XCTAssertEqual(rows[2].value, .disabled) } func testEverydayShowsMediumLengthFastModeOffAndClipboardOn() { let rows = OnboardingTemplateFeatureList.rows(for: .everyday) - XCTAssertEqual(rows[0].value, .detail("7-12 words")) + XCTAssertEqual(rows[0].value, .detail(OnboardingTemplate.everyday.wordCountPreset.displayLabel)) XCTAssertEqual(rows[1].value, .disabled) XCTAssertEqual(rows[2].value, .enabled) } func testPowerfulShowsLongLengthFastModeOffAndClipboardOn() { let rows = OnboardingTemplateFeatureList.rows(for: .powerful) - XCTAssertEqual(rows[0].value, .detail("12-20 words")) + XCTAssertEqual(rows[0].value, .detail(OnboardingTemplate.powerful.wordCountPreset.displayLabel)) XCTAssertEqual(rows[1].value, .disabled) XCTAssertEqual(rows[2].value, .enabled) }