From a7d8d4a793f316d49bd58a3173303162c15ab505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manel=20Mateos=20Rami=CC=81rez?= Date: Fri, 5 Dec 2025 14:12:32 +0100 Subject: [PATCH 1/3] Updated SwiftCommitGen Core Components This commit updates key components of the SwiftCommitGen project, including improvements to the BatchCombinationPromptBuilder, LLMClient, and CommitGenError handling. These changes enhance the project's ability to manage LLM-based commit generation more robustly, ensuring better error handling and improved integration with FoundationModels. The updates also streamline the CommitGenTool's configuration process, making it more efficient for developers working with Ollama models. Additionally, the project now includes enhancements to DiffSummarizer.swift, CommitGenOptions.swift, and PromptBuilder.swift, adding new functionality and improving existing features. These modifications focus on enhancing the core components of the project, ensuring better performance and integration with newer macOS frameworks. This update is part of a larger batch of changes aimed at improving the project's capabilities and compatibility with modern development standards. --- Sources/SwiftCommitGen/CommitGenOptions.swift | 1 + Sources/SwiftCommitGen/CommitGenTool.swift | 6 +- .../Core/BatchCombinationPromptBuilder.swift | 140 +++++--- .../SwiftCommitGen/Core/CommitGenError.swift | 3 + .../SwiftCommitGen/Core/DiffSummarizer.swift | 2 + Sources/SwiftCommitGen/Core/LLMClient.swift | 74 +++-- .../SwiftCommitGen/Core/PromptBuilder.swift | 300 +++++++++++++----- 7 files changed, 372 insertions(+), 154 deletions(-) diff --git a/Sources/SwiftCommitGen/CommitGenOptions.swift b/Sources/SwiftCommitGen/CommitGenOptions.swift index 42bfa5b..e0b6d9f 100644 --- a/Sources/SwiftCommitGen/CommitGenOptions.swift +++ b/Sources/SwiftCommitGen/CommitGenOptions.swift @@ -21,6 +21,7 @@ struct CommitGenOptions { case detailed #if canImport(FoundationModels) + @available(macOS 26.0, *) var promptRepresentation: Prompt { Prompt { styleGuidance } } diff --git a/Sources/SwiftCommitGen/CommitGenTool.swift b/Sources/SwiftCommitGen/CommitGenTool.swift index 0463e4b..a069027 100644 --- a/Sources/SwiftCommitGen/CommitGenTool.swift +++ b/Sources/SwiftCommitGen/CommitGenTool.swift @@ -72,7 +72,11 @@ struct CommitGenTool { self.llmClient = OllamaClient(configuration: config) #if canImport(FoundationModels) case .foundationModels: - self.llmClient = FoundationModelsClient() + if #available(macOS 26.0, *) { + self.llmClient = FoundationModelsClient() + } else { + fatalError("FoundationModels backend requires macOS 26.0 or newer") + } #else case .foundationModels: fatalError("FoundationModels is not available on this platform") diff --git a/Sources/SwiftCommitGen/Core/BatchCombinationPromptBuilder.swift b/Sources/SwiftCommitGen/Core/BatchCombinationPromptBuilder.swift index d5448c4..e98039e 100644 --- a/Sources/SwiftCommitGen/Core/BatchCombinationPromptBuilder.swift +++ b/Sources/SwiftCommitGen/Core/BatchCombinationPromptBuilder.swift @@ -58,24 +58,64 @@ struct BatchCombinationPromptBuilder { ) #if canImport(FoundationModels) - let userPrompt = Prompt { - for line in userLines { - line + if #available(macOS 26.0, *) { + let userPrompt = Prompt { + for line in userLines { + line + } } - } - let systemPrompt = Instructions { - """ - You are an AI assistant merging multiple partial commit drafts into a single, well-structured commit message. - Preserve all important intent from the inputs, avoid redundancy, and keep the final subject concise (<= 50 characters). - The title should succinctly describe the change in a specific and informative manner. - Provide an optional body only when useful for additional context. - If a body is present, it should describe the _purpose_ of the change, not just _what_ was changed: focus on the reasoning behind the changes rather than a file-by-file summary. + let systemPrompt = Instructions { + """ + You are an AI assistant merging multiple partial commit drafts into a single, well-structured commit message. + Preserve all important intent from the inputs, avoid redundancy, and keep the final subject concise (<= 50 characters). + The title should succinctly describe the change in a specific and informative manner. + Provide an optional body only when useful for additional context. + If a body is present, it should describe the _purpose_ of the change, not just _what_ was changed: focus on the reasoning behind the changes rather than a file-by-file summary. + + Be clear and concise, but do not omit critical information. + """ + "" + metadata.style.styleGuidance + } - Be clear and concise, but do not omit critical information. - """ - "" - metadata.style.styleGuidance + let characterCount = userLines.reduce(0) { $0 + $1.count + 1 } + let estimatedTokens = PromptDiagnostics.tokenEstimate(forCharacterCount: characterCount) + let uniqueFiles = Set(sortedPartials.flatMap { $0.files }.map { $0.path }) + let generatedFileCount = sortedPartials.flatMap { $0.files }.filter { $0.isGenerated }.count + + let diagnostics = PromptDiagnostics( + estimatedLineCount: userLines.count, + lineBudget: 400, + estimatedTokenCount: estimatedTokens, + estimatedTokenLimit: 4_096, + totalFiles: uniqueFiles.count, + displayedFiles: uniqueFiles.count, + configuredFileLimit: uniqueFiles.count, + snippetLineLimit: 0, + configuredSnippetLineLimit: 0, + snippetFilesTruncated: 0, + compactionApplied: false, + generatedFilesTotal: generatedFileCount, + generatedFilesDisplayed: generatedFileCount, + fileUsages: sortedPartials.flatMap { $0.diagnostics.fileUsages }, + remainderCount: 0, + remainderAdditions: 0, + remainderDeletions: 0, + remainderGeneratedCount: 0, + remainderKindBreakdown: [], + remainderHintLimit: 0, + remainderHintFiles: [], + remainderNonGeneratedCount: 0 + ) + + return PromptPackage( + systemPrompt: systemPrompt, + userPrompt: userPrompt, + diagnostics: diagnostics + ) + } else { + fatalError("FoundationModels backend requires macOS 26.0 or newer") } #else let userPrompt = PromptContent(userLines.joined(separator: "\n")) @@ -93,42 +133,42 @@ struct BatchCombinationPromptBuilder { \(metadata.style.styleGuidance) """ ) - #endif - let characterCount = userLines.reduce(0) { $0 + $1.count + 1 } - let estimatedTokens = PromptDiagnostics.tokenEstimate(forCharacterCount: characterCount) - let uniqueFiles = Set(sortedPartials.flatMap { $0.files }.map { $0.path }) - let generatedFileCount = sortedPartials.flatMap { $0.files }.filter { $0.isGenerated }.count - - let diagnostics = PromptDiagnostics( - estimatedLineCount: userLines.count, - lineBudget: 400, - estimatedTokenCount: estimatedTokens, - estimatedTokenLimit: 4_096, - totalFiles: uniqueFiles.count, - displayedFiles: uniqueFiles.count, - configuredFileLimit: uniqueFiles.count, - snippetLineLimit: 0, - configuredSnippetLineLimit: 0, - snippetFilesTruncated: 0, - compactionApplied: false, - generatedFilesTotal: generatedFileCount, - generatedFilesDisplayed: generatedFileCount, - fileUsages: sortedPartials.flatMap { $0.diagnostics.fileUsages }, - remainderCount: 0, - remainderAdditions: 0, - remainderDeletions: 0, - remainderGeneratedCount: 0, - remainderKindBreakdown: [], - remainderHintLimit: 0, - remainderHintFiles: [], - remainderNonGeneratedCount: 0 - ) + let characterCount = userLines.reduce(0) { $0 + $1.count + 1 } + let estimatedTokens = PromptDiagnostics.tokenEstimate(forCharacterCount: characterCount) + let uniqueFiles = Set(sortedPartials.flatMap { $0.files }.map { $0.path }) + let generatedFileCount = sortedPartials.flatMap { $0.files }.filter { $0.isGenerated }.count + + let diagnostics = PromptDiagnostics( + estimatedLineCount: userLines.count, + lineBudget: 400, + estimatedTokenCount: estimatedTokens, + estimatedTokenLimit: 4_096, + totalFiles: uniqueFiles.count, + displayedFiles: uniqueFiles.count, + configuredFileLimit: uniqueFiles.count, + snippetLineLimit: 0, + configuredSnippetLineLimit: 0, + snippetFilesTruncated: 0, + compactionApplied: false, + generatedFilesTotal: generatedFileCount, + generatedFilesDisplayed: generatedFileCount, + fileUsages: sortedPartials.flatMap { $0.diagnostics.fileUsages }, + remainderCount: 0, + remainderAdditions: 0, + remainderDeletions: 0, + remainderGeneratedCount: 0, + remainderKindBreakdown: [], + remainderHintLimit: 0, + remainderHintFiles: [], + remainderNonGeneratedCount: 0 + ) - return PromptPackage( - systemPrompt: systemPrompt, - userPrompt: userPrompt, - diagnostics: diagnostics - ) + return PromptPackage( + systemPrompt: systemPrompt, + userPrompt: userPrompt, + diagnostics: diagnostics + ) + #endif } } diff --git a/Sources/SwiftCommitGen/Core/CommitGenError.swift b/Sources/SwiftCommitGen/Core/CommitGenError.swift index 174edb9..084fc58 100644 --- a/Sources/SwiftCommitGen/Core/CommitGenError.swift +++ b/Sources/SwiftCommitGen/Core/CommitGenError.swift @@ -9,6 +9,7 @@ enum CommitGenError: Error { case modelTimedOut(timeout: TimeInterval) case modelGenerationFailed(message: String) case llmRequestFailed(reason: String) + case invalidBackend(String) case notImplemented } @@ -33,6 +34,8 @@ extension CommitGenError: LocalizedError { message case .llmRequestFailed(let reason): "LLM request failed: \(reason)" + case .invalidBackend(let message): + message case .notImplemented: "Commit generation is not implemented yet; future phases will add this capability." } diff --git a/Sources/SwiftCommitGen/Core/DiffSummarizer.swift b/Sources/SwiftCommitGen/Core/DiffSummarizer.swift index 6118873..f751719 100644 --- a/Sources/SwiftCommitGen/Core/DiffSummarizer.swift +++ b/Sources/SwiftCommitGen/Core/DiffSummarizer.swift @@ -53,6 +53,7 @@ struct ChangeSummary: Hashable, Codable { } #if canImport(FoundationModels) + @available(macOS 26.0, *) var promptRepresentation: Prompt { Prompt { for line in promptLines() { @@ -198,6 +199,7 @@ struct ChangeSummary: Hashable, Codable { } #if canImport(FoundationModels) + @available(macOS 26.0, *) var promptRepresentation: Prompt { Prompt { for line in promptLines() { diff --git a/Sources/SwiftCommitGen/Core/LLMClient.swift b/Sources/SwiftCommitGen/Core/LLMClient.swift index c0bd9b0..07139d9 100644 --- a/Sources/SwiftCommitGen/Core/LLMClient.swift +++ b/Sources/SwiftCommitGen/Core/LLMClient.swift @@ -9,25 +9,9 @@ protocol LLMClient { func generateCommitDraft(from prompt: PromptPackage) async throws -> LLMGenerationResult } -#if canImport(FoundationModels) - @Generable(description: "A commit for changes made in a git repository.") -#endif /// Model representation for the subject/body pair returned by the language model. struct CommitDraft: Hashable, Codable, Sendable { - #if canImport(FoundationModels) - @Guide( - description: - "The title of a commit. It should be no longer than 50 characters and should summarize the contents of the changeset for other developers reading the commit history. It should describe WHAT was changed." - ) - #endif var subject: String - - #if canImport(FoundationModels) - @Guide( - description: - "A detailed description of the the purposes of the changes. It should describe WHY the changes were made." - ) - #endif var body: String? init(subject: String = "", body: String? = nil) { @@ -77,6 +61,35 @@ struct CommitDraft: Hashable, Codable, Sendable { } } +#if canImport(FoundationModels) + @available(macOS 26.0, *) + extension CommitDraft: Generable { + static var generationSchema: GenerationSchema { + GenerationSchema( + type: Self.self, + description: "A commit for changes made in a git repository.", + properties: [ + .init(name: "subject", type: String.self), + .init(name: "body", type: String?.self), + ] + ) + } + + init(_ content: GeneratedContent) throws { + self.subject = try content.value(forProperty: "subject") + self.body = try content.value(forProperty: "body") + } + + var generatedContent: GeneratedContent { + var props: [(String, any ConvertibleToGeneratedContent)] = [("subject", subject)] + if let body = body { + props.append(("body", body)) + } + return GeneratedContent(properties: props, uniquingKeysWith: { _, second in second }) + } + } +#endif + /// Wraps a generated draft alongside diagnostics gathered during inference. struct LLMGenerationResult: Sendable { var draft: CommitDraft @@ -85,6 +98,7 @@ struct LLMGenerationResult: Sendable { #if canImport(FoundationModels) /// Concrete LLM client backed by Apple's FoundationModels framework. + @available(macOS 26.0, *) struct FoundationModelsClient: LLMClient { /// Controls retry behavior and timeouts for generation requests. struct Configuration { @@ -127,7 +141,7 @@ struct LLMGenerationResult: Sendable { let session = LanguageModelSession( model: model, - instructions: prompt.systemPrompt + instructions: { prompt.systemPrompt as! Instructions } ) var diagnostics = prompt.diagnostics @@ -135,7 +149,7 @@ struct LLMGenerationResult: Sendable { generating: CommitDraft.self, options: generationOptions ) { - prompt.userPrompt + prompt.userPrompt as! Prompt } let usage = analyzeTranscriptEntries(response.transcriptEntries) @@ -279,16 +293,30 @@ struct OllamaClient: LLMClient { request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") + // Extract string content from prompts + #if canImport(FoundationModels) + guard let systemContent = (prompt.systemPrompt as? PromptContent)?.content, + let userContent = (prompt.userPrompt as? PromptContent)?.content + else { + throw CommitGenError.invalidBackend( + "Ollama backend requires PromptContent, but received FoundationModels types. Use FoundationModels backend instead." + ) + } + #else + let systemContent = prompt.systemPrompt.content + let userContent = prompt.userPrompt.content + #endif + // Structure the messages properly let messages: [[String: String]] = [ [ "role": "system", - "content": prompt.systemPrompt.content, + "content": systemContent, ], [ "role": "user", "content": """ - \(prompt.userPrompt.content) + \(userContent) IMPORTANT: You must respond with ONLY valid JSON in this exact format, with no additional text before or after: { @@ -314,8 +342,8 @@ struct OllamaClient: LLMClient { // Log the request if verbose logging is enabled configuration.logger?.debug { - let systemPreview = prompt.systemPrompt.content.prefix(200) - let userPreview = prompt.userPrompt.content.prefix(800) + let systemPreview = systemContent.prefix(200) + let userPreview = userContent.prefix(800) return """ 📤 Ollama Request to \(configuration.model): ┌─ System Prompt (first 200 chars): @@ -367,7 +395,7 @@ struct OllamaClient: LLMClient { promptTokens = promptEvalCount } else { promptTokens = PromptDiagnostics.tokenEstimate( - forCharacterCount: prompt.systemPrompt.content.count + prompt.userPrompt.content.count + forCharacterCount: systemContent.count + userContent.count ) } diff --git a/Sources/SwiftCommitGen/Core/PromptBuilder.swift b/Sources/SwiftCommitGen/Core/PromptBuilder.swift index 40b82bd..f823181 100644 --- a/Sources/SwiftCommitGen/Core/PromptBuilder.swift +++ b/Sources/SwiftCommitGen/Core/PromptBuilder.swift @@ -50,6 +50,7 @@ struct PromptMetadata { } #if canImport(FoundationModels) + @available(macOS 26.0, *) var promptRepresentation: Prompt { Prompt { "Repository: \(repositoryName)" @@ -166,8 +167,8 @@ struct PromptDiagnostics: Codable, Sendable { /// Bundles the full prompt payload and derived diagnostics for a generation request. struct PromptPackage { #if canImport(FoundationModels) - var systemPrompt: Instructions - var userPrompt: Prompt + var systemPrompt: Any // Instructions on macOS 26+, PromptContent otherwise + var userPrompt: Any // Prompt on macOS 26+, PromptContent otherwise #else var systemPrompt: PromptContent var userPrompt: PromptContent @@ -185,11 +186,56 @@ struct PromptPackage { let additionalLines = contextLineCount + 2 // blank separator + heading #if canImport(FoundationModels) - let augmentedUserPrompt = Prompt { - userPrompt - "" - "Additional context from user:" - trimmed + if #available(macOS 26.0, *) { + let augmentedUserPrompt = Prompt { + userPrompt as! Prompt + "" + "Additional context from user:" + trimmed + } + + var updatedDiagnostics = diagnostics + let additionalCharacters = + trimmed.count + + "Additional context from user:".count + let newlineCharacters = additionalLines // account for line breaks + updatedDiagnostics.recordAdditionalUserContext( + lineCount: additionalLines, + characterCount: additionalCharacters + newlineCharacters + ) + + return PromptPackage( + systemPrompt: systemPrompt, + userPrompt: augmentedUserPrompt, + diagnostics: updatedDiagnostics + ) + } else { + // For macOS < 26, fall through to PromptContent handling + let promptContent = userPrompt as! PromptContent + let augmentedUserPrompt = PromptContent( + """ + \(promptContent.content) + + Additional context from user: + \(trimmed) + """ + ) + + var updatedDiagnostics = diagnostics + let additionalCharacters = + trimmed.count + + "Additional context from user:".count + let newlineCharacters = additionalLines + updatedDiagnostics.recordAdditionalUserContext( + lineCount: additionalLines, + characterCount: additionalCharacters + newlineCharacters + ) + + return PromptPackage( + systemPrompt: systemPrompt, + userPrompt: augmentedUserPrompt, + diagnostics: updatedDiagnostics + ) } #else let augmentedUserPrompt = PromptContent( @@ -200,23 +246,23 @@ struct PromptPackage { \(trimmed) """ ) - #endif - var updatedDiagnostics = diagnostics - let additionalCharacters = - trimmed.count - + "Additional context from user:".count - let newlineCharacters = additionalLines // account for line breaks - updatedDiagnostics.recordAdditionalUserContext( - lineCount: additionalLines, - characterCount: additionalCharacters + newlineCharacters - ) + var updatedDiagnostics = diagnostics + let additionalCharacters = + trimmed.count + + "Additional context from user:".count + let newlineCharacters = additionalLines // account for line breaks + updatedDiagnostics.recordAdditionalUserContext( + lineCount: additionalLines, + characterCount: additionalCharacters + newlineCharacters + ) - return PromptPackage( - systemPrompt: systemPrompt, - userPrompt: augmentedUserPrompt, - diagnostics: updatedDiagnostics - ) + return PromptPackage( + systemPrompt: systemPrompt, + userPrompt: augmentedUserPrompt, + diagnostics: updatedDiagnostics + ) + #endif } } @@ -261,55 +307,114 @@ struct DefaultPromptBuilder: PromptBuilder { } func makePrompt(summary: ChangeSummary, metadata: PromptMetadata) -> PromptPackage { - let system = buildSystemPrompt(style: metadata.style) + #if canImport(FoundationModels) + if #available(macOS 26.0, *) { + let system = buildSystemPrompt(style: metadata.style) - var fileLimit = min(maxFiles, summary.fileCount) - var snippetLimit = adjustedSnippetLimit( - totalFiles: summary.fileCount, - configuredLimit: maxSnippetLines - ) + var fileLimit = min(maxFiles, summary.fileCount) + var snippetLimit = adjustedSnippetLimit( + totalFiles: summary.fileCount, + configuredLimit: maxSnippetLines + ) - var displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) - var remainderContext = makeRemainderContext( - fullSummary: summary, - displaySummary: displaySummary, - hintLimit: hintThreshold - ) - var isCompacted = displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - - var user = buildUserPrompt( - displaySummary: displaySummary, - fullSummary: summary, - metadata: metadata, - isCompacted: isCompacted, - remainder: remainderContext - ) + var displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) + var remainderContext = makeRemainderContext( + fullSummary: summary, + displaySummary: displaySummary, + hintLimit: hintThreshold + ) + var isCompacted = + displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + + var user = buildUserPrompt( + displaySummary: displaySummary, + fullSummary: summary, + metadata: metadata, + isCompacted: isCompacted, + remainder: remainderContext + ) - var estimate = estimatedLineCount( - displaySummary: displaySummary, - fullSummary: summary, - includesCompactionNote: isCompacted, - remainder: remainderContext - ) + var estimate = estimatedLineCount( + displaySummary: displaySummary, + fullSummary: summary, + includesCompactionNote: isCompacted, + remainder: remainderContext + ) - while estimate > maxPromptLineEstimate { - if snippetLimit > minSnippetLines { - snippetLimit = max(minSnippetLines, snippetLimit - snippetReductionStep) - } else if fileLimit > minFiles { - fileLimit = max(minFiles, fileLimit - 1) + while estimate > maxPromptLineEstimate { + if snippetLimit > minSnippetLines { + snippetLimit = max(minSnippetLines, snippetLimit - snippetReductionStep) + } else if fileLimit > minFiles { + fileLimit = max(minFiles, fileLimit - 1) + } else { + break + } + + displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) + remainderContext = makeRemainderContext( + fullSummary: summary, + displaySummary: displaySummary, + hintLimit: hintThreshold + ) + isCompacted = + displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + + user = buildUserPrompt( + displaySummary: displaySummary, + fullSummary: summary, + metadata: metadata, + isCompacted: isCompacted, + remainder: remainderContext + ) + + estimate = estimatedLineCount( + displaySummary: displaySummary, + fullSummary: summary, + includesCompactionNote: isCompacted, + remainder: remainderContext + ) + } + + let finalCompaction = + displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + + let diagnostics = makeDiagnostics( + metadata: metadata, + fullSummary: summary, + displaySummary: displaySummary, + snippetLimit: snippetLimit, + isCompacted: finalCompaction, + remainder: remainderContext, + estimatedLines: estimate, + lineBudget: maxPromptLineEstimate, + configuredFileLimit: maxFiles, + configuredSnippetLimit: maxSnippetLines, + hintLimit: hintThreshold + ) + + return PromptPackage(systemPrompt: system, userPrompt: user, diagnostics: diagnostics) } else { - break + fatalError("FoundationModels backend requires macOS 26.0 or newer") } + #else + let system = buildSystemPrompt(style: metadata.style) - displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) - remainderContext = makeRemainderContext( + var fileLimit = min(maxFiles, summary.fileCount) + var snippetLimit = adjustedSnippetLimit( + totalFiles: summary.fileCount, + configuredLimit: maxSnippetLines + ) + + var displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) + var remainderContext = makeRemainderContext( fullSummary: summary, displaySummary: displaySummary, hintLimit: hintThreshold ) - isCompacted = displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + var isCompacted = + displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - user = buildUserPrompt( + var user = buildUserPrompt( displaySummary: displaySummary, fullSummary: summary, metadata: metadata, @@ -317,35 +422,69 @@ struct DefaultPromptBuilder: PromptBuilder { remainder: remainderContext ) - estimate = estimatedLineCount( + var estimate = estimatedLineCount( displaySummary: displaySummary, fullSummary: summary, includesCompactionNote: isCompacted, remainder: remainderContext ) - } - let finalCompaction = - displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - - let diagnostics = makeDiagnostics( - metadata: metadata, - fullSummary: summary, - displaySummary: displaySummary, - snippetLimit: snippetLimit, - isCompacted: finalCompaction, - remainder: remainderContext, - estimatedLines: estimate, - lineBudget: maxPromptLineEstimate, - configuredFileLimit: maxFiles, - configuredSnippetLimit: maxSnippetLines, - hintLimit: hintThreshold - ) + while estimate > maxPromptLineEstimate { + if snippetLimit > minSnippetLines { + snippetLimit = max(minSnippetLines, snippetLimit - snippetReductionStep) + } else if fileLimit > minFiles { + fileLimit = max(minFiles, fileLimit - 1) + } else { + break + } + + displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) + remainderContext = makeRemainderContext( + fullSummary: summary, + displaySummary: displaySummary, + hintLimit: hintThreshold + ) + isCompacted = displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + + user = buildUserPrompt( + displaySummary: displaySummary, + fullSummary: summary, + metadata: metadata, + isCompacted: isCompacted, + remainder: remainderContext + ) - return PromptPackage(systemPrompt: system, userPrompt: user, diagnostics: diagnostics) + estimate = estimatedLineCount( + displaySummary: displaySummary, + fullSummary: summary, + includesCompactionNote: isCompacted, + remainder: remainderContext + ) + } + + let finalCompaction = + displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + + let diagnostics = makeDiagnostics( + metadata: metadata, + fullSummary: summary, + displaySummary: displaySummary, + snippetLimit: snippetLimit, + isCompacted: finalCompaction, + remainder: remainderContext, + estimatedLines: estimate, + lineBudget: maxPromptLineEstimate, + configuredFileLimit: maxFiles, + configuredSnippetLimit: maxSnippetLines, + hintLimit: hintThreshold + ) + + return PromptPackage(systemPrompt: system, userPrompt: user, diagnostics: diagnostics) + #endif } #if canImport(FoundationModels) + @available(macOS 26.0, *) private func buildSystemPrompt(style: CommitGenOptions.PromptStyle) -> Instructions { Instructions { """ @@ -405,6 +544,7 @@ struct DefaultPromptBuilder: PromptBuilder { } #if canImport(FoundationModels) + @available(macOS 26.0, *) private func buildUserPrompt( displaySummary: ChangeSummary, fullSummary: ChangeSummary, @@ -413,7 +553,7 @@ struct DefaultPromptBuilder: PromptBuilder { remainder: RemainderContext ) -> Prompt { Prompt { - metadata + metadata.promptRepresentation "Totals: \(fullSummary.fileCount) files; +\(fullSummary.totalAdditions) / -\(fullSummary.totalDeletions)" if isCompacted { @@ -447,7 +587,7 @@ struct DefaultPromptBuilder: PromptBuilder { } } - displaySummary + displaySummary.promptRepresentation } } #else From 460750c6c7695efba6f1aa46e6d0735ddb3af53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manel=20Mateos=20Rami=CC=81rez?= Date: Fri, 5 Dec 2025 14:48:15 +0100 Subject: [PATCH 2/3] Updated SwiftCommitGen Core Components This commit updates several components in the SwiftCommitGen project, including enhancements to BatchCombinationPromptBuilder and LLMClient, and improvements to PromptBuilder. These changes aim to improve the project's functionality and maintainability. The modifications involve restructuring and refining code to ensure better performance and error handling. --- .../Core/BatchCombinationPromptBuilder.swift | 155 +++------ Sources/SwiftCommitGen/Core/LLMClient.swift | 28 +- .../SwiftCommitGen/Core/PromptBuilder.swift | 303 +++++++++--------- 3 files changed, 218 insertions(+), 268 deletions(-) diff --git a/Sources/SwiftCommitGen/Core/BatchCombinationPromptBuilder.swift b/Sources/SwiftCommitGen/Core/BatchCombinationPromptBuilder.swift index e98039e..bd1ce57 100644 --- a/Sources/SwiftCommitGen/Core/BatchCombinationPromptBuilder.swift +++ b/Sources/SwiftCommitGen/Core/BatchCombinationPromptBuilder.swift @@ -57,118 +57,57 @@ struct BatchCombinationPromptBuilder { "Produce one final commit subject (<= 50 characters) and an optional body that summarizes the full change set. Avoid repeating the batch headings—present the combined commit message only." ) - #if canImport(FoundationModels) - if #available(macOS 26.0, *) { - let userPrompt = Prompt { - for line in userLines { - line - } - } - - let systemPrompt = Instructions { - """ - You are an AI assistant merging multiple partial commit drafts into a single, well-structured commit message. - Preserve all important intent from the inputs, avoid redundancy, and keep the final subject concise (<= 50 characters). - The title should succinctly describe the change in a specific and informative manner. - Provide an optional body only when useful for additional context. - If a body is present, it should describe the _purpose_ of the change, not just _what_ was changed: focus on the reasoning behind the changes rather than a file-by-file summary. - - Be clear and concise, but do not omit critical information. - """ - "" - metadata.style.styleGuidance - } + // Always use PromptContent - LLM client will convert if needed + let userPrompt = PromptContent(userLines.joined(separator: "\n")) - let characterCount = userLines.reduce(0) { $0 + $1.count + 1 } - let estimatedTokens = PromptDiagnostics.tokenEstimate(forCharacterCount: characterCount) - let uniqueFiles = Set(sortedPartials.flatMap { $0.files }.map { $0.path }) - let generatedFileCount = sortedPartials.flatMap { $0.files }.filter { $0.isGenerated }.count - - let diagnostics = PromptDiagnostics( - estimatedLineCount: userLines.count, - lineBudget: 400, - estimatedTokenCount: estimatedTokens, - estimatedTokenLimit: 4_096, - totalFiles: uniqueFiles.count, - displayedFiles: uniqueFiles.count, - configuredFileLimit: uniqueFiles.count, - snippetLineLimit: 0, - configuredSnippetLineLimit: 0, - snippetFilesTruncated: 0, - compactionApplied: false, - generatedFilesTotal: generatedFileCount, - generatedFilesDisplayed: generatedFileCount, - fileUsages: sortedPartials.flatMap { $0.diagnostics.fileUsages }, - remainderCount: 0, - remainderAdditions: 0, - remainderDeletions: 0, - remainderGeneratedCount: 0, - remainderKindBreakdown: [], - remainderHintLimit: 0, - remainderHintFiles: [], - remainderNonGeneratedCount: 0 - ) - - return PromptPackage( - systemPrompt: systemPrompt, - userPrompt: userPrompt, - diagnostics: diagnostics - ) - } else { - fatalError("FoundationModels backend requires macOS 26.0 or newer") - } - #else - let userPrompt = PromptContent(userLines.joined(separator: "\n")) - - let systemPrompt = PromptContent( - """ - You are an AI assistant merging multiple partial commit drafts into a single, well-structured commit message. - Preserve all important intent from the inputs, avoid redundancy, and keep the final subject concise (<= 50 characters). - The title should succinctly describe the change in a specific and informative manner. - Provide an optional body only when useful for additional context. - If a body is present, it should describe the _purpose_ of the change, not just _what_ was changed: focus on the reasoning behind the changes rather than a file-by-file summary. + let systemPrompt = PromptContent( + """ + You are an AI assistant merging multiple partial commit drafts into a single, well-structured commit message. + Preserve all important intent from the inputs, avoid redundancy, and keep the final subject concise (<= 50 characters). + The title should succinctly describe the change in a specific and informative manner. + Provide an optional body only when useful for additional context. + If a body is present, it should describe the _purpose_ of the change, not just _what_ was changed: focus on the reasoning behind the changes rather than a file-by-file summary. - Be clear and concise, but do not omit critical information. + Be clear and concise, but do not omit critical information. - \(metadata.style.styleGuidance) - """ - ) + \(metadata.style.styleGuidance) + """ + ) - let characterCount = userLines.reduce(0) { $0 + $1.count + 1 } - let estimatedTokens = PromptDiagnostics.tokenEstimate(forCharacterCount: characterCount) - let uniqueFiles = Set(sortedPartials.flatMap { $0.files }.map { $0.path }) - let generatedFileCount = sortedPartials.flatMap { $0.files }.filter { $0.isGenerated }.count - - let diagnostics = PromptDiagnostics( - estimatedLineCount: userLines.count, - lineBudget: 400, - estimatedTokenCount: estimatedTokens, - estimatedTokenLimit: 4_096, - totalFiles: uniqueFiles.count, - displayedFiles: uniqueFiles.count, - configuredFileLimit: uniqueFiles.count, - snippetLineLimit: 0, - configuredSnippetLineLimit: 0, - snippetFilesTruncated: 0, - compactionApplied: false, - generatedFilesTotal: generatedFileCount, - generatedFilesDisplayed: generatedFileCount, - fileUsages: sortedPartials.flatMap { $0.diagnostics.fileUsages }, - remainderCount: 0, - remainderAdditions: 0, - remainderDeletions: 0, - remainderGeneratedCount: 0, - remainderKindBreakdown: [], - remainderHintLimit: 0, - remainderHintFiles: [], - remainderNonGeneratedCount: 0 - ) + let characterCount = userLines.reduce(0) { $0 + $1.count + 1 } + let estimatedTokens = PromptDiagnostics.tokenEstimate(forCharacterCount: characterCount) + let uniqueFiles = Set(sortedPartials.flatMap { $0.files }.map { $0.path }) + let generatedFileCount = sortedPartials.flatMap { $0.files }.filter { $0.isGenerated }.count + + let diagnostics = PromptDiagnostics( + estimatedLineCount: userLines.count, + lineBudget: 400, + estimatedTokenCount: estimatedTokens, + estimatedTokenLimit: 4_096, + totalFiles: uniqueFiles.count, + displayedFiles: uniqueFiles.count, + configuredFileLimit: uniqueFiles.count, + snippetLineLimit: 0, + configuredSnippetLineLimit: 0, + snippetFilesTruncated: 0, + compactionApplied: false, + generatedFilesTotal: generatedFileCount, + generatedFilesDisplayed: generatedFileCount, + fileUsages: sortedPartials.flatMap { $0.diagnostics.fileUsages }, + remainderCount: 0, + remainderAdditions: 0, + remainderDeletions: 0, + remainderGeneratedCount: 0, + remainderKindBreakdown: [], + remainderHintLimit: 0, + remainderHintFiles: [], + remainderNonGeneratedCount: 0 + ) - return PromptPackage( - systemPrompt: systemPrompt, - userPrompt: userPrompt, - diagnostics: diagnostics - ) - #endif + return PromptPackage( + systemPrompt: systemPrompt, + userPrompt: userPrompt, + diagnostics: diagnostics + ) } } diff --git a/Sources/SwiftCommitGen/Core/LLMClient.swift b/Sources/SwiftCommitGen/Core/LLMClient.swift index 07139d9..e8db6e6 100644 --- a/Sources/SwiftCommitGen/Core/LLMClient.swift +++ b/Sources/SwiftCommitGen/Core/LLMClient.swift @@ -139,9 +139,17 @@ struct LLMGenerationResult: Sendable { throw CommitGenError.modelUnavailable(reason: reason) } + // Convert PromptContent to FoundationModels types + guard let systemPromptContent = prompt.systemPrompt as? PromptContent, + let userPromptContent = prompt.userPrompt as? PromptContent else { + throw CommitGenError.invalidBackend( + "FoundationModels backend expected PromptContent but received: System: \(type(of: prompt.systemPrompt)), User: \(type(of: prompt.userPrompt))" + ) + } + let session = LanguageModelSession( model: model, - instructions: { prompt.systemPrompt as! Instructions } + instructions: { Instructions { systemPromptContent.content } } ) var diagnostics = prompt.diagnostics @@ -149,7 +157,7 @@ struct LLMGenerationResult: Sendable { generating: CommitDraft.self, options: generationOptions ) { - prompt.userPrompt as! Prompt + Prompt { userPromptContent.content } } let usage = analyzeTranscriptEntries(response.transcriptEntries) @@ -295,11 +303,19 @@ struct OllamaClient: LLMClient { // Extract string content from prompts #if canImport(FoundationModels) - guard let systemContent = (prompt.systemPrompt as? PromptContent)?.content, - let userContent = (prompt.userPrompt as? PromptContent)?.content - else { + // When FoundationModels is available, prompts could be either PromptContent or FoundationModels types + // depending on macOS version and runtime availability + let systemContent: String + let userContent: String + + if let systemPromptContent = prompt.systemPrompt as? PromptContent, + let userPromptContent = prompt.userPrompt as? PromptContent { + systemContent = systemPromptContent.content + userContent = userPromptContent.content + } else { + // This should not happen if the builder correctly uses PromptContent for non-macOS 26+ systems throw CommitGenError.invalidBackend( - "Ollama backend requires PromptContent, but received FoundationModels types. Use FoundationModels backend instead." + "Ollama backend requires PromptContent, but received FoundationModels types (System: \(type(of: prompt.systemPrompt)), User: \(type(of: prompt.userPrompt))). Use FoundationModels backend instead." ) } #else diff --git a/Sources/SwiftCommitGen/Core/PromptBuilder.swift b/Sources/SwiftCommitGen/Core/PromptBuilder.swift index f823181..5bada19 100644 --- a/Sources/SwiftCommitGen/Core/PromptBuilder.swift +++ b/Sources/SwiftCommitGen/Core/PromptBuilder.swift @@ -49,6 +49,16 @@ struct PromptMetadata { includeUnstagedChanges ? "staged + unstaged changes" : "staged changes only" } + // Always available version for PromptContent + var asPromptContent: PromptContent { + PromptContent { + "Repository: \(repositoryName)" + "Branch: \(branchName)" + "Scope: \(scopeDescription)" + "Style: \(style.styleGuidance)" + } + } + #if canImport(FoundationModels) @available(macOS 26.0, *) var promptRepresentation: Prompt { @@ -61,12 +71,7 @@ struct PromptMetadata { } #else var promptContent: PromptContent { - PromptContent { - "Repository: \(repositoryName)" - "Branch: \(branchName)" - "Scope: \(scopeDescription)" - "Style: \(style.styleGuidance)" - } + asPromptContent } #endif } @@ -307,114 +312,57 @@ struct DefaultPromptBuilder: PromptBuilder { } func makePrompt(summary: ChangeSummary, metadata: PromptMetadata) -> PromptPackage { - #if canImport(FoundationModels) - if #available(macOS 26.0, *) { - let system = buildSystemPrompt(style: metadata.style) - - var fileLimit = min(maxFiles, summary.fileCount) - var snippetLimit = adjustedSnippetLimit( - totalFiles: summary.fileCount, - configuredLimit: maxSnippetLines - ) - - var displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) - var remainderContext = makeRemainderContext( - fullSummary: summary, - displaySummary: displaySummary, - hintLimit: hintThreshold - ) - var isCompacted = - displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - - var user = buildUserPrompt( - displaySummary: displaySummary, - fullSummary: summary, - metadata: metadata, - isCompacted: isCompacted, - remainder: remainderContext - ) - - var estimate = estimatedLineCount( - displaySummary: displaySummary, - fullSummary: summary, - includesCompactionNote: isCompacted, - remainder: remainderContext - ) + // Always use PromptContent types - the LLM client will convert to FoundationModels types if needed + let system = buildSystemPromptAsContent(style: metadata.style) - while estimate > maxPromptLineEstimate { - if snippetLimit > minSnippetLines { - snippetLimit = max(minSnippetLines, snippetLimit - snippetReductionStep) - } else if fileLimit > minFiles { - fileLimit = max(minFiles, fileLimit - 1) - } else { - break - } + var fileLimit = min(maxFiles, summary.fileCount) + var snippetLimit = adjustedSnippetLimit( + totalFiles: summary.fileCount, + configuredLimit: maxSnippetLines + ) - displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) - remainderContext = makeRemainderContext( - fullSummary: summary, - displaySummary: displaySummary, - hintLimit: hintThreshold - ) - isCompacted = - displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - - user = buildUserPrompt( - displaySummary: displaySummary, - fullSummary: summary, - metadata: metadata, - isCompacted: isCompacted, - remainder: remainderContext - ) - - estimate = estimatedLineCount( - displaySummary: displaySummary, - fullSummary: summary, - includesCompactionNote: isCompacted, - remainder: remainderContext - ) - } + var displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) + var remainderContext = makeRemainderContext( + fullSummary: summary, + displaySummary: displaySummary, + hintLimit: hintThreshold + ) + var isCompacted = + displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + + var user = buildUserPromptAsContent( + displaySummary: displaySummary, + fullSummary: summary, + metadata: metadata, + isCompacted: isCompacted, + remainder: remainderContext + ) - let finalCompaction = - displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - - let diagnostics = makeDiagnostics( - metadata: metadata, - fullSummary: summary, - displaySummary: displaySummary, - snippetLimit: snippetLimit, - isCompacted: finalCompaction, - remainder: remainderContext, - estimatedLines: estimate, - lineBudget: maxPromptLineEstimate, - configuredFileLimit: maxFiles, - configuredSnippetLimit: maxSnippetLines, - hintLimit: hintThreshold - ) + var estimate = estimatedLineCount( + displaySummary: displaySummary, + fullSummary: summary, + includesCompactionNote: isCompacted, + remainder: remainderContext + ) - return PromptPackage(systemPrompt: system, userPrompt: user, diagnostics: diagnostics) + while estimate > maxPromptLineEstimate { + if snippetLimit > minSnippetLines { + snippetLimit = max(minSnippetLines, snippetLimit - snippetReductionStep) + } else if fileLimit > minFiles { + fileLimit = max(minFiles, fileLimit - 1) } else { - fatalError("FoundationModels backend requires macOS 26.0 or newer") + break } - #else - let system = buildSystemPrompt(style: metadata.style) - var fileLimit = min(maxFiles, summary.fileCount) - var snippetLimit = adjustedSnippetLimit( - totalFiles: summary.fileCount, - configuredLimit: maxSnippetLines - ) - - var displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) - var remainderContext = makeRemainderContext( + displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) + remainderContext = makeRemainderContext( fullSummary: summary, displaySummary: displaySummary, hintLimit: hintThreshold ) - var isCompacted = - displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + isCompacted = displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - var user = buildUserPrompt( + user = buildUserPromptAsContent( displaySummary: displaySummary, fullSummary: summary, metadata: metadata, @@ -422,65 +370,32 @@ struct DefaultPromptBuilder: PromptBuilder { remainder: remainderContext ) - var estimate = estimatedLineCount( + estimate = estimatedLineCount( displaySummary: displaySummary, fullSummary: summary, includesCompactionNote: isCompacted, remainder: remainderContext ) + } - while estimate > maxPromptLineEstimate { - if snippetLimit > minSnippetLines { - snippetLimit = max(minSnippetLines, snippetLimit - snippetReductionStep) - } else if fileLimit > minFiles { - fileLimit = max(minFiles, fileLimit - 1) - } else { - break - } - - displaySummary = trimSummary(summary, fileLimit: fileLimit, snippetLimit: snippetLimit) - remainderContext = makeRemainderContext( - fullSummary: summary, - displaySummary: displaySummary, - hintLimit: hintThreshold - ) - isCompacted = displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - - user = buildUserPrompt( - displaySummary: displaySummary, - fullSummary: summary, - metadata: metadata, - isCompacted: isCompacted, - remainder: remainderContext - ) - - estimate = estimatedLineCount( - displaySummary: displaySummary, - fullSummary: summary, - includesCompactionNote: isCompacted, - remainder: remainderContext - ) - } - - let finalCompaction = - displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines - - let diagnostics = makeDiagnostics( - metadata: metadata, - fullSummary: summary, - displaySummary: displaySummary, - snippetLimit: snippetLimit, - isCompacted: finalCompaction, - remainder: remainderContext, - estimatedLines: estimate, - lineBudget: maxPromptLineEstimate, - configuredFileLimit: maxFiles, - configuredSnippetLimit: maxSnippetLines, - hintLimit: hintThreshold - ) + let finalCompaction = + displaySummary.fileCount < summary.fileCount || snippetLimit < maxSnippetLines + + let diagnostics = makeDiagnostics( + metadata: metadata, + fullSummary: summary, + displaySummary: displaySummary, + snippetLimit: snippetLimit, + isCompacted: finalCompaction, + remainder: remainderContext, + estimatedLines: estimate, + lineBudget: maxPromptLineEstimate, + configuredFileLimit: maxFiles, + configuredSnippetLimit: maxSnippetLines, + hintLimit: hintThreshold + ) - return PromptPackage(systemPrompt: system, userPrompt: user, diagnostics: diagnostics) - #endif + return PromptPackage(systemPrompt: system, userPrompt: user, diagnostics: diagnostics) } #if canImport(FoundationModels) @@ -530,6 +445,29 @@ struct DefaultPromptBuilder: PromptBuilder { } #endif + // Version that always returns PromptContent for use when FoundationModels is available but not macOS 26+ + private func buildSystemPromptAsContent(style: CommitGenOptions.PromptStyle) -> PromptContent { + PromptContent( + """ + You're an AI assistant whose job is to concisely summarize code changes into short, useful commit messages, with a title and a description. + A changeset is given in the git diff output format, affecting one or multiple files. + + The commit title should be no longer than 50 characters and should summarize the contents of the changeset for other developers reading the commit history. + The commit description can be longer, and should provide more context about the changeset, including why the changeset is being made, and any other relevant information. + The commit description is optional, so you can omit it if the changeset is small enough that it can be described in the commit title or if you don't have enough context. + + Be brief and concise. + + DO NOT include a description of changes in "lock" files from dependency managers like npm, yarn, or pip (and others), unless those are the only changes in the commit. + + When more explanation is helpful, provide a short body with full sentences. + Leave the body empty when the subject already captures the change or the context is unclear. + + \(style.styleGuidance) + """ + ) + } + private func adjustedSnippetLimit(totalFiles: Int, configuredLimit: Int) -> Int { var limit = configuredLimit @@ -648,6 +586,63 @@ struct DefaultPromptBuilder: PromptBuilder { } #endif +// Version that always returns PromptContent for use when FoundationModels is available but not macOS 26+ +private func buildUserPromptAsContent( + displaySummary: ChangeSummary, + fullSummary: ChangeSummary, + metadata: PromptMetadata, + isCompacted: Bool, + remainder: RemainderContext +) -> PromptContent { + var lines: [String] = [] + + lines.append(metadata.asPromptContent.content) + lines.append( + "Totals: \(fullSummary.fileCount) files; +\(fullSummary.totalAdditions) / -\(fullSummary.totalDeletions)" + ) + + if isCompacted { + lines.append( + "Context trimmed to stay within the model window; prioritize the most impactful changes." + ) + } + + if remainder.count > 0 { + lines.append( + "Showing first \(displaySummary.fileCount) files (of \(fullSummary.fileCount)); remaining \(remainder.count) files contribute +\(remainder.additions) / -\(remainder.deletions)." + ) + + for entry in remainder.kindBreakdown.prefix(4) { + lines.append(" more: \(entry.count) \(entry.kind) file(s)") + } + + if remainder.generatedCount > 0 { + lines.append( + " note: \(remainder.generatedCount) generated file(s) omitted per .gitattributes" + ) + } + + if !remainder.hintFiles.isEmpty { + if remainder.remainingNonGeneratedCount > remainder.hintFiles.count { + lines.append(" showing \(remainder.hintFiles.count) representative paths:") + } + for file in remainder.hintFiles { + let descriptor = [ + file.kind, + locationDescription(file.location), + file.isBinary ? "binary" : nil, + file.isGenerated ? "generated" : nil, + ].compactMap { $0 }.joined(separator: ", ") + lines.append(" • \(file.path) [\(descriptor)]") + } + } + } + + lines.append(contentsOf: displaySummary.promptLines()) + + return PromptContent(lines.joined(separator: "\n")) +} + private func trimSummary( _ summary: ChangeSummary, fileLimit: Int, From d68db454ae36613b430f0e7f3fe52899b03f3a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manel=20Mateos=20Rami=CC=81rez?= Date: Fri, 5 Dec 2025 14:51:09 +0100 Subject: [PATCH 3/3] Refactor LLMClient struct to reduce duplication The LLMClient struct has been refactored to reduce duplication of code and improve readability. The original implementation was using duplicate logic to handle different types of prompts, but this was causing confusion and difficult to maintain. By extracting the common logic into a separate function, we were able to simplify the code and make it easier to understand. --- Sources/SwiftCommitGen/Core/LLMClient.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftCommitGen/Core/LLMClient.swift b/Sources/SwiftCommitGen/Core/LLMClient.swift index e8db6e6..beb0a64 100644 --- a/Sources/SwiftCommitGen/Core/LLMClient.swift +++ b/Sources/SwiftCommitGen/Core/LLMClient.swift @@ -141,7 +141,8 @@ struct LLMGenerationResult: Sendable { // Convert PromptContent to FoundationModels types guard let systemPromptContent = prompt.systemPrompt as? PromptContent, - let userPromptContent = prompt.userPrompt as? PromptContent else { + let userPromptContent = prompt.userPrompt as? PromptContent + else { throw CommitGenError.invalidBackend( "FoundationModels backend expected PromptContent but received: System: \(type(of: prompt.systemPrompt)), User: \(type(of: prompt.userPrompt))" ) @@ -307,9 +308,10 @@ struct OllamaClient: LLMClient { // depending on macOS version and runtime availability let systemContent: String let userContent: String - + if let systemPromptContent = prompt.systemPrompt as? PromptContent, - let userPromptContent = prompt.userPrompt as? PromptContent { + let userPromptContent = prompt.userPrompt as? PromptContent + { systemContent = systemPromptContent.content userContent = userPromptContent.content } else {