Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const commandIds = [
"acceptInput",
"focusPanel",
"toggleAutoApprove",
"generateCommitMessage",
] as const

export type CommandId = (typeof commandIds)[number]
Expand Down
115 changes: 115 additions & 0 deletions src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User‐facing strings (e.g. error and info messages) are hard‐coded. Consider wrapping these messages with the i18n function t() so that they are localizable.

Suggested change
vscode.window.showErrorMessage("No workspace folder open")
vscode.window.showErrorMessage(t("No workspace folder open"))

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

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)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a full task instance for generating a commit message is inappropriate and causes resource leaks. The task system is designed for interactive, multi-step user operations, not one-off API calls. This approach: (1) adds the task to the provider's task stack without proper cleanup, (2) creates unnecessary state and event listeners, (3) persists the task to disk unnecessarily. Consider using the API handler directly with buildApiHandler(apiConfiguration).createMessage() instead of createTask().


// Wait for the task to complete with a simpler approach
await new Promise<void>((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)
Comment on lines +296 to +310
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This polling mechanism is fragile and prone to race conditions. It checks task messages every 100ms for up to 30 seconds, but there's no guarantee the message structure will be as expected when using createTask(). The check relies on internal task message format (type === 'say' && say === 'completion_result') which could change or be missing. If the task fails, times out, or produces unexpected message format, this will silently fail after 30 seconds without proper error handling.


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()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task created for commit message generation is never properly cleaned up. After extracting the commit message, visibleProvider.clearTask() is called, but this only removes the task from the stack without proper disposal. The task will remain in task history, pollute the UI with unnecessary entries, and leak resources (event listeners, file watchers, etc.). Consider calling task.dispose() explicitly or better yet, avoid creating a task entirely for this use case.

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<RegisterCommandOptions, "provider">) => {
Expand Down
13 changes: 13 additions & 0 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -305,6 +311,13 @@
"group": "overflow@4",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
}
],
"scm/title": [
{
"command": "roo-cline.generateCommitMessage",
"group": "navigation",
"when": "scmProvider == git"
}
]
},
"keybindings": [
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
67 changes: 67 additions & 0 deletions src/utils/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,70 @@ export async function getWorkingState(cwd: string): Promise<string> {
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<string> {
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<string[]> {
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 []
}
}
Loading