From 29de8f3a3ae7b2a5d9e9cc4340200114e5dafa56 Mon Sep 17 00:00:00 2001 From: DawidGrzejek Date: Thu, 18 Dec 2025 14:44:03 +0100 Subject: [PATCH] feat: add custom commit message instructions support - Add customInstructions setting for inline custom rules - Add instructionsFilePath setting to load rules from file - Support both relative and absolute file paths - File instructions take precedence over inline instructions - Update README with configuration examples Addresses #2 --- README.md | 44 +++++++++++++++++++++-- package.json | 10 ++++++ src/extension.ts | 90 ++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 131 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b66d231..f649194 100644 --- a/README.md +++ b/README.md @@ -46,14 +46,17 @@ That's it. No configuration, no setup wizards, no complexity. ## Extension Settings -This extension keeps it simple with just two optional settings: +This extension provides several optional settings to customize your experience: -* `claude-commit.claudePath`: Custom path to Claude CLI executable (auto-detects by default) -* `claude-commit.debugMode`: Enable debug output for troubleshooting +- `claude-commit.claudePath`: Custom path to Claude CLI executable (auto-detects by default) +- `claude-commit.debugMode`: Enable debug output for troubleshooting +- `claude-commit.customInstructions`: Custom instructions for commit message generation +- `claude-commit.instructionsFilePath`: Path to a file containing custom commit message instructions ## Configuration Examples ### Using a custom Claude path + ```json { "claude-commit.claudePath": "/usr/local/bin/claude" @@ -61,12 +64,47 @@ This extension keeps it simple with just two optional settings: ``` ### Debug mode for troubleshooting + ```json { "claude-commit.debugMode": true } ``` +### Custom instructions (inline) + +Provide custom rules directly in your settings: + +```json +{ + "claude-commit.customInstructions": "Summarize changes into a single sentence suitable for release notes. Focus on user-facing impact rather than technical details." +} +``` + +### Custom instructions (from file) + +Use a file to share commit message conventions across your team: + +```json +{ + "claude-commit.instructionsFilePath": ".vscode/copilot/commit-message-instructions.md" +} +``` + +Then create `.vscode/copilot/commit-message-instructions.md`: + +```markdown +# Commit Message Instructions + +- Summarize changes into a sentence for release notes +- Focus on the "why" rather than the "what" +- Use imperative mood (e.g., "Add feature" not "Added feature") +- Maximum 50 characters for the subject line +- Include ticket number if applicable: [TICKET-123] Subject +``` + +The file path can be relative (from workspace root) or absolute. If both file and inline instructions are provided, the file takes precedence. + ## Troubleshooting ### Claude CLI not found diff --git a/package.json b/package.json index 9442916..0e17711 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,16 @@ "type": "boolean", "default": false, "description": "Enable debug mode to show CLI commands being executed" + }, + "claude-commit.customInstructions": { + "type": "string", + "default": "", + "description": "Custom instructions for commit message generation (e.g., 'Summarize changes into a sentence for release notes')" + }, + "claude-commit.instructionsFilePath": { + "type": "string", + "default": "", + "description": "Path to a file containing custom commit message instructions (e.g., '.vscode/copilot/commit-message-instructions.md')" } } } diff --git a/src/extension.ts b/src/extension.ts index 3bf6eab..f4a43e1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,8 @@ import * as vscode from 'vscode'; import { exec } from 'child_process'; import { promisify } from 'util'; +import * as fs from 'fs'; +import * as path from 'path'; const execAsync = promisify(exec); @@ -558,16 +560,69 @@ class CommitMessageGenerator { constructor() { this.config = vscode.workspace.getConfiguration('claude-commit'); this.debugMode = this.config.get('debugMode') || false; - + if (this.debugMode) { outputChannel.appendLine('\n=== CONFIGURATION ==='); outputChannel.appendLine(`claudePath: ${this.config.get('claudePath') || 'not set'}`); outputChannel.appendLine(`debugMode: ${this.debugMode}`); + outputChannel.appendLine(`customInstructions: ${this.config.get('customInstructions') || 'not set'}`); + outputChannel.appendLine(`instructionsFilePath: ${this.config.get('instructionsFilePath') || 'not set'}`); } - + this.cliExecutor = new ClaudeCLIExecutor(this.debugMode); } + private async loadCustomInstructions(workspacePath: string): Promise { + if (this.debugMode) { + outputChannel.appendLine('\n[INSTRUCTIONS] Loading custom instructions...'); + } + + let customInstructions = ''; + + // First, try to load from file + const instructionsFilePath = this.config.get('instructionsFilePath'); + if (instructionsFilePath && instructionsFilePath.trim() !== '') { + try { + // Resolve relative paths from workspace root + const resolvedPath = path.isAbsolute(instructionsFilePath) + ? instructionsFilePath + : path.join(workspacePath, instructionsFilePath); + + if (this.debugMode) { + outputChannel.appendLine(`[INSTRUCTIONS] Reading from file: ${resolvedPath}`); + } + + if (fs.existsSync(resolvedPath)) { + customInstructions = fs.readFileSync(resolvedPath, 'utf8').trim(); + if (this.debugMode) { + outputChannel.appendLine(`[INSTRUCTIONS] Loaded ${customInstructions.length} chars from file`); + } + } else { + if (this.debugMode) { + outputChannel.appendLine(`[INSTRUCTIONS] File not found: ${resolvedPath}`); + } + } + } catch (error: any) { + if (this.debugMode) { + outputChannel.appendLine(`[INSTRUCTIONS] Error reading file: ${error.message}`); + } + } + } + + // If no file instructions, use inline custom instructions + if (!customInstructions) { + const inlineInstructions = this.config.get('customInstructions'); + if (inlineInstructions && inlineInstructions.trim() !== '') { + customInstructions = inlineInstructions.trim(); + if (this.debugMode) { + outputChannel.appendLine(`[INSTRUCTIONS] Using inline instructions: ${customInstructions.length} chars`); + } + } + } + + return customInstructions; + } + async generateCommitMessage(repositoryPath?: string): Promise { if (this.debugMode) { outputChannel.appendLine('\n=== GENERATE COMMIT MESSAGE START ==='); @@ -637,9 +692,28 @@ class CommitMessageGenerator { if (this.debugMode) { outputChannel.appendLine(`\n[PROMPT] Creating prompt for ${fileType}...`); } - - // Build prompt with hardcoded conventional format - const prompt = `Generate a git commit message for the following changes. + + // Load custom instructions + const customInstructions = await this.loadCustomInstructions(cwd || ''); + + // Build prompt with default rules + let rulesSection = `Rules: +- Use conventional commit format +- Keep under 72 characters for the first line +- Be specific and clear +- Common types: feat, fix, docs, style, refactor, test, chore`; + + // If custom instructions exist, use them instead + if (customInstructions) { + rulesSection = `Custom Instructions: +${customInstructions}`; + + if (this.debugMode) { + outputChannel.appendLine(`[PROMPT] Using custom instructions (${customInstructions.length} chars)`); + } + } + + const prompt = `Generate a git commit message for the following changes. IMPORTANT: Return ONLY the commit message text itself. Do not include: - Any explanatory text like "Based on...", "Here's...", or "Here is..." @@ -652,11 +726,7 @@ Just return the raw commit message text that will be used directly in git commit Git diff: ${diff} -Rules: -- Use conventional commit format -- Keep under 72 characters for the first line -- Be specific and clear -- Common types: feat, fix, docs, style, refactor, test, chore +${rulesSection} Remember: Return ONLY the commit message text, nothing else.`;