11import Anthropic from "@anthropic-ai/sdk"
2- import { AnthropicError } from "@anthropic-ai/sdk/error"
32import * as vscode from "vscode"
43
5- const defaultModel = "claude-3-5-sonnet-20241022 "
4+ const defaultModel = "claude-3-5-sonnet-latest "
65const defaultMaxTokens = 1024
76const defaultTemperature = 0.4
7+ const defaultAllowedTypes = [ "feat" , "fix" , "refactor" , "chore" , "docs" , "style" , "test" , "perf" , "ci" ]
88
99export function activate ( context : vscode . ExtensionContext ) {
10- const disposable = vscode . commands . registerCommand ( "diffCommit.generateCommitMessage" , async ( ) => {
11- const workspaceRoot = vscode . workspace . workspaceFolders ?. [ 0 ] ?. uri ?. fsPath
12- if ( ! workspaceRoot ) {
13- vscode . window . showErrorMessage ( "No workspace folder found" )
14- return
15- }
16-
17- const config = vscode . workspace . getConfiguration ( "diffCommit" )
18-
19- // Get the Git extension
10+ function getRepo ( ) : any | undefined {
2011 const gitExtension = vscode . extensions . getExtension ( "vscode.git" ) ?. exports
2112 if ( ! gitExtension ) {
2213 vscode . window . showErrorMessage ( "Git extension not found" )
23- return
14+ return undefined
2415 }
25-
26- const git = gitExtension . getAPI ( 1 )
27- const repo = git . repositories [ 0 ]
28- if ( ! repo ) {
16+ const gitAPI = gitExtension . getAPI ( 1 )
17+ const gitRepo = gitAPI . repositories [ 0 ]
18+ if ( ! gitRepo ) {
2919 vscode . window . showErrorMessage ( "No Git repository found" )
30- return
20+ return undefined
3121 }
22+ return gitRepo
23+ }
3224
33- // Get the diff of staged changes
34- const diff = await repo . diff ( true )
35- if ( ! diff ) {
36- vscode . window . showErrorMessage ( "No changes detected" )
37- return
25+ async function setAPIKey ( ) : Promise < string | undefined > {
26+ try {
27+ const apiKey = await vscode . window . showInputBox ( {
28+ prompt : "Enter your Anthropic API Key" ,
29+ password : true ,
30+ placeHolder : "sk-ant-api..." ,
31+ } )
32+
33+ if ( ! apiKey ) {
34+ vscode . window . showErrorMessage ( "API Key is required" )
35+ return undefined
36+ }
37+
38+ if ( ! apiKey . startsWith ( "sk-ant-api" ) ) {
39+ vscode . window . showErrorMessage ( "Invalid Anthropic API Key format. Should start with sk-ant-api" )
40+ return undefined
41+ }
42+
43+ await context . secrets . store ( "anthropic-api-key" , apiKey )
44+ vscode . window . showInformationMessage ( "API Key updated successfully" )
45+
46+ return apiKey
47+ } catch ( error ) {
48+ console . error ( "Secrets storage error:" , error )
49+ vscode . window . showErrorMessage (
50+ `Failed to update API key in secure storage: ${ error instanceof Error ? error . message : String ( error ) } ` ,
51+ )
52+ return undefined
3853 }
54+ }
3955
40- let apiKey : string | undefined
56+ async function getAPIKey ( ) : Promise < string | undefined > {
4157 try {
42- // Try to get existing API key from secure storage only
43- apiKey = await context . secrets . get ( "anthropic-api-key" )
58+ return await context . secrets . get ( "anthropic-api-key" )
59+ } catch ( error ) {
60+ console . error ( "Secrets storage error:" , error )
61+ vscode . window . showErrorMessage (
62+ `Failed to access secure storage: ${ error instanceof Error ? error . message : String ( error ) } ` ,
63+ )
64+ return undefined
65+ }
66+ }
4467
45- // If no key exists, prompt for it
68+ async function deleteAPIKey ( ) : Promise < void > {
69+ try {
70+ const apiKey = await context . secrets . get ( "anthropic-api-key" )
4671 if ( ! apiKey ) {
47- apiKey = await vscode . window . showInputBox ( {
48- prompt : "Enter your Anthropic API Key" ,
49- password : true ,
50- placeHolder : "sk-ant-api..." ,
51- } )
52- if ( ! apiKey ) {
53- vscode . window . showErrorMessage ( "API Key is required" )
54- return
55- }
56- if ( ! apiKey . startsWith ( "sk-ant-api" ) ) {
57- vscode . window . showErrorMessage ( "Invalid Anthropic API Key format. Should start with sk-ant-api" )
58- return
59- }
60- // Store the new key in SecretStorage only
61- await context . secrets . store ( "anthropic-api-key" , apiKey )
72+ vscode . window . showWarningMessage ( "No API Key found to remove" )
73+ return
6274 }
75+ await context . secrets . delete ( "anthropic-api-key" )
76+ vscode . window . showInformationMessage ( "API Key deleted successfully" )
6377 } catch ( error ) {
6478 console . error ( "Secrets storage error:" , error )
65- vscode . window . showErrorMessage ( "Failed to access secure storage:" , JSON . stringify ( error ) )
66- return
79+ vscode . window . showErrorMessage (
80+ `Failed to delete API key from secure storage: ${ error instanceof Error ? error . message : String ( error ) } ` ,
81+ )
82+ }
83+ }
84+
85+ async function generateCommitMessage ( ) : Promise < string | undefined > {
86+ const workspaceRoot = vscode . workspace . workspaceFolders ?. [ 0 ] ?. uri ?. fsPath
87+ if ( ! workspaceRoot ) {
88+ vscode . window . showErrorMessage ( "No workspace folder found" )
89+ return undefined
90+ }
91+
92+ const config = vscode . workspace . getConfiguration ( "diffCommit" )
93+
94+ const gitRepo = getRepo ( )
95+ if ( ! gitRepo ) {
96+ return undefined
97+ }
98+
99+ const diff = await gitRepo . diff ( true )
100+ if ( ! diff ) {
101+ vscode . window . showErrorMessage ( "No changes detected" )
102+ return undefined
103+ }
104+
105+ const apiKey = ( await getAPIKey ( ) ) ?? ( await setAPIKey ( ) )
106+ if ( ! apiKey ) {
107+ vscode . window . showErrorMessage ( "API Key is required" )
108+ return undefined
67109 }
68110
69111 const anthropic = new Anthropic ( {
70112 apiKey,
71113 } )
72114
115+ const customInstructions = config . get < string > ( "customInstructions" ) || undefined
116+ const allowedTypes = config . get < string [ ] > ( "allowedTypes" ) || defaultAllowedTypes
117+ const model = config . get < string > ( "model" ) || defaultModel
118+ const maxTokens = config . get < number > ( "maxTokens" ) || defaultMaxTokens
119+ const temperature = config . get < number > ( "temperature" ) || defaultTemperature
120+ const systemPrompt =
121+ "You are a seasoned software developer with an extraordinary ability for writing detailed conventional commit messages and following 'instructions' and 'customInstructions' when generating them."
73122 const prompt = `
74- <task>
75- Generate a detailed conventional commit message for the following Git diff:\n\n${ diff } \n
76- </task>
77- <instructions>
78- - Use 'feat' | 'fix' | 'refactor' | 'chore' | 'docs' | 'style' | 'test' | 'perf' | 'ci' as appropriate for the type of change.
79- - Always include a scope.
80- - Never use '!' or 'BREAKING CHANGE' in the commit message.
81- - Output will use markdown formatting for lists etc.
82- - Output will ONLY contain the commit message.
83- - Do not include any other text or explanation in the output.
84- </instructions>
85- `
123+ <task>
124+ Generate a detailed conventional commit message for the following Git diff:
86125
126+ ${ diff }
127+ </task>
128+ <instructions>
129+ - Use ONLY ${ allowedTypes . map ( ( val ) => `'${ val } '` ) . join ( " | " ) } as appropriate for the type of change.
130+ - Always include a scope.
131+ - Never use '!' or 'BREAKING CHANGE' in the commit message.
132+ - Output will use markdown formatting for lists etc.
133+ - Output will ONLY contain the commit message.
134+ - Do not include any other text or explanation in the output.
135+ </instructions>
136+ ${ customInstructions ? `<customInstructions>\n${ customInstructions } \n</customInstructions>` : "" }
137+ ` . trim ( )
138+
139+ let message : Anthropic . Message | undefined = undefined
87140 try {
88- const message = await anthropic . messages . create ( {
89- model : config . get < string > ( "model" ) || defaultModel ,
90- max_tokens : config . get < number > ( "maxTokens" ) || defaultMaxTokens ,
91- temperature : config . get < number > ( "temperature" ) || defaultTemperature ,
92- system :
93- "You are a seasoned software developer with an extraordinary gift for writing detailed conventional commit messages." ,
141+ message = await anthropic . messages . create ( {
142+ model,
143+ max_tokens : maxTokens ,
144+ temperature,
145+ system : systemPrompt ,
94146 messages : [
95147 {
96148 role : "user" ,
@@ -99,39 +151,113 @@ export function activate(context: vscode.ExtensionContext) {
99151 ] ,
100152 } )
101153
102- let commitMessage
103- if ( message . content [ 0 ] . type === "text" ) {
104- commitMessage = message . content
105- . filter ( ( msg ) => msg . type === "text" )
106- . map ( ( msg ) => msg . text )
107- . join ( "\n" )
108- . replace ( / \n { 3 , } / g, "\n\n" )
109- . trim ( )
154+ let commitMessage : string | undefined
155+ commitMessage = message . content
156+ . filter ( ( msg ) => msg . type === "text" && "text" in msg )
157+ . map ( ( msg ) => msg . text )
158+ . join ( "\n" )
159+ . replace ( / \n { 3 , } / g, "\n\n" )
160+ . trim ( )
161+
162+ if ( ! commitMessage ) {
163+ vscode . window . showWarningMessage ( "No commit message was generated" )
164+ return undefined
110165 }
111166
112- if ( commitMessage ) {
113- console . log ( message . stop_reason , message . usage )
167+ // Replace bullets occasionally output by the model with hyphens
168+ return commitMessage . replace ( / \* \s / g, "- " )
169+ } catch ( error ) {
170+ if ( error instanceof Anthropic . APIError ) {
171+ const errorMessage = error . message || "Unknown Anthropic API error"
172+ console . error ( `Anthropic API Error (${ error . status } ):` , errorMessage )
114173
115- // Replace bullets occasionally output by the model with hyphens
116- const processedMessage = commitMessage . replace ( / \* \s / g, "- " )
174+ switch ( error . status ) {
175+ case 400 :
176+ vscode . window . showErrorMessage ( "Bad request. Review your prompt and try again." )
177+ break
178+ case 401 :
179+ vscode . window . showErrorMessage ( "Invalid API key. Please update your API key and try again." )
180+ break
181+ case 403 :
182+ vscode . window . showErrorMessage ( "Permission Denied. Review your prompt or API key and try again." )
183+ break
184+ case 429 :
185+ vscode . window . showErrorMessage ( `Rate limit exceeded. Please try again later: ${ errorMessage } ` )
186+ break
187+ case 500 :
188+ vscode . window . showErrorMessage ( "Anthropic API server error. Please try again later." )
189+ break
190+ default :
191+ vscode . window . showErrorMessage ( `Failed to generate commit message: ${ errorMessage } ` )
192+ break
193+ }
194+ } else {
195+ console . error ( `Unknown error: ${ error instanceof Error ? error . message : String ( error ) } ` )
196+ vscode . window . showErrorMessage (
197+ `Unknown error generating commit message: ${ error instanceof Error ? error . message : String ( error ) } ` ,
198+ )
199+ }
200+ return undefined
201+ } finally {
202+ console . log ( "[DiffCommit] Stop Reason: " , message ?. stop_reason )
203+ console . log ( "[DiffCommit] Usage: " , message ?. usage )
204+ }
205+ }
117206
118- // TODO: Add some verification for format and content like starts with `type enum`, includes scope, etc.
207+ // Register all commands
208+ const cmdUpdateAPIKey = vscode . commands . registerCommand ( "diffCommit.updateAPIKey" , setAPIKey )
209+ const cmdGetAPIKey = vscode . commands . registerCommand ( "diffCommit.getAPIKey" , getAPIKey )
210+ const cmdDeleteAPIKey = vscode . commands . registerCommand ( "diffCommit.deleteAPIKey" , deleteAPIKey )
211+ const cmdGenerateCommitMessage = vscode . commands . registerCommand ( "diffCommit.generateCommitMessage" , async ( ) => {
212+ try {
213+ const commitMessage = await generateCommitMessage ( )
214+ if ( ! commitMessage ) {
215+ return
216+ }
119217
120- // Set the commit message in the repository's input box
121- repo . inputBox . value = processedMessage
218+ // Set the commit message in the repository's input box
219+ const gitRepo = getRepo ( )
220+ if ( ! gitRepo ) {
221+ return
122222 }
223+ gitRepo . inputBox . value = commitMessage
123224 } catch ( error ) {
124- if ( error instanceof AnthropicError ) {
125- console . error ( "Anthropic API Error:" , error )
126- vscode . window . showErrorMessage ( `Failed to generate commit message: ${ error . message } ` )
127- } else {
128- console . error ( "Unknown error:" , error )
129- vscode . window . showErrorMessage ( "Unknown error generating commit message:" , JSON . stringify ( error ) )
225+ console . error ( "Error writing commit message to SCM:" , error )
226+ vscode . window . showErrorMessage (
227+ `Failed to write to SCM: ${ error instanceof Error ? error . message : String ( error ) } ` ,
228+ )
229+ }
230+ } )
231+
232+ const cmdPreviewCommitMessage = vscode . commands . registerCommand ( "diffCommit.previewCommitMessage" , async ( ) => {
233+ try {
234+ const commitMessage = await generateCommitMessage ( )
235+ if ( ! commitMessage ) {
236+ return
130237 }
238+
239+ // Create a new untitled markdown document with the commit message
240+ const document = await vscode . workspace . openTextDocument ( {
241+ content : commitMessage ,
242+ language : "markdown" ,
243+ } )
244+ await vscode . window . showTextDocument ( document )
245+ } catch ( error ) {
246+ console . error ( "Error opening commit message preview:" , error )
247+ vscode . window . showErrorMessage (
248+ `Failed to open commit message preview: ${ error instanceof Error ? error . message : String ( error ) } ` ,
249+ )
131250 }
132251 } )
133252
134- context . subscriptions . push ( disposable )
253+ // Push all commands to subscriptions
254+ context . subscriptions . push (
255+ cmdGenerateCommitMessage ,
256+ cmdPreviewCommitMessage ,
257+ cmdUpdateAPIKey ,
258+ cmdGetAPIKey ,
259+ cmdDeleteAPIKey ,
260+ )
135261}
136262
137263export function deactivate ( ) { }
0 commit comments