diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index d22ebdab2299..3443c9e52dc6 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -54,6 +54,7 @@ export const commandIds = [ "acceptInput", "focusPanel", "toggleAutoApprove", + "generateCommitMessage", ] as const export type CommandId = (typeof commandIds)[number] diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 41c127333d85..2d9291bcdb27 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -9,6 +9,8 @@ import { getCommand } from "../utils/commands" import { ClineProvider } from "../core/webview/ClineProvider" import { ContextProxy } from "../core/config/ContextProxy" import { focusPanel } from "../utils/focusPanel" +import { getStagedDiff, getStagedFiles } from "../utils/git" +import { getWorkspacePath } from "../utils/path" import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay" import { handleNewTask } from "./handleTask" @@ -233,6 +235,119 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt action: "toggleAutoApprove", }) }, + generateCommitMessage: async () => { + const cwd = getWorkspacePath() + if (!cwd) { + vscode.window.showErrorMessage("No workspace folder open") + return + } + + // Check if there are staged changes + const stagedFiles = await getStagedFiles(cwd) + if (stagedFiles.length === 0) { + vscode.window.showInformationMessage( + "No staged changes found. Please stage your changes first using 'git add'.", + ) + return + } + + // Get the staged diff + const stagedDiff = await getStagedDiff(cwd) + if ( + stagedDiff.startsWith("Failed") || + stagedDiff.startsWith("Git is not installed") || + stagedDiff.startsWith("Not a git repository") + ) { + vscode.window.showErrorMessage(stagedDiff) + return + } + + // Get the visible provider + const visibleProvider = getVisibleProviderOrLog(outputChannel) + if (!visibleProvider) { + vscode.window.showErrorMessage("Roo Code is not active. Please open the Roo Code sidebar first.") + return + } + + // Show progress notification + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Generating commit message...", + cancellable: false, + }, + async () => { + try { + // Create a task to generate the commit message + const prompt = `Generate a concise and descriptive commit message for the following staged changes. Follow conventional commit format (e.g., feat:, fix:, docs:, style:, refactor:, test:, chore:). The message should be clear and explain what changes were made and why. + +Staged files: +${stagedFiles.join("\n")} + +Diff of staged changes (truncated for context): +${stagedDiff} + +Please provide ONLY the commit message, without any additional explanation or formatting. The message should be on a single line unless a body is needed for complex changes.` + + // Create a task and wait for the response + const task = await visibleProvider.createTask(prompt) + + // Wait for the task to complete with a simpler approach + await new Promise((resolve) => { + let checkCount = 0 + const maxChecks = 300 // 30 seconds with 100ms intervals + + const checkInterval = setInterval(() => { + checkCount++ + const messages = task.clineMessages + + // Look for a message with type "say" and say "completion_result" + const completionMessage = messages.find( + (msg) => msg.type === "say" && msg.say === "completion_result", + ) + + if (completionMessage || checkCount >= maxChecks) { + clearInterval(checkInterval) + + if (completionMessage && completionMessage.text) { + // Extract the commit message from the completion result + const commitMessage = completionMessage.text.trim() + + if (commitMessage) { + // Get the SCM input box and set the message + const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports + if (gitExtension) { + const git = gitExtension.getAPI(1) + const repo = git.repositories[0] + if (repo) { + repo.inputBox.value = commitMessage + vscode.window.showInformationMessage( + "Commit message generated successfully!", + ) + } + } + } + } else if (checkCount >= maxChecks) { + vscode.window.showErrorMessage("Timeout generating commit message") + } + + // Clear the task + visibleProvider.clearTask() + resolve() + } + }, 100) + }) + } catch (error) { + vscode.window.showErrorMessage( + `Failed to generate commit message: ${error instanceof Error ? error.message : String(error)}`, + ) + if (visibleProvider) { + visibleProvider.clearTask() + } + } + }, + ) + }, }) export const openClineInNewTab = async ({ context, outputChannel }: Omit) => { diff --git a/src/package.json b/src/package.json index 782b6de070a9..903dbcf22969 100644 --- a/src/package.json +++ b/src/package.json @@ -179,6 +179,12 @@ "command": "roo-cline.toggleAutoApprove", "title": "%command.toggleAutoApprove.title%", "category": "%configuration.title%" + }, + { + "command": "roo-cline.generateCommitMessage", + "title": "%command.generateCommitMessage.title%", + "category": "%configuration.title%", + "icon": "$(sparkle)" } ], "menus": { @@ -305,6 +311,13 @@ "group": "overflow@4", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } + ], + "scm/title": [ + { + "command": "roo-cline.generateCommitMessage", + "group": "navigation", + "when": "scmProvider == git" + } ] }, "keybindings": [ diff --git a/src/package.nls.json b/src/package.nls.json index 1db69777ac17..90e87a579ee2 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -27,6 +27,7 @@ "command.terminal.explainCommand.title": "Explain This Command", "command.acceptInput.title": "Accept Input/Suggestion", "command.toggleAutoApprove.title": "Toggle Auto-Approve", + "command.generateCommitMessage.title": "Generate Commit Message", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled", "commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.", diff --git a/src/utils/git.ts b/src/utils/git.ts index 3bb562bf43f9..b9f57379b995 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -355,3 +355,70 @@ export async function getWorkingState(cwd: string): Promise { return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}` } } + +/** + * Gets the staged changes (diff) for commit message generation + * @param cwd The working directory + * @returns The staged diff or an error message + */ +export async function getStagedDiff(cwd: string): Promise { + try { + const isInstalled = await checkGitInstalled() + if (!isInstalled) { + return "Git is not installed" + } + + const isRepo = await checkGitRepo(cwd) + if (!isRepo) { + return "Not a git repository" + } + + // Get the staged diff + const { stdout: diff } = await execAsync("git diff --cached", { cwd }) + + if (!diff.trim()) { + return "No staged changes found. Please stage your changes first using 'git add'." + } + + // Limit the output to prevent overwhelming the AI + const lineLimit = GIT_OUTPUT_LINE_LIMIT + return truncateOutput(diff, lineLimit) + } catch (error) { + console.error("Error getting staged diff:", error) + return `Failed to get staged diff: ${error instanceof Error ? error.message : String(error)}` + } +} + +/** + * Gets a summary of staged files for commit message generation + * @param cwd The working directory + * @returns List of staged files with their status + */ +export async function getStagedFiles(cwd: string): Promise { + try { + const isInstalled = await checkGitInstalled() + if (!isInstalled) { + return [] + } + + const isRepo = await checkGitRepo(cwd) + if (!isRepo) { + return [] + } + + // Get list of staged files + const { stdout } = await execAsync("git diff --cached --name-status", { cwd }) + + if (!stdout.trim()) { + return [] + } + + return stdout + .trim() + .split("\n") + .filter((line) => line.length > 0) + } catch (error) { + console.error("Error getting staged files:", error) + return [] + } +}