diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index fd28f2e9945..d78d4a3e9e0 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -47,6 +47,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 f02ee8309a3..a350c2a5f8d 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -14,6 +14,12 @@ import { CodeIndexManager } from "../services/code-index/manager" import { importSettingsWithFeedback } from "../core/config/importExport" import { MdmService } from "../services/mdm/MdmService" import { t } from "../i18n" +import { + getGitDiff, + generateCommitMessageFromDiff, + getWorkspaceRoot, + setScmInputBoxMessage, +} from "../utils/commit-message-generator" /** * Helper to get the visible ClineProvider instance or log if not found. @@ -195,6 +201,82 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt action: "toggleAutoApprove", }) }, + generateCommitMessage: async () => { + const workspaceRoot = getWorkspaceRoot() + + if (!workspaceRoot) { + vscode.window.showErrorMessage(t("common:commit.no_workspace")) + return + } + + const visibleProvider = getVisibleProviderOrLog(outputChannel) + + if (!visibleProvider) { + return + } + + // Let the user optionally pick a different API profile + const listApiConfigMeta = await visibleProvider.providerSettingsManager.listConfig() + let apiConfiguration = visibleProvider.contextProxy.getProviderSettings() + + if (listApiConfigMeta.length > 1) { + const items = [ + { label: t("common:commit.use_current_profile"), id: undefined }, + ...listApiConfigMeta.map((config) => ({ + label: config.name ?? config.id, + id: config.id, + })), + ] + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: t("common:commit.select_profile"), + }) + + if (!selected) { + return // User cancelled + } + + if (selected.id) { + const { name: _, ...providerSettings } = await visibleProvider.providerSettingsManager.getProfile({ + id: selected.id, + }) + + if (providerSettings.apiProvider) { + apiConfiguration = providerSettings + } + } + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: t("common:commit.generating"), + cancellable: false, + }, + async () => { + try { + const diff = await getGitDiff(workspaceRoot) + + if (!diff.trim()) { + vscode.window.showInformationMessage(t("common:commit.no_changes")) + return + } + + const commitMessage = await generateCommitMessageFromDiff(apiConfiguration, diff) + const success = await setScmInputBoxMessage(commitMessage) + + if (success) { + // Focus the SCM view to show the generated message + await vscode.commands.executeCommand("workbench.view.scm") + } + } catch (error) { + vscode.window.showErrorMessage( + `${t("common:commit.generation_failed")}: ${error instanceof Error ? error.message : String(error)}`, + ) + } + }, + ) + }, }) export const openClineInNewTab = async ({ context, outputChannel }: Omit) => { diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 33188fce193..48ec1494992 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "No s'ha trobat cap carpeta de treball. Obre una carpeta per generar un missatge de commit.", + "generating": "Generant missatge de commit...", + "no_changes": "No s'han detectat canvis. Prepara o modifica fitxers abans de generar un missatge de commit.", + "generation_failed": "No s'ha pogut generar el missatge de commit", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Utilitza el perfil actual", + "select_profile": "Selecciona un perfil d'API per a la generació de missatges de commit" } } diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 861d9da5768..8591ff40028 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -246,5 +246,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Kein Arbeitsbereichsordner gefunden. Öffne einen Ordner, um eine Commit-Nachricht zu generieren.", + "generating": "Commit-Nachricht wird generiert...", + "no_changes": "Keine Änderungen erkannt. Bereite Dateien vor oder ändere sie, bevor du eine Commit-Nachricht generierst.", + "generation_failed": "Commit-Nachricht konnte nicht generiert werden", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Aktuelles Profil verwenden", + "select_profile": "Wähle ein API-Profil für die Commit-Nachricht-Generierung" } } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index d65fe183679..51aaadc43ef 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -243,5 +243,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "No workspace folder found. Open a folder to generate a commit message.", + "generating": "Generating commit message...", + "no_changes": "No changes detected. Stage or modify files before generating a commit message.", + "generation_failed": "Failed to generate commit message", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Use current profile", + "select_profile": "Select an API profile for commit message generation" } } diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 82be83956b0..a533653a6d6 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -246,5 +246,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "No se encontró carpeta de trabajo. Abre una carpeta para generar un mensaje de commit.", + "generating": "Generando mensaje de commit...", + "no_changes": "No se detectaron cambios. Prepara o modifica archivos antes de generar un mensaje de commit.", + "generation_failed": "Error al generar el mensaje de commit", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Usar perfil actual", + "select_profile": "Selecciona un perfil de API para la generación de mensajes de commit" } } diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 6fc05ff94a3..f768fb7d0b0 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Aucun dossier de travail trouvé. Ouvre un dossier pour générer un message de commit.", + "generating": "Génération du message de commit...", + "no_changes": "Aucun changement détecté. Prépare ou modifie des fichiers avant de générer un message de commit.", + "generation_failed": "Échec de la génération du message de commit", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Utiliser le profil actuel", + "select_profile": "Sélectionne un profil d'API pour la génération de messages de commit" } } diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 528ed6d45f5..ceb579ea7b7 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "कोई कार्यस्थान फ़ोल्डर नहीं मिला। कमिट संदेश बनाने के लिए एक फ़ोल्डर खोलें।", + "generating": "कमिट संदेश बनाया जा रहा है...", + "no_changes": "कोई बदलाव नहीं मिला। कमिट संदेश बनाने से पहले फ़ाइलें तैयार करें या बदलें।", + "generation_failed": "कमिट संदेश बनाने में विफल", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "वर्तमान प्रोफ़ाइल का उपयोग करें", + "select_profile": "कमिट संदेश बनाने के लिए एक API प्रोफ़ाइल चुनें" } } diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index cb1c3231fb8..243ef822f9a 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Folder workspace tidak ditemukan. Buka folder untuk membuat pesan commit.", + "generating": "Membuat pesan commit...", + "no_changes": "Tidak ada perubahan terdeteksi. Siapkan atau ubah file sebelum membuat pesan commit.", + "generation_failed": "Gagal membuat pesan commit", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Gunakan profil saat ini", + "select_profile": "Pilih profil API untuk pembuatan pesan commit" } } diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index b4e522cb732..8e7e7223927 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Nessuna cartella di lavoro trovata. Apri una cartella per generare un messaggio di commit.", + "generating": "Generazione del messaggio di commit...", + "no_changes": "Nessuna modifica rilevata. Prepara o modifica i file prima di generare un messaggio di commit.", + "generation_failed": "Impossibile generare il messaggio di commit", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Usa il profilo corrente", + "select_profile": "Seleziona un profilo API per la generazione del messaggio di commit" } } diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 7b63b6f7298..ae92ab3bb9a 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "ワークスペースフォルダーが見つかりません。コミットメッセージを生成するにはフォルダーを開いてください。", + "generating": "コミットメッセージを生成中...", + "no_changes": "変更が検出されませんでした。コミットメッセージを生成する前にファイルをステージするか変更してください。", + "generation_failed": "コミットメッセージの生成に失敗しました", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "現在のプロファイルを使用", + "select_profile": "コミットメッセージ生成用のAPIプロファイルを選択" } } diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index fbde3225bb1..aec996a38cf 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "워크스페이스 폴더를 찾을 수 없습니다. 커밋 메시지를 생성하려면 폴더를 열어주세요.", + "generating": "커밋 메시지 생성 중...", + "no_changes": "변경 사항이 없습니다. 커밋 메시지를 생성하기 전에 파일을 스테이지하거나 수정하세요.", + "generation_failed": "커밋 메시지 생성에 실패했습니다", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "현재 프로필 사용", + "select_profile": "커밋 메시지 생성에 사용할 API 프로필을 선택하세요" } } diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index eba274c96ec..bd1294f147d 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Geen werkruimtemap gevonden. Open een map om een commitbericht te genereren.", + "generating": "Commitbericht genereren...", + "no_changes": "Geen wijzigingen gedetecteerd. Stage of wijzig bestanden voordat je een commitbericht genereert.", + "generation_failed": "Kan commitbericht niet genereren", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Huidig profiel gebruiken", + "select_profile": "Selecteer een API-profiel voor het genereren van commitberichten" } } diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 20b568281bb..90b4f1860bf 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Nie znaleziono folderu roboczego. Otwórz folder, aby wygenerować wiadomość commita.", + "generating": "Generowanie wiadomości commita...", + "no_changes": "Nie wykryto zmian. Przygotuj lub zmodyfikuj pliki przed wygenerowaniem wiadomości commita.", + "generation_failed": "Nie udało się wygenerować wiadomości commita", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Użyj bieżącego profilu", + "select_profile": "Wybierz profil API do generowania wiadomości commita" } } diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 38abc8c8047..2e256bd8afc 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Nenhuma pasta de trabalho encontrada. Abra uma pasta para gerar uma mensagem de commit.", + "generating": "Gerando mensagem de commit...", + "no_changes": "Nenhuma alteração detectada. Prepare ou modifique arquivos antes de gerar uma mensagem de commit.", + "generation_failed": "Falha ao gerar mensagem de commit", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Usar perfil atual", + "select_profile": "Selecione um perfil de API para a geração de mensagens de commit" } } diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index d124f597318..71a04ae742f 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Папка рабочего пространства не найдена. Откройте папку для генерации сообщения коммита.", + "generating": "Генерация сообщения коммита...", + "no_changes": "Изменения не обнаружены. Подготовьте или измените файлы перед генерацией сообщения коммита.", + "generation_failed": "Не удалось сгенерировать сообщение коммита", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Использовать текущий профиль", + "select_profile": "Выберите профиль API для генерации сообщения коммита" } } diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 00dcf6fc33d..b00a3a5f353 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Çalışma alanı klasörü bulunamadı. Commit mesajı oluşturmak için bir klasör aç.", + "generating": "Commit mesajı oluşturuluyor...", + "no_changes": "Değişiklik algılanmadı. Commit mesajı oluşturmadan önce dosyaları hazırla veya değiştir.", + "generation_failed": "Commit mesajı oluşturulamadı", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Mevcut profili kullan", + "select_profile": "Commit mesajı oluşturma için bir API profili seç" } } diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index decd4ff53ef..0f18f74d2c2 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -258,5 +258,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "Không tìm thấy thư mục làm việc. Mở một thư mục để tạo thông điệp commit.", + "generating": "Đang tạo thông điệp commit...", + "no_changes": "Không phát hiện thay đổi nào. Hãy chuẩn bị hoặc chỉnh sửa tệp trước khi tạo thông điệp commit.", + "generation_failed": "Không thể tạo thông điệp commit", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "Sử dụng hồ sơ hiện tại", + "select_profile": "Chọn một hồ sơ API để tạo thông điệp commit" } } diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 6df1f78b167..047727812db 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -256,5 +256,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "未找到工作区文件夹。请打开一个文件夹以生成提交信息。", + "generating": "正在生成提交信息...", + "no_changes": "未检测到更改。请在生成提交信息之前暂存或修改文件。", + "generation_failed": "生成提交信息失败", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "使用当前配置", + "select_profile": "选择用于生成提交信息的 API 配置" } } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index be4a76fc5b9..2ddb6a92a31 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -251,5 +251,15 @@ "docsLink": { "label": "Docs", "url": "https://docs.roocode.com" + }, + "commit": { + "no_workspace": "未找到工作區資料夾。請開啟一個資料夾以產生提交訊息。", + "generating": "正在產生提交訊息...", + "no_changes": "未偵測到變更。請在產生提交訊息之前暫存或修改檔案。", + "generation_failed": "產生提交訊息失敗", + "no_git_extension": "Git extension is not available.", + "no_git_repo": "No git repository found.", + "use_current_profile": "使用目前的設定檔", + "select_profile": "選擇用於產生提交訊息的 API 設定檔" } } diff --git a/src/package.json b/src/package.json index 7c4889abd89..2f5a733237c 100644 --- a/src/package.json +++ b/src/package.json @@ -169,6 +169,12 @@ "command": "roo-cline.toggleAutoApprove", "title": "%command.toggleAutoApprove.title%", "category": "%configuration.title%" + }, + { + "command": "roo-cline.generateCommitMessage", + "title": "%command.generateCommitMessage.title%", + "icon": "$(sparkle)", + "category": "%configuration.title%" } ], "menus": { @@ -275,6 +281,13 @@ "group": "overflow@2", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } + ], + "scm/title": [ + { + "command": "roo-cline.generateCommitMessage", + "group": "navigation", + "when": "scmProvider == git" + } ] }, "keybindings": [ diff --git a/src/package.nls.ca.json b/src/package.nls.ca.json index 2781ed169cf..31cdcddd9b4 100644 --- a/src/package.nls.ca.json +++ b/src/package.nls.ca.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Explicar Aquesta Ordre", "command.acceptInput.title": "Acceptar Entrada/Suggeriment", "command.toggleAutoApprove.title": "Alternar Auto-Aprovació", + "command.generateCommitMessage.title": "Generar Missatge de Commit", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.de.json b/src/package.nls.de.json index a77a253ef06..fbc49f2b482 100644 --- a/src/package.nls.de.json +++ b/src/package.nls.de.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Diesen Befehl Erklären", "command.acceptInput.title": "Eingabe/Vorschlag Akzeptieren", "command.toggleAutoApprove.title": "Auto-Genehmigung Umschalten", + "command.generateCommitMessage.title": "Commit-Nachricht Generieren", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.es.json b/src/package.nls.es.json index a1c729080e2..a8bdf0a4c44 100644 --- a/src/package.nls.es.json +++ b/src/package.nls.es.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Explicar Este Comando", "command.acceptInput.title": "Aceptar Entrada/Sugerencia", "command.toggleAutoApprove.title": "Alternar Auto-Aprobación", + "command.generateCommitMessage.title": "Generar Mensaje de Commit", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.fr.json b/src/package.nls.fr.json index 2d009c0038d..c2a72cd679c 100644 --- a/src/package.nls.fr.json +++ b/src/package.nls.fr.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Expliquer cette Commande", "command.acceptInput.title": "Accepter l'Entrée/Suggestion", "command.toggleAutoApprove.title": "Basculer Auto-Approbation", + "command.generateCommitMessage.title": "Générer un Message de Commit", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.hi.json b/src/package.nls.hi.json index c51f3ee95ee..42e510f7f73 100644 --- a/src/package.nls.hi.json +++ b/src/package.nls.hi.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "यह कमांड समझाएं", "command.acceptInput.title": "इनपुट/सुझाव स्वीकारें", "command.toggleAutoApprove.title": "ऑटो-अनुमोदन टॉगल करें", + "command.generateCommitMessage.title": "कमिट संदेश बनाएं", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.id.json b/src/package.nls.id.json index 2a7607f3e7c..d2ccf119242 100644 --- a/src/package.nls.id.json +++ b/src/package.nls.id.json @@ -25,6 +25,7 @@ "command.terminal.explainCommand.title": "Jelaskan Perintah Ini", "command.acceptInput.title": "Terima Input/Saran", "command.toggleAutoApprove.title": "Alihkan Persetujuan Otomatis", + "command.generateCommitMessage.title": "Buat Pesan Commit", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Perintah yang dapat dijalankan secara otomatis ketika 'Selalu setujui operasi eksekusi' diaktifkan", "commands.deniedCommands.description": "Awalan perintah yang akan otomatis ditolak tanpa meminta persetujuan. Jika terjadi konflik dengan perintah yang diizinkan, pencocokan awalan terpanjang akan diprioritaskan. Tambahkan * untuk menolak semua perintah.", diff --git a/src/package.nls.it.json b/src/package.nls.it.json index c94471355d4..8cd01157d82 100644 --- a/src/package.nls.it.json +++ b/src/package.nls.it.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Spiega Questo Comando", "command.acceptInput.title": "Accetta Input/Suggerimento", "command.toggleAutoApprove.title": "Attiva/Disattiva Auto-Approvazione", + "command.generateCommitMessage.title": "Genera Messaggio di Commit", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.ja.json b/src/package.nls.ja.json index ff6040d7734..e88119359cc 100644 --- a/src/package.nls.ja.json +++ b/src/package.nls.ja.json @@ -25,6 +25,7 @@ "command.terminal.explainCommand.title": "このコマンドを説明", "command.acceptInput.title": "入力/提案を承認", "command.toggleAutoApprove.title": "自動承認を切替", + "command.generateCommitMessage.title": "コミットメッセージを生成", "configuration.title": "Roo Code", "commands.allowedCommands.description": "'常に実行操作を承認する'が有効な場合に自動実行できるコマンド", "commands.deniedCommands.description": "承認を求めずに自動的に拒否されるコマンドプレフィックス。許可されたコマンドとの競合がある場合、最長プレフィックスマッチが優先されます。すべてのコマンドを拒否するには * を追加してください。", diff --git a/src/package.nls.json b/src/package.nls.json index 177b392f775..e241f525fb0 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -25,6 +25,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/package.nls.ko.json b/src/package.nls.ko.json index f0912835b8b..57a13276977 100644 --- a/src/package.nls.ko.json +++ b/src/package.nls.ko.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "이 명령어 설명", "command.acceptInput.title": "입력/제안 수락", "command.toggleAutoApprove.title": "자동 승인 전환", + "command.generateCommitMessage.title": "커밋 메시지 생성", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.nl.json b/src/package.nls.nl.json index fef3ca7219c..e9275872643 100644 --- a/src/package.nls.nl.json +++ b/src/package.nls.nl.json @@ -25,6 +25,7 @@ "command.terminal.explainCommand.title": "Leg Dit Commando Uit", "command.acceptInput.title": "Invoer/Suggestie Accepteren", "command.toggleAutoApprove.title": "Auto-Goedkeuring Schakelen", + "command.generateCommitMessage.title": "Commitbericht Genereren", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Commando's die automatisch kunnen worden uitgevoerd wanneer 'Altijd goedkeuren uitvoerbewerkingen' is ingeschakeld", "commands.deniedCommands.description": "Commando-prefixen die automatisch worden geweigerd zonder om goedkeuring te vragen. Bij conflicten met toegestane commando's heeft de langste prefix-match voorrang. Voeg * toe om alle commando's te weigeren.", diff --git a/src/package.nls.pl.json b/src/package.nls.pl.json index 8c1f66450d1..9135c1a5969 100644 --- a/src/package.nls.pl.json +++ b/src/package.nls.pl.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Wyjaśnij tę Komendę", "command.acceptInput.title": "Akceptuj Wprowadzanie/Sugestię", "command.toggleAutoApprove.title": "Przełącz Auto-Zatwierdzanie", + "command.generateCommitMessage.title": "Generuj Wiadomość Commita", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.pt-BR.json b/src/package.nls.pt-BR.json index 84cbf42c097..717f0d0dd9e 100644 --- a/src/package.nls.pt-BR.json +++ b/src/package.nls.pt-BR.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Explicar Este Comando", "command.acceptInput.title": "Aceitar Entrada/Sugestão", "command.toggleAutoApprove.title": "Alternar Auto-Aprovação", + "command.generateCommitMessage.title": "Gerar Mensagem de Commit", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.ru.json b/src/package.nls.ru.json index be8df040323..da657759fe1 100644 --- a/src/package.nls.ru.json +++ b/src/package.nls.ru.json @@ -25,6 +25,7 @@ "command.terminal.explainCommand.title": "Объяснить эту команду", "command.acceptInput.title": "Принять ввод/предложение", "command.toggleAutoApprove.title": "Переключить Авто-Подтверждение", + "command.generateCommitMessage.title": "Сгенерировать Сообщение Коммита", "configuration.title": "Roo Code", "commands.allowedCommands.description": "Команды, которые могут быть автоматически выполнены, когда включена опция 'Всегда подтверждать операции выполнения'", "commands.deniedCommands.description": "Префиксы команд, которые будут автоматически отклонены без запроса подтверждения. В случае конфликтов с разрешенными командами приоритет имеет самое длинное совпадение префикса. Добавьте * чтобы отклонить все команды.", diff --git a/src/package.nls.tr.json b/src/package.nls.tr.json index a815188e8aa..d136be925a2 100644 --- a/src/package.nls.tr.json +++ b/src/package.nls.tr.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Bu Komutu Açıkla", "command.acceptInput.title": "Girişi/Öneriyi Kabul Et", "command.toggleAutoApprove.title": "Otomatik Onayı Değiştir", + "command.generateCommitMessage.title": "Commit Mesajı Oluştur", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.vi.json b/src/package.nls.vi.json index 6052080dfa3..4650e504f59 100644 --- a/src/package.nls.vi.json +++ b/src/package.nls.vi.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "Giải Thích Lệnh Này", "command.acceptInput.title": "Chấp Nhận Đầu Vào/Gợi Ý", "command.toggleAutoApprove.title": "Bật/Tắt Tự Động Phê Duyệt", + "command.generateCommitMessage.title": "Tạo Thông Điệp Commit", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.zh-CN.json b/src/package.nls.zh-CN.json index 9254d494d9b..43bc8d70dac 100644 --- a/src/package.nls.zh-CN.json +++ b/src/package.nls.zh-CN.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "解释此命令", "command.acceptInput.title": "接受输入/建议", "command.toggleAutoApprove.title": "切换自动批准", + "command.generateCommitMessage.title": "生成提交信息", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/package.nls.zh-TW.json b/src/package.nls.zh-TW.json index a8030d69141..c4c6f1835a3 100644 --- a/src/package.nls.zh-TW.json +++ b/src/package.nls.zh-TW.json @@ -15,6 +15,7 @@ "command.terminal.explainCommand.title": "解釋此命令", "command.acceptInput.title": "接受輸入/建議", "command.toggleAutoApprove.title": "切換自動批准", + "command.generateCommitMessage.title": "產生提交訊息", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/utils/__tests__/commit-message-generator.spec.ts b/src/utils/__tests__/commit-message-generator.spec.ts new file mode 100644 index 00000000000..edc9d50ddf8 --- /dev/null +++ b/src/utils/__tests__/commit-message-generator.spec.ts @@ -0,0 +1,157 @@ +import { generateCommitMessageFromDiff, getGitDiff } from "../commit-message-generator" +import * as singleCompletionHandlerModule from "../single-completion-handler" +import type { ProviderSettings } from "@roo-code/types" + +vi.mock("../single-completion-handler") + +// Mock child_process.exec to simulate git commands +const mockExecImpl = vi.fn() + +vi.mock("child_process", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + exec: (...args: any[]) => mockExecImpl(...args), + } +}) + +vi.mock("util", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + promisify: + (fn: any) => + (...args: any[]) => + new Promise((resolve, reject) => { + fn(...args, (err: Error | null, result: any) => { + if (err) { + reject(err) + } else { + resolve(result) + } + }) + }), + } +}) + +describe("commit-message-generator", () => { + let mockSingleCompletionHandler: ReturnType + + const mockApiConfig: ProviderSettings = { + apiProvider: "anthropic", + apiKey: "test-key", + apiModelId: "claude-sonnet-4-20250514", + } as ProviderSettings + + beforeEach(() => { + vi.clearAllMocks() + mockSingleCompletionHandler = vi.fn().mockResolvedValue("feat: add user authentication") + vi.mocked(singleCompletionHandlerModule).singleCompletionHandler = mockSingleCompletionHandler + }) + + describe("generateCommitMessageFromDiff", () => { + it("generates a commit message from a diff", async () => { + const diff = `diff --git a/src/auth.ts b/src/auth.ts ++export function login(user: string) { ++ return true ++}` + + const result = await generateCommitMessageFromDiff(mockApiConfig, diff) + + expect(result).toBe("feat: add user authentication") + expect(mockSingleCompletionHandler).toHaveBeenCalledWith(mockApiConfig, expect.stringContaining(diff)) + }) + + it("truncates very large diffs", async () => { + const largeDiff = "a".repeat(15000) + + await generateCommitMessageFromDiff(mockApiConfig, largeDiff) + + const calledPrompt = mockSingleCompletionHandler.mock.calls[0][1] + expect(calledPrompt).toContain("...(truncated)") + }) + + it("strips markdown code blocks from the result", async () => { + mockSingleCompletionHandler.mockResolvedValue("```\nfeat: add feature\n```") + + const result = await generateCommitMessageFromDiff(mockApiConfig, "some diff") + + expect(result).toBe("feat: add feature") + }) + + it("strips language-tagged markdown code blocks from the result", async () => { + mockSingleCompletionHandler.mockResolvedValue("```text\nfix: resolve null check\n```") + + const result = await generateCommitMessageFromDiff(mockApiConfig, "some diff") + + expect(result).toBe("fix: resolve null check") + }) + + it("propagates errors from the completion handler", async () => { + mockSingleCompletionHandler.mockRejectedValue(new Error("API Error")) + + await expect(generateCommitMessageFromDiff(mockApiConfig, "some diff")).rejects.toThrow("API Error") + }) + }) + + describe("getGitDiff", () => { + it("returns staged diff when available", async () => { + mockExecImpl.mockImplementation( + ( + cmd: string, + _opts: unknown, + callback: (err: Error | null, result: { stdout: string; stderr: string }) => void, + ) => { + if (cmd.includes("--cached")) { + callback(null, { stdout: "staged changes", stderr: "" }) + } else { + callback(null, { stdout: "unstaged changes", stderr: "" }) + } + }, + ) + + const result = await getGitDiff("/workspace") + + expect(result).toBe("staged changes") + expect(mockExecImpl).toHaveBeenCalledWith( + "git diff --cached --no-color", + expect.objectContaining({ cwd: "/workspace" }), + expect.any(Function), + ) + }) + + it("falls back to unstaged diff when nothing is staged", async () => { + mockExecImpl.mockImplementation( + ( + cmd: string, + _opts: unknown, + callback: (err: Error | null, result: { stdout: string; stderr: string }) => void, + ) => { + if (cmd.includes("--cached")) { + callback(null, { stdout: "", stderr: "" }) + } else { + callback(null, { stdout: "unstaged changes", stderr: "" }) + } + }, + ) + + const result = await getGitDiff("/workspace") + + expect(result).toBe("unstaged changes") + }) + + it("throws an error when git command fails", async () => { + mockExecImpl.mockImplementation( + ( + _cmd: string, + _opts: unknown, + callback: (err: Error | null, result: { stdout: string; stderr: string }) => void, + ) => { + callback(new Error("git not found"), { stdout: "", stderr: "" }) + }, + ) + + await expect(getGitDiff("/workspace")).rejects.toThrow("Failed to get git diff") + }) + }) +}) diff --git a/src/utils/commit-message-generator.ts b/src/utils/commit-message-generator.ts new file mode 100644 index 00000000000..b4a2ec70215 --- /dev/null +++ b/src/utils/commit-message-generator.ts @@ -0,0 +1,107 @@ +import * as vscode from "vscode" +import { exec } from "child_process" +import { promisify } from "util" + +import type { ProviderSettings } from "@roo-code/types" + +import { singleCompletionHandler } from "./single-completion-handler" +import { t } from "../i18n" + +const execAsync = promisify(exec) + +const MAX_DIFF_LENGTH = 10000 + +const COMMIT_MESSAGE_PROMPT = `You are a commit message generator. Given the following git diff, generate a concise and descriptive commit message following the Conventional Commits format. + +Rules: +- Use one of these types: feat, fix, refactor, docs, style, test, chore, perf, ci, build +- The first line should be the type, optional scope in parentheses, and a short description (max 72 chars) +- If the changes are complex, add a blank line followed by a more detailed description +- Focus on WHAT changed and WHY, not HOW +- Do NOT include any markdown formatting, code blocks, or extra explanation +- Output ONLY the commit message text + +Git diff: +` + +/** + * Gets the staged diff from the git repository. Falls back to unstaged diff + * if nothing is staged. + */ +export async function getGitDiff(workspaceRoot: string): Promise { + try { + // Try staged changes first + const { stdout: stagedDiff } = await execAsync("git diff --cached --no-color", { + cwd: workspaceRoot, + maxBuffer: 1024 * 1024, + }) + + if (stagedDiff.trim()) { + return stagedDiff + } + + // Fall back to unstaged changes + const { stdout: unstagedDiff } = await execAsync("git diff --no-color", { + cwd: workspaceRoot, + maxBuffer: 1024 * 1024, + }) + + return unstagedDiff + } catch (error) { + throw new Error(`Failed to get git diff: ${error instanceof Error ? error.message : String(error)}`) + } +} + +/** + * Generates a commit message from the given diff using the AI provider. + */ +export async function generateCommitMessageFromDiff(apiConfiguration: ProviderSettings, diff: string): Promise { + // Truncate very large diffs to avoid token limits + const truncatedDiff = diff.length > MAX_DIFF_LENGTH ? diff.substring(0, MAX_DIFF_LENGTH) + "\n...(truncated)" : diff + + const prompt = COMMIT_MESSAGE_PROMPT + truncatedDiff + + const result = await singleCompletionHandler(apiConfiguration, prompt) + + // Clean up the result - remove any markdown formatting the model might add + return result + .replace(/^```[^\n]*\n/, "") + .replace(/\n```$/, "") + .trim() +} + +/** + * Gets the workspace root for git operations. + */ +export function getWorkspaceRoot(): string | undefined { + const workspaceFolders = vscode.workspace.workspaceFolders + if (!workspaceFolders || workspaceFolders.length === 0) { + return undefined + } + return workspaceFolders[0].uri.fsPath +} + +/** + * Sets the SCM input box value with the generated commit message. + */ +export async function setScmInputBoxMessage(message: string): Promise { + const gitExtension = vscode.extensions.getExtension("vscode.git") + + if (!gitExtension) { + vscode.window.showErrorMessage(t("common:commit.no_git_extension")) + return false + } + + const git = gitExtension.isActive ? gitExtension.exports : await gitExtension.activate() + const api = git.getAPI(1) + + if (!api || api.repositories.length === 0) { + vscode.window.showErrorMessage(t("common:commit.no_git_repo")) + return false + } + + // Use the first repository (or the one matching the workspace) + const repo = api.repositories[0] + repo.inputBox.value = message + return true +}