Skip to content

Commit 53aa617

Browse files
committed
refactor(extension): improve error handling and add API key management
- Restructure extension code into separate functions for better maintainability - Add new commands for API key management (update/get/delete) - Enhance error handling for Anthropic API calls with specific error messages - Add preview commit message functionality in separate markdown document - Update Claude model to latest version - Add configurable allowed commit types - Improve logging with usage metrics and stop reasons - Add input validation for API key format - Extract repository access logic into separate function - Add custom instructions support for commit message generation
1 parent 7190aa2 commit 53aa617

File tree

1 file changed

+210
-84
lines changed

1 file changed

+210
-84
lines changed

src/extension.ts

Lines changed: 210 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,148 @@
11
import Anthropic from "@anthropic-ai/sdk"
2-
import { AnthropicError } from "@anthropic-ai/sdk/error"
32
import * as vscode from "vscode"
43

5-
const defaultModel = "claude-3-5-sonnet-20241022"
4+
const defaultModel = "claude-3-5-sonnet-latest"
65
const defaultMaxTokens = 1024
76
const defaultTemperature = 0.4
7+
const defaultAllowedTypes = ["feat", "fix", "refactor", "chore", "docs", "style", "test", "perf", "ci"]
88

99
export 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

137263
export function deactivate() {}

0 commit comments

Comments
 (0)