Skip to content

Commit 4d29f89

Browse files
committed
feat: add generate commit message feature for staged changes
- Add generateCommitMessage command to package.json with SCM menu integration - Implement git utility functions to get staged diff and files - Create command handler that uses AI to generate conventional commit messages - Add localization support for the new command - Add sparkle icon to the SCM title bar for easy access This feature allows users to generate AI-powered commit messages based on their staged changes, following conventional commit format.
1 parent b92a22b commit 4d29f89

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

packages/types/src/vscode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const commandIds = [
5454
"acceptInput",
5555
"focusPanel",
5656
"toggleAutoApprove",
57+
"generateCommitMessage",
5758
] as const
5859

5960
export type CommandId = (typeof commandIds)[number]

src/activate/registerCommands.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { getCommand } from "../utils/commands"
99
import { ClineProvider } from "../core/webview/ClineProvider"
1010
import { ContextProxy } from "../core/config/ContextProxy"
1111
import { focusPanel } from "../utils/focusPanel"
12+
import { getStagedDiff, getStagedFiles } from "../utils/git"
13+
import { getWorkspacePath } from "../utils/path"
1214

1315
import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay"
1416
import { handleNewTask } from "./handleTask"
@@ -233,6 +235,119 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
233235
action: "toggleAutoApprove",
234236
})
235237
},
238+
generateCommitMessage: async () => {
239+
const cwd = getWorkspacePath()
240+
if (!cwd) {
241+
vscode.window.showErrorMessage("No workspace folder open")
242+
return
243+
}
244+
245+
// Check if there are staged changes
246+
const stagedFiles = await getStagedFiles(cwd)
247+
if (stagedFiles.length === 0) {
248+
vscode.window.showInformationMessage(
249+
"No staged changes found. Please stage your changes first using 'git add'.",
250+
)
251+
return
252+
}
253+
254+
// Get the staged diff
255+
const stagedDiff = await getStagedDiff(cwd)
256+
if (
257+
stagedDiff.startsWith("Failed") ||
258+
stagedDiff.startsWith("Git is not installed") ||
259+
stagedDiff.startsWith("Not a git repository")
260+
) {
261+
vscode.window.showErrorMessage(stagedDiff)
262+
return
263+
}
264+
265+
// Get the visible provider
266+
const visibleProvider = getVisibleProviderOrLog(outputChannel)
267+
if (!visibleProvider) {
268+
vscode.window.showErrorMessage("Roo Code is not active. Please open the Roo Code sidebar first.")
269+
return
270+
}
271+
272+
// Show progress notification
273+
await vscode.window.withProgress(
274+
{
275+
location: vscode.ProgressLocation.Notification,
276+
title: "Generating commit message...",
277+
cancellable: false,
278+
},
279+
async () => {
280+
try {
281+
// Create a task to generate the commit message
282+
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.
283+
284+
Staged files:
285+
${stagedFiles.join("\n")}
286+
287+
Diff of staged changes (truncated for context):
288+
${stagedDiff}
289+
290+
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.`
291+
292+
// Create a task and wait for the response
293+
const task = await visibleProvider.createTask(prompt)
294+
295+
// Wait for the task to complete with a simpler approach
296+
await new Promise<void>((resolve) => {
297+
let checkCount = 0
298+
const maxChecks = 300 // 30 seconds with 100ms intervals
299+
300+
const checkInterval = setInterval(() => {
301+
checkCount++
302+
const messages = task.clineMessages
303+
304+
// Look for a message with type "say" and say "completion_result"
305+
const completionMessage = messages.find(
306+
(msg) => msg.type === "say" && msg.say === "completion_result",
307+
)
308+
309+
if (completionMessage || checkCount >= maxChecks) {
310+
clearInterval(checkInterval)
311+
312+
if (completionMessage && completionMessage.text) {
313+
// Extract the commit message from the completion result
314+
const commitMessage = completionMessage.text.trim()
315+
316+
if (commitMessage) {
317+
// Get the SCM input box and set the message
318+
const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports
319+
if (gitExtension) {
320+
const git = gitExtension.getAPI(1)
321+
const repo = git.repositories[0]
322+
if (repo) {
323+
repo.inputBox.value = commitMessage
324+
vscode.window.showInformationMessage(
325+
"Commit message generated successfully!",
326+
)
327+
}
328+
}
329+
}
330+
} else if (checkCount >= maxChecks) {
331+
vscode.window.showErrorMessage("Timeout generating commit message")
332+
}
333+
334+
// Clear the task
335+
visibleProvider.clearTask()
336+
resolve()
337+
}
338+
}, 100)
339+
})
340+
} catch (error) {
341+
vscode.window.showErrorMessage(
342+
`Failed to generate commit message: ${error instanceof Error ? error.message : String(error)}`,
343+
)
344+
if (visibleProvider) {
345+
visibleProvider.clearTask()
346+
}
347+
}
348+
},
349+
)
350+
},
236351
})
237352

238353
export const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterCommandOptions, "provider">) => {

src/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@
179179
"command": "roo-cline.toggleAutoApprove",
180180
"title": "%command.toggleAutoApprove.title%",
181181
"category": "%configuration.title%"
182+
},
183+
{
184+
"command": "roo-cline.generateCommitMessage",
185+
"title": "%command.generateCommitMessage.title%",
186+
"category": "%configuration.title%",
187+
"icon": "$(sparkle)"
182188
}
183189
],
184190
"menus": {
@@ -305,6 +311,13 @@
305311
"group": "overflow@4",
306312
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
307313
}
314+
],
315+
"scm/title": [
316+
{
317+
"command": "roo-cline.generateCommitMessage",
318+
"group": "navigation",
319+
"when": "scmProvider == git"
320+
}
308321
]
309322
},
310323
"keybindings": [

src/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"command.terminal.explainCommand.title": "Explain This Command",
2828
"command.acceptInput.title": "Accept Input/Suggestion",
2929
"command.toggleAutoApprove.title": "Toggle Auto-Approve",
30+
"command.generateCommitMessage.title": "Generate Commit Message",
3031
"configuration.title": "Roo Code",
3132
"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
3233
"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.",

src/utils/git.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,70 @@ export async function getWorkingState(cwd: string): Promise<string> {
355355
return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
356356
}
357357
}
358+
359+
/**
360+
* Gets the staged changes (diff) for commit message generation
361+
* @param cwd The working directory
362+
* @returns The staged diff or an error message
363+
*/
364+
export async function getStagedDiff(cwd: string): Promise<string> {
365+
try {
366+
const isInstalled = await checkGitInstalled()
367+
if (!isInstalled) {
368+
return "Git is not installed"
369+
}
370+
371+
const isRepo = await checkGitRepo(cwd)
372+
if (!isRepo) {
373+
return "Not a git repository"
374+
}
375+
376+
// Get the staged diff
377+
const { stdout: diff } = await execAsync("git diff --cached", { cwd })
378+
379+
if (!diff.trim()) {
380+
return "No staged changes found. Please stage your changes first using 'git add'."
381+
}
382+
383+
// Limit the output to prevent overwhelming the AI
384+
const lineLimit = GIT_OUTPUT_LINE_LIMIT
385+
return truncateOutput(diff, lineLimit)
386+
} catch (error) {
387+
console.error("Error getting staged diff:", error)
388+
return `Failed to get staged diff: ${error instanceof Error ? error.message : String(error)}`
389+
}
390+
}
391+
392+
/**
393+
* Gets a summary of staged files for commit message generation
394+
* @param cwd The working directory
395+
* @returns List of staged files with their status
396+
*/
397+
export async function getStagedFiles(cwd: string): Promise<string[]> {
398+
try {
399+
const isInstalled = await checkGitInstalled()
400+
if (!isInstalled) {
401+
return []
402+
}
403+
404+
const isRepo = await checkGitRepo(cwd)
405+
if (!isRepo) {
406+
return []
407+
}
408+
409+
// Get list of staged files
410+
const { stdout } = await execAsync("git diff --cached --name-status", { cwd })
411+
412+
if (!stdout.trim()) {
413+
return []
414+
}
415+
416+
return stdout
417+
.trim()
418+
.split("\n")
419+
.filter((line) => line.length > 0)
420+
} catch (error) {
421+
console.error("Error getting staged files:", error)
422+
return []
423+
}
424+
}

0 commit comments

Comments
 (0)