|
| 1 | +import { ContextProxy } from "../../core/config/ContextProxy" |
| 2 | +import { ProviderSettingsManager } from "../../core/config/ProviderSettingsManager" |
| 3 | +import { singleCompletionHandler as defaultSingleCompletionHandler } from "../../utils/single-completion-handler" |
| 4 | +import { supportPrompt } from "../../shared/support-prompt" |
| 5 | +import { addCustomInstructions as defaultAddCustomInstructions } from "../../core/prompts/sections/custom-instructions" |
| 6 | +import { TelemetryService } from "@roo-code/telemetry" |
| 7 | +import { TelemetryEventName, type ProviderSettings } from "@roo-code/types" |
| 8 | + |
| 9 | +import { GenerateMessageParams, PromptOptions, ProgressUpdate } from "./types/core" |
| 10 | + |
| 11 | +export interface CommitMessageContextProxy { |
| 12 | + isInitialized: boolean |
| 13 | + getProviderSettings(): ProviderSettings |
| 14 | + getValue(key: any): unknown |
| 15 | +} |
| 16 | + |
| 17 | +export interface CommitMessageGeneratorDependencies { |
| 18 | + getContextProxy?: () => CommitMessageContextProxy |
| 19 | + completePrompt?: (apiConfiguration: ProviderSettings, promptText: string) => Promise<string> |
| 20 | + addCustomInstructions?: typeof defaultAddCustomInstructions |
| 21 | + captureGenerated?: () => void |
| 22 | + logger?: Pick<Console, "warn"> |
| 23 | +} |
| 24 | + |
| 25 | +export class CommitMessageGenerator { |
| 26 | + private readonly providerSettingsManager: ProviderSettingsManager |
| 27 | + private readonly dependencies: Required<CommitMessageGeneratorDependencies> |
| 28 | + private previousGitContext: string | null = null |
| 29 | + private previousCommitMessage: string | null = null |
| 30 | + |
| 31 | + constructor( |
| 32 | + providerSettingsManager: ProviderSettingsManager, |
| 33 | + dependencies: CommitMessageGeneratorDependencies = {}, |
| 34 | + ) { |
| 35 | + this.providerSettingsManager = providerSettingsManager |
| 36 | + this.dependencies = { |
| 37 | + getContextProxy: dependencies.getContextProxy ?? (() => ContextProxy.instance), |
| 38 | + completePrompt: dependencies.completePrompt ?? defaultSingleCompletionHandler, |
| 39 | + addCustomInstructions: dependencies.addCustomInstructions ?? defaultAddCustomInstructions, |
| 40 | + captureGenerated: |
| 41 | + dependencies.captureGenerated ?? |
| 42 | + (() => TelemetryService.instance.captureEvent(TelemetryEventName.COMMIT_MSG_GENERATED)), |
| 43 | + logger: dependencies.logger ?? console, |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + async generateMessage(params: GenerateMessageParams): Promise<string> { |
| 48 | + const { gitContext, onProgress } = params |
| 49 | + |
| 50 | + try { |
| 51 | + onProgress?.({ |
| 52 | + message: "Generating commit message...", |
| 53 | + percentage: 75, |
| 54 | + }) |
| 55 | + |
| 56 | + const generatedMessage = await this.callAIForCommitMessage(gitContext, params.workspacePath, onProgress) |
| 57 | + |
| 58 | + this.previousGitContext = gitContext |
| 59 | + this.previousCommitMessage = generatedMessage |
| 60 | + |
| 61 | + this.dependencies.captureGenerated() |
| 62 | + |
| 63 | + onProgress?.({ |
| 64 | + message: "Commit message generated successfully", |
| 65 | + percentage: 100, |
| 66 | + }) |
| 67 | + |
| 68 | + return generatedMessage |
| 69 | + } catch (error) { |
| 70 | + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred" |
| 71 | + throw new Error(`Failed to generate commit message: ${errorMessage}`) |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + async buildPrompt(gitContext: string, options: PromptOptions, workspacePath: string): Promise<string> { |
| 76 | + const { customSupportPrompts = {}, previousContext, previousMessage } = options |
| 77 | + |
| 78 | + const customInstructions = await this.dependencies.addCustomInstructions("", "", workspacePath, "commit", { |
| 79 | + language: "en", |
| 80 | + }) |
| 81 | + |
| 82 | + const shouldGenerateDifferentMessage = |
| 83 | + (previousContext === gitContext || this.previousGitContext === gitContext) && |
| 84 | + (previousMessage !== null || this.previousCommitMessage !== null) |
| 85 | + |
| 86 | + const targetPreviousMessage = previousMessage || this.previousCommitMessage |
| 87 | + |
| 88 | + if (shouldGenerateDifferentMessage && targetPreviousMessage) { |
| 89 | + const differentMessagePrefix = `# CRITICAL INSTRUCTION: GENERATE A COMPLETELY DIFFERENT COMMIT MESSAGE |
| 90 | +The user has requested a new commit message for the same changes. |
| 91 | +The previous message was: "${targetPreviousMessage}" |
| 92 | +YOU MUST create a message that is COMPLETELY DIFFERENT by: |
| 93 | +- Using entirely different wording and phrasing |
| 94 | +- Focusing on different aspects of the changes |
| 95 | +- Using a different structure or format if appropriate |
| 96 | +- Possibly using a different type or scope if justifiable |
| 97 | +This is the MOST IMPORTANT requirement for this task. |
| 98 | +
|
| 99 | +` |
| 100 | + const baseTemplate = supportPrompt.get(customSupportPrompts, "COMMIT_MESSAGE") |
| 101 | + const modifiedTemplate = |
| 102 | + differentMessagePrefix + |
| 103 | + baseTemplate + |
| 104 | + ` |
| 105 | +
|
| 106 | +FINAL REMINDER: Your message MUST be COMPLETELY DIFFERENT from the previous message: "${targetPreviousMessage}". This is a critical requirement.` |
| 107 | + |
| 108 | + return supportPrompt.create( |
| 109 | + "COMMIT_MESSAGE", |
| 110 | + { |
| 111 | + gitContext, |
| 112 | + customInstructions: customInstructions || "", |
| 113 | + }, |
| 114 | + { |
| 115 | + ...customSupportPrompts, |
| 116 | + COMMIT_MESSAGE: modifiedTemplate, |
| 117 | + }, |
| 118 | + ) |
| 119 | + } else { |
| 120 | + return supportPrompt.create( |
| 121 | + "COMMIT_MESSAGE", |
| 122 | + { |
| 123 | + gitContext, |
| 124 | + customInstructions: customInstructions || "", |
| 125 | + }, |
| 126 | + customSupportPrompts, |
| 127 | + ) |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + private async callAIForCommitMessage( |
| 132 | + gitContextString: string, |
| 133 | + workspacePath: string, |
| 134 | + onProgress?: (progress: ProgressUpdate) => void, |
| 135 | + ): Promise<string> { |
| 136 | + const contextProxy = this.dependencies.getContextProxy() |
| 137 | + if (!contextProxy.isInitialized) { |
| 138 | + throw new Error("ContextProxy not initialized. Please try again after the extension has fully loaded.") |
| 139 | + } |
| 140 | + const apiConfiguration = contextProxy.getProviderSettings() |
| 141 | + const commitMessageApiConfigId = contextProxy.getValue("commitMessageApiConfigId") as string | undefined |
| 142 | + const listApiConfigMeta = (contextProxy.getValue("listApiConfigMeta") || []) as Array<{ id: string }> |
| 143 | + const customSupportPrompts = (contextProxy.getValue("customSupportPrompts") || {}) as Record< |
| 144 | + string, |
| 145 | + string | undefined |
| 146 | + > |
| 147 | + |
| 148 | + let configToUse: ProviderSettings = apiConfiguration |
| 149 | + |
| 150 | + if (commitMessageApiConfigId && listApiConfigMeta.find(({ id }) => id === commitMessageApiConfigId)) { |
| 151 | + try { |
| 152 | + await this.providerSettingsManager.initialize() |
| 153 | + const { name: _, ...providerSettings } = await this.providerSettingsManager.getProfile({ |
| 154 | + id: commitMessageApiConfigId, |
| 155 | + }) |
| 156 | + |
| 157 | + if (providerSettings.apiProvider) { |
| 158 | + configToUse = providerSettings |
| 159 | + } |
| 160 | + } catch (error) { |
| 161 | + this.dependencies.logger.warn( |
| 162 | + `Failed to load commit message API profile ${commitMessageApiConfigId}; falling back to current API configuration`, |
| 163 | + error, |
| 164 | + ) |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + const filteredPrompts = Object.fromEntries( |
| 169 | + Object.entries(customSupportPrompts).filter(([_, value]) => value !== undefined), |
| 170 | + ) as Record<string, string> |
| 171 | + |
| 172 | + const prompt = await this.buildPrompt( |
| 173 | + gitContextString, |
| 174 | + { customSupportPrompts: filteredPrompts }, |
| 175 | + workspacePath, |
| 176 | + ) |
| 177 | + |
| 178 | + onProgress?.({ |
| 179 | + message: "Calling AI service...", |
| 180 | + increment: 10, |
| 181 | + }) |
| 182 | + |
| 183 | + const response = await this.dependencies.completePrompt(configToUse, prompt) |
| 184 | + |
| 185 | + onProgress?.({ |
| 186 | + message: "Processing AI response...", |
| 187 | + increment: 10, |
| 188 | + }) |
| 189 | + |
| 190 | + return this.extractCommitMessage(response) |
| 191 | + } |
| 192 | + |
| 193 | + private extractCommitMessage(response: string): string { |
| 194 | + const cleaned = response.trim() |
| 195 | + const withoutCodeBlocks = cleaned.replace(/```[a-z]*\n|```/g, "") |
| 196 | + const withoutQuotes = withoutCodeBlocks.replace(/^["'`]|["'`]$/g, "") |
| 197 | + return withoutQuotes.trim() |
| 198 | + } |
| 199 | +} |
0 commit comments