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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/build/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const commandsSchema = z.array(
command: z.string(),
title: z.string(),
category: z.string().optional(),
icon: z.string().optional(),
icon: z.union([z.string(), z.object({ light: z.string(), dark: z.string() })]).optional(),
}),
)

Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ export const globalSettingsSchema = z.object({
* Tools in this list will be excluded from prompt generation and rejected at execution time.
*/
disabledTools: z.array(toolNamesSchema).optional(),

commitMessageApiConfigId: z.string().optional(),
})

export type GlobalSettings = z.infer<typeof globalSettingsSchema>
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export enum TelemetryEventName {
TELEMETRY_SETTINGS_CHANGED = "Telemetry Settings Changed",
MODEL_CACHE_EMPTY_RESPONSE = "Model Cache Empty Response",
READ_FILE_LEGACY_FORMAT_USED = "Read File Legacy Format Used",

COMMIT_MSG_GENERATED = "Commit Message Generated",
}

/**
Expand Down Expand Up @@ -206,6 +208,7 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
TelemetryEventName.MODE_SETTINGS_CHANGED,
TelemetryEventName.CUSTOM_MODE_CREATED,
TelemetryEventName.READ_FILE_LEGACY_FORMAT_USED,
TelemetryEventName.COMMIT_MSG_GENERATED,
]),
properties: telemetryPropertiesSchema,
}),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export type ExtensionState = Pick<
| "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId"
| "commitMessageApiConfigId"
| "customCondensingPrompt"
| "codebaseIndexConfig"
| "codebaseIndexModels"
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2098,6 +2098,7 @@ export class ClineProvider
customModePrompts,
customSupportPrompts,
enhancementApiConfigId,
commitMessageApiConfigId,
autoApprovalEnabled,
customModes,
experiments,
Expand Down Expand Up @@ -2250,6 +2251,7 @@ export class ClineProvider
customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
enhancementApiConfigId,
commitMessageApiConfigId,
autoApprovalEnabled: autoApprovalEnabled ?? false,
customModes,
experiments: experiments ?? experimentDefault,
Expand Down Expand Up @@ -2456,6 +2458,7 @@ export class ClineProvider
customModePrompts: stateValues.customModePrompts ?? {},
customSupportPrompts: stateValues.customSupportPrompts ?? {},
enhancementApiConfigId: stateValues.enhancementApiConfigId,
commitMessageApiConfigId: stateValues.commitMessageApiConfigId,
experiments: stateValues.experiments ?? experimentDefault,
autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
customModes,
Expand Down
1 change: 0 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1606,7 +1606,6 @@ export const webviewMessageHandler = async (
await updateGlobalState("enhancementApiConfigId", message.text)
await provider.postStateToWebview()
break

case "autoApprovalEnabled":
await updateGlobalState("autoApprovalEnabled", message.bool ?? false)
await provider.postStateToWebview()
Expand Down
9 changes: 9 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
import { initializeI18n } from "./i18n"
import { initializeModelCacheRefresh } from "./api/providers/fetchers/modelCache"
import { initZooCodeAuth } from "./services/zoo-code-auth"
import { registerCommitMessageProvider } from "./services/commit-message"

/**
* Built using https://github.com/microsoft/vscode-webview-ui-toolkit
Expand Down Expand Up @@ -256,6 +257,14 @@ export async function activate(context: vscode.ExtensionContext) {

registerCommands({ context, outputChannel, provider })

try {
registerCommitMessageProvider(context, outputChannel)
} catch (error) {
outputChannel.appendLine(
`Failed to register commit message provider: ${error instanceof Error ? error.message : String(error)}`,
)
}

/**
* We use the text document content provider API to show the left side for diff
* view by creating a virtual document for the original content. This makes it
Expand Down
51 changes: 51 additions & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,56 @@
"connected": "Zoo Code: Successfully connected! You can now use Zoo Code as your AI provider.",
"disconnected": "Zoo Code: Disconnected successfully."
}
},
"commitMessage": {
"activated": "Zoo Code commit message generator activated",
"gitNotFound": "⚠️ Git repository not found or git not available",
"gitInitError": "⚠️ Git initialization error: {{error}}",
"generating": "Zoo: Generating commit message...",
"noChanges": "Zoo: No changes found to analyze",
"generated": "Zoo: Commit message generated!",
"generationFailed": "Zoo: Failed to generate commit message: {{errorMessage}}",
"contextWarnings": "Zoo: Git context warning: {{warnings}}",
"generatingFromUnstaged": "Zoo: Generating message using unstaged changes",
"activationFailed": "Zoo: Failed to activate message generator: {{error}}",
"providerRegistered": "Zoo: Commit message provider registered",
"initializing": "Initializing...",
"discoveringFiles": "Discovering files...",
"foundChanges": "Found {{count}} changes",
"gettingContext": "Getting git context...",
"errors": {
"connectionFailed": "Failed to connect to Zoo Code extension",
"timeout": "Request timed out after 30 seconds",
"invalidResponse": "Invalid response format received from extension",
"missingMessage": "No commit message received from extension",
"noChanges": "No changes found to commit",
"noProject": "No project available",
"noWorkspacePath": "Could not determine workspace path for Git repository",
"workspaceNotFound": "Could not determine workspace path for Git repository",
"processingError": "Error processing commit message generation: {{error}}"
},
"error": {
"title": "Error",
"workspacePathNotFound": "Could not determine workspace path for Git repository",
"generationFailed": "Failed to generate commit message: {{error}}",
"processingFailed": "Error processing commit message generation: {{error}}",
"unknown": "Unknown error"
},
"dialogs": {
"info": "AI Commit Message",
"error": "Error",
"success": "Success",
"title": "AI Commit Message"
},
"progress": {
"title": "Generating Commit Message",
"analyzing": "Analyzing changes...",
"connecting": "Connecting to Zoo Code...",
"generating": "Generating commit message..."
},
"ui": {
"generateButton": "Generate Commit Message",
"generateButtonTooltip": "Generates commit message using AI to analyze your code changes"
}
}
}
21 changes: 21 additions & 0 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@
"command": "zoo-code.toggleAutoApprove",
"title": "%command.toggleAutoApprove.title%",
"category": "%configuration.title%"
},
{
"command": "zoo-code.generateCommitMessage",
"title": "%command.generateCommitMessage.title%",
"icon": {
"light": "assets/icons/panel_light.png",
"dark": "assets/icons/panel_dark.png"
}
}
],
"menus": {
Expand Down Expand Up @@ -207,6 +215,19 @@
"group": "1_actions@3"
}
],
"scm/input": [
{
"command": "zoo-code.generateCommitMessage",
"group": "navigation"
}
],
"scm/title": [
{
"command": "zoo-code.generateCommitMessage",
"when": "scmProvider == git",
"group": "navigation"
}
],
"view/title": [
{
"command": "zoo-code.plusButtonClicked",
Expand Down
1 change: 1 addition & 0 deletions src/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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 with Zoo",
"configuration.title": "Zoo Code",
"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
"commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.",
Expand Down
199 changes: 199 additions & 0 deletions src/services/commit-message/CommitMessageGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { ContextProxy } from "../../core/config/ContextProxy"
import { ProviderSettingsManager } from "../../core/config/ProviderSettingsManager"
import { singleCompletionHandler as defaultSingleCompletionHandler } from "../../utils/single-completion-handler"
import { supportPrompt } from "../../shared/support-prompt"
import { addCustomInstructions as defaultAddCustomInstructions } from "../../core/prompts/sections/custom-instructions"
import { TelemetryService } from "@roo-code/telemetry"
import { TelemetryEventName, type ProviderSettings } from "@roo-code/types"

import { GenerateMessageParams, PromptOptions, ProgressUpdate } from "./types/core"

export interface CommitMessageContextProxy {
isInitialized: boolean
getProviderSettings(): ProviderSettings
getValue(key: any): unknown
}

export interface CommitMessageGeneratorDependencies {
getContextProxy?: () => CommitMessageContextProxy
completePrompt?: (apiConfiguration: ProviderSettings, promptText: string) => Promise<string>
addCustomInstructions?: typeof defaultAddCustomInstructions
captureGenerated?: () => void
logger?: Pick<Console, "warn">
}

export class CommitMessageGenerator {
private readonly providerSettingsManager: ProviderSettingsManager
private readonly dependencies: Required<CommitMessageGeneratorDependencies>
private previousGitContext: string | null = null
private previousCommitMessage: string | null = null

constructor(
providerSettingsManager: ProviderSettingsManager,
dependencies: CommitMessageGeneratorDependencies = {},
) {
this.providerSettingsManager = providerSettingsManager
this.dependencies = {
getContextProxy: dependencies.getContextProxy ?? (() => ContextProxy.instance),
completePrompt: dependencies.completePrompt ?? defaultSingleCompletionHandler,
addCustomInstructions: dependencies.addCustomInstructions ?? defaultAddCustomInstructions,
captureGenerated:
dependencies.captureGenerated ??
(() => TelemetryService.instance.captureEvent(TelemetryEventName.COMMIT_MSG_GENERATED)),
logger: dependencies.logger ?? console,
}
}

async generateMessage(params: GenerateMessageParams): Promise<string> {
const { gitContext, onProgress } = params

try {
onProgress?.({
message: "Generating commit message...",
percentage: 75,
})

const generatedMessage = await this.callAIForCommitMessage(gitContext, params.workspacePath, onProgress)

this.previousGitContext = gitContext
this.previousCommitMessage = generatedMessage

this.dependencies.captureGenerated()

onProgress?.({
message: "Commit message generated successfully",
percentage: 100,
})

return generatedMessage
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
throw new Error(`Failed to generate commit message: ${errorMessage}`)
}
}

async buildPrompt(gitContext: string, options: PromptOptions, workspacePath: string): Promise<string> {
const { customSupportPrompts = {}, previousContext, previousMessage } = options

const customInstructions = await this.dependencies.addCustomInstructions("", "", workspacePath, "commit", {
language: "en",
})

const shouldGenerateDifferentMessage =
(previousContext === gitContext || this.previousGitContext === gitContext) &&
(previousMessage !== null || this.previousCommitMessage !== null)

const targetPreviousMessage = previousMessage || this.previousCommitMessage

if (shouldGenerateDifferentMessage && targetPreviousMessage) {
const differentMessagePrefix = `# CRITICAL INSTRUCTION: GENERATE A COMPLETELY DIFFERENT COMMIT MESSAGE
The user has requested a new commit message for the same changes.
The previous message was: "${targetPreviousMessage}"
YOU MUST create a message that is COMPLETELY DIFFERENT by:
- Using entirely different wording and phrasing
- Focusing on different aspects of the changes
- Using a different structure or format if appropriate
- Possibly using a different type or scope if justifiable
This is the MOST IMPORTANT requirement for this task.

`
const baseTemplate = supportPrompt.get(customSupportPrompts, "COMMIT_MESSAGE")
const modifiedTemplate =
differentMessagePrefix +
baseTemplate +
`

FINAL REMINDER: Your message MUST be COMPLETELY DIFFERENT from the previous message: "${targetPreviousMessage}". This is a critical requirement.`

return supportPrompt.create(
"COMMIT_MESSAGE",
{
gitContext,
customInstructions: customInstructions || "",
},
{
...customSupportPrompts,
COMMIT_MESSAGE: modifiedTemplate,
},
)
} else {
return supportPrompt.create(
"COMMIT_MESSAGE",
{
gitContext,
customInstructions: customInstructions || "",
},
customSupportPrompts,
)
}
}

private async callAIForCommitMessage(
gitContextString: string,
workspacePath: string,
onProgress?: (progress: ProgressUpdate) => void,
): Promise<string> {
const contextProxy = this.dependencies.getContextProxy()
if (!contextProxy.isInitialized) {
throw new Error("ContextProxy not initialized. Please try again after the extension has fully loaded.")
}
const apiConfiguration = contextProxy.getProviderSettings()
const commitMessageApiConfigId = contextProxy.getValue("commitMessageApiConfigId") as string | undefined
const listApiConfigMeta = (contextProxy.getValue("listApiConfigMeta") || []) as Array<{ id: string }>
const customSupportPrompts = (contextProxy.getValue("customSupportPrompts") || {}) as Record<
string,
string | undefined
>

let configToUse: ProviderSettings = apiConfiguration

if (commitMessageApiConfigId && listApiConfigMeta.find(({ id }) => id === commitMessageApiConfigId)) {
try {
await this.providerSettingsManager.initialize()
const { name: _, ...providerSettings } = await this.providerSettingsManager.getProfile({
id: commitMessageApiConfigId,
})

if (providerSettings.apiProvider) {
configToUse = providerSettings
}
} catch (error) {
this.dependencies.logger.warn(
`Failed to load commit message API profile ${commitMessageApiConfigId}; falling back to current API configuration`,
error,
)
}
}

const filteredPrompts = Object.fromEntries(
Object.entries(customSupportPrompts).filter(([_, value]) => value !== undefined),
) as Record<string, string>

const prompt = await this.buildPrompt(
gitContextString,
{ customSupportPrompts: filteredPrompts },
workspacePath,
)

onProgress?.({
message: "Calling AI service...",
increment: 10,
})

const response = await this.dependencies.completePrompt(configToUse, prompt)

onProgress?.({
message: "Processing AI response...",
increment: 10,
})

return this.extractCommitMessage(response)
}

private extractCommitMessage(response: string): string {
const cleaned = response.trim()
const withoutCodeBlocks = cleaned.replace(/```[a-z]*\n|```/g, "")
const withoutQuotes = withoutCodeBlocks.replace(/^["'`]|["'`]$/g, "")
return withoutQuotes.trim()
}
}
Loading