From c970b8030e7ceeb1cd090c8a28e2dc5fb40deded Mon Sep 17 00:00:00 2001 From: Bee <68532117+abeatrix@users.noreply.github.com> Date: Mon, 21 Jul 2025 10:43:14 -0700 Subject: [PATCH] Refactor: Git commit message generation as a module (#5031) * Refactor: Git commit message generation as a module Implement Git commit message generation as a module Refactors the Git commit message generation functionality into a separate module for better organization and maintainability. The changes include: - Moving the commit message generation logic from `src/core/controller/index.ts` to a new module `src/integrations/git/commit-message-generator.ts`. - Adding a command to abort commit message generation. - Updating the `cline.generateGitCommitMessage` command to use the new module. - Adding a context key to disable the generate commit message button when a commit is generating. This refactoring improves the codebase by: - Separating concerns: The commit message generation logic is now isolated in its own module, making it easier to understand and maintain. - Improving testability: The new module can be tested independently of the controller. - Promoting code reuse: The commit message generation logic can be reused in other parts of the application if needed. * clean up * changeset added --- .changeset/nasty-vans-mate.md | 5 + package.json | 21 ++- src/core/controller/index.ts | 134 +----------------- src/extension.ts | 24 +--- .../git/commit-message-generator.ts | 134 +++++++++++++++--- 5 files changed, 148 insertions(+), 170 deletions(-) create mode 100644 .changeset/nasty-vans-mate.md diff --git a/.changeset/nasty-vans-mate.md b/.changeset/nasty-vans-mate.md new file mode 100644 index 0000000000..431617a479 --- /dev/null +++ b/.changeset/nasty-vans-mate.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Refactor Git commit message generation to support output streaming. diff --git a/package.json b/package.json index 760f3425d8..230ceef9e2 100644 --- a/package.json +++ b/package.json @@ -186,6 +186,12 @@ "category": "Cline", "icon": "$(robot)" }, + { + "command": "cline.abortGitCommitMessage", + "title": "Generate Commit Message with Cline - Stop", + "category": "Cline", + "icon": "$(debug-stop)" + }, { "command": "cline.explainCode", "title": "Explain with Cline", @@ -213,7 +219,7 @@ }, { "command": "cline.generateGitCommitMessage", - "when": "scmProvider == git" + "when": "config.git.enabled && scmProvider == git" }, { "command": "cline.focusChatInput", @@ -306,13 +312,22 @@ { "command": "cline.generateGitCommitMessage", "group": "navigation", - "when": "scmProvider == git" + "when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit" + }, + { + "command": "cline.abortGitCommitMessage", + "group": "navigation", + "when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit" } ], "commandPalette": [ { "command": "cline.generateGitCommitMessage", - "when": "scmProvider == git" + "when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit" + }, + { + "command": "cline.abortGitCommitMessage", + "when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit" } ] }, diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index c450331878..df5ce7addc 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -34,7 +34,7 @@ import { sendStateUpdate } from "./state/subscribeToState" import { sendAddToInputEvent } from "./ui/subscribeToAddToInput" import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog" import { AuthService } from "@/services/auth/AuthService" -import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window" +import { ShowMessageType } from "@/shared/proto/host/window" import { getHostBridgeProvider } from "@/hosts/host-providers" import { clineEnvConfig } from "@/config" @@ -852,135 +852,5 @@ export class Controller { // secrets - // Git commit message generation - - async generateGitCommitMessage() { - try { - // Check if there's a workspace folder open - const cwd = await getCwd() - if (!cwd) { - getHostBridgeProvider().windowClient.showMessage({ - type: ShowMessageType.ERROR, - message: "No workspace folder open", - }) - return - } - - // Get the git diff - const gitDiff = await getWorkingState(cwd) - if (gitDiff === "No changes in working directory") { - getHostBridgeProvider().windowClient.showMessage({ - type: ShowMessageType.INFORMATION, - message: "No changes in workspace for commit message", - }) - return - } - - // Show a progress notification - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: "Generating commit message...", - cancellable: false, - }, - async (progress, token) => { - try { - // Format the git diff into a prompt - const prompt = `Based on the following git diff, generate a concise and descriptive commit message: - -${gitDiff.length > 5000 ? gitDiff.substring(0, 5000) + "\n\n[Diff truncated due to size]" : gitDiff} - -The commit message should: -1. Start with a short summary (50-72 characters) -2. Use the imperative mood (e.g., "Add feature" not "Added feature") -3. Describe what was changed and why -4. Be clear and descriptive - -Commit message:` - - // Get the current API configuration - const { apiConfiguration } = await getAllExtensionState(this.context) - const currentMode = await this.getCurrentMode() - - // Build the API handler - const apiHandler = buildApiHandler(apiConfiguration, currentMode) - - // Create a system prompt - const systemPrompt = - "You are a helpful assistant that generates concise and descriptive git commit messages based on git diffs." - - // Create a message for the API - const messages = [ - { - role: "user" as const, - content: prompt, - }, - ] - - // Call the API directly - const stream = apiHandler.createMessage(systemPrompt, messages) - - // Collect the response - let response = "" - for await (const chunk of stream) { - if (chunk.type === "text") { - response += chunk.text - } - } - - // Extract the commit message - const commitMessage = extractCommitMessage(response) - - // Apply the commit message to the Git input box - if (commitMessage) { - // Get the Git extension API - const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports - if (gitExtension) { - const api = gitExtension.getAPI(1) - if (api && api.repositories.length > 0) { - const repo = api.repositories[0] - repo.inputBox.value = commitMessage - const message = "Commit message generated and applied" - getHostBridgeProvider().windowClient.showMessage({ - type: ShowMessageType.INFORMATION, - message, - }) - } else { - const message = "No Git repositories found" - getHostBridgeProvider().windowClient.showMessage({ - type: ShowMessageType.ERROR, - message, - }) - } - } else { - const message = "Git extension not found" - getHostBridgeProvider().windowClient.showMessage({ - type: ShowMessageType.ERROR, - message, - }) - } - } else { - const message = "Failed to generate commit message" - getHostBridgeProvider().windowClient.showMessage({ - type: ShowMessageType.ERROR, - message, - }) - } - } catch (innerError) { - const innerErrorMessage = innerError instanceof Error ? innerError.message : String(innerError) - getHostBridgeProvider().windowClient.showMessage({ - type: ShowMessageType.ERROR, - message: `Failed to generate commit message: ${innerErrorMessage}`, - }) - } - }, - ) - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - getHostBridgeProvider().windowClient.showMessage({ - type: ShowMessageType.ERROR, - message: `Failed to generate commit message: ${errorMessage}`, - }) - } - } + // dev } diff --git a/src/extension.ts b/src/extension.ts index 2c75bfe21f..ec1d5b5549 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,6 @@ import { DIFF_VIEW_URI_SCHEME } from "@hosts/vscode/VscodeDiffViewProvider" import assert from "node:assert" import { posthogClientProvider } from "./services/posthog/PostHogClientProvider" import { WebviewProvider } from "./core/webview" -import { Controller } from "./core/controller" import { sendMcpButtonClickedEvent } from "./core/controller/ui/subscribeToMcpButtonClicked" import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChatButtonClicked" import { ErrorService } from "./services/error/ErrorService" @@ -40,7 +39,8 @@ import { AuthService } from "./services/auth/AuthService" import { writeTextToClipboard, readTextFromClipboard } from "@/utils/env" import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider" import { getHostBridgeProvider } from "@hosts/host-providers" -import { ShowMessageRequest, ShowMessageType } from "./shared/proto/host/window" +import { ShowMessageType } from "./shared/proto/host/window" +import { GitCommitGenerator } from "./integrations/git/commit-message-generator" /* Built using https://github.com/microsoft/vscode-webview-ui-toolkit @@ -666,21 +666,11 @@ export async function activate(context: vscode.ExtensionContext) { // Register the generateGitCommitMessage command handler context.subscriptions.push( - vscode.commands.registerCommand("cline.generateGitCommitMessage", async () => { - // Get the controller from any instance, without activating the view - const controller = WebviewProvider.getAllInstances()[0]?.controller - - if (controller) { - // Call the controller method to generate commit message - await controller.generateGitCommitMessage() - } else { - // Create a temporary controller just for this operation - const outputChannel = vscode.window.createOutputChannel("Cline Commit Generator") - const tempController = new Controller(context, outputChannel, () => Promise.resolve(true), uuidv4()) - - await tempController.generateGitCommitMessage() - outputChannel.dispose() - } + vscode.commands.registerCommand("cline.generateGitCommitMessage", async (scm) => { + await GitCommitGenerator?.generate?.(context, scm) + }), + vscode.commands.registerCommand("cline.abortGitCommitMessage", () => { + GitCommitGenerator?.abort?.() }), ) diff --git a/src/integrations/git/commit-message-generator.ts b/src/integrations/git/commit-message-generator.ts index 5036ec6663..8e39fcc934 100644 --- a/src/integrations/git/commit-message-generator.ts +++ b/src/integrations/git/commit-message-generator.ts @@ -1,7 +1,116 @@ import * as vscode from "vscode" import { writeTextToClipboard } from "@utils/env" import { getHostBridgeProvider } from "@/hosts/host-providers" -import { ShowMessageType, ShowTextDocumentRequest, ShowMessageRequest } from "@/shared/proto/host/window" +import { ShowMessageType, ShowTextDocumentRequest } from "@/shared/proto/host/window" +import { buildApiHandler } from "@/api" +import { getAllExtensionState } from "@/core/storage/state" +import { getWorkingState } from "@/utils/git" +import { getCwd } from "@/utils/path" + +/** + * Git commit message generator module + */ +export const GitCommitGenerator = { + generate, + abort, +} + +let commitGenerationAbortController: AbortController | undefined = undefined + +async function generate(context: vscode.ExtensionContext, scm?: vscode.SourceControl) { + const cwd = await getCwd() + if (!context || !cwd) { + getHostBridgeProvider().windowClient.showMessage({ + type: ShowMessageType.ERROR, + message: "No workspace folder open", + }) + return + } + + const gitDiff = await getWorkingState(cwd) + if (gitDiff === "No changes in working directory") { + getHostBridgeProvider().windowClient.showMessage({ + type: ShowMessageType.INFORMATION, + message: "No changes in workspace for commit message", + }) + return + } + + const inputBox = scm?.inputBox + if (!inputBox) { + vscode.window.showErrorMessage("Git extension not found or no repositories available") + return + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.SourceControl, + title: "Generating commit message...", + cancellable: true, + }, + () => performCommitGeneration(context, gitDiff, inputBox), + ) +} + +async function performCommitGeneration(context: vscode.ExtensionContext, gitDiff: string, inputBox: any) { + try { + vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", true) + + const truncatedDiff = gitDiff.length > 5000 ? gitDiff.substring(0, 5000) + "\n\n[Diff truncated due to size]" : gitDiff + + const prompt = `Based on the following git diff, generate a concise and descriptive commit message: +${truncatedDiff} +The commit message should: +1. Start with a short summary (50-72 characters) +2. Use the imperative mood (e.g., "Add feature" not "Added feature") +3. Describe what was changed and why +4. Be clear and descriptive +Commit message:` + + // Get the current API configuration + const { apiConfiguration } = await getAllExtensionState(context) + // Set to use Act mode for now by default + // TODO: A new mode for commit generation + const currentMode = "act" + + // Build the API handler + const apiHandler = buildApiHandler(apiConfiguration, currentMode) + + // Create a system prompt + const systemPrompt = + "You are a helpful assistant that generates concise and descriptive git commit messages based on git diffs." + + // Create a message for the API + const messages = [{ role: "user" as const, content: prompt }] + + commitGenerationAbortController = new AbortController() + const stream = apiHandler.createMessage(systemPrompt, messages) + + let response = "" + for await (const chunk of stream) { + commitGenerationAbortController.signal.throwIfAborted() + if (chunk.type === "text") { + response += chunk.text + inputBox.value = extractCommitMessage(response) + } + } + + if (!inputBox.value) { + throw new Error("empty API response") + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`) + } finally { + vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", false) + } +} + +function abort() { + commitGenerationAbortController?.abort() + vscode.commands.executeCommand("setContext", "cline.isGeneratingCommit", false) +} + /** * Formats the git diff into a prompt for the AI * @param gitDiff The git diff to format @@ -31,26 +140,15 @@ Commit message:` /** * Extracts the commit message from the AI response - * @param aiResponse The response from the AI + * @param str String containing the AI response * @returns The extracted commit message */ -export function extractCommitMessage(aiResponse: string): string { +export function extractCommitMessage(str: string): string { // Remove any markdown formatting or extra text - let message = aiResponse.trim() - - // Remove markdown code blocks if present - if (message.startsWith("```") && message.endsWith("```")) { - message = message.substring(3, message.length - 3).trim() - - // Remove language identifier if present (e.g., ```git) - const firstLineBreak = message.indexOf("\n") - if (firstLineBreak > 0 && firstLineBreak < 20) { - // Reasonable length for a language identifier - message = message.substring(firstLineBreak).trim() - } - } - - return message + return str + .trim() + .replace(/^```[^\n]*\n?|```$/g, "") + .trim() } /**