This commit is contained in:
roomote-v0[bot] 2026-04-09 15:26:24 +08:00 committed by GitHub
commit d426d6d594
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 558 additions and 0 deletions

View file

@ -47,6 +47,7 @@ export const commandIds = [
"acceptInput",
"focusPanel",
"toggleAutoApprove",
"generateCommitMessage",
] as const
export type CommandId = (typeof commandIds)[number]

View file

@ -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<RegisterCommandOptions, "provider">) => {

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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 प्रोफ़ाइल चुनें"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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プロファイルを選択"
}
}

View file

@ -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 프로필을 선택하세요"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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 для генерации сообщения коммита"
}
}

View file

@ -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ç"
}
}

View file

@ -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"
}
}

View file

@ -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 配置"
}
}

View file

@ -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 設定檔"
}
}

View file

@ -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": [

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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": "承認を求めずに自動的に拒否されるコマンドプレフィックス。許可されたコマンドとの競合がある場合、最長プレフィックスマッチが優先されます。すべてのコマンドを拒否するには * を追加してください。",

View file

@ -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.",

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Префиксы команд, которые будут автоматически отклонены без запроса подтверждения. В случае конфликтов с разрешенными командами приоритет имеет самое длинное совпадение префикса. Добавьте * чтобы отклонить все команды.",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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<typeof import("child_process")>()
return {
...actual,
exec: (...args: any[]) => mockExecImpl(...args),
}
})
vi.mock("util", async (importOriginal) => {
const actual = await importOriginal<typeof import("util")>()
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<typeof vi.fn>
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")
})
})
})

View file

@ -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<string> {
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<string> {
// 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<boolean> {
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
}