diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index 10384db8ed..6526ea07be 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" * ExperimentId */ -export const experimentIds = ["powerSteering", "multiFileApplyDiff"] as const +export const experimentIds = ["powerSteering", "multiFileApplyDiff", "aiCommitMessages"] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -19,6 +19,7 @@ export type ExperimentId = z.infer export const experimentsSchema = z.object({ powerSteering: z.boolean().optional(), multiFileApplyDiff: z.boolean().optional(), + aiCommitMessages: z.boolean().optional(), }) export type Experiments = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index e713cafa4c..5e37eb6e55 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -90,6 +90,7 @@ export const globalSettingsSchema = z.object({ codebaseIndexConfig: codebaseIndexConfigSchema.optional(), language: languagesSchema.optional(), + commitLanguage: languagesSchema.optional(), telemetrySetting: telemetrySettingsSchema.optional(), diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index 00f6bbbcba..b57213c5a2 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -53,6 +53,7 @@ export const commandIds = [ "focusInput", "acceptInput", "focusPanel", + "git.generateCommitMessage", ] as const export type CommandId = (typeof commandIds)[number] diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 8e84981d8a..5a3584ace7 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -14,6 +14,7 @@ import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRe import { handleNewTask } from "./handleTask" import { CodeIndexManager } from "../services/code-index/manager" import { importSettingsWithFeedback } from "../core/config/importExport" +import { generateCommitMessage } from "../integrations/git/generateCommitMessage" import { t } from "../i18n" /** @@ -217,6 +218,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt visibleProvider.postMessageToWebview({ type: "acceptInput" }) }, + "git.generateCommitMessage": () => generateCommitMessage(context), }) export const openClineInNewTab = async ({ context, outputChannel }: Omit) => { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 51cb9a275b..76223a2765 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1398,6 +1398,7 @@ export class ClineProvider telemetrySetting, showRooIgnoredFiles, language, + commitLanguage, maxReadFileLine, terminalCompressProgressBar, historyPreviewCollapsed, @@ -1496,6 +1497,7 @@ export class ClineProvider machineId, showRooIgnoredFiles: showRooIgnoredFiles ?? true, language: language ?? formatLanguage(vscode.env.language), + commitLanguage: commitLanguage ?? formatLanguage(vscode.env.language), renderContext: this.renderContext, maxReadFileLine: maxReadFileLine ?? -1, maxConcurrentFileReads: maxConcurrentFileReads ?? 5, @@ -1632,6 +1634,7 @@ export class ClineProvider terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true, mode: stateValues.mode ?? defaultModeSlug, language: stateValues.language ?? formatLanguage(vscode.env.language), + commitLanguage: stateValues.commitLanguage ?? formatLanguage(vscode.env.language), mcpEnabled: stateValues.mcpEnabled ?? true, enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true, alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index cac94aa0ce..ad63736e8a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1109,6 +1109,11 @@ export const webviewMessageHandler = async ( await updateGlobalState("language", message.text as Language) await provider.postStateToWebview() break + case "commitLanguage": + changeLanguage(message.text ?? "en") + await updateGlobalState("commitLanguage", message.text as Language) + await provider.postStateToWebview() + break case "showRooIgnoredFiles": await updateGlobalState("showRooIgnoredFiles", message.bool ?? true) await provider.postStateToWebview() @@ -1423,6 +1428,19 @@ export const webviewMessageHandler = async ( await updateGlobalState("experiments", updatedExperiments) + // Also update workspace settings to trigger the context update. + await vscode.workspace + .getConfiguration(Package.name) + .update("experiments", updatedExperiments, vscode.ConfigurationTarget.Global) + + if (message.values.aiCommitMessages !== undefined) { + await vscode.commands.executeCommand( + "setContext", + "roo-cline.aiCommitMessagesEnabled", + message.values.aiCommitMessages, + ) + } + await provider.postStateToWebview() break } diff --git a/src/extension.ts b/src/extension.ts index 9e3daad662..1692fee1c9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -99,6 +99,28 @@ export async function activate(context: vscode.ExtensionContext) { const contextProxy = await ContextProxy.getInstance(context) const codeIndexManager = CodeIndexManager.getInstance(context) + const setAiCommitMessagesContext = () => { + const config = vscode.workspace.getConfiguration(Package.name) + const experiments = config.get("experiments", {}) as { aiCommitMessages?: boolean } + vscode.commands.executeCommand( + "setContext", + "roo-cline.aiCommitMessagesEnabled", + !!experiments.aiCommitMessages, + ) + } + + // Set the initial context + setAiCommitMessagesContext() + + // Update the context when the configuration changes + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(`${Package.name}.experiments`)) { + setAiCommitMessagesContext() + } + }), + ) + try { await codeIndexManager?.initialize(contextProxy) } catch (error) { diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index eb90a77179..0fc5ad6fad 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -26,6 +26,9 @@ "just_this_message": "Només aquest missatge", "this_and_subsequent": "Aquest i tots els missatges posteriors" }, + "git": { + "generatingCommitMessage": "Generant missatge de commit amb {{modelName}}..." + }, "errors": { "invalid_data_uri": "Format d'URI de dades no vàlid", "error_copying_image": "Error copiant la imatge: {{errorMessage}}", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 48e34db479..2018101b81 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -22,6 +22,9 @@ "just_this_message": "Nur diese Nachricht", "this_and_subsequent": "Diese und alle nachfolgenden Nachrichten" }, + "git": { + "generatingCommitMessage": "Commit-Nachricht mit {{modelName}} wird generiert..." + }, "errors": { "invalid_data_uri": "Ungültiges Daten-URI-Format", "error_copying_image": "Fehler beim Kopieren des Bildes: {{errorMessage}}", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 855a69c23b..90341c4111 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -22,6 +22,9 @@ "just_this_message": "Just this message", "this_and_subsequent": "This and all subsequent messages" }, + "git": { + "generatingCommitMessage": "Generating commit message with {{modelName}}..." + }, "errors": { "invalid_data_uri": "Invalid data URI format", "error_copying_image": "Error copying image: {{errorMessage}}", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 0de842ee44..734b651457 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -22,6 +22,9 @@ "just_this_message": "Solo este mensaje", "this_and_subsequent": "Este y todos los mensajes posteriores" }, + "git": { + "generatingCommitMessage": "Generando mensaje de commit con {{modelName}}..." + }, "errors": { "invalid_data_uri": "Formato de URI de datos no válido", "error_copying_image": "Error copiando la imagen: {{errorMessage}}", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 2fcef289f4..5d412fd74e 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -22,6 +22,9 @@ "just_this_message": "Uniquement ce message", "this_and_subsequent": "Ce message et tous les messages suivants" }, + "git": { + "generatingCommitMessage": "Génération du message de commit avec {{modelName}}..." + }, "errors": { "invalid_data_uri": "Format d'URI de données invalide", "error_copying_image": "Erreur lors de la copie de l'image : {{errorMessage}}", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index b799cc03b7..faa2d8fca1 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -22,6 +22,9 @@ "just_this_message": "सिर्फ यह संदेश", "this_and_subsequent": "यह और सभी बाद के संदेश" }, + "git": { + "generatingCommitMessage": "{{modelName}} के साथ कमिट संदेश उत्पन्न हो रहा है..." + }, "errors": { "invalid_data_uri": "अमान्य डेटा URI फॉर्मेट", "error_copying_image": "छवि कॉपी करने में त्रुटि: {{errorMessage}}", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 9ddd5ca2de..fc2070bca4 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -22,6 +22,9 @@ "just_this_message": "Hanya pesan ini", "this_and_subsequent": "Ini dan semua pesan selanjutnya" }, + "git": { + "generatingCommitMessage": "Membuat pesan komit dengan {{modelName}}..." + }, "errors": { "invalid_data_uri": "Format data URI tidak valid", "error_copying_image": "Error menyalin gambar: {{errorMessage}}", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index d34a14d840..7f830c3a5f 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -22,6 +22,9 @@ "just_this_message": "Solo questo messaggio", "this_and_subsequent": "Questo e tutti i messaggi successivi" }, + "git": { + "generatingCommitMessage": "Generazione del messaggio di commit con {{modelName}}..." + }, "errors": { "invalid_data_uri": "Formato URI dati non valido", "error_copying_image": "Errore durante la copia dell'immagine: {{errorMessage}}", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 0e6afe18cf..4bb727d08b 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -22,6 +22,9 @@ "just_this_message": "このメッセージのみ", "this_and_subsequent": "これ以降のすべてのメッセージ" }, + "git": { + "generatingCommitMessage": "{{modelName}}でコミットメッセージを生成しています..." + }, "errors": { "invalid_data_uri": "データURIフォーマットが無効です", "error_copying_image": "画像のコピー中にエラーが発生しました:{{errorMessage}}", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index affba735ab..566d9878b2 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -22,6 +22,9 @@ "just_this_message": "이 메시지만", "this_and_subsequent": "이 메시지와 모든 후속 메시지" }, + "git": { + "generatingCommitMessage": "{{modelName}}(으)로 커밋 메시지 생성 중..." + }, "errors": { "invalid_data_uri": "잘못된 데이터 URI 형식", "error_copying_image": "이미지 복사 중 오류 발생: {{errorMessage}}", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index b0748a5aa6..c3efdfe734 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -22,6 +22,9 @@ "just_this_message": "Alleen dit bericht", "this_and_subsequent": "Dit en alle volgende berichten" }, + "git": { + "generatingCommitMessage": "Commitbericht genereren met {{modelName}}..." + }, "errors": { "invalid_data_uri": "Ongeldig data-URI-formaat", "error_copying_image": "Fout bij kopiëren van afbeelding: {{errorMessage}}", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index b8cd29976f..53fb6196dd 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -22,6 +22,9 @@ "just_this_message": "Tylko tę wiadomość", "this_and_subsequent": "Tę i wszystkie kolejne wiadomości" }, + "git": { + "generatingCommitMessage": "Generowanie wiadomości commit z {{modelName}}..." + }, "errors": { "invalid_data_uri": "Nieprawidłowy format URI danych", "error_copying_image": "Błąd kopiowania obrazu: {{errorMessage}}", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 25779949d2..fa0775d8d5 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -26,6 +26,9 @@ "just_this_message": "Apenas esta mensagem", "this_and_subsequent": "Esta e todas as mensagens subsequentes" }, + "git": { + "generatingCommitMessage": "Gerando mensagem de commit com {{modelName}}..." + }, "errors": { "invalid_data_uri": "Formato de URI de dados inválido", "error_copying_image": "Erro ao copiar imagem: {{errorMessage}}", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 006b7af222..5bbe425b4b 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -22,6 +22,9 @@ "just_this_message": "Только это сообщение", "this_and_subsequent": "Это и все последующие сообщения" }, + "git": { + "generatingCommitMessage": "Генерация сообщения коммита с помощью {{modelName}}..." + }, "errors": { "invalid_data_uri": "Неверный формат URI данных", "error_copying_image": "Ошибка копирования изображения: {{errorMessage}}", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 87635c61ef..6d4c334a8c 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -22,6 +22,9 @@ "just_this_message": "Sadece bu mesajı", "this_and_subsequent": "Bu ve sonraki tüm mesajları" }, + "git": { + "generatingCommitMessage": "{{modelName}} ile commit mesajı oluşturuluyor..." + }, "errors": { "invalid_data_uri": "Geçersiz veri URI formatı", "error_copying_image": "Resim kopyalanırken hata oluştu: {{errorMessage}}", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 7321b3e8a6..73aa2a46b6 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -22,6 +22,9 @@ "just_this_message": "Chỉ tin nhắn này", "this_and_subsequent": "Tin nhắn này và tất cả tin nhắn tiếp theo" }, + "git": { + "generatingCommitMessage": "Đang tạo thông điệp commit với {{modelName}}..." + }, "errors": { "invalid_data_uri": "Định dạng URI dữ liệu không hợp lệ", "error_copying_image": "Lỗi khi sao chép hình ảnh: {{errorMessage}}", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 9ebdd04b95..ea8bb7626f 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -22,6 +22,9 @@ "just_this_message": "仅此消息", "this_and_subsequent": "此消息及所有后续消息" }, + "git": { + "generatingCommitMessage": "正在使用 {{modelName}} 生成提交消息..." + }, "errors": { "invalid_mcp_config": "项目MCP配置格式无效", "invalid_mcp_settings_format": "MCP设置JSON格式无效。请确保您的设置遵循正确的JSON格式。", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index a63e87f61a..a2391c40dc 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -22,6 +22,9 @@ "just_this_message": "僅這則訊息", "this_and_subsequent": "這則訊息及所有後續訊息" }, + "git": { + "generatingCommitMessage": "正在使用 {{modelName}} 產生提交訊息..." + }, "errors": { "invalid_data_uri": "資料 URI 格式無效", "error_copying_image": "複製圖片時發生錯誤:{{errorMessage}}", diff --git a/src/integrations/git/__tests__/generateCommitMessage.spec.ts b/src/integrations/git/__tests__/generateCommitMessage.spec.ts new file mode 100644 index 0000000000..56f16685b7 --- /dev/null +++ b/src/integrations/git/__tests__/generateCommitMessage.spec.ts @@ -0,0 +1,176 @@ +import * as vscode from "vscode" +import { simpleGit, SimpleGit } from "simple-git" +import { generateCommitMessage } from "../generateCommitMessage" +import { ContextProxy } from "../../../core/config/ContextProxy" +import { buildApiHandler } from "../../../api" +import * as fs from "fs/promises" +import { t } from "../../../i18n" + +vi.mock("vscode", () => ({ + extensions: { + getExtension: vi.fn(), + }, + window: { + showErrorMessage: vi.fn(), + showInformationMessage: vi.fn(), + withProgress: vi.fn((options, task) => task()), + }, + ProgressLocation: { + Notification: 15, + }, + workspace: { + workspaceFolders: [ + { + uri: { + fsPath: "/test/repo", + }, + }, + ], + }, +})) + +vi.mock("simple-git") +vi.mock("../../../core/config/ContextProxy") +vi.mock("../../../api") +vi.mock("fs/promises") +vi.mock("../../../i18n") + +describe("generateCommitMessage", () => { + let context: vscode.ExtensionContext + let mockGit: any + + beforeEach(() => { + vi.clearAllMocks() + + context = { + globalState: { + get: vi.fn(), + update: vi.fn(), + }, + } as any + + mockGit = { + diff: vi.fn(), + status: vi.fn().mockResolvedValue({ not_added: [] }), + } as any + + vi.mocked(simpleGit).mockReturnValue(mockGit) + + const mockGitExtension = { + activate: vi.fn().mockResolvedValue(undefined), + exports: { + getAPI: vi.fn().mockReturnValue({ + repositories: [ + { + rootUri: { fsPath: "/test/repo" }, + inputBox: { value: "" }, + }, + ], + }), + }, + } + vi.mocked(vscode.extensions.getExtension).mockReturnValue(mockGitExtension as any) + + vi.mocked(ContextProxy.getInstance).mockResolvedValue({ + getProviderSettings: vi.fn().mockReturnValue({ provider: "test-provider" }), + getGlobalSettings: vi.fn().mockReturnValue({ language: "en", commitLanguage: "en" }), + } as any) + + const mockProvider = { + getModel: vi.fn().mockReturnValue({ id: "test-model" }), + createMessage: vi.fn().mockImplementation(async function* () { + yield { type: "text", text: "feat: Test commit" } + }), + } + vi.mocked(buildApiHandler).mockReturnValue(mockProvider as any) + vi.mocked(t).mockImplementation((key) => key) + }) + + test("should show error if git extension is not found", async () => { + vi.mocked(vscode.extensions.getExtension).mockReturnValue(undefined) + await generateCommitMessage(context) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Git extension not found.") + }) + + test("should show error if no git repository is found", async () => { + const mockGitExtension = { + activate: vi.fn().mockResolvedValue(undefined), + exports: { + getAPI: vi.fn().mockReturnValue({ repositories: [] }), + }, + } + vi.mocked(vscode.extensions.getExtension).mockReturnValue(mockGitExtension as any) + + await generateCommitMessage(context) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("No Git repository found.") + }) + + test("should show info if no changes are found", async () => { + mockGit.diff.mockResolvedValue("") + mockGit.status.mockResolvedValue({ not_added: [] }) + + await generateCommitMessage(context) + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("No changes found.") + }) + + test("should show error if AI provider is not configured", async () => { + vi.mocked(ContextProxy.getInstance).mockResolvedValue({ + getProviderSettings: vi.fn().mockReturnValue(null), + } as any) + + mockGit.diff.mockResolvedValue("diff --git a/file.txt b/file.txt") + await generateCommitMessage(context) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("AI provider not configured.") + }) + + test("should generate commit message for tracked files", async () => { + const diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-old\n+new" + mockGit.diff.mockResolvedValue(diff) + + await generateCommitMessage(context) + + const api = vscode.extensions.getExtension("vscode.git")?.exports.getAPI(1) + expect(api.repositories[0].inputBox.value).toBe("feat: Test commit") + }) + + test("should generate commit message for untracked files", async () => { + mockGit.diff.mockResolvedValue("") + mockGit.status.mockResolvedValue({ not_added: ["new_file.txt"] }) + vi.mocked(fs.readFile).mockResolvedValue("new content") + + await generateCommitMessage(context) + + const api = vscode.extensions.getExtension("vscode.git")?.exports.getAPI(1) + expect(api.repositories[0].inputBox.value).toBe("feat: Test commit") + }) + + test("should generate commit message for both tracked and untracked files", async () => { + const diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-old\n+new" + mockGit.diff.mockResolvedValue(diff) + mockGit.status.mockResolvedValue({ not_added: ["new_file.txt"] }) + vi.mocked(fs.readFile).mockResolvedValue("new content") + + await generateCommitMessage(context) + + const api = vscode.extensions.getExtension("vscode.git")?.exports.getAPI(1) + expect(api.repositories[0].inputBox.value).toBe("feat: Test commit") + }) + + test("should handle error when reading untracked file", async () => { + mockGit.diff.mockResolvedValue("") + mockGit.status.mockResolvedValue({ not_added: ["new_file.txt"] }) + vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found")) + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + await generateCommitMessage(context) + + expect(consoleErrorSpy).toHaveBeenCalledWith("Could not read untracked file new_file.txt", expect.any(Error)) + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("No changes found.") + const api = vscode.extensions.getExtension("vscode.git")?.exports.getAPI(1) + expect(api.repositories[0].inputBox.value).toBe("") + consoleErrorSpy.mockRestore() + }) +}) diff --git a/src/integrations/git/generateCommitMessage.ts b/src/integrations/git/generateCommitMessage.ts new file mode 100644 index 0000000000..701af11ff1 --- /dev/null +++ b/src/integrations/git/generateCommitMessage.ts @@ -0,0 +1,101 @@ +import * as vscode from "vscode" +import * as path from "path" +import * as fs from "fs/promises" +import { buildApiHandler } from "../../api" +import { t } from "../../i18n" +import { simpleGit, SimpleGit } from "simple-git" +import { ContextProxy } from "../../core/config/ContextProxy" +import { Anthropic } from "@anthropic-ai/sdk" + +async function getGitApi(): Promise { + const extension = vscode.extensions.getExtension("vscode.git") + if (!extension) { + vscode.window.showErrorMessage("Git extension not found.") + return + } + await extension.activate() + return extension.exports.getAPI(1) +} + +async function getChanges(git: SimpleGit, repoPath: string): Promise { + const trackedFilesDiff = await git.diff() + const status = await git.status() + const untrackedFiles = status.not_added + + let untrackedFilesContent = "" + for (const file of untrackedFiles) { + const filePath = path.join(repoPath, file) + try { + const content = await fs.readFile(filePath, "utf-8") + untrackedFilesContent += `\n--- a/${file}\n+++ b/${file}\n${content}` + } catch (e) { + console.error(`Could not read untracked file ${file}`, e) + } + } + + return `${trackedFilesDiff}\n${untrackedFilesContent}`.trim() +} + +function createPrompt(diff: string, language: string): string { + return `Create a git commit message in ${language} from the following diff:\n${diff}.Remember to print only commit messages text without any extra markdown or content that would require special tools to display. Adhere to best git commit message practices. Remember to use ${language} in this commit message.` +} + +export async function generateCommitMessage(context: vscode.ExtensionContext) { + const gitApi = await getGitApi() + if (!gitApi) { + return + } + + if (gitApi.repositories.length === 0) { + vscode.window.showErrorMessage("No Git repository found.") + return + } + + const repoPath = gitApi.repositories[0].rootUri.fsPath + const git = simpleGit(repoPath) + const diff = await getChanges(git, repoPath) + + if (!diff) { + vscode.window.showInformationMessage("No changes found.") + return + } + + const contextProxy = await ContextProxy.getInstance(context) + const providerSettings = contextProxy.getProviderSettings() + if (!providerSettings) { + vscode.window.showErrorMessage("AI provider not configured.") + return + } + const provider = buildApiHandler(providerSettings) + const modelName = provider.getModel().id + const settings = contextProxy.getGlobalSettings() + const commitLanguage = settings.commitLanguage || settings.language || "en" + const prompt = createPrompt(diff, commitLanguage) + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: prompt, + }, + ] + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: t("common:git.generatingCommitMessage", { modelName }), + cancellable: false, + }, + async () => { + const stream = provider.createMessage("", messages, { taskId: "generate-commit" }) + + let commitMessage = "" + for await (const chunk of stream) { + if (chunk.type === "text") { + commitMessage += chunk.text + } + } + + const finalMessage = commitMessage.trim() + gitApi.repositories[0].inputBox.value = finalMessage + }, + ) +} diff --git a/src/package.json b/src/package.json index 71085517db..0b47aefb15 100644 --- a/src/package.json +++ b/src/package.json @@ -169,9 +169,21 @@ "command": "roo-cline.acceptInput", "title": "%command.acceptInput.title%", "category": "%configuration.title%" + }, + { + "command": "roo-cline.git.generateCommitMessage", + "title": "%command.git.generateCommitMessage.title%", + "category": "Roo Code", + "icon": "$(sparkle)" } ], "menus": { + "commandPalette": [ + { + "command": "roo-cline.git.generateCommitMessage", + "when": "roo-cline.aiCommitMessagesEnabled" + } + ], "editor/context": [ { "submenu": "roo-cline.contextMenu", @@ -280,6 +292,13 @@ "group": "navigation@6", "when": "activeWebviewPanelId == roo-cline.TabPanelProvider" } + ], + "scm/title": [ + { + "command": "roo-cline.git.generateCommitMessage", + "group": "navigation", + "when": "scmProvider == git && roo-cline.aiCommitMessagesEnabled" + } ] }, "submenus": [ @@ -333,6 +352,19 @@ "type": "boolean", "default": true, "description": "%settings.enableCodeActions.description%" + }, + "roo-cline.experiments": { + "type": "object", + "default": { + "aiCommitMessages": false + }, + "properties": { + "aiCommitMessages": { + "type": "boolean", + "default": false, + "description": "Enable AI-powered commit messages" + } + } } } } diff --git a/src/package.nls.ca.json b/src/package.nls.ca.json index f20f269e20..514bea052d 100644 --- a/src/package.nls.ca.json +++ b/src/package.nls.ca.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Corregir Aquesta Ordre", "command.terminal.explainCommand.title": "Explicar Aquesta Ordre", "command.acceptInput.title": "Acceptar Entrada/Suggeriment", + "command.git.generateCommitMessage.title": "Genera el 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 781b310668..50f8bdb54d 100644 --- a/src/package.nls.de.json +++ b/src/package.nls.de.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Diesen Befehl Reparieren", "command.terminal.explainCommand.title": "Diesen Befehl Erklären", "command.acceptInput.title": "Eingabe/Vorschlag Akzeptieren", + "command.git.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 4938f5ea64..6178225271 100644 --- a/src/package.nls.es.json +++ b/src/package.nls.es.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Corregir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", "command.acceptInput.title": "Aceptar Entrada/Sugerencia", + "command.git.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 ada2502e4f..cf67fd6ceb 100644 --- a/src/package.nls.fr.json +++ b/src/package.nls.fr.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Corriger cette Commande", "command.terminal.explainCommand.title": "Expliquer cette Commande", "command.acceptInput.title": "Accepter l'Entrée/Suggestion", + "command.git.generateCommitMessage.title": "Générer le 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 f06e21cf06..38ffa25a83 100644 --- a/src/package.nls.hi.json +++ b/src/package.nls.hi.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "यह कमांड ठीक करें", "command.terminal.explainCommand.title": "यह कमांड समझाएं", "command.acceptInput.title": "इनपुट/सुझाव स्वीकारें", + "command.git.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 c7461aab6b..103821dce8 100644 --- a/src/package.nls.id.json +++ b/src/package.nls.id.json @@ -14,6 +14,7 @@ "command.settings.title": "Pengaturan", "command.documentation.title": "Dokumentasi", "command.openInNewTab.title": "Buka di Tab Baru", + "command.git.generateCommitMessage.title": "Hasilkan Pesan Komit", "command.explainCode.title": "Jelaskan Kode", "command.fixCode.title": "Perbaiki Kode", "command.improveCode.title": "Tingkatkan Kode", diff --git a/src/package.nls.it.json b/src/package.nls.it.json index cc63935e60..5862235532 100644 --- a/src/package.nls.it.json +++ b/src/package.nls.it.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Correggi Questo Comando", "command.terminal.explainCommand.title": "Spiega Questo Comando", "command.acceptInput.title": "Accetta Input/Suggerimento", + "command.git.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 7e601d3c78..a638b4a576 100644 --- a/src/package.nls.ja.json +++ b/src/package.nls.ja.json @@ -14,6 +14,7 @@ "command.settings.title": "設定", "command.documentation.title": "ドキュメント", "command.openInNewTab.title": "新しいタブで開く", + "command.git.generateCommitMessage.title": "コミットメッセージを生成", "command.explainCode.title": "コードの説明", "command.fixCode.title": "コードの修正", "command.improveCode.title": "コードの改善", diff --git a/src/package.nls.json b/src/package.nls.json index b6880b8bfe..abf5df47f0 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -25,6 +25,7 @@ "command.terminal.fixCommand.title": "Fix This Command", "command.terminal.explainCommand.title": "Explain This Command", "command.acceptInput.title": "Accept Input/Suggestion", + "command.git.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", "settings.vsCodeLmModelSelector.description": "Settings for VSCode Language Model API", diff --git a/src/package.nls.ko.json b/src/package.nls.ko.json index e305da4c4c..1fefc3b44e 100644 --- a/src/package.nls.ko.json +++ b/src/package.nls.ko.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "이 명령어 수정", "command.terminal.explainCommand.title": "이 명령어 설명", "command.acceptInput.title": "입력/제안 수락", + "command.git.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 3cd1880941..2550ecae01 100644 --- a/src/package.nls.nl.json +++ b/src/package.nls.nl.json @@ -14,6 +14,7 @@ "command.settings.title": "Instellingen", "command.documentation.title": "Documentatie", "command.openInNewTab.title": "Openen in Nieuw Tabblad", + "command.git.generateCommitMessage.title": "Commitbericht genereren", "command.explainCode.title": "Leg Code Uit", "command.fixCode.title": "Repareer Code", "command.improveCode.title": "Verbeter Code", diff --git a/src/package.nls.pl.json b/src/package.nls.pl.json index 275c404d06..d01a0946f5 100644 --- a/src/package.nls.pl.json +++ b/src/package.nls.pl.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Napraw tę Komendę", "command.terminal.explainCommand.title": "Wyjaśnij tę Komendę", "command.acceptInput.title": "Akceptuj Wprowadzanie/Sugestię", + "command.git.generateCommitMessage.title": "Generuj komunikat 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 057f255c44..79f3c10a4c 100644 --- a/src/package.nls.pt-BR.json +++ b/src/package.nls.pt-BR.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Corrigir Este Comando", "command.terminal.explainCommand.title": "Explicar Este Comando", "command.acceptInput.title": "Aceitar Entrada/Sugestão", + "command.git.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 02a5bcf93d..2beb31fea4 100644 --- a/src/package.nls.ru.json +++ b/src/package.nls.ru.json @@ -14,6 +14,7 @@ "command.settings.title": "Настройки", "command.documentation.title": "Документация", "command.openInNewTab.title": "Открыть в новой вкладке", + "command.git.generateCommitMessage.title": "Создать сообщение коммита", "command.explainCode.title": "Объяснить код", "command.fixCode.title": "Исправить код", "command.improveCode.title": "Улучшить код", diff --git a/src/package.nls.tr.json b/src/package.nls.tr.json index dda6e9e8d1..28270bd68a 100644 --- a/src/package.nls.tr.json +++ b/src/package.nls.tr.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Bu Komutu Düzelt", "command.terminal.explainCommand.title": "Bu Komutu Açıkla", "command.acceptInput.title": "Girişi/Öneriyi Kabul Et", + "command.git.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 985465acb7..c075552305 100644 --- a/src/package.nls.vi.json +++ b/src/package.nls.vi.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "Sửa Lệnh Này", "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.git.generateCommitMessage.title": "Tạo tin nhắn 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 25d4e15c0a..59c6f28614 100644 --- a/src/package.nls.zh-CN.json +++ b/src/package.nls.zh-CN.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "修复此命令", "command.terminal.explainCommand.title": "解释此命令", "command.acceptInput.title": "接受输入/建议", + "command.git.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 918b4bff33..23e4fa8059 100644 --- a/src/package.nls.zh-TW.json +++ b/src/package.nls.zh-TW.json @@ -14,6 +14,7 @@ "command.terminal.fixCommand.title": "修復此命令", "command.terminal.explainCommand.title": "解釋此命令", "command.acceptInput.title": "接受輸入/建議", + "command.git.generateCommitMessage.title": "產生提交訊息", "views.activitybar.title": "Roo Code", "views.contextMenu.label": "Roo Code", "views.terminalMenu.label": "Roo Code", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 73ebf59d4c..e0f5246519 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -203,6 +203,7 @@ export type ExtensionState = Pick< | "fuzzyMatchThreshold" // | "experiments" // Optional in GlobalSettings, required here. | "language" + | "commitLanguage" // | "telemetrySetting" // Optional in GlobalSettings, required here. // | "mcpEnabled" // Optional in GlobalSettings, required here. // | "enableMcpServerCreation" // Optional in GlobalSettings, required here. diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 7efc97e8c7..42b9f61e2b 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -144,6 +144,7 @@ export interface WebviewMessage { | "browserConnectionResult" | "remoteBrowserEnabled" | "language" + | "commitLanguage" | "maxReadFileLine" | "maxConcurrentFileReads" | "searchFiles" diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 4a8f06d62a..27e714d0fd 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -28,6 +28,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: false, multiFileApplyDiff: false, + aiCommitMessages: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) @@ -36,6 +37,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: true, multiFileApplyDiff: false, + aiCommitMessages: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) }) @@ -44,6 +46,7 @@ describe("experiments", () => { const experiments: Record = { powerSteering: false, multiFileApplyDiff: false, + aiCommitMessages: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 1edadf654f..a525a59edd 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -3,6 +3,7 @@ import type { AssertEqual, Equals, Keys, Values, ExperimentId, Experiments } fro export const EXPERIMENT_IDS = { MULTI_FILE_APPLY_DIFF: "multiFileApplyDiff", POWER_STEERING: "powerSteering", + AI_COMMIT_MESSAGES: "aiCommitMessages", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -16,6 +17,7 @@ interface ExperimentConfig { export const experimentConfigsMap: Record = { MULTI_FILE_APPLY_DIFF: { enabled: false }, POWER_STEERING: { enabled: false }, + AI_COMMIT_MESSAGES: { enabled: false }, } export const experimentDefault = Object.fromEntries( diff --git a/webview-ui/src/components/settings/CommitLanguageSettings.tsx b/webview-ui/src/components/settings/CommitLanguageSettings.tsx new file mode 100644 index 0000000000..c2342b41e8 --- /dev/null +++ b/webview-ui/src/components/settings/CommitLanguageSettings.tsx @@ -0,0 +1,54 @@ +import { HTMLAttributes } from "react" +import type { Language } from "@roo-code/types" +import { LANGUAGES } from "@roo/language" +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" +import { cn } from "@src/lib/utils" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { SetCachedStateField } from "./types" + +type CommitLanguageSettingsProps = HTMLAttributes & { + commitLanguage: string + setCachedStateField: SetCachedStateField<"commitLanguage"> + aiCommitMessagesEnabled?: boolean // Dodano prop +} + +export const CommitLanguageSettings = ({ + commitLanguage, + setCachedStateField, + aiCommitMessagesEnabled, // Dodano do destrukturyzacji + className, + ...props +}: CommitLanguageSettingsProps) => { + const { t } = useAppTranslation() + + return ( + <> + {aiCommitMessagesEnabled && ( // Warunkowe renderowanie z wcięciem i paskiem +
+
+ {t("settings:sections.commitLanguage")} +
+ +
+ )} + + ) +} diff --git a/webview-ui/src/components/settings/ExperimentalFeature.tsx b/webview-ui/src/components/settings/ExperimentalFeature.tsx index a96a00a426..0f9cd2f0a0 100644 --- a/webview-ui/src/components/settings/ExperimentalFeature.tsx +++ b/webview-ui/src/components/settings/ExperimentalFeature.tsx @@ -16,7 +16,9 @@ export const ExperimentalFeature = ({ enabled, onChange, experimentKey }: Experi const descriptionKey = experimentKey ? `settings:experimental.${experimentKey}.description` : "" return ( -
+
+ {" "} + {/* Dodano flex-col i gap-2 */}
onChange(e.target.checked)}> {t(nameKey)} diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 79d8afefb2..6379c6df52 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -1,7 +1,7 @@ import { HTMLAttributes } from "react" import { FlaskConical } from "lucide-react" -import type { Experiments, CodebaseIndexConfig, CodebaseIndexModels, ProviderSettings } from "@roo-code/types" +import type { Experiments, CodebaseIndexConfig, CodebaseIndexModels, ProviderSettings, Language } from "@roo-code/types" import { EXPERIMENT_IDS, experimentConfigsMap } from "@roo/experiments" @@ -14,17 +14,19 @@ import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { ExperimentalFeature } from "./ExperimentalFeature" import { CodeIndexSettings } from "./CodeIndexSettings" +import { CommitLanguageSettings } from "./CommitLanguageSettings" type ExperimentalSettingsProps = HTMLAttributes & { experiments: Experiments setExperimentEnabled: SetExperimentEnabled - setCachedStateField: SetCachedStateField<"codebaseIndexConfig"> + setCachedStateField: SetCachedStateField<"codebaseIndexConfig" | "commitLanguage"> // Combined the types // CodeIndexSettings props codebaseIndexModels: CodebaseIndexModels | undefined codebaseIndexConfig: CodebaseIndexConfig | undefined apiConfiguration: ProviderSettings setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void areSettingsCommitted: boolean + commitLanguage: Language } export const ExperimentalSettings = ({ @@ -36,6 +38,7 @@ export const ExperimentalSettings = ({ apiConfiguration, setApiConfigurationField, areSettingsCommitted, + commitLanguage, className, ...props }: ExperimentalSettingsProps) => { @@ -65,6 +68,27 @@ export const ExperimentalSettings = ({ } /> ) + } else if (config[0] === "AI_COMMIT_MESSAGES") { + // Dodano warunek dla AI_COMMIT_MESSAGES + return ( + <> + + setExperimentEnabled(EXPERIMENT_IDS.AI_COMMIT_MESSAGES, enabled) + } + /> + + } + aiCommitMessagesEnabled={experiments?.aiCommitMessages} // Przekazujemy prop + /> + + ) } return (
+
{t("settings:sections.pluginLanguage")}