diff --git a/Cotabby.xcodeproj/project.pbxproj b/Cotabby.xcodeproj/project.pbxproj index 385002c..061b488 100644 --- a/Cotabby.xcodeproj/project.pbxproj +++ b/Cotabby.xcodeproj/project.pbxproj @@ -211,6 +211,7 @@ E17CAA453B1F534D284F0D89 /* PermissionHostApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6ACCB12E4DB32D2F2BEA567 /* PermissionHostApp.swift */; }; E27E6377D36D4981301568DD /* LaunchAtLoginStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E8508D9355D0271A00C5 /* LaunchAtLoginStateTests.swift */; }; E313639E71AE1374D2B9A956 /* SuggestionWorkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2D97BAA3618A7D0357AC44 /* SuggestionWorkController.swift */; }; + E4382BEA8A8551612E5966B9 /* BaseCompletionPromptRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EF79E6144D6C6AD062B569 /* BaseCompletionPromptRenderer.swift */; }; E51FA12B690428CA431328FC /* WritingPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48B95B6665109B6C6A63B42 /* WritingPaneView.swift */; }; E6EE3C13FA31F261CD734C69 /* DownloadOutcomeClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE1975F3B5F4A70478DBF41 /* DownloadOutcomeClassifier.swift */; }; E912D4617AE1376061DF1F00 /* LanguageSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4793D4EA5D36D7E5CC216C27 /* LanguageSupportTests.swift */; }; @@ -226,6 +227,7 @@ F08C139B246C1EC7BB435455 /* MenuBarPresentationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00824BDD8D0E9B3063827C78 /* MenuBarPresentationObserver.swift */; }; F0DEEE8A866ABB560E7A7E6A /* LaunchAtLoginService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220CD4AFA1E96A37BC4514AD /* LaunchAtLoginService.swift */; }; F496D63FD0A163D222D8C76F /* EmojiPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A3B81B92E0743C6152ED8DD /* EmojiPickerController.swift */; }; + F4A01E4F12F0183449BCCBB9 /* BaseCompletionPromptRendererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9966D09D8D80F54BF2734E00 /* BaseCompletionPromptRendererTests.swift */; }; F4EEE6291095B0BF2D3FBA21 /* GhostTextColorPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E44E393DD58B978B1EAB6CF /* GhostTextColorPreset.swift */; }; F78F594F77C26C233377E71F /* KeyCodeLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAAE6B395FAB604DF059280A /* KeyCodeLabels.swift */; }; F8E86FA4D6CEEBFA7FB55F8D /* KeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A567677424A82D9EEF47495 /* KeyRecorderView.swift */; }; @@ -358,6 +360,7 @@ 82F7F7355967725162DF2D1B /* CustomRulesEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRulesEditor.swift; sourceTree = ""; }; 83A810F9D28A18BA6F2066C7 /* MenuBarSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarSections.swift; sourceTree = ""; }; 85BF316556FDA64CB8AD07B6 /* PermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionManager.swift; sourceTree = ""; }; + 85EF79E6144D6C6AD062B569 /* BaseCompletionPromptRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCompletionPromptRenderer.swift; sourceTree = ""; }; 86460C747AA883FDE756BDBA /* SuggestionSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionSettingsModel.swift; sourceTree = ""; }; 8724ECA8FABBC82B0A2B943B /* FoundationModelAvailabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationModelAvailabilityService.swift; sourceTree = ""; }; 8896D976C7F116EBA0F3969F /* ChromiumAccessibilityEnabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromiumAccessibilityEnabler.swift; sourceTree = ""; }; @@ -372,6 +375,7 @@ 9458F0820B3161FE9CF1DDAF /* GhostFontSizeStabilizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostFontSizeStabilizer.swift; sourceTree = ""; }; 96495E4147D828C0B1B22765 /* ClipboardContentDistiller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardContentDistiller.swift; sourceTree = ""; }; 979A7867966180A545BB44C4 /* PerformanceMetricsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMetricsStore.swift; sourceTree = ""; }; + 9966D09D8D80F54BF2734E00 /* BaseCompletionPromptRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCompletionPromptRendererTests.swift; sourceTree = ""; }; 9A7CDA90E128350BFF1A9D66 /* LanguageTagsEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageTagsEditor.swift; sourceTree = ""; }; 9AA0117B322C625F6D4BBEAB /* MenuBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = ""; }; 9B55A4362AB7F0528C661C4C /* SuggestionTextNormalizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionTextNormalizerTests.swift; sourceTree = ""; }; @@ -691,6 +695,7 @@ children = ( A168A7B6A7AD11559B60C56B /* ApplicationBundleMetadataTests.swift */, C046CB4F3CB4BFE9391DB5DE /* AXTextGeometryResolverTests.swift */, + 9966D09D8D80F54BF2734E00 /* BaseCompletionPromptRendererTests.swift */, 04D853218B0A77B0CE090828 /* BrowserAppDetectorTests.swift */, 18D990E515E1AE4F312F4E95 /* BundledRuntimeLocatorTests.swift */, EFD89799BB82AF7A92559AEB /* ClipboardContentDistillerTests.swift */, @@ -842,6 +847,7 @@ children = ( 352AF5B2834FEE1F597394E4 /* ApplicationBundleMetadata.swift */, AC70775535A3428991025AB8 /* AXHelper.swift */, + 85EF79E6144D6C6AD062B569 /* BaseCompletionPromptRenderer.swift */, B997EC69E1C65B1E18234221 /* BrowserAppDetector.swift */, AA33F5FFAC5B99384E15CE3E /* BundledRuntimeLocator.swift */, E3C84377F352140759B448C9 /* CaretGeometrySelector.swift */, @@ -1025,6 +1031,7 @@ C4C6734678797669055988E0 /* AppUpdateManager.swift in Sources */, 66C23A7C2FCDE0266FF425F8 /* ApplicationBundleMetadata.swift in Sources */, EF0DE5E045F328F1E912A02A /* AppsPaneView.swift in Sources */, + E4382BEA8A8551612E5966B9 /* BaseCompletionPromptRenderer.swift in Sources */, 49C91DE326A590708D76102A /* BrowserAppDetector.swift in Sources */, 3CBBC3BFAC0DC8952EE24EF7 /* BundledRuntimeLocator.swift in Sources */, 76FD91607794883F8E121450 /* CaretGeometrySelector.swift in Sources */, @@ -1183,6 +1190,7 @@ files = ( 6D0E79CF3C1A8CE53046FCE5 /* AXTextGeometryResolverTests.swift in Sources */, A36481222BB5B2A67349D389 /* ApplicationBundleMetadataTests.swift in Sources */, + F4A01E4F12F0183449BCCBB9 /* BaseCompletionPromptRendererTests.swift in Sources */, 5C0D3B6012C7412001BE3773 /* BrowserAppDetectorTests.swift in Sources */, 58AC3193D846FDE88513377D /* BundledRuntimeLocatorTests.swift in Sources */, 8865B95FE84198D70390DF80 /* ClipboardContentDistillerTests.swift in Sources */, diff --git a/Cotabby/Models/SuggestionEngineModels.swift b/Cotabby/Models/SuggestionEngineModels.swift index 1938346..0269e2e 100644 --- a/Cotabby/Models/SuggestionEngineModels.swift +++ b/Cotabby/Models/SuggestionEngineModels.swift @@ -86,6 +86,10 @@ struct SuggestionSettingsSnapshot: Equatable, Sendable { /// When true, the screenshot/OCR visual-context pipeline is skipped entirely for lower-latency /// suggestions. Defaults to false. Only affects visual context — predictions still run. let isFastModeEnabled: Bool + /// Experimental: when true and the Open Source engine is selected, the local path uses the + /// base-model continuation prompt (no instruction preamble, prefix last) instead of the + /// instruction-rendered prompt. Default false, so existing installs are byte-for-byte unchanged. + let useBaseCompletionPipeline: Bool /// User preference for how suggestions are presented (inline ghost text vs popup card vs auto /// based on caret geometry quality). Travels in the snapshot so consumers can react to changes /// without subscribing to the settings model directly. diff --git a/Cotabby/Models/SuggestionSettingsModel.swift b/Cotabby/Models/SuggestionSettingsModel.swift index c773e94..0aacf19 100644 --- a/Cotabby/Models/SuggestionSettingsModel.swift +++ b/Cotabby/Models/SuggestionSettingsModel.swift @@ -39,6 +39,9 @@ final class SuggestionSettingsModel: ObservableObject { @Published private(set) var selectedWordCountPreset: SuggestionWordCountPreset @Published private(set) var isClipboardContextEnabled: Bool @Published private(set) var isFastModeEnabled: Bool + /// Experimental, opt-in via the `cotabbyBaseCompletionPipelineEnabled` default. Routes the local + /// llama path through the base-model continuation prompt. No UI yet; read at launch. + @Published private(set) var useBaseCompletionPipeline: Bool /// Whether the Performance pane is recording per-request latency. Defaults to false so the /// default user never pays any extra storage or write cost — recording only kicks in once the /// user opts in from Settings. @@ -93,6 +96,7 @@ final class SuggestionSettingsModel: ObservableObject { private static let legacyShortPresetRawValue = "3-7" private static let clipboardContextEnabledDefaultsKey = "cotabbyClipboardContextEnabled" private static let fastModeEnabledDefaultsKey = "cotabbyFastModeEnabled" + private static let baseCompletionPipelineEnabledDefaultsKey = "cotabbyBaseCompletionPipelineEnabled" private static let performanceTrackingEnabledDefaultsKey = "cotabbyPerformanceTrackingEnabled" private static let menuBarWordCountVisibleDefaultsKey = "cotabbyMenuBarWordCountVisible" private static let mirrorPreferenceDefaultsKey = "cotabbyMirrorPreference" @@ -190,6 +194,10 @@ final class SuggestionSettingsModel: ObservableObject { // into fast mode turns it off. let resolvedFastModeEnabled = userDefaults.object(forKey: Self.fastModeEnabledDefaultsKey) as? Bool ?? false + // Experimental base-model pipeline. Defaults to false so the merged-but-dark path changes + // nothing for existing users until the flag is explicitly set. + let resolvedBaseCompletionPipelineEnabled = + userDefaults.object(forKey: Self.baseCompletionPipelineEnabledDefaultsKey) as? Bool ?? false // Defaults to false so the metrics ring buffer stays empty until the user explicitly opts // in from the Performance pane. let resolvedPerformanceTrackingEnabled = @@ -318,6 +326,7 @@ final class SuggestionSettingsModel: ObservableObject { selectedWordCountPreset = resolvedWordCountPreset isClipboardContextEnabled = resolvedClipboardContextEnabled isFastModeEnabled = resolvedFastModeEnabled + useBaseCompletionPipeline = resolvedBaseCompletionPipelineEnabled isPerformanceTrackingEnabled = resolvedPerformanceTrackingEnabled isMenuBarWordCountVisible = resolvedMenuBarWordCountVisible mirrorPreference = resolvedMirrorPreference @@ -353,6 +362,7 @@ final class SuggestionSettingsModel: ObservableObject { persistSelectedWordCountPreset(resolvedWordCountPreset) persistClipboardContextEnabled(resolvedClipboardContextEnabled) persistFastModeEnabled(resolvedFastModeEnabled) + userDefaults.set(resolvedBaseCompletionPipelineEnabled, forKey: Self.baseCompletionPipelineEnabledDefaultsKey) persistPerformanceTrackingEnabled(resolvedPerformanceTrackingEnabled) persistMenuBarWordCountVisible(resolvedMenuBarWordCountVisible) persistMirrorPreference(resolvedMirrorPreference) @@ -410,6 +420,7 @@ final class SuggestionSettingsModel: ObservableObject { isMultiLineEnabled: isMultiLineEnabled, autoAcceptTrailingPunctuation: autoAcceptTrailingPunctuation, isFastModeEnabled: isFastModeEnabled, + useBaseCompletionPipeline: useBaseCompletionPipeline, mirrorPreference: mirrorPreference, acceptanceGranularity: acceptanceGranularity ) @@ -1109,8 +1120,8 @@ extension SuggestionSettingsModel: SuggestionSettingsProviding { // The outer CombineLatest stack is already at Combine's per-operator cap, so each new // top-level setting gets layered above via another `CombineLatest`. `extendedContext` joins // alongside `acceptanceGranularity` here for the same reason. - return Publishers.CombineLatest3(primary, $acceptanceGranularity, $extendedContext) - .map { primaryTuple, granularity, extendedContext in + return Publishers.CombineLatest4(primary, $acceptanceGranularity, $extendedContext, $useBaseCompletionPipeline) + .map { primaryTuple, granularity, extendedContext, baseCompletionEnabled in let (combinedSettings, presentationToggles, profile, timing) = primaryTuple let (globallyEnabled, disabledAppRules, engine, wordCountPreset) = combinedSettings let (clipboardContextEnabled, fastModeEnabled, mirrorPreference) = presentationToggles @@ -1131,6 +1142,7 @@ extension SuggestionSettingsModel: SuggestionSettingsProviding { isMultiLineEnabled: multiLine, autoAcceptTrailingPunctuation: autoAcceptPunctuation, isFastModeEnabled: fastModeEnabled, + useBaseCompletionPipeline: baseCompletionEnabled, mirrorPreference: mirrorPreference, acceptanceGranularity: granularity ) diff --git a/Cotabby/Support/BaseCompletionPromptRenderer.swift b/Cotabby/Support/BaseCompletionPromptRenderer.swift new file mode 100644 index 0000000..d700127 --- /dev/null +++ b/Cotabby/Support/BaseCompletionPromptRenderer.swift @@ -0,0 +1,112 @@ +import Foundation + +/// File overview: +/// Renders the prompt for the experimental base-model completion pipeline (Open Source engine with +/// `useBaseCompletionPipeline` enabled). +/// +/// Why this exists separately from `LlamaPromptRenderer`: +/// `LlamaPromptRenderer` wraps the user's text in an instruction blob ("Task: ... do not answer the +/// user ...") for instruction-tuned models. A *base* model has no instruction-following channel and +/// will happily continue a bare "Task:" line as if it were the document, so that prompt shape leaks +/// scaffolding into the ghost text. This renderer instead treats the model as a pure text continuer: +/// +/// - No task preamble and no standalone `Label:` lines. +/// - Custom instructions work by *conditioning*, not obedience: persona, voice, and language are +/// folded into a short framing sentence that makes the desired continuation the most likely one. +/// - Supporting context (notes/screen/clipboard) is included as compact prose ahead of the prefix. +/// - The single invariant that locates the caret is that `prefixText` is the LAST thing in the +/// prompt, with trailing whitespace trimmed so generation begins at a clean word boundary. +enum BaseCompletionPromptRenderer { + static func prompt( + prefixText: String, + applicationName: String, + userName: String?, + customRules: [String] = [], + extendedContext: String? = nil, + languageInstruction: String? = nil, + clipboardContext: String? = nil, + visualContextSummary: String? = nil + ) -> String { + var preface: [String] = [] + + // Persona/voice/language framing, phrased as a description of the document rather than a + // command, because a base model conditions on description but ignores instructions. Emitted + // only when the user supplied something, so a bare field stays pure continuation (the + // strongest base-model setup). `applicationName` is intentionally not stated as a label here; + // app/window metadata biases a base model toward code/numbers over prose. + if let framing = authorFraming( + userName: userName, + customRules: customRules, + languageInstruction: languageInstruction + ) { + preface.append(framing) + } + + // Free-form reference notes (glossary/terminology) ahead of the prefix so the user's terms + // become likelier continuations through in-context conditioning. + if let extendedContext, !extendedContext.isEmpty { + preface.append("Notes the writer keeps in mind: \(extendedContext)") + } + if let visualContextSummary, !visualContextSummary.isEmpty { + preface.append("Nearby on screen: \(visualContextSummary)") + } + if let clipboardContext, !clipboardContext.isEmpty { + preface.append("On the clipboard: \(clipboardContext)") + } + + // Trailing whitespace is trimmed so the model continues from a clean word boundary instead of + // being asked to emit a leading-space token (which base models do poorly). A prefix ending + // mid-word keeps its final partial word, so mid-word continuation still works. Output spacing + // is reconciled downstream by `SuggestionTextNormalizer`. + let trimmedPrefix = Self.trimmingTrailingWhitespace(prefixText) + + guard !preface.isEmpty else { + // No context to condition on: hand the model the bare text and let it continue. + return trimmedPrefix + } + + // A blank line separates the conditioning preface from the live text without a label the + // model could copy. The prefix remains the final bytes of the prompt. + return preface.joined(separator: "\n") + "\n\n" + trimmedPrefix + } + + /// Builds the conditioning sentence from persona/style/language, or nil when none were supplied. + private static func authorFraming( + userName: String?, + customRules: [String], + languageInstruction: String? + ) -> String? { + let name = (userName ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + let rules = customRules + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + let language = (languageInstruction ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + + guard !name.isEmpty || !rules.isEmpty || !language.isEmpty else { + return nil + } + + var sentence = "The following is text" + if !name.isEmpty { + sentence += " written by \(name)" + } + if !rules.isEmpty { + sentence += " in a \(rules.joined(separator: ", ")) style" + } + sentence += "." + if !language.isEmpty { + // `languageInstruction` is already a soft directive sentence; append it verbatim. + sentence += " \(language)" + } + return sentence + } + + /// Drops trailing spaces, tabs, and newlines so the base-model prompt ends at a word boundary. + static func trimmingTrailingWhitespace(_ text: String) -> String { + var view = Substring(text) + while let last = view.last, last.isWhitespace { + view = view.dropLast() + } + return String(view) + } +} diff --git a/Cotabby/Support/SuggestionRequestFactory.swift b/Cotabby/Support/SuggestionRequestFactory.swift index 996b0db..b13223e 100644 --- a/Cotabby/Support/SuggestionRequestFactory.swift +++ b/Cotabby/Support/SuggestionRequestFactory.swift @@ -62,17 +62,33 @@ enum SuggestionRequestFactory { let boundedVisualContextSummary = activeVisualContextSummary( rawSummary: visualContextSummary ) - let prompt = LlamaPromptRenderer.prompt( - prefixText: prefixText, - applicationName: context.applicationName, - completionLengthInstruction: completionLengthInstruction, - userName: userName, - customRules: customRules, - extendedContext: activeExtendedContext, - languageInstruction: languageInstruction, - clipboardContext: boundedClipboardContext, - visualContextSummary: boundedVisualContextSummary - ) + let prompt: String + if settings.useBaseCompletionPipeline, settings.selectedEngine == .llamaOpenSource { + // Base-model continuation path: no instruction blob, prefix last, trailing-trimmed. + // Custom instructions/persona condition the output rather than being obeyed. + prompt = BaseCompletionPromptRenderer.prompt( + prefixText: prefixText, + applicationName: context.applicationName, + userName: userName, + customRules: customRules, + extendedContext: activeExtendedContext, + languageInstruction: languageInstruction, + clipboardContext: boundedClipboardContext, + visualContextSummary: boundedVisualContextSummary + ) + } else { + prompt = LlamaPromptRenderer.prompt( + prefixText: prefixText, + applicationName: context.applicationName, + completionLengthInstruction: completionLengthInstruction, + userName: userName, + customRules: customRules, + extendedContext: activeExtendedContext, + languageInstruction: languageInstruction, + clipboardContext: boundedClipboardContext, + visualContextSummary: boundedVisualContextSummary + ) + } let request = SuggestionRequest( context: context, diff --git a/CotabbyTests/BaseCompletionPromptRendererTests.swift b/CotabbyTests/BaseCompletionPromptRendererTests.swift new file mode 100644 index 0000000..bf3ad05 --- /dev/null +++ b/CotabbyTests/BaseCompletionPromptRendererTests.swift @@ -0,0 +1,80 @@ +import XCTest +@testable import Cotabby + +/// Pure-function tests for the experimental base-model prompt. The contract: no instruction +/// preamble or standalone labels, the prefix is always the final bytes, trailing whitespace is +/// trimmed (mid-word prefixes preserved), and persona/style/context only appear when supplied. +final class BaseCompletionPromptRendererTests: XCTestCase { + + func test_bareField_returnsTrimmedPrefixOnly() { + let prompt = BaseCompletionPromptRenderer.prompt( + prefixText: "I am writing to ", + applicationName: "Mail", + userName: nil + ) + XCTAssertEqual(prompt, "I am writing to") + } + + func test_noInstructionPreambleOrScaffoldingLabels() { + let prompt = BaseCompletionPromptRenderer.prompt( + prefixText: "Once upon", + applicationName: "Notes", + userName: "Jacob", + customRules: ["friendly", "concise"] + ) + XCTAssertFalse(prompt.contains("Task:")) + XCTAssertFalse(prompt.contains("This is autocomplete")) + XCTAssertFalse(prompt.contains("Text before caret:")) + XCTAssertFalse(prompt.contains("Do not answer")) + } + + func test_prefixIsAlwaysLastEvenWithAllContext() { + let prompt = BaseCompletionPromptRenderer.prompt( + prefixText: "the meeting is at", + applicationName: "Slack", + userName: "Jacob", + customRules: ["terse"], + extendedContext: "Project Matcha ships in June.", + languageInstruction: "Write in English.", + clipboardContext: "zoom link", + visualContextSummary: "Calendar: Q3 planning 3pm" + ) + XCTAssertTrue(prompt.hasSuffix("the meeting is at")) + } + + func test_personaFramingConditionsOnNameStyleAndLanguage() { + let prompt = BaseCompletionPromptRenderer.prompt( + prefixText: "Hi team,", + applicationName: "Mail", + userName: "Jacob", + customRules: ["friendly", "professional"], + languageInstruction: "Write in English." + ) + XCTAssertTrue(prompt.contains("written by Jacob")) + XCTAssertTrue(prompt.contains("friendly, professional")) + XCTAssertTrue(prompt.contains("Write in English.")) + XCTAssertTrue(prompt.hasSuffix("Hi team,")) + } + + func test_trailingWhitespaceTrimmedButMidWordPreserved() { + XCTAssertEqual( + BaseCompletionPromptRenderer.prompt(prefixText: "doing my aft", applicationName: "X", userName: nil), + "doing my aft" + ) + XCTAssertEqual( + BaseCompletionPromptRenderer.prompt(prefixText: "see you \n", applicationName: "X", userName: nil), + "see you" + ) + } + + func test_contextOnlyAppearsWhenSupplied() { + let withContext = BaseCompletionPromptRenderer.prompt( + prefixText: "Status:", + applicationName: "Slack", + userName: nil, + visualContextSummary: "build is green" + ) + XCTAssertTrue(withContext.contains("Nearby on screen: build is green")) + XCTAssertTrue(withContext.hasSuffix("Status:")) + } +} diff --git a/CotabbyTests/CotabbyTestFixtures.swift b/CotabbyTests/CotabbyTestFixtures.swift index 80a6ba6..f11410e 100644 --- a/CotabbyTests/CotabbyTestFixtures.swift +++ b/CotabbyTests/CotabbyTestFixtures.swift @@ -224,6 +224,7 @@ enum CotabbyTestFixtures { isMultiLineEnabled: Bool = false, autoAcceptTrailingPunctuation: Bool = true, isFastModeEnabled: Bool = false, + useBaseCompletionPipeline: Bool = false, mirrorPreference: MirrorPreference = .auto, acceptanceGranularity: AcceptanceGranularity = .word ) -> SuggestionSettingsSnapshot { @@ -242,6 +243,7 @@ enum CotabbyTestFixtures { isMultiLineEnabled: isMultiLineEnabled, autoAcceptTrailingPunctuation: autoAcceptTrailingPunctuation, isFastModeEnabled: isFastModeEnabled, + useBaseCompletionPipeline: useBaseCompletionPipeline, mirrorPreference: mirrorPreference, acceptanceGranularity: acceptanceGranularity )