From 44a8509cf050e4e5fbc8c599097dc08cc3014c12 Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Fri, 25 Oct 2024 21:09:08 +1000 Subject: [PATCH 01/10] feat(preview): add commit message preview functionality Adds a new command and content provider to enable previewing commit messages: - Implements `previewCommitMessage` command to show editable previews - Creates custom URI scheme `commit-preview` for preview documents - Maintains document content state using in-memory Maps - Handles document lifecycle events (save, change, close) - Syncs preview content with SCM input box on save/close - Cleans up resources when preview is closed - Preserves original message to detect modifications --- src/cmdPreview.ts | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/cmdPreview.ts diff --git a/src/cmdPreview.ts b/src/cmdPreview.ts new file mode 100644 index 0000000..080cfad --- /dev/null +++ b/src/cmdPreview.ts @@ -0,0 +1,82 @@ +import * as vscode from "vscode" + +export function activate(context: vscode.ExtensionContext) { + // Keep track of document content in memory + const documentContent = new Map() + const originalContent = new Map() + + // Create event emitter for content changes + const onDidChangeEmitter = new vscode.EventEmitter() + + // Register the content provider + const provider = { + onDidChange: onDidChangeEmitter.event, + provideTextDocumentContent: (uri: vscode.Uri) => documentContent.get(uri.path) || "", + } + + const registration = vscode.workspace.registerTextDocumentContentProvider("commit-preview", provider) + + const disposable = vscode.commands.registerCommand( + "extension.previewCommitMessage", + async (aiGeneratedMessage: string) => { + // Create URI for this preview + const uri = vscode.Uri.parse("commit-preview:Commit Message Preview") + + // Store initial content + documentContent.set(uri.path, aiGeneratedMessage) + originalContent.set(uri.path, aiGeneratedMessage) + + // Open the document + const doc = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(doc, { + preview: true, + preserveFocus: false, + }) + + // Handle saves + const saveListener = vscode.workspace.onDidSaveTextDocument((savedDoc) => { + if (savedDoc === doc) { + const content = documentContent.get(uri.path) + if (content) { + vscode.scm.inputBox.value = content + vscode.window.setStatusBarMessage("Commit message updated", 2000) + } + } + }) + + // Handle document changes + const changeListener = vscode.workspace.onDidChangeTextDocument((e) => { + if (e.document === doc) { + documentContent.set(uri.path, e.document.getText()) + onDidChangeEmitter.fire(uri) + } + }) + + // Handle closing + const closeListener = vscode.workspace.onDidCloseTextDocument((closedDoc) => { + if (closedDoc === doc) { + const current = documentContent.get(uri.path) + const original = originalContent.get(uri.path) + + // Only update SCM if content was modified + if (current && original && current !== original) { + vscode.scm.inputBox.value = current + } + + // Cleanup + documentContent.delete(uri.path) + originalContent.delete(uri.path) + saveListener.dispose() + changeListener.dispose() + closeListener.dispose() + } + }) + + context.subscriptions.push(saveListener, changeListener, closeListener) + }, + ) + + context.subscriptions.push(disposable, registration) +} + +export function deactivate() {} From 6bf56142759432cf153399320c978ae628f3aff6 Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Fri, 25 Oct 2024 21:09:08 +1000 Subject: [PATCH 02/10] feat(preview): add commit message preview functionality Adds a new command and content provider to enable previewing commit messages: - Implements `previewCommitMessage` command to show editable previews - Creates custom URI scheme `commit-preview` for preview documents - Maintains document content state using in-memory Maps - Handles document lifecycle events (save, change, close) - Syncs preview content with SCM input box on save/close - Cleans up resources when preview is closed - Preserves original message to detect modifications --- src/cmdPreview.ts | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/cmdPreview.ts diff --git a/src/cmdPreview.ts b/src/cmdPreview.ts new file mode 100644 index 0000000..080cfad --- /dev/null +++ b/src/cmdPreview.ts @@ -0,0 +1,82 @@ +import * as vscode from "vscode" + +export function activate(context: vscode.ExtensionContext) { + // Keep track of document content in memory + const documentContent = new Map() + const originalContent = new Map() + + // Create event emitter for content changes + const onDidChangeEmitter = new vscode.EventEmitter() + + // Register the content provider + const provider = { + onDidChange: onDidChangeEmitter.event, + provideTextDocumentContent: (uri: vscode.Uri) => documentContent.get(uri.path) || "", + } + + const registration = vscode.workspace.registerTextDocumentContentProvider("commit-preview", provider) + + const disposable = vscode.commands.registerCommand( + "extension.previewCommitMessage", + async (aiGeneratedMessage: string) => { + // Create URI for this preview + const uri = vscode.Uri.parse("commit-preview:Commit Message Preview") + + // Store initial content + documentContent.set(uri.path, aiGeneratedMessage) + originalContent.set(uri.path, aiGeneratedMessage) + + // Open the document + const doc = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(doc, { + preview: true, + preserveFocus: false, + }) + + // Handle saves + const saveListener = vscode.workspace.onDidSaveTextDocument((savedDoc) => { + if (savedDoc === doc) { + const content = documentContent.get(uri.path) + if (content) { + vscode.scm.inputBox.value = content + vscode.window.setStatusBarMessage("Commit message updated", 2000) + } + } + }) + + // Handle document changes + const changeListener = vscode.workspace.onDidChangeTextDocument((e) => { + if (e.document === doc) { + documentContent.set(uri.path, e.document.getText()) + onDidChangeEmitter.fire(uri) + } + }) + + // Handle closing + const closeListener = vscode.workspace.onDidCloseTextDocument((closedDoc) => { + if (closedDoc === doc) { + const current = documentContent.get(uri.path) + const original = originalContent.get(uri.path) + + // Only update SCM if content was modified + if (current && original && current !== original) { + vscode.scm.inputBox.value = current + } + + // Cleanup + documentContent.delete(uri.path) + originalContent.delete(uri.path) + saveListener.dispose() + changeListener.dispose() + closeListener.dispose() + } + }) + + context.subscriptions.push(saveListener, changeListener, closeListener) + }, + ) + + context.subscriptions.push(disposable, registration) +} + +export function deactivate() {} From 3924e8c0b7cd61ab4d1d1f17aa28a12732be4520 Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Fri, 25 Oct 2024 21:31:07 +1000 Subject: [PATCH 03/10] refactor(architecture): implement class-based modular architecture - Extract core functionality into dedicated manager classes: - APIKeyManager for API key operations - CommitMessageGenerator for message generation logic - ConfigManager for configuration handling - GitManager for Git operations - Move API key management logic from extension.ts to APIKeyManager - Create CommitMessageGenerator class to handle prompt generation and API calls - Implement ConfigManager for centralised configuration access - Add GitManager to encapsulate Git-related operations - Update extension.ts to use new manager classes - Improve error handling and logging across components --- src/apiKeyManager.ts | 65 ++++++++++ src/commitMessageGenerator.ts | 104 ++++++++++++++++ src/configManager.ts | 38 ++++++ src/extension.ts | 215 +++------------------------------- src/gitManager.ts | 34 ++++++ 5 files changed, 259 insertions(+), 197 deletions(-) create mode 100644 src/apiKeyManager.ts create mode 100644 src/commitMessageGenerator.ts create mode 100644 src/configManager.ts create mode 100644 src/gitManager.ts diff --git a/src/apiKeyManager.ts b/src/apiKeyManager.ts new file mode 100644 index 0000000..8e07529 --- /dev/null +++ b/src/apiKeyManager.ts @@ -0,0 +1,65 @@ +import * as vscode from "vscode" + +export class APIKeyManager { + constructor(private context: vscode.ExtensionContext) {} + + async setAPIKey(): Promise { + try { + const apiKey = await vscode.window.showInputBox({ + prompt: "Enter your Anthropic API Key", + password: true, + placeHolder: "sk-ant-api...", + }) + + if (!apiKey) { + vscode.window.showErrorMessage("API Key is required") + return undefined + } + + if (!apiKey.startsWith("sk-ant-api")) { + vscode.window.showErrorMessage("Invalid Anthropic API Key format. Should start with sk-ant-api") + return undefined + } + + await this.context.secrets.store("anthropic-api-key", apiKey) + vscode.window.showInformationMessage("API Key updated successfully") + + return apiKey + } catch (error) { + console.error("Secrets storage error:", error) + vscode.window.showErrorMessage( + `Failed to update API key in secure storage: ${error instanceof Error ? error.message : String(error)}`, + ) + return undefined + } + } + + async getAPIKey(): Promise { + try { + return await this.context.secrets.get("anthropic-api-key") + } catch (error) { + console.error("Secrets storage error:", error) + vscode.window.showErrorMessage( + `Failed to access secure storage: ${error instanceof Error ? error.message : String(error)}`, + ) + return undefined + } + } + + async deleteAPIKey(): Promise { + try { + const apiKey = await this.context.secrets.get("anthropic-api-key") + if (!apiKey) { + vscode.window.showWarningMessage("No API Key found to remove") + return + } + await this.context.secrets.delete("anthropic-api-key") + vscode.window.showInformationMessage("API Key deleted successfully") + } catch (error) { + console.error("Secrets storage error:", error) + vscode.window.showErrorMessage( + `Failed to delete API key from secure storage: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } +} diff --git a/src/commitMessageGenerator.ts b/src/commitMessageGenerator.ts new file mode 100644 index 0000000..5ec7a57 --- /dev/null +++ b/src/commitMessageGenerator.ts @@ -0,0 +1,104 @@ +import Anthropic from "@anthropic-ai/sdk" +import * as vscode from "vscode" +import { CommitConfig } from "./configManager" + +export class CommitMessageGenerator { + constructor(private apiKey: string) {} + + async generateMessage(diff: string, config: CommitConfig): Promise { + const anthropic = new Anthropic({ + apiKey: this.apiKey, + }) + + const systemPrompt = + "You are a seasoned software developer with an extraordinary ability for writing detailed conventional commit messages and following 'instructions' and 'customInstructions' when generating them." + + const prompt = ` + + Generate a detailed conventional commit message for the following Git diff: + + ${diff} + + + - Use ONLY ${config.allowedTypes.map((val) => `'${val}'`).join(" | ")} as appropriate for the type of change. + - Always include a scope. + - Never use '!' or 'BREAKING CHANGE' in the commit message. + - Output will use markdown formatting for lists etc. + - Output will ONLY contain the commit message. + - Do not include any other text or explanation in the output. + + ${config.customInstructions ? `\n${config.customInstructions}\n` : ""} + `.trim() + + let message: Anthropic.Message | undefined = undefined + try { + message = await anthropic.messages.create({ + model: config.model, + max_tokens: config.maxTokens, + temperature: config.temperature, + system: systemPrompt, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }) + + let commitMessage: string | undefined + commitMessage = message.content + .filter((msg) => msg.type === "text" && "text" in msg) + .map((msg) => msg.text) + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trim() + + if (!commitMessage) { + vscode.window.showWarningMessage("No commit message was generated") + return undefined + } + + // Replace bullets occasionally output by the model with hyphens + return commitMessage.replace(/\*\s/g, "- ") + } catch (error) { + this.handleError(error) + return undefined + } finally { + console.log("[DiffCommit] Stop Reason: ", message?.stop_reason) + console.log("[DiffCommit] Usage: ", message?.usage) + } + } + + private handleError(error: unknown): void { + if (error instanceof Anthropic.APIError) { + const errorMessage = error.message || "Unknown Anthropic API error" + console.error(`Anthropic API Error (${error.status}):`, errorMessage) + + switch (error.status) { + case 400: + vscode.window.showErrorMessage("Bad request. Review your prompt and try again.") + break + case 401: + vscode.window.showErrorMessage("Invalid API key. Please update your API key and try again.") + break + case 403: + vscode.window.showErrorMessage("Permission Denied. Review your prompt or API key and try again.") + break + case 429: + vscode.window.showErrorMessage(`Rate limit exceeded. Please try again later: ${errorMessage}`) + break + case 500: + vscode.window.showErrorMessage("Anthropic API server error. Please try again later.") + break + default: + vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`) + break + } + } else { + console.error(`Unknown error: ${error instanceof Error ? error.message : String(error)}`) + vscode.window.showErrorMessage( + `Unknown error generating commit message: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } +} diff --git a/src/configManager.ts b/src/configManager.ts new file mode 100644 index 0000000..c22f283 --- /dev/null +++ b/src/configManager.ts @@ -0,0 +1,38 @@ +import * as vscode from "vscode" + +export interface CommitConfig { + customInstructions?: string + allowedTypes: string[] + model: string + maxTokens: number + temperature: number +} + +export class ConfigManager { + private static readonly defaultModel = "claude-3-5-sonnet-latest" + private static readonly defaultMaxTokens = 1024 + private static readonly defaultTemperature = 0.4 + private static readonly defaultAllowedTypes = [ + "feat", + "fix", + "refactor", + "chore", + "docs", + "style", + "test", + "perf", + "ci", + ] + + getConfig(): CommitConfig { + const config = vscode.workspace.getConfiguration("diffCommit") + + return { + customInstructions: config.get("customInstructions"), + allowedTypes: config.get("allowedTypes") || ConfigManager.defaultAllowedTypes, + model: config.get("model") || ConfigManager.defaultModel, + maxTokens: config.get("maxTokens") || ConfigManager.defaultMaxTokens, + temperature: config.get("temperature") || ConfigManager.defaultTemperature, + } + } +} diff --git a/src/extension.ts b/src/extension.ts index 428047a..4952f1f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,88 +1,15 @@ -import Anthropic from "@anthropic-ai/sdk" import * as vscode from "vscode" - -const defaultModel = "claude-3-5-sonnet-latest" -const defaultMaxTokens = 1024 -const defaultTemperature = 0.4 -const defaultAllowedTypes = ["feat", "fix", "refactor", "chore", "docs", "style", "test", "perf", "ci"] +import { APIKeyManager } from "./apiKeyManager" +import { CommitMessageGenerator } from "./commitMessageGenerator" +import { ConfigManager } from "./configManager" +import { GitManager } from "./gitManager" export function activate(context: vscode.ExtensionContext) { let previewDocument: vscode.TextDocument | undefined - function getRepo(): any | undefined { - const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports - if (!gitExtension) { - vscode.window.showErrorMessage("Git extension not found") - return undefined - } - const gitAPI = gitExtension.getAPI(1) - const gitRepo = gitAPI.repositories[0] - if (!gitRepo) { - vscode.window.showErrorMessage("No Git repository found") - return undefined - } - return gitRepo - } - - async function setAPIKey(): Promise { - try { - const apiKey = await vscode.window.showInputBox({ - prompt: "Enter your Anthropic API Key", - password: true, - placeHolder: "sk-ant-api...", - }) - - if (!apiKey) { - vscode.window.showErrorMessage("API Key is required") - return undefined - } - - if (!apiKey.startsWith("sk-ant-api")) { - vscode.window.showErrorMessage("Invalid Anthropic API Key format. Should start with sk-ant-api") - return undefined - } - - await context.secrets.store("anthropic-api-key", apiKey) - vscode.window.showInformationMessage("API Key updated successfully") - - return apiKey - } catch (error) { - console.error("Secrets storage error:", error) - vscode.window.showErrorMessage( - `Failed to update API key in secure storage: ${error instanceof Error ? error.message : String(error)}`, - ) - return undefined - } - } - - async function getAPIKey(): Promise { - try { - return await context.secrets.get("anthropic-api-key") - } catch (error) { - console.error("Secrets storage error:", error) - vscode.window.showErrorMessage( - `Failed to access secure storage: ${error instanceof Error ? error.message : String(error)}`, - ) - return undefined - } - } - - async function deleteAPIKey(): Promise { - try { - const apiKey = await context.secrets.get("anthropic-api-key") - if (!apiKey) { - vscode.window.showWarningMessage("No API Key found to remove") - return - } - await context.secrets.delete("anthropic-api-key") - vscode.window.showInformationMessage("API Key deleted successfully") - } catch (error) { - console.error("Secrets storage error:", error) - vscode.window.showErrorMessage( - `Failed to delete API key from secure storage: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } + const apiKeyManager = new APIKeyManager(context) + const gitManager = new GitManager() + const configManager = new ConfigManager() async function generateCommitMessage(): Promise { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath @@ -91,138 +18,35 @@ export function activate(context: vscode.ExtensionContext) { return undefined } - const config = vscode.workspace.getConfiguration("diffCommit") - - const gitRepo = getRepo() - if (!gitRepo) { - return undefined - } - - const diff = await gitRepo.diff(true) + const diff = await gitManager.getDiff() if (!diff) { vscode.window.showErrorMessage("No changes detected") return undefined } - const apiKey = (await getAPIKey()) ?? (await setAPIKey()) + const apiKey = (await apiKeyManager.getAPIKey()) ?? (await apiKeyManager.setAPIKey()) if (!apiKey) { vscode.window.showErrorMessage("API Key is required") return undefined } - const anthropic = new Anthropic({ - apiKey, - }) - - const customInstructions = config.get("customInstructions") || undefined - const allowedTypes = config.get("allowedTypes") || defaultAllowedTypes - const model = config.get("model") || defaultModel - const maxTokens = config.get("maxTokens") || defaultMaxTokens - const temperature = config.get("temperature") || defaultTemperature - const systemPrompt = - "You are a seasoned software developer with an extraordinary ability for writing detailed conventional commit messages and following 'instructions' and 'customInstructions' when generating them." - const prompt = ` - - Generate a detailed conventional commit message for the following Git diff: - - ${diff} - - - - Use ONLY ${allowedTypes.map((val) => `'${val}'`).join(" | ")} as appropriate for the type of change. - - Always include a scope. - - Never use '!' or 'BREAKING CHANGE' in the commit message. - - Output will use markdown formatting for lists etc. - - Output will ONLY contain the commit message. - - Do not include any other text or explanation in the output. - - ${customInstructions ? `\n${customInstructions}\n` : ""} - `.trim() - - let message: Anthropic.Message | undefined = undefined - try { - message = await anthropic.messages.create({ - model, - max_tokens: maxTokens, - temperature, - system: systemPrompt, - messages: [ - { - role: "user", - content: prompt, - }, - ], - }) - - let commitMessage: string | undefined - commitMessage = message.content - .filter((msg) => msg.type === "text" && "text" in msg) - .map((msg) => msg.text) - .join("\n") - .replace(/\n{3,}/g, "\n\n") - .trim() - - if (!commitMessage) { - vscode.window.showWarningMessage("No commit message was generated") - return undefined - } - - // Replace bullets occasionally output by the model with hyphens - return commitMessage.replace(/\*\s/g, "- ") - } catch (error) { - if (error instanceof Anthropic.APIError) { - const errorMessage = error.message || "Unknown Anthropic API error" - console.error(`Anthropic API Error (${error.status}):`, errorMessage) - - switch (error.status) { - case 400: - vscode.window.showErrorMessage("Bad request. Review your prompt and try again.") - break - case 401: - vscode.window.showErrorMessage("Invalid API key. Please update your API key and try again.") - break - case 403: - vscode.window.showErrorMessage("Permission Denied. Review your prompt or API key and try again.") - break - case 429: - vscode.window.showErrorMessage(`Rate limit exceeded. Please try again later: ${errorMessage}`) - break - case 500: - vscode.window.showErrorMessage("Anthropic API server error. Please try again later.") - break - default: - vscode.window.showErrorMessage(`Failed to generate commit message: ${errorMessage}`) - break - } - } else { - console.error(`Unknown error: ${error instanceof Error ? error.message : String(error)}`) - vscode.window.showErrorMessage( - `Unknown error generating commit message: ${error instanceof Error ? error.message : String(error)}`, - ) - } - return undefined - } finally { - console.log("[DiffCommit] Stop Reason: ", message?.stop_reason) - console.log("[DiffCommit] Usage: ", message?.usage) - } + const config = configManager.getConfig() + const generator = new CommitMessageGenerator(apiKey) + return await generator.generateMessage(diff, config) } // Register all commands - const cmdUpdateAPIKey = vscode.commands.registerCommand("diffCommit.updateAPIKey", setAPIKey) - const cmdGetAPIKey = vscode.commands.registerCommand("diffCommit.getAPIKey", getAPIKey) - const cmdDeleteAPIKey = vscode.commands.registerCommand("diffCommit.deleteAPIKey", deleteAPIKey) + const cmdUpdateAPIKey = vscode.commands.registerCommand("diffCommit.updateAPIKey", () => apiKeyManager.setAPIKey()) + const cmdGetAPIKey = vscode.commands.registerCommand("diffCommit.getAPIKey", () => apiKeyManager.getAPIKey()) + const cmdDeleteAPIKey = vscode.commands.registerCommand("diffCommit.deleteAPIKey", () => apiKeyManager.deleteAPIKey()) + const cmdGenerateCommitMessage = vscode.commands.registerCommand("diffCommit.generateCommitMessage", async () => { try { const commitMessage = await generateCommitMessage() if (!commitMessage) { return } - - // Set the commit message in the repository's input box - const gitRepo = getRepo() - if (!gitRepo) { - return - } - gitRepo.inputBox.value = commitMessage + gitManager.setCommitMessage(commitMessage) } catch (error) { console.error("Error writing commit message to SCM:", error) vscode.window.showErrorMessage( @@ -253,10 +77,7 @@ export function activate(context: vscode.ExtensionContext) { const onSave = vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { if (document === previewDocument) { - const gitRepo = getRepo() - if (gitRepo) { - gitRepo.inputBox.value = document.getText() - } + gitManager.setCommitMessage(document.getText()) } }) diff --git a/src/gitManager.ts b/src/gitManager.ts new file mode 100644 index 0000000..b087223 --- /dev/null +++ b/src/gitManager.ts @@ -0,0 +1,34 @@ +import * as vscode from "vscode" + +export class GitManager { + getRepo(): any | undefined { + const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports + if (!gitExtension) { + vscode.window.showErrorMessage("Git extension not found") + return undefined + } + const gitAPI = gitExtension.getAPI(1) + const gitRepo = gitAPI.repositories[0] + if (!gitRepo) { + vscode.window.showErrorMessage("No Git repository found") + return undefined + } + return gitRepo + } + + async getDiff(): Promise { + const gitRepo = this.getRepo() + if (!gitRepo) { + return undefined + } + + return await gitRepo.diff(true) + } + + setCommitMessage(message: string): void { + const gitRepo = this.getRepo() + if (gitRepo) { + gitRepo.inputBox.value = message + } + } +} From c817e1dc0f87015f83d5a3f505320d067fc19ef9 Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Sat, 26 Oct 2024 09:11:34 +1000 Subject: [PATCH 04/10] build(types): add git extension type definitions - Add comprehensive TypeScript type definitions for Git extension API - Include interfaces for Repository, Branch, Commit and Remote operations - Define enums for ForcePushMode, RefType, Status and GitErrorCodes - Add types for credentials, remote sources and branch protection - Include interfaces for various providers and handlers - Add type definitions for Git extension API versioning --- src/git.d.ts | 417 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 src/git.d.ts diff --git a/src/git.d.ts b/src/git.d.ts new file mode 100644 index 0000000..30fe083 --- /dev/null +++ b/src/git.d.ts @@ -0,0 +1,417 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, Command, Disposable, Event, ProviderResult, Uri } from "vscode" +export { ProviderResult } from "vscode" + +export interface Git { + readonly path: string +} + +export interface InputBox { + value: string +} + +export const enum ForcePushMode { + Force, + ForceWithLease, + ForceWithLeaseIfIncludes, +} + +export const enum RefType { + Head, + RemoteHead, + Tag, +} + +export interface Ref { + readonly type: RefType + readonly name?: string + readonly commit?: string + readonly remote?: string +} + +export interface UpstreamRef { + readonly remote: string + readonly name: string + readonly commit?: string +} + +export interface Branch extends Ref { + readonly upstream?: UpstreamRef + readonly ahead?: number + readonly behind?: number +} + +export interface CommitShortStat { + readonly files: number + readonly insertions: number + readonly deletions: number +} + +export interface Commit { + readonly hash: string + readonly message: string + readonly parents: string[] + readonly authorDate?: Date + readonly authorName?: string + readonly authorEmail?: string + readonly commitDate?: Date + readonly shortStat?: CommitShortStat +} + +export interface Submodule { + readonly name: string + readonly path: string + readonly url: string +} + +export interface Remote { + readonly name: string + readonly fetchUrl?: string + readonly pushUrl?: string + readonly isReadOnly: boolean +} + +export const enum Status { + INDEX_MODIFIED, + INDEX_ADDED, + INDEX_DELETED, + INDEX_RENAMED, + INDEX_COPIED, + + MODIFIED, + DELETED, + UNTRACKED, + IGNORED, + INTENT_TO_ADD, + INTENT_TO_RENAME, + TYPE_CHANGED, + + ADDED_BY_US, + ADDED_BY_THEM, + DELETED_BY_US, + DELETED_BY_THEM, + BOTH_ADDED, + BOTH_DELETED, + BOTH_MODIFIED, +} + +export interface Change { + /** + * Returns either `originalUri` or `renameUri`, depending + * on whether this change is a rename change. When + * in doubt always use `uri` over the other two alternatives. + */ + readonly uri: Uri + readonly originalUri: Uri + readonly renameUri: Uri | undefined + readonly status: Status +} + +export interface RepositoryState { + readonly HEAD: Branch | undefined + readonly refs: Ref[] + readonly remotes: Remote[] + readonly submodules: Submodule[] + readonly rebaseCommit: Commit | undefined + + readonly mergeChanges: Change[] + readonly indexChanges: Change[] + readonly workingTreeChanges: Change[] + readonly untrackedChanges: Change[] + + readonly onDidChange: Event +} + +export interface RepositoryUIState { + readonly selected: boolean + readonly onDidChange: Event +} + +/** + * Log options. + */ +export interface LogOptions { + /** Max number of log entries to retrieve. If not specified, the default is 32. */ + readonly maxEntries?: number + readonly path?: string + /** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */ + readonly range?: string + readonly reverse?: boolean + readonly sortByAuthorDate?: boolean + readonly shortStats?: boolean + readonly author?: string + readonly refNames?: string[] + readonly maxParents?: number + readonly skip?: number +} + +export interface CommitOptions { + all?: boolean | "tracked" + amend?: boolean + signoff?: boolean + signCommit?: boolean + empty?: boolean + noVerify?: boolean + requireUserConfig?: boolean + useEditor?: boolean + verbose?: boolean + /** + * string - execute the specified command after the commit operation + * undefined - execute the command specified in git.postCommitCommand + * after the commit operation + * null - do not execute any command after the commit operation + */ + postCommitCommand?: string | null +} + +export interface FetchOptions { + remote?: string + ref?: string + all?: boolean + prune?: boolean + depth?: number +} + +export interface InitOptions { + defaultBranch?: string +} + +export interface RefQuery { + readonly contains?: string + readonly count?: number + readonly pattern?: string | string[] + readonly sort?: "alphabetically" | "committerdate" +} + +export interface BranchQuery extends RefQuery { + readonly remote?: boolean +} + +export interface Repository { + readonly rootUri: Uri + readonly inputBox: InputBox + readonly state: RepositoryState + readonly ui: RepositoryUIState + + readonly onDidCommit: Event + + getConfigs(): Promise<{ key: string; value: string }[]> + getConfig(key: string): Promise + setConfig(key: string, value: string): Promise + getGlobalConfig(key: string): Promise + + getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }> + detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> + buffer(ref: string, path: string): Promise + show(ref: string, path: string): Promise + getCommit(ref: string): Promise + + add(paths: string[]): Promise + revert(paths: string[]): Promise + clean(paths: string[]): Promise + + apply(patch: string, reverse?: boolean): Promise + diff(cached?: boolean): Promise + diffWithHEAD(): Promise + diffWithHEAD(path: string): Promise + diffWith(ref: string): Promise + diffWith(ref: string, path: string): Promise + diffIndexWithHEAD(): Promise + diffIndexWithHEAD(path: string): Promise + diffIndexWith(ref: string): Promise + diffIndexWith(ref: string, path: string): Promise + diffBlobs(object1: string, object2: string): Promise + diffBetween(ref1: string, ref2: string): Promise + diffBetween(ref1: string, ref2: string, path: string): Promise + + hashObject(data: string): Promise + + createBranch(name: string, checkout: boolean, ref?: string): Promise + deleteBranch(name: string, force?: boolean): Promise + getBranch(name: string): Promise + getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise + getBranchBase(name: string): Promise + setBranchUpstream(name: string, upstream: string): Promise + + checkIgnore(paths: string[]): Promise> + + getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise + + getMergeBase(ref1: string, ref2: string): Promise + + tag(name: string, upstream: string): Promise + deleteTag(name: string): Promise + + status(): Promise + checkout(treeish: string): Promise + + addRemote(name: string, url: string): Promise + removeRemote(name: string): Promise + renameRemote(name: string, newName: string): Promise + + fetch(options?: FetchOptions): Promise + fetch(remote?: string, ref?: string, depth?: number): Promise + pull(unshallow?: boolean): Promise + push(remoteName?: string, branchName?: string, setUpstream?: boolean, force?: ForcePushMode): Promise + + blame(path: string): Promise + log(options?: LogOptions): Promise + + commit(message: string, opts?: CommitOptions): Promise + merge(ref: string): Promise + mergeAbort(): Promise +} + +export interface RemoteSource { + readonly name: string + readonly description?: string + readonly url: string | string[] +} + +export interface RemoteSourceProvider { + readonly name: string + readonly icon?: string // codicon name + readonly supportsQuery?: boolean + getRemoteSources(query?: string): ProviderResult + getBranches?(url: string): ProviderResult + publishRepository?(repository: Repository): Promise +} + +export interface RemoteSourcePublisher { + readonly name: string + readonly icon?: string // codicon name + publishRepository(repository: Repository): Promise +} + +export interface Credentials { + readonly username: string + readonly password: string +} + +export interface CredentialsProvider { + getCredentials(host: Uri): ProviderResult +} + +export interface PostCommitCommandsProvider { + getCommands(repository: Repository): Command[] +} + +export interface PushErrorHandler { + handlePushError( + repository: Repository, + remote: Remote, + refspec: string, + error: Error & { gitErrorCode: GitErrorCodes }, + ): Promise +} + +export interface BranchProtection { + readonly remote: string + readonly rules: BranchProtectionRule[] +} + +export interface BranchProtectionRule { + readonly include?: string[] + readonly exclude?: string[] +} + +export interface BranchProtectionProvider { + onDidChangeBranchProtection: Event + provideBranchProtection(): BranchProtection[] +} + +export type APIState = "uninitialized" | "initialized" + +export interface PublishEvent { + repository: Repository + branch?: string +} + +export interface API { + readonly state: APIState + readonly onDidChangeState: Event + readonly onDidPublish: Event + readonly git: Git + readonly repositories: Repository[] + readonly onDidOpenRepository: Event + readonly onDidCloseRepository: Event + + toGitUri(uri: Uri, ref: string): Uri + getRepository(uri: Uri): Repository | null + init(root: Uri, options?: InitOptions): Promise + openRepository(root: Uri): Promise + + registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable + registerCredentialsProvider(provider: CredentialsProvider): Disposable + registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable + registerPushErrorHandler(handler: PushErrorHandler): Disposable + registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable +} + +export interface GitExtension { + readonly enabled: boolean + readonly onDidChangeEnablement: Event + + /** + * Returns a specific API version. + * + * Throws error if git extension is disabled. You can listen to the + * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event + * to know when the extension becomes enabled/disabled. + * + * @param version Version number. + * @returns API instance + */ + getAPI(version: 1): API +} + +export const enum GitErrorCodes { + BadConfigFile = "BadConfigFile", + AuthenticationFailed = "AuthenticationFailed", + NoUserNameConfigured = "NoUserNameConfigured", + NoUserEmailConfigured = "NoUserEmailConfigured", + NoRemoteRepositorySpecified = "NoRemoteRepositorySpecified", + NotAGitRepository = "NotAGitRepository", + NotAtRepositoryRoot = "NotAtRepositoryRoot", + Conflict = "Conflict", + StashConflict = "StashConflict", + UnmergedChanges = "UnmergedChanges", + PushRejected = "PushRejected", + ForcePushWithLeaseRejected = "ForcePushWithLeaseRejected", + ForcePushWithLeaseIfIncludesRejected = "ForcePushWithLeaseIfIncludesRejected", + RemoteConnectionError = "RemoteConnectionError", + DirtyWorkTree = "DirtyWorkTree", + CantOpenResource = "CantOpenResource", + GitNotFound = "GitNotFound", + CantCreatePipe = "CantCreatePipe", + PermissionDenied = "PermissionDenied", + CantAccessRemote = "CantAccessRemote", + RepositoryNotFound = "RepositoryNotFound", + RepositoryIsLocked = "RepositoryIsLocked", + BranchNotFullyMerged = "BranchNotFullyMerged", + NoRemoteReference = "NoRemoteReference", + InvalidBranchName = "InvalidBranchName", + BranchAlreadyExists = "BranchAlreadyExists", + NoLocalChanges = "NoLocalChanges", + NoStashFound = "NoStashFound", + LocalChangesOverwritten = "LocalChangesOverwritten", + NoUpstreamBranch = "NoUpstreamBranch", + IsInSubmodule = "IsInSubmodule", + WrongCase = "WrongCase", + CantLockRef = "CantLockRef", + CantRebaseMultipleBranches = "CantRebaseMultipleBranches", + PatchDoesNotApply = "PatchDoesNotApply", + NoPathFound = "NoPathFound", + UnknownPath = "UnknownPath", + EmptyCommitMessage = "EmptyCommitMessage", + BranchFastForwardRejected = "BranchFastForwardRejected", + BranchNotYetBorn = "BranchNotYetBorn", + TagConflict = "TagConflict", + CherryPickEmpty = "CherryPickEmpty", + CherryPickConflict = "CherryPickConflict", +} From b62714c6c1be424776f23c9be4b42f73bbd4e06c Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Sat, 26 Oct 2024 09:12:54 +1000 Subject: [PATCH 05/10] test(testing): update test for message formatting with newlines - Add newlines between error message titles and details - Update test cases to reflect new error message format - Fix spelling of 'Unauthorized' to 'Unauthorised' in test description - Maintain consistent error message formatting across all error handlers --- test/anthropicResponseHandling.test.ts | 4 ++-- test/errorHandling.test.ts | 26 +++++++++++++------------- test/gitIntegration.test.ts | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/anthropicResponseHandling.test.ts b/test/anthropicResponseHandling.test.ts index a2fb72a..07e08fc 100644 --- a/test/anthropicResponseHandling.test.ts +++ b/test/anthropicResponseHandling.test.ts @@ -143,7 +143,7 @@ describe("Anthropic API Response Handling", () => { await generateCommitMessage() expect(window.showErrorMessage).toHaveBeenCalledWith( - "Rate limit exceeded. Please try again later: Rate limit exceeded", + "Rate limit exceeded. Please try again later:\n\nRate limit exceeded", ) expect(mockGitRepo.inputBox.value).toBe("") }) @@ -166,7 +166,7 @@ describe("Anthropic API Response Handling", () => { const generateCommitMessage = getCommand("diffCommit.generateCommitMessage") await generateCommitMessage() - expect(window.showErrorMessage).toHaveBeenCalledWith("Failed to generate commit message: Unknown error") + expect(window.showErrorMessage).toHaveBeenCalledWith("Failed to generate commit message:\n\nUnknown error") expect(mockGitRepo.inputBox.value).toBe("") }) }) diff --git a/test/errorHandling.test.ts b/test/errorHandling.test.ts index b199037..f260639 100644 --- a/test/errorHandling.test.ts +++ b/test/errorHandling.test.ts @@ -185,10 +185,10 @@ describe("Error Handling", () => { await mockCommands["diffCommit.generateCommitMessage"]() expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Bad request. Review your prompt and try again.") - expect(console.error).toHaveBeenCalledWith("Anthropic API Error (400):", "Bad request") + expect(console.error).toHaveBeenCalledWith("Anthropic API Error (400):\n\nBad request") }) - it("handles 401 Unauthorized error", async () => { + it("handles 401 Unauthorised error", async () => { const apiError = new Anthropic.APIError(401, "Invalid API key", "api_error", {}) mockAnthropicCreate.mockRejectedValue(apiError) @@ -198,7 +198,7 @@ describe("Error Handling", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( "Invalid API key. Please update your API key and try again.", ) - expect(console.error).toHaveBeenCalledWith("Anthropic API Error (401):", "Invalid API key") + expect(console.error).toHaveBeenCalledWith("Anthropic API Error (401):\n\nInvalid API key") }) it("handles 403 Forbidden error", async () => { @@ -211,7 +211,7 @@ describe("Error Handling", () => { expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( "Permission Denied. Review your prompt or API key and try again.", ) - expect(console.error).toHaveBeenCalledWith("Anthropic API Error (403):", "Permission denied") + expect(console.error).toHaveBeenCalledWith("Anthropic API Error (403):\n\nPermission denied") }) it("handles 429 Rate Limit error", async () => { @@ -222,9 +222,9 @@ describe("Error Handling", () => { await mockCommands["diffCommit.generateCommitMessage"]() expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - "Rate limit exceeded. Please try again later: Too many requests", + "Rate limit exceeded. Please try again later:\n\nToo many requests", ) - expect(console.error).toHaveBeenCalledWith("Anthropic API Error (429):", "Too many requests") + expect(console.error).toHaveBeenCalledWith("Anthropic API Error (429):\n\nToo many requests") }) it("handles 500 Server error", async () => { @@ -235,7 +235,7 @@ describe("Error Handling", () => { await mockCommands["diffCommit.generateCommitMessage"]() expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Anthropic API server error. Please try again later.") - expect(console.error).toHaveBeenCalledWith("Anthropic API Error (500):", "Internal server error") + expect(console.error).toHaveBeenCalledWith("Anthropic API Error (500):\n\nInternal server error") }) it("handles unknown API error status", async () => { @@ -245,8 +245,8 @@ describe("Error Handling", () => { activate(mockContext) await mockCommands["diffCommit.generateCommitMessage"]() - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to generate commit message: Unknown error") - expect(console.error).toHaveBeenCalledWith("Anthropic API Error (418):", "Unknown error") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to generate commit message:\n\nUnknown error") + expect(console.error).toHaveBeenCalledWith("Anthropic API Error (418):\n\nUnknown error") }) }) @@ -284,8 +284,8 @@ describe("Error Handling", () => { await mockCommands["diffCommit.generateCommitMessage"]() // Verify console.error was called with the expected error - expect(console.error).toHaveBeenCalledWith("Error writing commit message to SCM:", expect.any(Error)) - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to write to SCM: SCM error") + expect(console.error).toHaveBeenCalledWith("Error writing commit message to SCM:\n\nError: SCM error") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to write to SCM:\n\nSCM error") // Verify finally block still logs expect(console.log).toHaveBeenCalledWith("[DiffCommit] Stop Reason: ", mockMessage.stop_reason) expect(console.log).toHaveBeenCalledWith("[DiffCommit] Usage: ", mockMessage.usage) @@ -322,9 +322,9 @@ describe("Error Handling", () => { await mockCommands["diffCommit.previewCommitMessage"]() // Verify console.error was called with the expected error - expect(console.error).toHaveBeenCalledWith("Error opening commit message preview:", expect.any(Error)) + expect(console.error).toHaveBeenCalledWith("Error opening commit message preview:\n\nError: Show document error") expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - "Failed to open commit message preview: Show document error", + "Failed to open commit message preview:\n\nShow document error", ) // Verify finally block still logs expect(console.log).toHaveBeenCalledWith("[DiffCommit] Stop Reason: ", mockMessage.stop_reason) diff --git a/test/gitIntegration.test.ts b/test/gitIntegration.test.ts index dfa9272..a39f011 100644 --- a/test/gitIntegration.test.ts +++ b/test/gitIntegration.test.ts @@ -196,7 +196,7 @@ describe("Git Integration", () => { const generateCommitMessage = getCommand("diffCommit.generateCommitMessage") await generateCommitMessage() - expect(window.showErrorMessage).toHaveBeenCalledWith("Failed to write to SCM: Diff error") + expect(window.showErrorMessage).toHaveBeenCalledWith("Failed to write to SCM:\n\nDiff error") expect(mockAnthropicCreate).not.toHaveBeenCalled() }) From 8ebce46ac611b55d076789c1aa10ccbd7c931d6e Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Sat, 26 Oct 2024 09:13:53 +1000 Subject: [PATCH 06/10] refactor(apiKeyManager): optimize vscode imports and simplify namespace usage - Replace wildcard import with specific named imports - Import `ExtensionContext` and `window` directly from vscode - Remove redundant `vscode` namespace references throughout the file - Update type annotations to use imported types --- src/apiKeyManager.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/apiKeyManager.ts b/src/apiKeyManager.ts index 8e07529..bca0e92 100644 --- a/src/apiKeyManager.ts +++ b/src/apiKeyManager.ts @@ -1,33 +1,33 @@ -import * as vscode from "vscode" +import { type ExtensionContext, window } from "vscode" export class APIKeyManager { - constructor(private context: vscode.ExtensionContext) {} + constructor(private context: ExtensionContext) {} async setAPIKey(): Promise { try { - const apiKey = await vscode.window.showInputBox({ + const apiKey = await window.showInputBox({ prompt: "Enter your Anthropic API Key", password: true, placeHolder: "sk-ant-api...", }) if (!apiKey) { - vscode.window.showErrorMessage("API Key is required") + window.showErrorMessage("API Key is required") return undefined } if (!apiKey.startsWith("sk-ant-api")) { - vscode.window.showErrorMessage("Invalid Anthropic API Key format. Should start with sk-ant-api") + window.showErrorMessage("Invalid Anthropic API Key format. Should start with sk-ant-api") return undefined } await this.context.secrets.store("anthropic-api-key", apiKey) - vscode.window.showInformationMessage("API Key updated successfully") + window.showInformationMessage("API Key updated successfully") return apiKey } catch (error) { console.error("Secrets storage error:", error) - vscode.window.showErrorMessage( + window.showErrorMessage( `Failed to update API key in secure storage: ${error instanceof Error ? error.message : String(error)}`, ) return undefined @@ -39,7 +39,7 @@ export class APIKeyManager { return await this.context.secrets.get("anthropic-api-key") } catch (error) { console.error("Secrets storage error:", error) - vscode.window.showErrorMessage( + window.showErrorMessage( `Failed to access secure storage: ${error instanceof Error ? error.message : String(error)}`, ) return undefined @@ -50,14 +50,14 @@ export class APIKeyManager { try { const apiKey = await this.context.secrets.get("anthropic-api-key") if (!apiKey) { - vscode.window.showWarningMessage("No API Key found to remove") + window.showWarningMessage("No API Key found to remove") return } await this.context.secrets.delete("anthropic-api-key") - vscode.window.showInformationMessage("API Key deleted successfully") + window.showInformationMessage("API Key deleted successfully") } catch (error) { console.error("Secrets storage error:", error) - vscode.window.showErrorMessage( + window.showErrorMessage( `Failed to delete API key from secure storage: ${error instanceof Error ? error.message : String(error)}`, ) } From ae0b490b98482d16c50421ffa80755d6d5c597e2 Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Sat, 26 Oct 2024 09:14:21 +1000 Subject: [PATCH 07/10] refactor(commit-generator): optimize imports and improve error handling - Replace full vscode import with specific window import - Add type-only import for CommitConfig - Simplify commit message processing by combining regex replacements - Improve error message formatting for better readability - Remove redundant comment and code duplication - Clean up string template handling for error messages --- src/commitMessageGenerator.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/commitMessageGenerator.ts b/src/commitMessageGenerator.ts index 5ec7a57..97b4bd4 100644 --- a/src/commitMessageGenerator.ts +++ b/src/commitMessageGenerator.ts @@ -1,6 +1,6 @@ import Anthropic from "@anthropic-ai/sdk" -import * as vscode from "vscode" -import { CommitConfig } from "./configManager" +import { window } from "vscode" +import type { CommitConfig } from "./configManager" export class CommitMessageGenerator { constructor(private apiKey: string) {} @@ -25,7 +25,7 @@ export class CommitMessageGenerator { - Never use '!' or 'BREAKING CHANGE' in the commit message. - Output will use markdown formatting for lists etc. - Output will ONLY contain the commit message. - - Do not include any other text or explanation in the output. + - Do not explain the output. ${config.customInstructions ? `\n${config.customInstructions}\n` : ""} `.trim() @@ -45,21 +45,21 @@ export class CommitMessageGenerator { ], }) - let commitMessage: string | undefined - commitMessage = message.content + let commitMessage = message.content .filter((msg) => msg.type === "text" && "text" in msg) .map((msg) => msg.text) .join("\n") - .replace(/\n{3,}/g, "\n\n") + .replace(/\n{3,}/g, "\n\n") // Replace 3 or more newlines with 2 newlines + .replace(/(? Date: Sat, 26 Oct 2024 09:15:03 +1000 Subject: [PATCH 08/10] refactor(config-manager): optimize vscode imports and simplify namespace usage - Import only required workspace module from vscode - Reorder CommitConfig interface properties alphabetically - Reorder ConfigManager static properties alphabetically - Simplify config retrieval using destructured workspace import --- src/configManager.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/configManager.ts b/src/configManager.ts index c22f283..d6d8300 100644 --- a/src/configManager.ts +++ b/src/configManager.ts @@ -1,17 +1,14 @@ -import * as vscode from "vscode" +import { workspace } from "vscode" export interface CommitConfig { - customInstructions?: string allowedTypes: string[] - model: string + customInstructions?: string maxTokens: number + model: string temperature: number } export class ConfigManager { - private static readonly defaultModel = "claude-3-5-sonnet-latest" - private static readonly defaultMaxTokens = 1024 - private static readonly defaultTemperature = 0.4 private static readonly defaultAllowedTypes = [ "feat", "fix", @@ -23,15 +20,18 @@ export class ConfigManager { "perf", "ci", ] + private static readonly defaultMaxTokens = 1024 + private static readonly defaultModel = "claude-3-5-sonnet-latest" + private static readonly defaultTemperature = 0.4 getConfig(): CommitConfig { - const config = vscode.workspace.getConfiguration("diffCommit") + const config = workspace.getConfiguration("diffCommit") return { - customInstructions: config.get("customInstructions"), allowedTypes: config.get("allowedTypes") || ConfigManager.defaultAllowedTypes, - model: config.get("model") || ConfigManager.defaultModel, + customInstructions: config.get("customInstructions"), maxTokens: config.get("maxTokens") || ConfigManager.defaultMaxTokens, + model: config.get("model") || ConfigManager.defaultModel, temperature: config.get("temperature") || ConfigManager.defaultTemperature, } } From 7c8631764d941426dc4e4c1343c00b93924a300c Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Sat, 26 Oct 2024 09:16:12 +1000 Subject: [PATCH 09/10] refactor(git-manager): optimize vscode imports and improve type safety - Use specific imports from vscode instead of importing entire namespace - Add proper type annotation for GitExtension - Remove redundant type annotations for class methods - Clean up return type declarations --- src/gitManager.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/gitManager.ts b/src/gitManager.ts index b087223..60bd1a0 100644 --- a/src/gitManager.ts +++ b/src/gitManager.ts @@ -1,22 +1,23 @@ -import * as vscode from "vscode" +import { extensions, window } from "vscode" +import { GitExtension } from "./git" export class GitManager { - getRepo(): any | undefined { - const gitExtension = vscode.extensions.getExtension("vscode.git")?.exports + getRepo() { + const gitExtension = extensions.getExtension("vscode.git")?.exports if (!gitExtension) { - vscode.window.showErrorMessage("Git extension not found") + window.showErrorMessage("Git extension not found") return undefined } const gitAPI = gitExtension.getAPI(1) const gitRepo = gitAPI.repositories[0] if (!gitRepo) { - vscode.window.showErrorMessage("No Git repository found") + window.showErrorMessage("No Git repository found") return undefined } return gitRepo } - async getDiff(): Promise { + async getDiff() { const gitRepo = this.getRepo() if (!gitRepo) { return undefined @@ -25,7 +26,7 @@ export class GitManager { return await gitRepo.diff(true) } - setCommitMessage(message: string): void { + setCommitMessage(message: string) { const gitRepo = this.getRepo() if (gitRepo) { gitRepo.inputBox.value = message From 946fb5c9ab9748f0d29fc753ca6e3a337efd9fa4 Mon Sep 17 00:00:00 2001 From: Tim Stone Date: Sat, 26 Oct 2024 09:17:11 +1000 Subject: [PATCH 10/10] refactor(extension): optimize vscode imports and improve error message formatting - Replace wildcard import with specific named imports - Add explicit type imports for ExtensionContext and TextDocument - Improve error message formatting with newlines for better readability - Remove redundant string concatenation in error logging --- src/extension.ts | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 4952f1f..02f0577 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,32 +1,32 @@ -import * as vscode from "vscode" +import { commands, window, workspace, type ExtensionContext, type TextDocument } from "vscode" import { APIKeyManager } from "./apiKeyManager" import { CommitMessageGenerator } from "./commitMessageGenerator" import { ConfigManager } from "./configManager" import { GitManager } from "./gitManager" -export function activate(context: vscode.ExtensionContext) { - let previewDocument: vscode.TextDocument | undefined +export function activate(context: ExtensionContext) { + let previewDocument: TextDocument | undefined const apiKeyManager = new APIKeyManager(context) const gitManager = new GitManager() const configManager = new ConfigManager() async function generateCommitMessage(): Promise { - const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath + const workspaceRoot = workspace.workspaceFolders?.[0]?.uri?.fsPath if (!workspaceRoot) { - vscode.window.showErrorMessage("No workspace folder found") + window.showErrorMessage("No workspace folder found") return undefined } const diff = await gitManager.getDiff() if (!diff) { - vscode.window.showErrorMessage("No changes detected") + window.showErrorMessage("No changes detected") return undefined } const apiKey = (await apiKeyManager.getAPIKey()) ?? (await apiKeyManager.setAPIKey()) if (!apiKey) { - vscode.window.showErrorMessage("API Key is required") + window.showErrorMessage("API Key is required") return undefined } @@ -36,11 +36,11 @@ export function activate(context: vscode.ExtensionContext) { } // Register all commands - const cmdUpdateAPIKey = vscode.commands.registerCommand("diffCommit.updateAPIKey", () => apiKeyManager.setAPIKey()) - const cmdGetAPIKey = vscode.commands.registerCommand("diffCommit.getAPIKey", () => apiKeyManager.getAPIKey()) - const cmdDeleteAPIKey = vscode.commands.registerCommand("diffCommit.deleteAPIKey", () => apiKeyManager.deleteAPIKey()) + const cmdUpdateAPIKey = commands.registerCommand("diffCommit.updateAPIKey", () => apiKeyManager.setAPIKey()) + const cmdGetAPIKey = commands.registerCommand("diffCommit.getAPIKey", () => apiKeyManager.getAPIKey()) + const cmdDeleteAPIKey = commands.registerCommand("diffCommit.deleteAPIKey", () => apiKeyManager.deleteAPIKey()) - const cmdGenerateCommitMessage = vscode.commands.registerCommand("diffCommit.generateCommitMessage", async () => { + const cmdGenerateCommitMessage = commands.registerCommand("diffCommit.generateCommitMessage", async () => { try { const commitMessage = await generateCommitMessage() if (!commitMessage) { @@ -48,40 +48,38 @@ export function activate(context: vscode.ExtensionContext) { } gitManager.setCommitMessage(commitMessage) } catch (error) { - console.error("Error writing commit message to SCM:", error) - vscode.window.showErrorMessage( - `Failed to write to SCM: ${error instanceof Error ? error.message : String(error)}`, - ) + console.error(`Error writing commit message to SCM:\n\n${error}`) + window.showErrorMessage(`Failed to write to SCM:\n\n${error instanceof Error ? error.message : String(error)}`) } }) - const cmdPreviewCommitMessage = vscode.commands.registerCommand("diffCommit.previewCommitMessage", async () => { + const cmdPreviewCommitMessage = commands.registerCommand("diffCommit.previewCommitMessage", async () => { try { const commitMessage = await generateCommitMessage() if (!commitMessage) { return } - previewDocument = await vscode.workspace.openTextDocument({ + previewDocument = await workspace.openTextDocument({ content: commitMessage, language: "markdown", }) - await vscode.window.showTextDocument(previewDocument) + await window.showTextDocument(previewDocument) } catch (error) { - console.error("Error opening commit message preview:", error) - vscode.window.showErrorMessage( - `Failed to open commit message preview: ${error instanceof Error ? error.message : String(error)}`, + console.error(`Error opening commit message preview:\n\n${error}`) + window.showErrorMessage( + `Failed to open commit message preview:\n\n${error instanceof Error ? error.message : String(error)}`, ) } }) - const onSave = vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { + const onSave = workspace.onDidSaveTextDocument((document: TextDocument) => { if (document === previewDocument) { gitManager.setCommitMessage(document.getText()) } }) - const onClose = vscode.workspace.onDidCloseTextDocument((document: vscode.TextDocument) => { + const onClose = workspace.onDidCloseTextDocument((document: TextDocument) => { if (document === previewDocument) { previewDocument = undefined }