From 25e4d7cddea8b8a0986a762153499d7352d95fed Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:18:27 -0700 Subject: [PATCH 1/2] feat(settings): complete the search index and give every row a symbol and subtext The Settings search index had drifted from the panes: Ghost Text Size, Suggest in Integrated Terminals, and the runtime Status row were not findable, and several index titles/symbols no longer matched the rows they navigate to (Toggle Tabby vs Toggle Cotabby, Battery Model vs On Battery, battery.100 vs battery.100.bolt). - Index: add ghostTextSize, suggestInIntegratedTerminals, modelStatus; align titles and symbols with the actual rows. - Symbols: the integrated-terminal toggle and the three typo toggles had no leading symbol; the Apple Intelligence Availability and models folder Path rows were bare LabeledContent with no symbol or subtext. All rows now use SettingsRowLabel with a symbol and a one-line description. - Naming: Toggle Tabby -> Toggle Cotabby (stale pre-rename title); Title Case for 'Switch Based on Power Source' and 'Also Use LM Studio Models'; About's update button gains its symbol. - Tests: new SettingsIndexTests pin the hygiene rules (every item has a title/symbol/keywords, unique ids, and real queries land on recently shipped settings) so the index can't silently drift again. - project.pbxproj regenerated by xcodegen for the new test file. --- Cotabby.xcodeproj/project.pbxproj | 4 ++ Cotabby/UI/Settings/Panes/AboutPaneView.swift | 4 +- Cotabby/UI/Settings/Panes/AppsPaneView.swift | 8 ++-- .../Panes/EngineAndModelPaneView.swift | 21 +++++++-- .../UI/Settings/Panes/ShortcutsPaneView.swift | 2 +- .../UI/Settings/Panes/WritingPaneView.swift | 9 ++-- Cotabby/UI/Settings/SettingsIndex.swift | 39 ++++++++++++---- CotabbyTests/SettingsIndexTests.swift | 46 +++++++++++++++++++ 8 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 CotabbyTests/SettingsIndexTests.swift diff --git a/Cotabby.xcodeproj/project.pbxproj b/Cotabby.xcodeproj/project.pbxproj index 66ce36c6..126ca0b3 100644 --- a/Cotabby.xcodeproj/project.pbxproj +++ b/Cotabby.xcodeproj/project.pbxproj @@ -547,6 +547,7 @@ F4CFF323CDA02530ED0EBE57 /* MPL-2.0.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4E283DF8948B10268B46811F /* MPL-2.0.txt */; }; F4EEE6291095B0BF2D3FBA21 /* GhostTextColorPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E44E393DD58B978B1EAB6CF /* GhostTextColorPreset.swift */; }; F596D7438459A7A4246A39CE /* GhostTextPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A594D02B0EF3C2DD62EFE69A /* GhostTextPreview.swift */; }; + F71FD79FAC8B59C1CBD9E2E0 /* SettingsIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2FFC2055C52FB837DEEB8F /* SettingsIndexTests.swift */; }; F7237FDB0665465F1C7EDCDE /* CustomRulesEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82F7F7355967725162DF2D1B /* CustomRulesEditor.swift */; }; F77DB394E9D6C6C482131BF9 /* VisualContextModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE97A8169438D593C6C23412 /* VisualContextModels.swift */; }; F78F594F77C26C233377E71F /* KeyCodeLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAAE6B395FAB604DF059280A /* KeyCodeLabels.swift */; }; @@ -685,6 +686,7 @@ 5976600F428C1265121D4C0C /* SystemSettingsWindowLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemSettingsWindowLocator.swift; sourceTree = ""; }; 59E299BE2E9D42A33D5D2F5D /* ScreenTextExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTextExtractor.swift; sourceTree = ""; }; 5A03E565A11581FD2150B142 /* CompletionRenderMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionRenderMode.swift; sourceTree = ""; }; + 5A2FFC2055C52FB837DEEB8F /* SettingsIndexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsIndexTests.swift; sourceTree = ""; }; 5A3B81B92E0743C6152ED8DD /* EmojiPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerController.swift; sourceTree = ""; }; 5A567677424A82D9EEF47495 /* KeyRecorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyRecorderView.swift; sourceTree = ""; }; 5AD3F4F9FBE82007E4E15F58 /* GhostSuggestionLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostSuggestionLayoutTests.swift; sourceTree = ""; }; @@ -1263,6 +1265,7 @@ D5A5591BEB9EE7B6E9064412 /* SelfCaptureGateTests.swift */, 2D7360A6D4261989A66658ED /* SentenceBoundaryClassifierTests.swift */, 2BC293F6125E2B14DCF05AD9 /* SettingsAttentionEvaluatorTests.swift */, + 5A2FFC2055C52FB837DEEB8F /* SettingsIndexTests.swift */, 0850B07CCDBA67C756C6EC59 /* ShortcutConflictTests.swift */, D562A73C7C680F2AA65F9F7F /* SpellingDictionaryResourceTests.swift */, E0871985CB1F877EC422E18C /* SpellingLanguageResolverTests.swift */, @@ -2177,6 +2180,7 @@ AF26E77871200BB1FAAEBE79 /* SelfCaptureGateTests.swift in Sources */, 1D1C6FF0B8F50AC14A1000F4 /* SentenceBoundaryClassifierTests.swift in Sources */, C618C5595DA9C57C806A3E03 /* SettingsAttentionEvaluatorTests.swift in Sources */, + F71FD79FAC8B59C1CBD9E2E0 /* SettingsIndexTests.swift in Sources */, 8441299082E6B68F7F88911B /* ShortcutConflictTests.swift in Sources */, 303652F15C0FE55595669D81 /* SpellingDictionaryResourceTests.swift in Sources */, 66D0D9F605AF462F569A5CFD /* SpellingLanguageResolverTests.swift in Sources */, diff --git a/Cotabby/UI/Settings/Panes/AboutPaneView.swift b/Cotabby/UI/Settings/Panes/AboutPaneView.swift index db50692b..bb4f8357 100644 --- a/Cotabby/UI/Settings/Panes/AboutPaneView.swift +++ b/Cotabby/UI/Settings/Panes/AboutPaneView.swift @@ -46,8 +46,10 @@ struct AboutPaneView: View { Spacer(minLength: 12) - Button("Check for Updates") { + Button { appUpdateManager.checkForUpdates() + } label: { + Label("Check for Updates", systemImage: "arrow.triangle.2.circlepath") } } .padding(.vertical, 4) diff --git a/Cotabby/UI/Settings/Panes/AppsPaneView.swift b/Cotabby/UI/Settings/Panes/AppsPaneView.swift index baa4249f..f39954d1 100644 --- a/Cotabby/UI/Settings/Panes/AppsPaneView.swift +++ b/Cotabby/UI/Settings/Panes/AppsPaneView.swift @@ -42,10 +42,10 @@ struct AppsPaneView: View { Toggle(isOn: suggestInIntegratedTerminalsBinding) { SettingsRowLabel( title: "Suggest in Integrated Terminals", - description: "Show ghost text in the VS Code and Cursor integrated terminal. " - + "Off by default so suggestions don't overlap shell prompts and command " - + "output — the code editor and Copilot chat in the same window keep " - + "suggesting either way." + description: "Show ghost text in VS Code and Cursor integrated terminals. " + + "Off by default so suggestions stay out of shell prompts; the editor " + + "and chat in the same window keep suggesting either way.", + systemImage: "terminal" ) } } diff --git a/Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift b/Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift index 747563c4..169bbddb 100644 --- a/Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift +++ b/Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift @@ -94,7 +94,7 @@ struct EngineAndModelPaneView: View { ) ) { SettingsRowLabel( - title: "Switch based on power source", + title: "Switch Based on Power Source", description: "Use a different engine or model on battery vs. while plugged in. " + "For example, Apple Intelligence on battery to save power and a larger local " + "model while charging.", @@ -173,11 +173,18 @@ struct EngineAndModelPaneView: View { @ViewBuilder private var appleIntelligenceSections: some View { Section("Apple Intelligence") { - LabeledContent("Availability") { + LabeledContent { Text(foundationModelAvailabilityService.userVisibleMessage) .foregroundStyle(foundationModelAvailabilityService.isAvailable ? .green : .orange) .multilineTextAlignment(.trailing) .fixedSize(horizontal: false, vertical: true) + } label: { + SettingsRowLabel( + title: "Availability", + description: "Whether this Mac can run Apple Intelligence. Requires a supported " + + "Apple Silicon Mac with Apple Intelligence turned on in System Settings.", + systemImage: "apple.logo" + ) } } } @@ -245,7 +252,7 @@ struct EngineAndModelPaneView: View { } Section("Folder") { - LabeledContent("Path") { + LabeledContent { VStack(alignment: .trailing, spacing: 8) { Text(modelDownloadManager.modelsDirectoryPath) .font(.callout.monospaced()) @@ -262,11 +269,17 @@ struct EngineAndModelPaneView: View { } } } + } label: { + SettingsRowLabel( + title: "Models Folder", + description: "Where downloaded model files are stored on this Mac.", + systemImage: "folder" + ) } Toggle(isOn: $lmStudioSourceEnabled) { SettingsRowLabel( - title: "Also use LM Studio models", + title: "Also Use LM Studio Models", description: lmStudioModelsURL == nil ? "Install LM Studio to load models from its library here." : "Add models from your LM Studio library (~/.lmstudio/models) to the picker " + diff --git a/Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift b/Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift index c8cad252..e795907b 100644 --- a/Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift +++ b/Cotabby/UI/Settings/Panes/ShortcutsPaneView.swift @@ -125,7 +125,7 @@ struct ShortcutsPaneView: View { ) } label: { SettingsRowLabel( - title: "Toggle Tabby", + title: "Toggle Cotabby", description: "Turn Cotabby on or off globally without opening the menu bar.", systemImage: "power.circle" ) diff --git a/Cotabby/UI/Settings/Panes/WritingPaneView.swift b/Cotabby/UI/Settings/Panes/WritingPaneView.swift index 9a20a6a5..8d5874b3 100644 --- a/Cotabby/UI/Settings/Panes/WritingPaneView.swift +++ b/Cotabby/UI/Settings/Panes/WritingPaneView.swift @@ -49,14 +49,16 @@ struct WritingPaneView: View { Toggle(isOn: suppressCompletionsOnTypoBinding) { SettingsRowLabel( title: "Hide Suggestions on Typo", - description: "Stops normal completions while the current word appears misspelled." + description: "Stops normal completions while the current word appears misspelled.", + systemImage: "eye.slash" ) } Toggle(isOn: offerTypoCorrectionsBinding) { SettingsRowLabel( title: "Offer Corrections on Typo", - description: "Shows a green replacement you can apply with your accept key." + description: "Shows a green replacement you can apply with your accept key.", + systemImage: "checkmark.bubble" ) } .disabled(!suggestionSettings.suppressCompletionsOnTypo) @@ -64,7 +66,8 @@ struct WritingPaneView: View { Toggle(isOn: automaticallyFixTyposBinding) { SettingsRowLabel( title: "Automatically Fix Typos", - description: "After you press Space, replaces a misspelled word without requiring your accept key." + description: "After you press Space, replaces a misspelled word without requiring your accept key.", + systemImage: "checkmark.circle" ) } .disabled(!suggestionSettings.suppressCompletionsOnTypo) diff --git a/Cotabby/UI/Settings/SettingsIndex.swift b/Cotabby/UI/Settings/SettingsIndex.swift index 0c4f96d8..aea0d28b 100644 --- a/Cotabby/UI/Settings/SettingsIndex.swift +++ b/Cotabby/UI/Settings/SettingsIndex.swift @@ -25,6 +25,7 @@ enum SettingsItem: String, CaseIterable, Identifiable { case showKeyHint case ghostTextColor case ghostTextOpacity + case ghostTextSize // Emoji case emojiPicker case emojiSkinTone @@ -45,6 +46,7 @@ enum SettingsItem: String, CaseIterable, Identifiable { // Engine & Model case engine case appleIntelligenceAvailability + case modelStatus case selectedModel case powerBasedModelSwitching case batteryModel @@ -60,6 +62,7 @@ enum SettingsItem: String, CaseIterable, Identifiable { case toggleTabby // Apps case disabledApps + case suggestInIntegratedTerminals // Permissions case accessibility case inputMonitoring @@ -94,6 +97,7 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .showKeyHint: return "Show Accept-Key Hint" case .ghostTextColor: return "Ghost Text Color" case .ghostTextOpacity: return "Ghost Text Opacity" + case .ghostTextSize: return "Ghost Text Size" case .emojiPicker: return "Inline Emoji Picker" case .emojiSkinTone: return "Skin Tone" case .emojiPeopleStyle: return "People Emoji Style" @@ -110,10 +114,11 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .contextLivePreview: return "Live Preview" case .engine: return "Engine" case .appleIntelligenceAvailability: return "Apple Intelligence Availability" + case .modelStatus: return "Model Status" case .selectedModel: return "Selected Model" - case .powerBasedModelSwitching: return "Switch Models Based on Power Source" - case .batteryModel: return "Battery Model" - case .pluggedInModel: return "Plugged-in Model" + case .powerBasedModelSwitching: return "Switch Based on Power Source" + case .batteryModel: return "On Battery" + case .pluggedInModel: return "Plugged In" case .downloadModels: return "Download Models" case .huggingFaceBrowser: return "Hugging Face Model Browser" case .modelsFolder: return "Models Folder" @@ -121,12 +126,13 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .acceptanceMode: return "Acceptance Mode" case .acceptWord: return "Accept Word" case .acceptEntireSuggestion: return "Accept Entire Suggestion" - case .toggleTabby: return "Toggle Tabby" + case .toggleTabby: return "Toggle Cotabby" case .disabledApps: return "Disabled Apps" + case .suggestInIntegratedTerminals: return "Suggest in Integrated Terminals" case .accessibility: return "Accessibility" case .inputMonitoring: return "Input Monitoring" case .screenRecording: return "Screen Recording" - case .performanceTracking: return "Performance Tracking" + case .performanceTracking: return "Enable Performance Tracking" case .resourceUsage: return "Live Resource Usage" case .recentRequests: return "Recent Requests" case .checkForUpdates: return "Check for Updates" @@ -154,6 +160,7 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .showKeyHint: return "keyboard" case .ghostTextColor: return "paintpalette" case .ghostTextOpacity: return "circle.lefthalf.filled" + case .ghostTextSize: return "textformat.size" case .emojiPicker: return "face.smiling" case .emojiSkinTone: return "hand.raised.fingers.spread" case .emojiPeopleStyle: return "person.2" @@ -170,9 +177,10 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .contextLivePreview: return "text.cursor" case .engine: return "cpu" case .appleIntelligenceAvailability: return "apple.logo" + case .modelStatus: return "info.circle" case .selectedModel: return "shippingbox" - case .powerBasedModelSwitching: return "battery.100" - case .batteryModel: return "battery.50" + case .powerBasedModelSwitching: return "battery.100.bolt" + case .batteryModel: return "battery.25" case .pluggedInModel: return "powerplug" case .downloadModels: return "arrow.down.circle" case .huggingFaceBrowser: return "magnifyingglass" @@ -183,6 +191,7 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .acceptEntireSuggestion: return "text.insert" case .toggleTabby: return "power.circle" case .disabledApps: return "nosign" + case .suggestInIntegratedTerminals: return "terminal" case .accessibility: return "accessibility" case .inputMonitoring: return "keyboard" case .screenRecording: return "camera.viewfinder" @@ -204,7 +213,7 @@ enum SettingsItem: String, CaseIterable, Identifiable { .allowMultiLine, .acceptPunctuation, .inlineMacros, .onboarding: return .general case .suggestionDisplay, .showFieldIndicator, .showWordCount, .showKeyHint, - .ghostTextColor, .ghostTextOpacity: + .ghostTextColor, .ghostTextOpacity, .ghostTextSize: return .appearance case .emojiPicker, .emojiSkinTone, .emojiPeopleStyle, .emojiHistory: return .emoji @@ -213,13 +222,13 @@ enum SettingsItem: String, CaseIterable, Identifiable { return .writing case .extendedContext, .contextLivePreview: return .context - case .engine, .appleIntelligenceAvailability, .selectedModel, + case .engine, .appleIntelligenceAvailability, .modelStatus, .selectedModel, .powerBasedModelSwitching, .batteryModel, .pluggedInModel, .downloadModels, .huggingFaceBrowser, .modelsFolder, .lmStudio: return .engineAndModel case .acceptanceMode, .acceptWord, .acceptEntireSuggestion, .toggleTabby: return .shortcuts - case .disabledApps: + case .disabledApps, .suggestInIntegratedTerminals: return .apps case .accessibility, .inputMonitoring, .screenRecording: return .permissions @@ -277,6 +286,9 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .ghostTextOpacity: return ["opacity", "transparency", "fade", "alpha", "translucent", "dim", "brightness", "visibility"] + case .ghostTextSize: + return ["size", "font size", "scale", "bigger", "smaller", "larger", "text size", + "zoom", "multiplier", "too big", "too small"] case .emojiPicker: return ["emoji", "smile", "picker", "inline", "colon", "emoticon", "face", "symbol"] @@ -326,6 +338,9 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .appleIntelligenceAvailability: return ["apple intelligence", "availability", "available", "supported", "compatibility", "status", "macos", "device support"] + case .modelStatus: + return ["status", "loaded", "ready", "runtime", "running", "health", + "model loaded", "loading"] case .selectedModel: return ["model", "gguf", "pick", "selected", "active model", "choose model", "current model", "default model"] @@ -366,6 +381,10 @@ enum SettingsItem: String, CaseIterable, Identifiable { case .disabledApps: return ["apps", "disable", "exclude", "block", "ignore", "blacklist", "deny list", "exception", "app exclusion", "skip", "off in"] + case .suggestInIntegratedTerminals: + return ["terminal", "terminals", "integrated terminal", "vscode", "vs code", + "cursor", "shell", "xterm", "command line", "cli", "console", + "ghost text in terminal"] case .accessibility: return ["accessibility", "ax", "permission", "access", "system settings", "privacy", "grant", "allow"] diff --git a/CotabbyTests/SettingsIndexTests.swift b/CotabbyTests/SettingsIndexTests.swift new file mode 100644 index 00000000..af7fc8f7 --- /dev/null +++ b/CotabbyTests/SettingsIndexTests.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import Cotabby + +/// Pins the hygiene rules of the Settings search index. The index drifts silently when a new +/// setting ships without an entry (it simply never appears in search), so these tests make the +/// cheap invariants loud: every item must carry a non-empty title, symbol, and keyword set, and +/// the queries users actually type for recently shipped settings must land on them. +final class SettingsIndexTests: XCTestCase { + func test_everyItemHasTitleSymbolAndKeywords() { + for item in SettingsItem.allCases { + XCTAssertFalse(item.title.isEmpty, "\(item) needs a title") + XCTAssertFalse(item.systemImage.isEmpty, "\(item) needs an SF Symbol") + XCTAssertFalse(item.keywords.isEmpty, "\(item) needs search keywords") + } + } + + func test_itemIdsAreUnique() { + let ids = SettingsItem.allCases.map(\.id) + XCTAssertEqual(ids.count, Set(ids).count, "duplicate SettingsItem ids break Identifiable lists") + } + + func test_searchFindsRecentlyShippedSettings() { + // Each pair pins one real query for a setting that previously shipped without an index + // entry. If one of these fails, a rename or removal broke search for that setting. + let expectations: [(query: String, item: SettingsItem)] = [ + ("ghost text size", .ghostTextSize), + ("terminal", .suggestInIntegratedTerminals), + ("vscode", .suggestInIntegratedTerminals), + ("typo", .automaticallyFixTypos), + ("model status", .modelStatus), + ("battery", .batteryModel), + ("plugged", .pluggedInModel) + ] + for expectation in expectations { + XCTAssertTrue( + SettingsItem.results(for: expectation.query).contains(expectation.item), + "query \"\(expectation.query)\" should surface \(expectation.item)" + ) + } + } + + func test_blankQueryReturnsNothing() { + XCTAssertTrue(SettingsItem.results(for: " ").isEmpty) + XCTAssertTrue(SettingsItem.results(for: "").isEmpty) + } +} From 896451cc0bdbe6c163b94f3d2634df35e4af3a99 Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:21:11 -0700 Subject: [PATCH 2/2] fix(settings): match Model Status row label to its search index title The Engine & Model "Runtime" status row rendered as "Status" while the search index lists it as "Model Status", so a search result and the row it navigates to disagreed. Promote the row label to "Model Status" so the two match and the label stays descriptive in the flat search results list, outside its pane's "Runtime" section context. --- Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift b/Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift index 169bbddb..f94ec1b3 100644 --- a/Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift +++ b/Cotabby/UI/Settings/Panes/EngineAndModelPaneView.swift @@ -201,7 +201,7 @@ struct EngineAndModelPaneView: View { .fixedSize(horizontal: false, vertical: true) } label: { SettingsRowLabel( - title: "Status", + title: "Model Status", description: "Whether the local model is loaded and ready to generate. " + "Loading takes a few seconds the first time.", systemImage: "info.circle"