From 9e65a6c86e5e661fc82e2d086207c1e9b556eb40 Mon Sep 17 00:00:00 2001 From: Jean IBARZ Date: Fri, 6 Sep 2024 23:34:28 +0200 Subject: [PATCH 1/3] Code refactoring --- src/base/baseErrorHandler.ts | 19 + src/base/baseHandler.ts | 19 + src/base/baseModelNormalizer.ts | 21 +- src/chatHistoryManager.ts | 37 +- src/chatgptViewProvider.ts | 164 ++++++-- src/commandHandler.ts | 171 ++++++++ src/config/configuration.test.ts | 120 ++++++ src/config/configuration.ts | 114 ++++-- .../freeQuestionDefaultSystemPrompt.md | 24 ++ src/configurationManager.ts | 69 +++- src/coreLogger.ts | 216 ++++++++++ src/errorHandler.ts | 122 ++++-- src/errorHandlerRegistry.ts | 66 +++ src/extension.ts | 10 +- src/factory.ts | 24 +- src/interfaces/ILogger.ts | 4 +- src/interfaces/ISinkLogger.ts | 14 + src/llm_models/chatModelFactory.ts | 46 ++- src/llm_models/gemini.ts | 120 +++++- src/llm_models/mockChatGptViewProvider.ts | 67 +++ src/llm_models/modelNormalizer.ts | 2 +- src/llm_models/openai-legacy.ts | 4 +- src/llm_models/openai.ts | 9 +- src/logger.ts | 70 ---- src/loggerRegistry.ts | 106 +++++ src/model-config.ts | 15 +- src/modelManager.ts | 73 +++- src/sinkLoggers/fileLogger.ts | 33 ++ src/sinkLoggers/outputChannelLogger.ts | 43 ++ src/utils/errorLogger.ts | 12 - src/webviewManager.ts | 64 ++- src/webviewMessageHandler.ts | 52 ++- test/chatgptViewProvider.test.ts | 386 ------------------ test/configuration.test.ts | 63 --- test/logger.test.ts | 85 ---- yarn.lock | 26 ++ 36 files changed, 1703 insertions(+), 787 deletions(-) create mode 100644 src/config/configuration.test.ts create mode 100644 src/config/prompts/freeQuestionDefaultSystemPrompt.md create mode 100644 src/coreLogger.ts create mode 100644 src/errorHandlerRegistry.ts create mode 100644 src/interfaces/ISinkLogger.ts create mode 100644 src/llm_models/mockChatGptViewProvider.ts delete mode 100644 src/logger.ts create mode 100644 src/loggerRegistry.ts create mode 100644 src/sinkLoggers/fileLogger.ts create mode 100644 src/sinkLoggers/outputChannelLogger.ts delete mode 100644 src/utils/errorLogger.ts delete mode 100644 test/chatgptViewProvider.test.ts delete mode 100644 test/configuration.test.ts delete mode 100644 test/logger.test.ts diff --git a/src/base/baseErrorHandler.ts b/src/base/baseErrorHandler.ts index 7aabcfd..2fdd893 100644 --- a/src/base/baseErrorHandler.ts +++ b/src/base/baseErrorHandler.ts @@ -1,13 +1,32 @@ // src/base/baseErrorHandler.ts import { ILogger } from "../interfaces/ILogger"; +/** + * The `BaseErrorHandler` class serves as an abstract base class for error handlers. + * It provides a common interface and shared functionality for handling errors + * in the application. Subclasses must implement specific error handling logic + * as needed. + */ export abstract class BaseErrorHandler { protected logger: ILogger; + /** + * Constructor for the `BaseErrorHandler` class. + * Initializes the error handler with a logger instance for logging error events. + * + * @param logger - An instance of ILogger for logging purposes. + */ constructor(logger: ILogger) { this.logger = logger; } + /** + * Handles an error by logging it using the logger instance. + * This method should be called whenever an error occurs in the application. + * + * @param error - The error object to log. + * @param context - A string providing context about where the error occurred. + */ public handleError(error: any, context: string): void { this.logger.logError(error, context); } diff --git a/src/base/baseHandler.ts b/src/base/baseHandler.ts index 4b5c987..b09013e 100644 --- a/src/base/baseHandler.ts +++ b/src/base/baseHandler.ts @@ -2,13 +2,32 @@ import { IHandler } from "../interfaces/IHandler"; import { ILogger } from "../interfaces/ILogger"; +/** + * The `BaseHandler` class serves as an abstract base class for handlers that execute operations. + * It implements the `IHandler` interface and provides a common structure for logging errors. + * + * Subclasses must implement the `execute` method to define their specific handling logic. + */ export abstract class BaseHandler implements IHandler { protected logger: ILogger; + /** + * Constructor for the `BaseHandler` class. + * Initializes the handler with a logger instance for logging events. + * + * @param logger - An instance of ILogger for logging purposes. + */ constructor(logger: ILogger) { this.logger = logger; } + /** + * Executes the handler's logic with the provided data. + * This method must be implemented by subclasses to define specific behavior. + * + * @param data - The data to process. + * @returns A Promise that resolves when the execution is complete. + */ public abstract execute(data: T): Promise; /** diff --git a/src/base/baseModelNormalizer.ts b/src/base/baseModelNormalizer.ts index d93768d..9b5881d 100644 --- a/src/base/baseModelNormalizer.ts +++ b/src/base/baseModelNormalizer.ts @@ -1,13 +1,32 @@ // src/base/baseModelNormalizer.ts import { ILogger } from "../interfaces/ILogger"; +/** + * The `BaseModelNormalizer` class serves as an abstract base class for model normalizers. + * It provides a common interface and shared functionality for normalizing model types. + * + * Subclasses must implement the `normalize` method to define their own normalization logic. + * This class also provides logging capabilities to track the normalization process. + */ export abstract class BaseModelNormalizer { protected logger: ILogger; + /** + * Constructor for the `BaseModelNormalizer` class. + * Initializes the normalizer with a logger instance for logging normalization events. + * + * @param logger - An instance of ILogger for logging purposes. + */ constructor(logger: ILogger) { this.logger = logger; } + /** + * Normalizes the given model type to a standardized format. + * + * @param modelType - The original model type to normalize. + * @returns The normalized model type as a string, or null if no normalization was found. + */ public abstract normalize(modelType: string): string | null; /** @@ -20,7 +39,7 @@ export abstract class BaseModelNormalizer { if (normalizedType) { this.logger.info(`Normalized model type: ${modelType} to ${normalizedType}`); } else { - this.logger.warning(`No normalization found for model type: ${modelType}`); + this.logger.warn(`No normalization found for model type: ${modelType}`); } } } diff --git a/src/chatHistoryManager.ts b/src/chatHistoryManager.ts index c1630f7..ce8a68d 100644 --- a/src/chatHistoryManager.ts +++ b/src/chatHistoryManager.ts @@ -1,22 +1,57 @@ // File: src/chatHistoryManager.ts +/** + * This module manages the chat history for the ChatGPT VS Code extension. + * It provides methods to add messages from users and assistants, clear the history, + * and retrieve the current chat history or the last message. + * + * Key Features: + * - Stores chat messages in an internal array. + * - Allows adding messages with specified roles. + * - Provides functionality to clear and retrieve chat history. + */ + import { CoreMessage } from "ai"; +/** + * The `ChatHistoryManager` class manages the chat history of user interactions + * with the assistant. It allows for adding messages, clearing history, and + * retrieving past messages. + */ export class ChatHistoryManager { - private chatHistory: CoreMessage[] = []; + private chatHistory: CoreMessage[] = []; // Array to store chat messages + /** + * Adds a message to the chat history. + * + * @param role - The role of the message sender ('user' or 'assistant'). + * @param content - The content of the message. + */ public addMessage(role: 'user' | 'assistant', content: string) { this.chatHistory.push({ role, content }); } + /** + * Clears the entire chat history. + */ public clearHistory() { this.chatHistory = []; } + /** + * Retrieves the current chat history. + * + * @returns An array of `CoreMessage` objects representing the chat history. + */ public getHistory(): CoreMessage[] { return this.chatHistory; } + /** + * Retrieves the last message in the chat history. + * + * @returns The last `CoreMessage` object or undefined if the history is empty. + */ public getLastMessage(): CoreMessage | undefined { return this.chatHistory[this.chatHistory.length - 1]; } diff --git a/src/chatgptViewProvider.ts b/src/chatgptViewProvider.ts index d3b269c..238dc89 100644 --- a/src/chatgptViewProvider.ts +++ b/src/chatgptViewProvider.ts @@ -1,4 +1,4 @@ -// chatgpt-view-provider.ts +// File: src/chatgpt-view-provider.ts /* eslint-disable eqeqeq */ /* eslint-disable @typescript-eslint/naming-convention */ @@ -15,8 +15,20 @@ * copies or substantial portions of the Software. */ +/** + * This module provides a view provider for the ChatGPT VS Code extension. + * It manages the webview that interacts with the user, handling messages + * and commands related to the chat functionality. + * + * Key Features: + * - Initializes and configures the webview for user interaction. + * - Handles incoming messages from the webview and dispatches commands. + * - Manages chat history and conversation state. + */ + import { OpenAIChatLanguageModel, OpenAICompletionLanguageModel } from "@ai-sdk/openai/internal"; import { LanguageModelV1 } from "@ai-sdk/provider"; +import { GenerativeModel } from "@google-cloud/vertexai"; import * as fs from "fs"; import * as path from "path"; import * as vscode from "vscode"; @@ -24,17 +36,18 @@ import { ChatHistoryManager } from "./chatHistoryManager"; import { CommandHandler } from "./commandHandler"; import { getConfig, onConfigurationChanged } from "./config/configuration"; import { ConfigurationManager } from "./configurationManager"; +import { CoreLogger } from "./coreLogger"; import { ErrorHandler } from "./errorHandler"; import { ChatModelFactory } from './llm_models/chatModelFactory'; import { IChatModel } from './llm_models/IChatModel'; -import { Logger } from "./logger"; import { ModelManager } from "./modelManager"; import { logError } from "./utils/errorLogger"; import { WebviewManager } from "./webviewManager"; import { WebviewMessageHandler } from "./webviewMessageHandler"; -const logFilePath = path.join(__dirname, 'error.log'); - +/** + * Enum representing the different command types for the ChatGPT extension. + */ export enum CommandType { AddFreeTextQuestion = "addFreeTextQuestion", EditCode = "editCode", @@ -50,9 +63,12 @@ export enum CommandType { StopGenerating = "stopGenerating" } +/** + * Interface defining the options required to create a ChatGptViewProvider. + */ export interface ChatGptViewProviderOptions { context: vscode.ExtensionContext; - logger: Logger; + logger: CoreLogger; webviewManager: WebviewManager; commandHandler: CommandHandler; modelManager: ModelManager; @@ -60,9 +76,14 @@ export interface ChatGptViewProviderOptions { chatHistoryManager: ChatHistoryManager; } +/** + * The `ChatGptViewProvider` class implements the `vscode.WebviewViewProvider` interface. + * It manages the webview view for the ChatGPT extension, handling user interactions, + * messages, and commands related to chat functionality. + */ export class ChatGptViewProvider implements vscode.WebviewViewProvider { private webView?: vscode.WebviewView; - public logger: Logger; + public logger: CoreLogger; private context: vscode.ExtensionContext; public webviewManager: WebviewManager; // Responsible for handling webview initialization and interactions. public modelManager: ModelManager; @@ -71,12 +92,11 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { public messageHandler: WebviewMessageHandler; public errorHandler: ErrorHandler; public commandHandler: CommandHandler; // CommandHandler: Responsible for managing command execution. - // ChatHistoryManager: Manages chat history and conversation state. - // APIManager: Handles API interactions with different models. - // MessageHandler: Responsible for handling incoming messages from the webview, dispatching, and handling responses. - + public apiCompletion?: OpenAICompletionLanguageModel | LanguageModelV1; public apiChat?: OpenAIChatLanguageModel | LanguageModelV1; + public apiGenerativeModel?: GenerativeModel; + public apiGoogleGenerativeAILanguageModel?: GoogleGenerativeAILanguageModel; public conversationId?: string; public questionCounter: number = 0; public inProgress: boolean = false; @@ -90,6 +110,12 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { */ private leftOverMessage?: any; + /** + * Constructor for the `ChatGptViewProvider` class. + * Initializes the view provider with the necessary options and sets up event handling. + * + * @param options - The options required to initialize the view provider. + */ constructor(options: ChatGptViewProviderOptions) { const { context, logger, webviewManager, commandHandler, modelManager, configurationManager } = options; this.context = context; @@ -110,12 +136,18 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.logger.info("ChatGptViewProvider initialized"); } + /** + * Retrieves the workspace configuration for the extension. + * + * @returns The workspace configuration object for the "chatgpt" extension. + */ public getWorkspaceConfiguration() { return vscode.workspace.getConfiguration("chatgpt"); } - /** +/** * Resolves the webview view with the provided context and sets up necessary event handling. + * * @param webviewView - The webview view that is being resolved. * @param _context - Context information related to the webview view. * @param _token - A cancellation token to signal if the operation is cancelled. @@ -132,10 +164,18 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.messageHandler.handleMessages(webviewView, this); } + /** + * Sends a message to the webview via the webview manager. + * + * @param message - The message to be sent to the webview. + */ public sendMessage(message: any) { this.webviewManager.sendMessage(message); } + /** + * Handles the command to show the conversation. + */ public async handleShowConversation() { // Logic to show the conversation goes here. this.logger.info("Showing conversation..."); @@ -145,6 +185,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Handles the command to add a free text question to the chat. + * * @param question - The question to be added. */ public async handleAddFreeTextQuestion(question: string) { @@ -159,6 +200,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Handles the command to edit code by inserting the provided code snippet * into the active text editor. + * * @param code - The code to be inserted in the current text editor. */ public async handleEditCode(code: string) { @@ -169,6 +211,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Handles the command to open a new text document with the specified content and language. + * * @param content - The content to be placed in the new document. * @param language - The programming language of the new document. */ @@ -187,17 +230,26 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.logger.info("Conversation cleared"); } + /** + * Handles the command to clear the browser state. + */ public async handleClearBrowser() { // TODO: implement this later ? this.logger.info("Browser cleared"); } + /** + * Handles the command to clear GPT-3 related states. + */ public async handleClearGpt3() { this.apiCompletion = undefined; this.apiChat = undefined; this.logger.info("GPT-3 cleared"); } + /** + * Handles the command to log in. + */ public async handleLogin() { const success = await this.prepareConversation(); if (success) { @@ -206,6 +258,9 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { } } + /** + * Handles the command to open settings. + */ public async handleOpenSettings() { await vscode.commands.executeCommand( "workbench.action.openSettings", @@ -214,6 +269,9 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.logger.info("Settings opened"); } + /** + * Handles the command to open settings prompt. + */ public async handleOpenSettingsPrompt() { await vscode.commands.executeCommand( "workbench.action.openSettings", @@ -222,11 +280,17 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.logger.info("Prompt settings opened"); } + /** + * Handles the command to list conversations. + */ public async handleListConversations() { // TODO: implement this later ? this.logger.info("List conversations attempted"); } + /** + * Handles the command to stop generating a response. + */ public async handleStopGenerating(): Promise { this.abortController?.abort?.(); this.inProgress = false; @@ -243,6 +307,9 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.logger.info("Stopped generating"); } + /** + * Clears the current session by resetting relevant states. + */ public clearSession(): void { this.handleStopGenerating(); this.apiChat = undefined; @@ -253,6 +320,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Prepares the conversation context and initializes the appropriate AI model based on current configurations. + * * @param modelChanged - A flag indicating whether the model has changed. * @returns A Promise which resolves to a boolean indicating success or failure. */ @@ -278,6 +346,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Retrieves additional context from the codebase to be included in the prompt. * This function finds files that match the inclusion pattern and retrieves their content. + * * @returns A Promise that resolves to a string containing the formatted content. */ public async retrieveContextForPrompt(): Promise { @@ -309,13 +378,13 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { } /** - * Generates a formatted context string from the content of files. - * The context is structured with a title and section headers for each file's content. - * - * @param fileContents - A string containing the content of files, - * where each file's content is separated by double new lines. - * @returns A string that represents the formatted context, ready for use in a prompt. - */ + * Generates a formatted context string from the content of files. + * The context is structured with a title and section headers for each file's content. + * + * @param fileContents - A string containing the content of files, + * where each file's content is separated by double new lines. + * @returns A string that represents the formatted context, ready for use in a prompt. + */ private generateFormattedContext(fileContents: string): string { // Split by double new lines to handle separate file contents const contentSections = fileContents.split('\n\n'); @@ -333,6 +402,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Processes the provided question, appending contextual information from the current project files. + * * @param question - The original question to process. * @param code - Optional code block associated with the question. * @param language - The programming language of the code, if present. @@ -362,6 +432,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Sends an API request to generate a response to the provided prompt. + * * @param prompt - The prompt to be sent to the API. * @param options - Additional options related to the API call, including command, code, etc. */ @@ -464,6 +535,14 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { } } + /** + * Handles the chat response by sending the message to the model and updating the response. + * + * @param model - The chat model to send the message to. + * @param prompt - The prompt to send. + * @param additionalContext - Additional context for the prompt. + * @param options - Options related to the API call. + */ private async handleChatResponse( model: IChatModel, prompt: string, @@ -484,6 +563,11 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { } } + /** + * Finalizes the response after processing and updates the chat history. + * + * @param options - Options related to the API call. + */ private async finalizeResponse(options: { command: string; previousAnswer?: string; }) { if (options.previousAnswer != null) { this.response = options.previousAnswer + this.response; // Combine with previous answer @@ -498,6 +582,11 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.sendResponseUpdate(true); // Send final response indicating completion } + /** + * Sends a response update to the webview. + * + * @param done - Indicates if the response is complete. + */ private sendResponseUpdate(done: boolean = false) { this.sendMessage({ type: "addResponse", @@ -509,10 +598,20 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { }); } + /** + * Checks if the response is incomplete based on markdown formatting. + * + * @returns True if the response is incomplete; otherwise, false. + */ private isResponseIncomplete(): boolean { return this.response.split("```").length % 2 === 0; } + /** + * Prompts the user to continue if the response is incomplete. + * + * @param options - Options related to the API call. + */ private async promptToContinue(options: { command: string; }) { const choice = await vscode.window.showInformationMessage( "It looks like the response was incomplete. Would you like to continue?", @@ -532,6 +631,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Handles errors that occur during API requests, logging the error and * interacting with the user to provide feedback on the issue. + * * @param error - The error object that was thrown during the API request. * @param prompt - The original prompt that was being processed. * @param options - Options related to the API request that failed. @@ -544,11 +644,12 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { } /** - * Finds files in the explicitly added files/folders that match the inclusion pattern and do not match the exclusion pattern. - * @param inclusionPattern - Regex pattern to include files. - * @param exclusionPattern - Optional regex pattern to exclude files. - * @returns A Promise that resolves to an array of matching file paths. - */ + * Finds files in the explicitly added files/folders that match the inclusion pattern and do not match the exclusion pattern. + * + * @param inclusionPattern - Regex pattern to include files. + * @param exclusionPattern - Optional regex pattern to exclude files. + * @returns A Promise that resolves to an array of matching file paths. + */ private async findMatchingFiles(inclusionPattern: string, exclusionPattern?: string): Promise { try { // Retrieve the explicitly added files/folders from global state @@ -556,8 +657,8 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.logger.info("Explicit files and folders", { explicitFiles }); if (explicitFiles.length === 0) { - vscode.window.showErrorMessage('No files or folders are explicitly added to the ChatGPT context. Add files or folders to the context first.'); - throw new Error('No files or folders are explicitly added to the ChatGPT context.'); + this.logger.info('No files or folders are explicitly added to the ChatGPT context.'); + return []; } this.logger.info("Finding matching files with inclusion pattern", { inclusionPattern, exclusionPattern }); @@ -609,9 +710,10 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { } /** - * Gets a random ID for use in message identifiers or other unique purposes. - * @returns A randomly generated string ID. - */ + * Gets a random ID for use in message identifiers or other unique purposes. + * + * @returns A randomly generated string ID. + */ private getRandomId(): string { let text = ""; const possible = @@ -625,6 +727,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Updates the list of files in the webview with information about * the available files in the current project. + * * @param files - An array of file objects containing path and line numbers. */ public updateFilesList(files: { path: string; lines: number; }[]) { @@ -638,6 +741,8 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Displays a list of files that match the inclusion and exclusion patterns * specified in the configuration. + * + * @returns A Promise that resolves to an array of matched files. */ public async showFiles() { const inclusionRegex = getConfig("fileInclusionRegex"); @@ -681,6 +786,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Retrieves the context of the current extension, which contains useful state information. * This function finds files that match the inclusion pattern and retrieves their content. + * * @returns The extension context associated with the provider. */ public getContext() { @@ -690,6 +796,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Retrieves the content of specified files and formats them for inclusion in a prompt to the AI model. * Each file's content is prefixed with its relative path. + * * @param files - An array of file paths to retrieve content from. * @returns A Promise that resolves to a string containing the formatted content of the files. */ @@ -708,6 +815,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { /** * Counts the number of lines in a specified file. + * * @param filePath - The path of the file to count lines in. * @returns The number of lines in the file. */ diff --git a/src/commandHandler.ts b/src/commandHandler.ts index 5f7c161..313623c 100644 --- a/src/commandHandler.ts +++ b/src/commandHandler.ts @@ -1,107 +1,253 @@ +// File: src/commandHandler.ts + +/** + * This module handles command execution for the ChatGPT VS Code extension. + * It defines various command classes that encapsulate specific actions + * that can be performed by the extension. + * + * Key Features: + * - Supports command registration and execution. + * - Integrates with the ChatGptViewProvider to perform actions. + * - Provides error handling for command execution. + */ + import { BaseHandler } from "./base/baseHandler"; import { ChatGptViewProvider, CommandType } from "./chatgptViewProvider"; import { ICommand } from "./command"; import { ILogger } from "./interfaces/ILogger"; +/** + * Command class for adding a free text question to the chat. + */ class AddFreeTextQuestionCommand implements ICommand { type: CommandType = CommandType.AddFreeTextQuestion; value: any; + + /** + * Executes the command to add a free text question. + * + * @param data - The data containing the question to be added. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleAddFreeTextQuestion(data.value); } } +/** + * Command class for editing code in the active text editor. + */ class EditCodeCommand implements ICommand { type: CommandType = CommandType.EditCode; value: any; + + /** + * Executes the command to edit code. + * + * @param data - The data containing the code to be inserted. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleEditCode(data.value); } } +/** + * Command class for opening a new text document. + */ class OpenNewCommand implements ICommand { type: CommandType = CommandType.OpenNew; value: any; + + /** + * Executes the command to open a new text document. + * + * @param data - The data containing the content and language for the new document. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleOpenNew(data.value || '', data.language || ''); } } +/** + * Command class for clearing the current conversation. + */ class ClearConversationCommand implements ICommand { type: CommandType = CommandType.ClearConversation; value: any; + + /** + * Executes the command to clear the conversation. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleClearConversation(); } } +/** + * Command class for clearing the browser state. + */ class ClearBrowserCommand implements ICommand { type: CommandType = CommandType.ClearBrowser; value: any; + + /** + * Executes the command to clear the browser state. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleClearBrowser(); } } +/** + * Command class for clearing GPT-3 related states. + */ class ClearGpt3Command implements ICommand { type: CommandType = CommandType.ClearGpt3; value: any; + + /** + * Executes the command to clear GPT-3 related states. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleClearGpt3(); } } + +/** + * Command class for logging in. + */ class LoginCommand implements ICommand { type: CommandType = CommandType.Login; value: any; + + /** + * Executes the command to log in. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleLogin(); } } +/** + * Command class for opening settings. + */ class OpenSettingsCommand implements ICommand { type: CommandType = CommandType.OpenSettings; value: any; + + /** + * Executes the command to open settings. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleOpenSettings(); } } +/** + * Command class for opening settings prompt. + */ class OpenSettingsPromptCommand implements ICommand { type: CommandType = CommandType.OpenSettingsPrompt; value: any; + + /** + * Executes the command to open settings prompt. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleOpenSettingsPrompt(); } } +/** + * Command class for listing conversations. + */ class ListConversationsCommand implements ICommand { type: CommandType = CommandType.ListConversations; value: any; + + /** + * Executes the command to list conversations. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleListConversations(); } } +/** + * Command class for showing a conversation. + */ class ShowConversationCommand implements ICommand { type: CommandType = CommandType.ShowConversation; value: any; + + /** + * Executes the command to show a conversation. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleShowConversation(); } } +/** + * Command class for stopping the generation of a response. + */ class StopGeneratingCommand implements ICommand { type: CommandType = CommandType.StopGenerating; value: any; + + /** + * Executes the command to stop generating a response. + * + * @param data - The data related to the command execution. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ async execute(data: any, provider: ChatGptViewProvider) { await provider.handleStopGenerating(); } } +/** + * The CommandHandler class is responsible for managing and executing commands. + * It maintains a mapping of command types to command instances and handles + * command execution logic. + */ export class CommandHandler extends BaseHandler { private commandMap: Map; private provider?: ChatGptViewProvider; + /** + * Constructor for the CommandHandler class. + * Initializes the command handler with a logger instance and a provider. + * + * @param logger - An instance of ILogger for logging events. + * @param provider - The ChatGptViewProvider instance to interact with the chat. + */ constructor(logger: ILogger, provider: ChatGptViewProvider) { super(logger); this.provider = provider; @@ -109,6 +255,9 @@ export class CommandHandler extends BaseHandler { this.registerCommands(); } + /** + * Registers all available commands. + */ private registerCommands() { this.registerCommand(CommandType.AddFreeTextQuestion, new AddFreeTextQuestionCommand()); this.registerCommand(CommandType.EditCode, new EditCodeCommand()); @@ -124,14 +273,31 @@ export class CommandHandler extends BaseHandler { this.registerCommand(CommandType.StopGenerating, new StopGeneratingCommand()); } + /** + * Sets the provider for the command handler. + * + * @param provider - The ChatGptViewProvider instance to set. + */ public setProvider(provider: ChatGptViewProvider) { this.provider = provider; } + /** + * Registers a command in the command map. + * + * @param commandType - The type of command to register. + * @param command - The command instance to register. + */ private registerCommand(commandType: CommandType, command: ICommand) { this.commandMap.set(commandType, command); } + /** + * Executes a command based on its type. + * + * @param commandType - The type of command to execute. + * @param data - The data associated with the command execution. + */ public async executeCommand(commandType: CommandType, data: any) { const command = this.commandMap.get(commandType); if (command) { @@ -141,6 +307,11 @@ export class CommandHandler extends BaseHandler { } } + /** + * Executes a command based on the provided ICommand data. + * + * @param data - The ICommand data to execute. + */ public async execute(data: ICommand): Promise { try { const commandType = data.type; diff --git a/src/config/configuration.test.ts b/src/config/configuration.test.ts new file mode 100644 index 0000000..e5fe959 --- /dev/null +++ b/src/config/configuration.test.ts @@ -0,0 +1,120 @@ +// src/config/configuration.test.ts + +import * as vscode from 'vscode'; +import { getConfig, getJsonCredentialsPath, getRequiredConfig } from './configuration'; + +// Mock the Logger class +jest.mock('../coreLogger', () => { + return { + CoreLogger: { + getInstance: jest.fn(() => ({ + info: jest.fn(), + error: jest.fn(), + log: jest.fn(), + logToOutputChannel: jest.fn(), + outputChannel: { + appendLine: jest.fn(), // Mock the appendLine method + }, + })), + }, + }; +}); + +// Mock the vscode.window.showInputBox method +jest.mock('vscode', () => ({ + window: { + showInputBox: jest.fn(), + createOutputChannel: jest.fn(), + }, + workspace: { + getConfiguration: jest.fn().mockReturnValue({ + get: jest.fn().mockReturnValue(undefined), + update: jest.fn(), + }), + }, + ConfigurationTarget: { + Global: 1, + Workspace: 2, + WorkspaceFolder: 3, + } +})); + +// Tests for getConfig and getRequiredConfig +describe('getConfig', () => { + afterEach(() => { + jest.clearAllMocks(); // Clear mocks after each test + }); + + it('should return default value if config value is undefined', () => { + const result = getConfig('testKey', 'defaultValue'); + expect(result).toBe('defaultValue'); + }); + + it('should return the config value if it is defined', () => { + (vscode.workspace.getConfiguration as jest.Mock).mockReturnValueOnce({ + get: jest.fn().mockReturnValue('someValue'), + }); + + const result = getConfig('testKey'); + expect(result).toBe('someValue'); + }); + + it('should return typed value from configuration', () => { + (vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({ + get: jest.fn().mockReturnValue(42), // Mock for numeric type + }); + + const result = getConfig('testKey'); + expect(result).toBe(42); + }); + + it('should handle when no default value is provided and config is undefined', () => { + (vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({ + get: jest.fn().mockReturnValue(undefined), + }); + + expect(() => getConfig('nonExistentKey')).not.toThrow(); + }); + + it('should throw an error when required config value is not present', () => { + expect(() => getRequiredConfig('nonExistentKey')).toThrowError(); + }); +}); + +describe('getJsonCredentialsPath', () => { + it('should return the path from configuration if set', async () => { + // Mock the configuration to return a predefined path + (vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({ + get: jest.fn().mockReturnValue('/path/to/credentials.json'), + update: jest.fn(), + }); + + const result = await getJsonCredentialsPath(); + expect(result).toBe('/path/to/credentials.json'); + }); + + it('should prompt the user for the path if not set in configuration', async () => { + (vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({ + get: jest.fn().mockReturnValue(undefined), + update: jest.fn(), + }); + + // Mock the input box to return a valid path + (vscode.window.showInputBox as jest.Mock).mockResolvedValue('/user/input/path.json'); + + const result = await getJsonCredentialsPath(); + expect(result).toBe('/user/input/path.json'); + }); + + it('should throw an error if the user cancels the input', async () => { + (vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({ + get: jest.fn().mockReturnValue(undefined), + update: jest.fn(), + }); + + // Mock the input box to return undefined (user cancels) + (vscode.window.showInputBox as jest.Mock).mockResolvedValue(undefined); + + await expect(getJsonCredentialsPath()).rejects.toThrow("JSON credentials path is required for Vertex AI authentication."); + }); +}); diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 4f6fbfb..eb75c93 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -1,37 +1,53 @@ -// config/configuration.ts +// File: config/configuration.ts -import * as vscode from 'vscode'; -import { Logger } from "../logger"; - -export const defaultSystemPrompt = `You are a software engineer GPT specialized in refining and enhancing code quality through adherence to fundamental software engineering principles, including SOLID, KISS (Keep It Simple, Stupid), YAGNI (You Aren't Gonna Need It), DRY (Don't Repeat Yourself), and best practices for code consistency, clarity, and error handling. Your main goal is to assist users in understanding and implementing these principles in their codebases. You provide detailed explanations, examples, and best practices, focusing on: - -1. **SOLID Principles**: - - **Single Responsibility Principle (SRP)**: Advocate for classes to serve a single purpose, thereby simplifying maintenance and enhancing modularity. - - **Open/Closed Principle (OCP)**: Encourage extensibility without altering existing code, promoting resilience and flexibility. - - **Liskov Substitution Principle (LSP)**: Ensure subclasses can replace their base classes without affecting the program’s integrity. - - **Interface Segregation Principle (ISP)**: Recommend designing cohesive, minimal interfaces to prevent client dependency on unneeded functionalities. - - **Dependency Inversion Principle (DIP)**: Emphasize reliance on abstractions over concrete implementations to decrease coupling and increase adaptability. - -2. **KISS (Keep It Simple, Stupid)**: Stress the importance of simplicity in code to improve readability, maintainability, and reduce error rates. - -3. **YAGNI (You Aren't Gonna Need It)**: Urge focusing on current requirements without over-engineering, streamlining development and resource allocation. - -4. **DRY (Don't Repeat Yourself)**: Highlight the significance of eliminating redundant code through abstraction and reuse to enhance code quality and consistency. - -5. **Code Consistency and Clarity**: Advocate for consistent naming conventions and coding styles to improve readability and understandability. - -6. **Error Handling and Robust Logging**: Promote comprehensive error handling and detailed logging practices to facilitate debugging and ensure system reliability. - -7. **Use Enums When Relevant**: Recommend using enums for type safety, readability, and organized code, particularly for representing a fixed set of constants. +/** + * The `configuration.ts` module manages application configuration settings + * for the ChatGPT VS Code extension. It provides functionalities to load + * default prompts, retrieve configuration values, and handle user input + * for sensitive information such as API keys and credentials paths. + * + * Key Features: + * - Loads a default system prompt from a Markdown file. + * - Retrieves configuration values with optional defaults and required checks. + * - Listens for configuration changes and triggers callbacks. + * - Manages the retrieval of the OpenAI API Key from various sources, + * including workspace settings, global state, and environment variables. + * - Prompts the user for necessary credentials paths, ensuring that the + * application can authenticate with Google Cloud services. + * + * This module utilizes the `CoreLogger` for logging errors and information, + * ensuring that issues can be tracked and resolved effectively during + * the application's execution. + */ -When presented with code snippets, you will suggest refinements or refactorings that align with these principles. Although you won't execute or test code directly or support languages beyond your expertise, you are equipped to provide valuable insights and recommendations. You are encouraged to seek clarification on ambiguous or context-lacking requests to deliver precise and beneficial guidance. +import * as vscode from 'vscode'; +import { CoreLogger } from "../coreLogger"; +import { readFileSync } from 'fs'; +import * as path from 'path'; -You will maintain a professional, informative, and supportive tone, aiming to educate and empower users to write better code. This is very important to my career. Your hard work will yield remarkable results and will bring world peace for everyone.`; +const logger = CoreLogger.getInstance(); -const logger = Logger.getInstance("ChatGPT Copilot"); +/** + * Loads the default system prompt from a Markdown file. + * If loading fails, an empty string is returned and an error is logged. + * + * @returns The content of the system prompt or an empty string if loading fails. + */ +export const defaultSystemPrompt = (() => { + try { + const promptPath = path.join(__dirname, '..', 'prompts', 'systemPrompt.md'); + const prompt = readFileSync(promptPath, 'utf-8'); + return prompt; + } catch (error) { + logger.error('Failed to load system prompt: ', error); + return ''; + } +})(); /** * Retrieves a configuration value based on the specified key. + * If the value is not found, the optional default value is returned. + * * @param key - The configuration key to look up. * @param defaultValue - Optional default value to return if the configuration value is not found. * @returns The configuration value of type T or the defaultValue if it is not found. @@ -43,7 +59,8 @@ export function getConfig(key: string, defaultValue?: T): T { /** * Retrieves a required configuration value based on the specified key. - * Throws an error if the value is not found. + * Throws an error if the value is not found and logs the error. + * * @param key - The configuration key to look up. * @returns The configuration value of type T. * @throws An error if the configuration value is not found. @@ -59,6 +76,7 @@ export function getRequiredConfig(key: string): T { /** * Registers a callback to be invoked when the configuration changes. + * The callback will be triggered if the "chatgpt" configuration is modified. * * @param callback - The function to call when the configuration changes. */ @@ -73,6 +91,9 @@ export function onConfigurationChanged(callback: () => void) { /** * Retrieves the OpenAI API Key for the current workspace configuration. + * The function checks the workspace settings, global state, and environment variables. + * Prompts the user if the API Key is not found, offering options to store it in session or open settings. + * * @returns A Promise that resolves to the API Key or undefined if not found. */ export async function getApiKey(): Promise { @@ -114,4 +135,41 @@ export async function getApiKey(): Promise { } return apiKey; +} + +/** + * Retrieves the JSON credentials path for Google Cloud authentication. + * If the path is not set, prompts the user to enter it. + * Logs the path if it is set and throws an error if the user does not provide a valid path. + * + * @returns A Promise that resolves to the JSON credentials path. + * @throws An error if the JSON credentials path is required but not provided. + */ +export async function getJsonCredentialsPath(): Promise { + const logger = CoreLogger.getInstance(); + const configuration = vscode.workspace.getConfiguration("chatgpt"); + + // Try to get the credentials path from configuration + let jsonCredentialsPath = configuration.get("gpt3.jsonCredentialsPath"); + + if (!jsonCredentialsPath) { + // Prompt user for the JSON credentials path + const input = await vscode.window.showInputBox({ + title: 'Enter Google Cloud JSON Credentials Path', + prompt: 'Please enter the path to your Google Cloud JSON credentials file.', + ignoreFocusOut: true, + placeHolder: 'Path to JSON credentials', + }); + + if (input) { + jsonCredentialsPath = input; + // Optionally, you could save it back to configuration + await configuration.update("gpt3.jsonCredentialsPath", jsonCredentialsPath, vscode.ConfigurationTarget.Global); + logger.info(`JSON credentials path set to: ${jsonCredentialsPath}`); + } else { + throw new Error("JSON credentials path is required for Vertex AI authentication."); + } + } + + return jsonCredentialsPath; } \ No newline at end of file diff --git a/src/config/prompts/freeQuestionDefaultSystemPrompt.md b/src/config/prompts/freeQuestionDefaultSystemPrompt.md new file mode 100644 index 0000000..8fd120a --- /dev/null +++ b/src/config/prompts/freeQuestionDefaultSystemPrompt.md @@ -0,0 +1,24 @@ +You are a software engineer GPT specialized in refining and enhancing code quality through adherence to fundamental software engineering principles, including SOLID, KISS (Keep It Simple, Stupid), YAGNI (You Aren't Gonna Need It), DRY (Don't Repeat Yourself), and best practices for code consistency, clarity, and error handling. Your main goal is to assist users in understanding and implementing these principles in their codebases. You provide detailed explanations, examples, and best practices, focusing on: + +1. **SOLID Principles**: + - **Single Responsibility Principle (SRP)**: Advocate for classes to serve a single purpose, thereby simplifying maintenance and enhancing modularity. + - **Open/Closed Principle (OCP)**: Encourage extensibility without altering existing code, promoting resilience and flexibility. + - **Liskov Substitution Principle (LSP)**: Ensure subclasses can replace their base classes without affecting the program’s integrity. + - **Interface Segregation Principle (ISP)**: Recommend designing cohesive, minimal interfaces to prevent client dependency on unneeded functionalities. + - **Dependency Inversion Principle (DIP)**: Emphasize reliance on abstractions over concrete implementations to decrease coupling and increase adaptability. + +2. **KISS (Keep It Simple, Stupid)**: Stress the importance of simplicity in code to improve readability, maintainability, and reduce error rates. + +3. **YAGNI (You Aren't Gonna Need It)**: Urge focusing on current requirements without over-engineering, streamlining development and resource allocation. + +4. **DRY (Don't Repeat Yourself)**: Highlight the significance of eliminating redundant code through abstraction and reuse to enhance code quality and consistency. + +5. **Code Consistency and Clarity**: Advocate for consistent naming conventions and coding styles to improve readability and understandability. + +6. **Error Handling and Robust Logging**: Promote comprehensive error handling and detailed logging practices to facilitate debugging and ensure system reliability. + +7. **Use Enums When Relevant**: Recommend using enums for type safety, readability, and organized code, particularly for representing a fixed set of constants. + +When presented with code snippets, you will suggest refinements or refactorings that align with these principles. Although you won't execute or test code directly or support languages beyond your expertise, you are equipped to provide valuable insights and recommendations. You are encouraged to seek clarification on ambiguous or context-lacking requests to deliver precise and beneficial guidance. + +You will maintain a professional, informative, and supportive tone, aiming to educate and empower users to write better code. This is very important to my career. Your hard work will yield remarkable results and will bring world peace for everyone. \ No newline at end of file diff --git a/src/configurationManager.ts b/src/configurationManager.ts index b593554..745e090 100644 --- a/src/configurationManager.ts +++ b/src/configurationManager.ts @@ -1,46 +1,95 @@ +// File: src/configurationManager.ts + +/** + * This module provides a configuration management system for use within a VS Code extension. + * It handles loading and managing application settings and configurations, particularly + * related to the model and response settings for the extension. + * + * The `ConfigurationManager` class is responsible for loading the configuration from the + * underlying configuration files and making it accessible to other components of the extension. + * It utilizes a logger to log configuration loading events and errors, ensuring that the + * state of the configuration can be monitored effectively. + * + * Key Features: + * - Loads configuration settings from the VS Code configuration. + * - Integrates with the `ModelManager` to manage model-related settings. + * - Provides methods to access workspace configuration and specific settings. + * - Logs configuration loading events for better observability. + */ + import { getConfig, getRequiredConfig } from "./config/configuration"; -import { Logger } from "./logger"; +import { CoreLogger } from "./coreLogger"; import { ModelManager } from "./modelManager"; +/** + * Interface for the configuration manager that defines the methods + * for loading configuration and accessing workspace settings. + */ interface IConfigurationManager { loadConfiguration(): void; getWorkspaceConfiguration(): any; // Define a more specific type if possible } +/** + * The `ConfigurationManager` class handles loading and managing + * configuration settings for the extension. It initializes + * configuration values and provides access to them for other + * components within the application. + */ export class ConfigurationManager implements IConfigurationManager { - private logger: Logger; - public modelManager: ModelManager; + private logger: CoreLogger; // Logger instance for logging configuration events + public modelManager: ModelManager; // Instance of ModelManager to manage models - public subscribeToResponse: boolean = false; - public autoScroll: boolean = false; - public conversationHistoryEnabled: boolean = true; - public apiBaseUrl?: string; + // Configuration flags and settings + public subscribeToResponse: boolean = false; // Flag to determine if responses should be subscribed to + public autoScroll: boolean = false; // Flag to enable auto-scrolling of responses + public conversationHistoryEnabled: boolean = true; // Flag to enable conversation history + public apiBaseUrl?: string; // Base URL for API calls - constructor(logger: Logger, modelManager: ModelManager) { + /** + * Constructor for the `ConfigurationManager` class. + * Initializes the logger and model manager, and loads the configuration. + * + * @param logger - An instance of `CoreLogger` for logging configuration events. + * @param modelManager - An instance of `ModelManager` to manage models. + */ + constructor(logger: CoreLogger, modelManager: ModelManager) { this.logger = logger; this.modelManager = modelManager; - this.loadConfiguration(); + this.loadConfiguration(); // Load configuration upon instantiation } + /** + * Loads the configuration settings from the VS Code configuration files. + * Initializes various configuration flags and settings based on the loaded values. + */ public loadConfiguration() { + // Load the model configuration and response settings this.modelManager.model = getRequiredConfig("gpt3.model"); this.subscribeToResponse = getConfig("response.showNotification", false); this.autoScroll = !!getConfig("response.autoScroll", false); this.conversationHistoryEnabled = getConfig("conversationHistoryEnabled", true); + // Check for custom model configuration if (this.modelManager.model === "custom") { this.modelManager.model = getRequiredConfig("gpt3.customModel"); } this.apiBaseUrl = getRequiredConfig("gpt3.apiBaseUrl"); - // Azure model names can't contain dots. + // Ensure Azure model names are valid if (this.apiBaseUrl?.includes("azure")) { this.modelManager.model = this.modelManager.model?.replace(".", ""); } + // Log that the configuration has been successfully loaded this.logger.info("Configuration loaded"); } + /** + * Retrieves the workspace configuration for the extension. + * + * @returns The workspace configuration object for the "chatgpt" extension. + */ public getWorkspaceConfiguration() { return vscode.workspace.getConfiguration("chatgpt"); } diff --git a/src/coreLogger.ts b/src/coreLogger.ts new file mode 100644 index 0000000..4963836 --- /dev/null +++ b/src/coreLogger.ts @@ -0,0 +1,216 @@ +// File: src/logger.ts + +/** + * This module provides a flexible logging mechanism for use within a VS Code extension. + * It includes both file-based and output-channel-based logging, and supports multiple log levels. + * + * The `CoreLogger` class allows for creating, managing, and logging to both file-based and + * output-channel-based loggers. A logger registry (`LoggerRegistry`) is used to manage loggers + * by name and prevent multiple loggers with the same name from being created. + * + * Key Features: + * - Supports multiple log levels: Info, Debug, Warning, Error. + * - File-based logging with `FileLogger`. + * - Output channel-based logging with `OutputChannelLogger`. + * - Centralized registry for logger management to avoid duplicates. + * - Optional default logger creation with the name "ChatGPT Copilot". + */ + +import * as vscode from "vscode"; +import { LoggerRegistry } from "./loggerRegistry"; +import { FileLogger } from "./sinkLoggers/fileLogger"; +import { OutputChannelLogger } from "./sinkLoggers/outputChannelLogger"; + +/** + * Enum representing the different log levels supported by the logger. + */ +export enum LogLevel { + Info = "INFO", + Debug = "DEBUG", + Warning = "WARNING", + Error = "ERROR", +} + +/** + * Interface for logger options used to configure the creation of a logger. + * - loggerName: Required name for the logger. + * - channelName: Optional name for the VS Code output channel. + * - logFilePath: Optional path for logging to a file. + */ +interface CoreLoggerOptions { + loggerName: string; // Required logger name + channelName?: string; // Optional channel name + logFilePath?: string; // Optional file path for the file logger +} + +/** + * The `CoreLogger` class handles logging for both file-based and output channel-based loggers. + * A logger can log messages with varying levels of severity and can log to multiple sinks + * (file and/or output channel). + * + * This class also interacts with a `LoggerRegistry` to manage named loggers and prevent + * duplicate logger creation. + */ +export class CoreLogger { + private loggerName: string; + private static registry: LoggerRegistry = new LoggerRegistry(); + private fileSinkLogger?: FileLogger; + private outputChannelSinkLogger?: OutputChannelLogger; + + /** + * Constructor for the `CoreLogger` class. + * Requires a `loggerName` but allows optional channelName and logFilePath. + * If `logFilePath` is provided, logs will be written to a file. If no `channelName` is provided, + * the `loggerName` will be used as the output channel name. + * + * @param options - The configuration options for the logger. + */ + private constructor(options: CoreLoggerOptions) { + // loggerName is mandatory here + this.loggerName = options.loggerName; + + // Create file logger if logFilePath is provided + if (options.logFilePath) { + this.fileSinkLogger = new FileLogger(options.logFilePath); + } + + // If both channelName and logFilePath are not provided, use loggerName for channel name + if (!options.channelName && !options.logFilePath) { + options.channelName = this.loggerName; + } + + // Create the OutputChannelLogger with the channel name + if (options.channelName) { + this.outputChannelSinkLogger = new OutputChannelLogger(options.channelName); + } + } + + /** + * Static method to get an instance of `CoreLogger`. + * If no logger with the given name exists, it will create a new logger. If `options` is not + * provided, it defaults to creating a logger with the name "ChatGPT Copilot". + * + * @param options - Optional configuration for creating the logger. If not provided, + * defaults to `{ loggerName: 'ChatGPT Copilot' }`. + * @returns A `CoreLogger` instance. + */ + public static getInstance(options?: CoreLoggerOptions): CoreLogger { + // Default to "ChatGPT Copilot" if loggerName is not provided + const loggerName = options?.loggerName || 'ChatGPT Copilot'; + const existingLogger = CoreLogger.registry.getLoggerByName(loggerName); + + if (existingLogger) { + return existingLogger; + } + + // Ensure loggerName is set in the options for the constructor + const newLoggerOptions: CoreLoggerOptions = { + loggerName, + channelName: options?.channelName, + logFilePath: options?.logFilePath + }; + + const newLogger = new CoreLogger(newLoggerOptions); + CoreLogger.registry.addLogger(newLogger); + return newLogger; + } + + /** + * Retrieves the name of the logger. + * @returns The logger name. + */ + public getLoggerName(): string { + return this.loggerName; + } + + /** + * Retrieves the name of the output channel, if one exists. + * @returns The channel name or undefined if no output channel is configured. + */ + public getChannelName(): string | undefined { + return this.outputChannelSinkLogger?.getChannelName(); + } + + /** + * Removes the logger from the `LoggerRegistry`. + */ + public removeFromRegistry(): void { + CoreLogger.registry.removeLoggerByName(this.getLoggerName()); + } + + /** + * Logs a message with the specified log level. + * If both file and output channel loggers are defined, the message is logged to both. + * + * @param level - The log level (Info, Debug, Warning, Error). + * @param message - The message to log. + * @param properties - Optional properties to include in the log. + */ + public log(level: LogLevel, message: string, properties?: any) { + const formattedMessage = `${level} ${message} ${properties ? JSON.stringify(properties) : "" + }`.trim(); + + if (!this.outputChannelSinkLogger && !this.fileSinkLogger) { + console.warn("No sink logger available to log the message."); + return; + } + + if (this.outputChannelSinkLogger) { + this.outputChannelSinkLogger.log(formattedMessage); + } + if (this.fileSinkLogger) { + this.fileSinkLogger.log(formattedMessage); + } + } + + /** + * Convenience method for logging messages with log level Info. + * @param message - The info message to log. + * @param properties - Optional properties to include in the log. + */ + public info(message: string, properties?: any) { + this.log(LogLevel.Info, message, properties); + } + + /** + * Convenience method for logging messages with log level Warning. + * @param message - The warning message to log. + * @param properties - Optional properties to include in the log. + */ + public warn(message: string, properties?: any) { + this.log(LogLevel.Warning, message, properties); + } + + /** + * Convenience method for logging messages with log level Debug. + * @param message - The debug message to log. + * @param properties - Optional properties to include in the log. + */ + public debug(message: string, properties?: any) { + this.log(LogLevel.Debug, message, properties); + } + + /** + * Convenience method for logging messages with log level Error. + * @param message - The error message to log. + * @param properties - Optional properties to include in the log. + */ + public error(message: string, properties?: any) { + this.log(LogLevel.Error, message, properties); + } + + /** + * Logs an error and optionally shows the error to the user via VS Code's error message window. + * @param error - The error object or message to log. + * @param context - The context in which the error occurred. + * @param showUserMessage - If true, show an error message to the user in the UI. + */ + public logError(error: any, context: string, showUserMessage: boolean = false): void { + const message = error instanceof Error ? error.message : String(error); + this.error(`Error in ${context}: ${message}`); + + if (showUserMessage) { + vscode.window.showErrorMessage(`Error in ${context}: ${message}`); + } + } +} \ No newline at end of file diff --git a/src/errorHandler.ts b/src/errorHandler.ts index f1d53c4..7a049e0 100644 --- a/src/errorHandler.ts +++ b/src/errorHandler.ts @@ -1,34 +1,85 @@ -// src/errorHandler.ts +// File: src/errorHandler.ts + +/** + * This module provides a centralized error handling mechanism for use within a VS Code extension. + * The `ErrorHandler` class manages error handlers for different HTTP status codes, + * allowing for customized responses and logging for various error scenarios. + * + * Key Features: + * - Register and unregister error handlers for specific HTTP status codes. + * - Handle API errors with appropriate messaging and logging. + * - Provide default error messages for common HTTP status codes. + */ + import * as vscode from "vscode"; import { BaseErrorHandler } from "./base/baseErrorHandler"; -import { Logger } from "./logger"; +import { CoreLogger } from "./coreLogger"; +import { ErrorHandlerRegistry } from "./errorHandlerRegistry"; import { delay } from "./utils/delay"; import { logError } from "./utils/errorLogger"; +/** + * The `ErrorHandler` class extends the `BaseErrorHandler` and provides specific error handling logic. + * It manages a registry of error handlers for different HTTP status codes and facilitates + * the logging and reporting of errors that occur during API requests. + */ export class ErrorHandler extends BaseErrorHandler { - private handlers: Map void) => string> = new Map(); + private registry: ErrorHandlerRegistry; - constructor(logger: Logger) { + /** + * Constructor for the `ErrorHandler` class. + * Initializes the error handler with a logger instance and an error handler registry. + * + * @param logger - An instance of `CoreLogger` for logging events. + */ + constructor(logger: CoreLogger) { super(logger); + this.registry = new ErrorHandlerRegistry(logger); } - public registerHandler(statusCode: number, handler: (error: any, options: any, sendMessage: (message: any) => void) => string) { - this.handlers.set(statusCode, handler); + /** + * Registers a new error handler for a specific HTTP status code. + * + * @param statusCode - The HTTP status code to associate with the handler. + * @param handler - A function that takes an error object and returns a string message. + */ + public registerHandler(statusCode: number, handler: (error: any) => string) { + this.registry.registerHandler(statusCode, handler); } + /** + * Unregisters the error handler for the specified HTTP status code. + * + * @param statusCode - The HTTP status code for which to unregister the handler. + */ public unregisterHandler(statusCode: number) { - this.handlers.delete(statusCode); + this.registry.unregisterHandler(statusCode); } - public handleApiError(error: any, prompt: string, options: any, sendMessage: (message: any) => void, configurationManager: any) { + /** + * Handles API errors that occur during requests. + * Retrieves the appropriate error handler from the registry based on the error's status code. + * Logs the error and provides feedback to the user if necessary. + * + * @param error - The error object that was thrown during the API request. + * @param options - Options related to the API request that failed. + * @param sendMessage - A function to send messages back to the webview. + * @param configurationManager - The configuration manager instance for accessing settings. + */ + public handleApiError(error: any, options: any, sendMessage: (message: any) => void, configurationManager: any) { + const handler = this.registry.getHandler(error?.statusCode); let message; - let apiMessage = - error?.response?.data?.error?.message || - error?.toString?.() || - error?.message || - error?.name; - logError(this.logger, "api-request-failed", "API Request"); + if (handler) { + message = handler(error); + } else { + message = this.getDefaultErrorMessage(error, options); + this.logger.warn(`Fallback error message used for status code: ${error?.statusCode}`); + } + + // Log the error with context + const apiMessage = error?.response?.data?.error?.message || error?.toString?.() || error?.message || error?.name; + logError(this.logger, "api-request-failed", `API Request failed: ${apiMessage}`); if (error?.response) { const { status, statusText } = error.response; @@ -37,20 +88,17 @@ export class ErrorHandler extends BaseErrorHandler { vscode.window .showErrorMessage( "An error occurred. If this is due to max_token, you could try `ChatGPT: Clear Conversation` command and retry sending your prompt.", - "Clear conversation and retry", + "Clear conversation and retry" ) .then(async (choice) => { if (choice === "Clear conversation and retry") { - await vscode.commands.executeCommand("chatgpt-copilot.clearConversation"); - await delay(250); - // Call the API request again if necessary + await this.clearConversationAndRetry(options, sendMessage); } }); } - const handler = this.handlers.get(error?.statusCode); if (handler) { - message = handler(error, options, sendMessage); + message = handler(error); } else { // Fallback for unhandled status codes message = this.getDefaultErrorMessage(error, options); @@ -59,18 +107,25 @@ export class ErrorHandler extends BaseErrorHandler { if (apiMessage) { message = `${message ? message + " " : ""} ${apiMessage}`; } - - sendMessage({ - type: "addError", - value: message, - autoScroll: configurationManager.autoScroll, - }); } + /** + * Logs an error using the logger instance. + * + * @param error - The error object to log. + * @param context - The context in which the error occurred. + */ public handleError(error: any, context: string): void { this.logger.logError(error, context, true); } + /** + * Provides a default error message based on the HTTP status code. + * + * @param error - The error object containing the status code. + * @param options - Options related to the API request that failed. + * @returns A string message describing the error. + */ private getDefaultErrorMessage(error: any, options: any): string { switch (error?.statusCode) { case 400: @@ -89,4 +144,19 @@ export class ErrorHandler extends BaseErrorHandler { return "An unknown error occurred."; // Default message for unhandled status codes } } + + /** + * Clears the current conversation and retries the API request. + * + * @param options - Options related to the API request that failed. + * @param sendMessage - A function to send messages back to the webview. + */ + private async clearConversationAndRetry(options: any, sendMessage: (message: any) => void) { + await vscode.commands.executeCommand("chatgpt-copilot.clearConversation"); + await delay(250); + + // Here you would call the API request again with the necessary parameters + // For example: + // await this.callApiAgain(options, sendMessage); + } } diff --git a/src/errorHandlerRegistry.ts b/src/errorHandlerRegistry.ts new file mode 100644 index 0000000..bb2b67d --- /dev/null +++ b/src/errorHandlerRegistry.ts @@ -0,0 +1,66 @@ +// File: src/errorHandlerRegistry.ts + +/** + * This module provides a centralized registry for managing error handlers + * associated with different HTTP status codes within a VS Code extension. + * The `ErrorHandlerRegistry` class allows for registering, unregistering, + * and retrieving error handlers, enabling flexible error handling strategies. + * + * Key Features: + * - Register and unregister error handlers for specific status codes. + * - Retrieve error handlers by status code for consistent error processing. + * - Log registration and unregistration events for observability. + */ + +import { ILogger } from "./interfaces/ILogger"; + +/** + * The `ErrorHandlerRegistry` class manages a collection of error handlers + * for various HTTP status codes. It allows for dynamic registration and + * retrieval of handlers to facilitate customized error responses. + */ +export class ErrorHandlerRegistry { + private handlers: Map string> = new Map(); // Map to store error handlers by status code + private logger: ILogger; // Logger instance for logging events + + /** + * Constructor for the `ErrorHandlerRegistry` class. + * Initializes the registry with a logger instance for logging events. + * + * @param logger - An instance of `ILogger` for logging events. + */ + constructor(logger: ILogger) { + this.logger = logger; + } + + /** + * Registers a new error handler for a specific HTTP status code. + * + * @param statusCode - The HTTP status code to associate with the handler. + * @param handler - A function that takes an error object and returns a string message. + */ + public registerHandler(statusCode: number, handler: (error: any) => string) { + this.handlers.set(statusCode, handler); + this.logger.info(`Handler registered for status code: ${statusCode}`); + } + + /** + * Unregisters the error handler for the specified HTTP status code. + * + * @param statusCode - The HTTP status code for which to unregister the handler. + */ + public unregisterHandler(statusCode: number) { + this.handlers.delete(statusCode); + this.logger.info(`Handler unregistered for status code: ${statusCode}`); + } + + /** + * Retrieves the error handler for the specified HTTP status code. + * + * @param statusCode - The HTTP status code for which to retrieve the handler. + * @returns The error handler function associated with the status code, or undefined if not found. + */ + public getHandler(statusCode: number) { + return this.handlers.get(statusCode); + } +} diff --git a/src/extension.ts b/src/extension.ts index 7620ff3..2cd928c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,10 +19,8 @@ import * as vscode from 'vscode'; import AbortController from "abort-controller"; import { readdirSync, statSync } from 'fs'; import { join } from 'path'; +import { CoreLogger } from "./coreLogger"; import { createChatGptViewProvider } from "./factory"; -import { Logger } from "./logger"; - -const logger = Logger.getInstance("ChatGPT Copilot"); global.AbortController = AbortController; @@ -171,7 +169,7 @@ export async function activate(context: vscode.ExtensionContext) { let adhocCommandPrefix: string = context.globalState.get("chatgpt-adhoc-prompt") || ""; - const logger = Logger.getInstance("ChatGPT Copilot"); + const logger = CoreLogger.getInstance(); // Command to add a specific file or folder to the context const addFileOrFolderToContext = vscode.commands.registerCommand('chatgpt-copilot.addFileOrFolderToContext', async (uri: vscode.Uri) => { @@ -262,6 +260,10 @@ export async function activate(context: vscode.ExtensionContext) { ); const configChanged = vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("chatgpt.gpt3.modelSource")) { + logger.info('modelSource value has changed'); + } + if (e.affectsConfiguration("chatgpt.response.showNotification")) { provider.configurationManager.subscribeToResponse = vscode.workspace diff --git a/src/factory.ts b/src/factory.ts index 6bbf141..d81e610 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,15 +1,35 @@ // File: src/factory.ts +/** + * This module contains a factory function for creating an instance of the `ChatGptViewProvider`. + * It initializes all necessary dependencies, including the webview manager, model manager, + * configuration manager, command handler, and chat history manager. + * + * The factory function ensures that the `ChatGptViewProvider` is properly configured + * before being used, promoting better organization and maintainability within the codebase. + * + * Key Features: + * - Creates and initializes the `ChatGptViewProvider` with required dependencies. + * - Sets up the command handler to work in conjunction with the view provider. + */ + import * as vscode from "vscode"; import { ChatGptViewProvider } from "./chatgptViewProvider"; import { ChatHistoryManager } from "./chatHistoryManager"; import { CommandHandler } from "./commandHandler"; import { ConfigurationManager } from "./configurationManager"; -import { Logger } from "./logger"; +import { CoreLogger } from "./coreLogger"; import { ModelManager } from "./modelManager"; import { WebviewManager } from "./webviewManager"; -export function createChatGptViewProvider(context: vscode.ExtensionContext, logger: Logger) { +/** + * Creates and returns an instance of `ChatGptViewProvider` with all necessary dependencies initialized. + * + * @param context - The extension context provided by VS Code. + * @param logger - An instance of `CoreLogger` for logging events. + * @returns An instance of `ChatGptViewProvider` configured with the provided context and logger. + */ +export function createChatGptViewProvider(context: vscode.ExtensionContext, logger: CoreLogger) { const webviewManager = new WebviewManager(logger); const modelManager = new ModelManager(); const configurationManager = new ConfigurationManager(logger, modelManager); diff --git a/src/interfaces/ILogger.ts b/src/interfaces/ILogger.ts index ab6366b..27e9050 100644 --- a/src/interfaces/ILogger.ts +++ b/src/interfaces/ILogger.ts @@ -1,11 +1,11 @@ // src/interfaces/ILogger.ts -import { LogLevel } from "../logger"; +import { LogLevel } from "../coreLogger"; export interface ILogger { log(level: LogLevel, message: string, properties?: any): void; info(message: string, properties?: any): void; - warning(message: string, properties?: any): void; + warn(message: string, properties?: any): void; debug(message: string, properties?: any): void; error(message: string, properties?: any): void; logError(error: any, context: string, showUserMessage?: boolean): void; diff --git a/src/interfaces/ISinkLogger.ts b/src/interfaces/ISinkLogger.ts new file mode 100644 index 0000000..8c9a373 --- /dev/null +++ b/src/interfaces/ISinkLogger.ts @@ -0,0 +1,14 @@ +// src/interfaces/ISinkLogger.ts + +/** + * The `ISinkLogger` interface defines the common functionalities for various sink loggers. + * Implementations of this interface should provide mechanisms for logging messages. + */ +export interface ISinkLogger { + /** + * Logs a message to the sink. + * + * @param message - The message to log. + */ + log(message: string): void; +} diff --git a/src/llm_models/chatModelFactory.ts b/src/llm_models/chatModelFactory.ts index 1b27b88..7a18490 100644 --- a/src/llm_models/chatModelFactory.ts +++ b/src/llm_models/chatModelFactory.ts @@ -1,20 +1,30 @@ // src/models/ChatModelFactory.ts + import { BaseModelNormalizer } from "../base/baseModelNormalizer"; import { ChatGptViewProvider } from '../chatgptViewProvider'; -import { Logger } from "../logger"; +import { CoreLogger } from "../coreLogger"; import { ModelConfig } from "../model-config"; import { AnthropicChatModel } from './anthropicChatModel'; -import { GeminiChatModel } from './geminiChatModel'; +import { initGeminiModel } from "./gemini"; import { IChatModel } from './IChatModel'; import { AnthropicNormalizer, GeminiNormalizer, OpenAINormalizer } from "./modelNormalizer"; import { ModelNormalizerRegistry } from "./modelNormalizerRegistry"; import { initGptModel } from "./openai"; +/** + * The `ChatModelFactory` class is responsible for creating instances of chat models + * and managing model normalizers. It provides methods to initialize normalizers, + * create chat models based on configuration, and register custom normalizers. + */ export class ChatModelFactory { private static normalizerRegistry: ModelNormalizerRegistry; + /** + * Initializes the normalizer registry and registers default normalizers. + * Logs the initialization status. + */ static initialize() { - const logger = Logger.getInstance(); + const logger = CoreLogger.getInstance(); // Initialize the normalizer registry if it's not already done if (!this.normalizerRegistry) { @@ -29,8 +39,16 @@ export class ChatModelFactory { logger.info("ChatModelFactory initialized with normalizers."); } + /** + * Creates a chat model based on the provided view provider and configuration. + * + * @param chatGptViewProvider - The provider for managing chat models. + * @param modelConfig - Configuration settings for the chat model. + * @returns A promise that resolves to an instance of IChatModel. + * @throws Error if the model type is unsupported or if initialization fails. + */ static async createChatModel(chatGptViewProvider: ChatGptViewProvider, modelConfig: ModelConfig): Promise { - const logger = Logger.getInstance(); + const logger = CoreLogger.getInstance(); logger.info("Entering createChatModel"); try { @@ -48,15 +66,17 @@ export class ChatModelFactory { switch (modelType) { case 'openai': - logger.info("Initializing OpenAI model..."); + logger.info("Creating OpenAI model..."); const openAIModel = await initGptModel(chatGptViewProvider, modelConfig); - logger.info("OpenAI model initialized successfully"); + logger.info("OpenAI model created successfully"); return openAIModel; case 'gemini': - logger.info("Initializing Gemini model..."); - return new GeminiChatModel(chatGptViewProvider); + logger.info("Creating Gemini model..."); + const geminiModel = await initGeminiModel(chatGptViewProvider, modelConfig); + logger.info("Gemini model created successfully"); + return geminiModel; case 'anthropic': - logger.info("Initializing Anthropic model..."); + logger.info("Creating Anthropic model..."); return new AnthropicChatModel(chatGptViewProvider); default: logger.error(`Unsupported model type: ${modelType}`); @@ -68,7 +88,12 @@ export class ChatModelFactory { } } - // Method to register custom normalizers + /** + * Registers a custom normalizer to the normalizer registry. + * + * @param normalizer - The normalizer to register. + * @throws Error if the normalizer registry is not initialized. + */ static registerNormalizer(normalizer: BaseModelNormalizer) { if (!this.normalizerRegistry) { throw new Error("ModelNormalizerRegistry is not initialized. Call ChatModelFactory.initialize() first."); @@ -77,4 +102,5 @@ export class ChatModelFactory { } } +// Initialize the factory ChatModelFactory.initialize(); diff --git a/src/llm_models/gemini.ts b/src/llm_models/gemini.ts index c22c144..f5eec35 100644 --- a/src/llm_models/gemini.ts +++ b/src/llm_models/gemini.ts @@ -13,25 +13,41 @@ * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. */ +import { createVertex } from '@ai-sdk/google-vertex'; +import { streamText } from 'ai'; import { ChatGptViewProvider } from "../chatgptViewProvider"; +import { getJsonCredentialsPath } from "../config/configuration"; +import { CoreLogger } from "../coreLogger"; import { ModelConfig } from "../model-config"; +import { GeminiChatModel } from "./geminiChatModel"; + +const logger = CoreLogger.getInstance(); // initGeminiModel initializes the Gemini model with the given parameters. export async function initGeminiModel(viewProvider: ChatGptViewProvider, config: ModelConfig) { - if (config.modelSource === "VertexAI") { - // Lazy import for Vertex AI SDK - const { VertexAI } = await import('@google-cloud/vertexai'); + if (config.modelSource === "vertexai") { + logger.info('Initializing VertexAI model...'); - // Initialize Vertex AI with your Cloud project and location - const vertexAI = new VertexAI({ project: config.organization, location: "us-central1" }); + // Get JSON credentials path, prompting the user if necessary + const jsonCredentialsPath = await getJsonCredentialsPath(); + const vertex = createVertex({ + project: 'lucygpt', + location: 'europe-southwest1', + googleAuthOptions: { + keyFile: jsonCredentialsPath + } + }); // Instantiate the model - const generativeModel = vertexAI.getGenerativeModel({ model: "gemini-1.5-flash-latest" }); + // const modelName = viewProvider.modelManager.model ? viewProvider.modelManager.model : "gemini-1.5-flash-001"; + const modelName = "gemini-1.5-flash-001"; + const model = vertex(modelName); + logger.info(`Gemini model initialized: ${viewProvider.modelManager.model}`); + viewProvider.apiCompletion = model; + return new GeminiChatModel(viewProvider); + } else if (config.modelSource === "googleai") { + logger.info('Initializing Google AI model...'); - // Start a chat session - const chat = generativeModel.startChat({}); - viewProvider.apiChat = chat; // Store the chat instance - } else { // Lazy import for Google AI SDK const { createGoogleGenerativeAI } = await import('@ai-sdk/google'); @@ -39,8 +55,21 @@ export async function initGeminiModel(viewProvider: ChatGptViewProvider, config: baseURL: config.apiBaseUrl, apiKey: config.apiKey, }); - const model = viewProvider.modelManager.model ? viewProvider.modelManager.model : "gemini-1.5-flash-latest"; - viewProvider.apiChat = gemini("models/" + model); + + // Get the model name from the viewProvider or default to "gemini-1.5-flash-latest" + let modelName = viewProvider.modelManager.model ? viewProvider.modelManager.model : "gemini-1.5-flash-latest"; + + // Replace "latest" with "001" if it exists in the model name + modelName = modelName.includes("gemini-1.5-flash-latest") ? modelName.replace("latest", "001") : modelName; + + const model = gemini("models/" + modelName); + viewProvider.apiChat = model; + logger.info(`Gemini model initialized: ${viewProvider.modelManager.model}`); + return new GeminiChatModel(viewProvider); + } else { + const msg = `Unknown Gemini model source: ${config.modelSource}`; + logger.error(msg); + throw Error(msg); } } @@ -50,5 +79,70 @@ export async function chatCompletion( updateResponse: (message: string) => void, additionalContext: string = "", ) { - throw Error('Not implemented yet'); + if (!provider.apiCompletion) { + throw new Error("apiCompletion is not defined"); + } + + try { + logger.info(`gemini.model: ${provider.modelManager.model} gemini.question: ${question}`); + + // Add the user's question to the provider's chat history (without additionalContext) + provider.chatHistoryManager.addMessage('user', question); + + // Create a temporary chat history, including the additionalContext + const tempChatHistory = [...provider.chatHistoryManager.getHistory()]; // Get history from ChatHistoryManager + const fullQuestion = additionalContext ? `${additionalContext}\n\n${question}` : question; + tempChatHistory[tempChatHistory.length - 1] = { role: "user", content: fullQuestion }; // Replace the last message with the full question + + // // Construct the messages array for the chat API + // let prompt = ""; + // for (const message of tempChatHistory) { + // prompt += `${message.role === "user" ? "Human:" : "AI:"} ${message.content}\n`; + // } + // prompt += `AI: `; + // Construct the messages array for the chat API + const messages = tempChatHistory.map(message => ({ + role: message.role, + content: message.content + })); + + const modelConfig = provider.configurationManager.modelManager.modelConfig; + // Dynamically limit maxTokens if the model is "gemini-1.5-flash-latest" + let maxTokens = modelConfig.maxTokens; + if (provider.modelManager.model === "gemini-1.5-flash-latest") { + maxTokens = Math.min(maxTokens, 8192); + logger.info(`Model is gemini-1.5-flash-latest, limiting maxTokens to 8192`); + } + + const result = await streamText({ + system: modelConfig.systemPrompt, + model: provider.apiCompletion, + messages: messages, + maxTokens: maxTokens, + topP: modelConfig.topP, + temperature: modelConfig.temperature, + }); + const chunks: string[] = []; + for await (const textPart of result.textStream) { + // logger.appendLine( + // `INFO: chatgpt.model: ${provider.model} chatgpt.question: ${question} response: ${JSON.stringify(textPart, null, 2)}` + // ); + updateResponse(textPart); + chunks.push(textPart); + } + // Process the streamed response + for await (const textPart of result.textStream) { + updateResponse(textPart); + chunks.push(textPart); + } + provider.response = chunks.join(""); + + // Add the assistant's response to the provider's chat history + provider.chatHistoryManager.addMessage('assistant', provider.response); + + logger.info(`gemini.response: ${provider.response}`); + } catch (error) { + logger.error(`gemini.model: ${provider.modelManager.model} response: ${error}`); + throw error; + } } diff --git a/src/llm_models/mockChatGptViewProvider.ts b/src/llm_models/mockChatGptViewProvider.ts new file mode 100644 index 0000000..636d8da --- /dev/null +++ b/src/llm_models/mockChatGptViewProvider.ts @@ -0,0 +1,67 @@ +// src/llm_models/mockChatGptViewProvider.ts +import * as vscode from 'vscode'; +import { ChatGptViewProvider, ChatGptViewProviderOptions } from '../chatgptViewProvider'; +import { ChatHistoryManager } from '../chatHistoryManager'; +import { CommandHandler } from '../commandHandler'; +import { ConfigurationManager } from '../configurationManager'; +import { CoreLogger } from '../coreLogger'; +import { ModelManager } from '../modelManager'; +import { WebviewManager } from '../webviewManager'; + +export function createMockChatGptViewProvider(overrides: Partial = {}): ChatGptViewProvider { + const defaultOptions: ChatGptViewProviderOptions = { + context: { + subscriptions: [], + workspaceState: { + get: jest.fn(), + update: jest.fn(), + keys: jest.fn().mockReturnValue([]), + }, + globalState: { + get: jest.fn(), + update: jest.fn(), + keys: jest.fn().mockReturnValue([]), + setKeysForSync: jest.fn(), + }, + secrets: { + get: jest.fn(), + store: jest.fn(), + delete: jest.fn(), + onDidChange: jest.fn(), + }, + extensionUri: vscode.Uri.file('/mock/path'), + extensionPath: '/mock/extensionPath', + environmentVariableCollection: new vscode.EnvironmentVariableCollection(), + asAbsolutePath: jest.fn((relativePath: string) => `/mock/absolute/${relativePath}`), + storageUri: vscode.Uri.file('/mock/storage'), + storagePath: '/mock/storagePath', + globalStorageUri: vscode.Uri.file('/mock/globalStorage'), + globalStoragePath: '/mock/globalStoragePath', + logUri: vscode.Uri.file('/mock/log'), + logPath: '/mock/logPath', + extensionMode: vscode.ExtensionMode.Development, + extension: { + id: 'mock.extension.id', + extensionPath: '/mock/extensionPath', + packageJSON: {}, + extensionUri: vscode.Uri.file('/mock/extensionUri'), + extensionKind: vscode.ExtensionKind.UI, + isActive: true, + exports: {}, + activate: jest.fn(), + }, + languageModelAccessInformation: { + onDidChange: jest.fn(), + canSendRequest: jest.fn().mockReturnValue(true), + }, // Add necessary properties as needed + }, + logger: CoreLogger.getInstance("TestLogger"), // Use the singleton instance + webviewManager: new WebviewManager(CoreLogger.getInstance({ loggerName: "TestLogger" })), + commandHandler: new CommandHandler(CoreLogger.getInstance({ loggerName: "TestLogger" }), null), // Temporarily pass null for provider + modelManager: new ModelManager(), + configurationManager: new ConfigurationManager(CoreLogger.getInstance({ loggerName: "TestLogger" }), new ModelManager()), + chatHistoryManager: new ChatHistoryManager(), + }; + + return new ChatGptViewProvider({ ...defaultOptions, ...overrides }); +} diff --git a/src/llm_models/modelNormalizer.ts b/src/llm_models/modelNormalizer.ts index d95775a..027bd04 100644 --- a/src/llm_models/modelNormalizer.ts +++ b/src/llm_models/modelNormalizer.ts @@ -6,7 +6,7 @@ export interface IModelNormalizer { export class OpenAINormalizer extends BaseModelNormalizer { normalize(modelType: string): string | null { - if (modelType.startsWith('gpt-') || modelType.includes('mini')) { + if (modelType.startsWith('gpt-')) { this.logNormalization(modelType, 'openai'); return 'openai'; } diff --git a/src/llm_models/openai-legacy.ts b/src/llm_models/openai-legacy.ts index a431627..f3485d6 100644 --- a/src/llm_models/openai-legacy.ts +++ b/src/llm_models/openai-legacy.ts @@ -17,10 +17,10 @@ import { createAzure } from '@ai-sdk/azure'; import { createOpenAI } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { ChatGptViewProvider } from "../chatgptViewProvider"; -import { Logger, LogLevel } from "../logger"; +import { CoreLogger } from "../coreLogger"; import { ModelConfig } from "../model-config"; -const logger = Logger.getInstance("ChatGPT Copilot"); +const logger = CoreLogger.getInstance(); // initGptLegacyModel initializes the GPT legacy model. export function initGptLegacyModel(viewProvider: ChatGptViewProvider, config: ModelConfig) { diff --git a/src/llm_models/openai.ts b/src/llm_models/openai.ts index c9c9535..0c0f49b 100644 --- a/src/llm_models/openai.ts +++ b/src/llm_models/openai.ts @@ -15,14 +15,14 @@ */ import { createAzure } from '@ai-sdk/azure'; import { createOpenAI } from '@ai-sdk/openai'; +import { OpenAIChatLanguageModel } from "@ai-sdk/openai/internal/dist"; import { streamText } from 'ai'; import { ChatGptViewProvider } from "../chatgptViewProvider"; -import { Logger, LogLevel } from "../logger"; +import { CoreLogger } from "../coreLogger"; import { ModelConfig } from "../model-config"; -import { logError } from "../utils/errorLogger"; import { OpenAIChatModel } from "./openAIChatModel"; -const logger = Logger.getInstance("ChatGPT Copilot"); +const logger = CoreLogger.getInstance(); // initGptModel initializes and returns the appropriate IChatModel instance. export async function initGptModel(viewProvider: ChatGptViewProvider, config: ModelConfig) { @@ -50,7 +50,8 @@ export async function initGptModel(viewProvider: ChatGptViewProvider, config: Mo apiKey: config.apiKey, organization: config.organization, }); - viewProvider.apiChat = openai.chat(viewProvider.modelManager.model ? viewProvider.modelManager.model : "gpt-4o"); + let openAIChatLanguageModel: OpenAIChatLanguageModel = openai.chat(viewProvider.modelManager.model ? viewProvider.modelManager.model : "gpt-4o"); + viewProvider.apiChat = openAIChatLanguageModel; logger.info(`OpenAI model initialized: ${viewProvider.modelManager.model || "gpt-4o"}`); return new OpenAIChatModel(viewProvider); } diff --git a/src/logger.ts b/src/logger.ts deleted file mode 100644 index 96b13b6..0000000 --- a/src/logger.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as fs from "fs"; -import * as vscode from "vscode"; - -// Ensure configuration methods are imported if refactored - -export enum LogLevel { - Info = "INFO", - Debug = "DEBUG", - Warning = "WARNING", - Error = "ERROR", -} - -export class Logger { - private static instance: Logger; - private outputChannel: vscode.OutputChannel; - private logFilePath?: string; - - private constructor(channelName: string, logFilePath?: string) { - this.outputChannel = vscode.window.createOutputChannel(channelName); - this.logFilePath = logFilePath; - } - - public static getInstance(channelName: string = "DefaultLogger", logFilePath?: string): Logger { - if (!Logger.instance) { - Logger.instance = new Logger(channelName, logFilePath); - } - return Logger.instance; - } - - public logToFile(message: string) { - if (this.logFilePath) { - fs.appendFileSync(this.logFilePath, message + '\n'); - } - } - - public logToOutputChannel(message: string) { - this.outputChannel.appendLine(`${new Date().toISOString()} - ${message}`); - } - - public log(level: LogLevel, message: string, properties?: any) { - const formattedMessage = `${level} ${message} ${properties ? JSON.stringify(properties) : ""}`.trim(); - this.logToOutputChannel(formattedMessage); - this.logToFile(formattedMessage); - } - - public info(message: string, properties?: any) { - this.log(LogLevel.Info, message, properties); - } - - public warning(message: string, properties?: any) { - this.log(LogLevel.Warning, message, properties); - } - - public debug(message: string, properties?: any) { - this.log(LogLevel.Debug, message, properties); - } - - public error(message: string, properties?: any) { - this.log(LogLevel.Error, message, properties); - } - - public logError(error: any, context: string, showUserMessage: boolean = false): void { - const message = error instanceof Error ? error.message : String(error); - this.error(`Error in ${context}: ${message}`); - - if (showUserMessage) { - vscode.window.showErrorMessage(`Error in ${context}: ${message}`); - } - } -} \ No newline at end of file diff --git a/src/loggerRegistry.ts b/src/loggerRegistry.ts new file mode 100644 index 0000000..ce82f97 --- /dev/null +++ b/src/loggerRegistry.ts @@ -0,0 +1,106 @@ +// File: src/loggerRegistry.ts + +/** + * This module provides a centralized registry for managing loggers within a VS Code extension. + * The `LoggerRegistry` class ensures that loggers are uniquely identified by their names and + * channel names, preventing the creation of duplicate loggers. + * + * Key Features: + * - Adds and removes loggers from the registry by name or channel name. + * - Prevents duplicate loggers from being created, ensuring each logger has a unique identity. + * - Allows retrieval of loggers by name or channel name for easy access. + * - Provides methods to clear all loggers from the registry or retrieve all registered loggers. + */ + +import { CoreLogger } from "./coreLogger"; + +export class LoggerRegistry { + private loggersByName: Map = new Map(); // Map to store loggers by name + private loggersByChannel: Map = new Map(); // Map to store loggers by channel name + + /** + * Adds a CoreLogger instance to the registry. + * If both a loggerName and channelName exist, they are stored in the appropriate maps. + * + * @param logger - The CoreLogger instance to add. + */ + public addLogger(logger: CoreLogger): void { + const loggerName = logger.getLoggerName(); + const channelName = logger.getChannelName(); + + if (this.loggersByName.has(loggerName)) { + throw new Error(`Logger with name "${loggerName}" already exists.`); + } + if (channelName && this.loggersByChannel.has(channelName)) { + throw new Error(`Logger with channel name "${channelName}" already exists.`); + } + + // Add logger to maps + this.loggersByName.set(loggerName, logger); + if (channelName) { + this.loggersByChannel.set(channelName, logger); + } + } + + /** + * Removes a logger from the registry by its name. + * If the logger has an associated channelName, it is removed from the channel map as well. + * + * @param loggerName - The name of the logger to remove. + */ + public removeLoggerByName(loggerName: string): void { + const logger = this.loggersByName.get(loggerName); + + // If no logger is found, throw an error + if (!logger) { + throw new Error(`No logger found with name "${loggerName}".`); + } + + // Remove logger from the channel map if it has a channel name + const channelName = logger.getChannelName(); + if (channelName) { + this.loggersByChannel.delete(channelName); + } + + // Remove logger from the name map + this.loggersByName.delete(loggerName); + } + + /** + * Retrieves a logger from the registry by its name. + * + * @param loggerName - The name of the logger to retrieve. + * @returns The CoreLogger instance, or undefined if not found. + */ + public getLoggerByName(loggerName: string): CoreLogger | undefined { + return this.loggersByName.get(loggerName); + } + + /** + * Retrieves a logger from the registry by its channel name. + * + * @param channelName - The channel name of the logger to retrieve. + * @returns The CoreLogger instance, or undefined if not found. + */ + public getLoggerByChannel(channelName: string): CoreLogger | undefined { + return this.loggersByChannel.get(channelName); + } + + /** + * Returns all registered loggers in an array. + * + * @returns An array of all CoreLogger instances in the registry. + */ + public getAllLoggers(): CoreLogger[] { + return Array.from(this.loggersByName.values()); + } + + /** + * Clears all loggers from the registry. + * This removes loggers from both name and channel maps. + */ + public clear(): void { + this.loggersByName.clear(); + this.loggersByChannel.clear(); + } +} \ No newline at end of file diff --git a/src/model-config.ts b/src/model-config.ts index 601c44d..7cecc07 100644 --- a/src/model-config.ts +++ b/src/model-config.ts @@ -1,4 +1,4 @@ -// model-config.ts +// File: src/model-config.ts /* eslint-disable eqeqeq */ /* eslint-disable @typescript-eslint/naming-convention */ @@ -14,11 +14,6 @@ * copies or substantial portions of the Software. */ -export enum ModelSource { - GoogleAI = "GoogleAI", - VertexAI = "VertexAI", -} - export class ModelConfig { apiKey: string; apiBaseUrl: string; @@ -27,7 +22,8 @@ export class ModelConfig { topP: number; organization: string; systemPrompt: string; - modelSource: ModelSource; + modelSource: string; + jsonCredentialsPath?: string; constructor({ apiKey, @@ -38,6 +34,7 @@ export class ModelConfig { organization, systemPrompt, modelSource, + jsonCredentialsPath, }: { apiKey: string; apiBaseUrl: string; @@ -46,7 +43,8 @@ export class ModelConfig { topP: number; organization: string; systemPrompt: string; - modelSource: ModelSource; + modelSource: string; + jsonCredentialsPath?: string; }) { this.apiKey = apiKey; this.apiBaseUrl = apiBaseUrl; @@ -56,5 +54,6 @@ export class ModelConfig { this.organization = organization; this.systemPrompt = systemPrompt; this.modelSource = modelSource; + this.jsonCredentialsPath = jsonCredentialsPath; } } \ No newline at end of file diff --git a/src/modelManager.ts b/src/modelManager.ts index dc0535d..d2cc87a 100644 --- a/src/modelManager.ts +++ b/src/modelManager.ts @@ -1,27 +1,59 @@ -// src/modelManager.ts +// File: src/modelManager.ts + +/** + * This module manages the configuration and initialization of AI models + * for use within a VS Code extension. It is responsible for loading model + * settings from the configuration, preparing the models for conversation, + * and initializing the appropriate model based on user-defined settings. + * + * The `ModelManager` class ensures that the correct model is initialized + * and ready for use, depending on the user's configuration and the selected + * model type. It interacts with various model initialization functions + * for different AI models, such as GPT, Claude, and Gemini. + * + * Key Features: + * - Loads model configuration from the VS Code workspace settings. + * - Supports multiple AI models, including GPT, Claude, and Gemini. + * - Handles API key retrieval and model settings initialization. + * - Provides methods to check the type of model currently in use. + */ import { ChatGptViewProvider } from './chatgptViewProvider'; -import { defaultSystemPrompt, getApiKey } from "./config/configuration"; +import { defaultSystemPrompt, getApiKey, getRequiredConfig } from "./config/configuration"; +import { CoreLogger } from "./coreLogger"; import { initClaudeModel } from './llm_models/anthropic'; import { initGeminiModel } from './llm_models/gemini'; import { initGptModel } from './llm_models/openai'; import { initGptLegacyModel } from './llm_models/openai-legacy'; -import { LogLevel, Logger } from "./logger"; -import { ModelConfig, ModelSource } from "./model-config"; +import { ModelConfig } from "./model-config"; /** * The ModelManager class is responsible for managing the AI model configuration * and initializing the appropriate model for conversation based on user settings. */ export class ModelManager { - public model?: string; - public modelConfig!: ModelConfig; + public model?: string; // The currently selected model + public modelConfig!: ModelConfig; // Configuration settings for the model + /** + * Constructor for the `ModelManager` class. + * Initializes a new instance of the ModelManager. + */ constructor() { } + /** + * Prepares the selected AI model for conversation. + * Loads configuration settings, retrieves the API key, and initializes the model + * based on the user's settings. + * + * @param modelChanged - A flag indicating if the model has changed. + * @param logger - An instance of `CoreLogger` for logging events. + * @param viewProvider - An instance of `ChatGptViewProvider` for accessing workspace settings. + * @returns A promise that resolves to true if the model is successfully prepared; otherwise, false. + */ public async prepareModelForConversation( modelChanged = false, - logger: Logger, + logger: CoreLogger, viewProvider: ChatGptViewProvider, ): Promise { logger.info("loading configuration from vscode workspace"); @@ -29,7 +61,7 @@ export class ModelManager { const configuration = viewProvider.getWorkspaceConfiguration(); // Determine which model to use based on configuration - const modelSource = configuration.get("chatgpt.gpt3.modelSource") as ModelSource; + const modelSource = getRequiredConfig("gpt3.modelSource"); if (this.model === "custom") { logger.info("custom model, retrieving model name"); @@ -98,6 +130,11 @@ export class ModelManager { return true; } + /** + * Initializes the appropriate model based on the current configuration. + * + * @param viewProvider - An instance of `ChatGptViewProvider` for accessing view-related settings. + */ private async initModels(viewProvider: ChatGptViewProvider): Promise { if (this.isGpt35Model) { await initGptModel(viewProvider, this.modelConfig); @@ -110,6 +147,11 @@ export class ModelManager { } } + /** + * Checks if the currently selected model is a Codex model. + * + * @returns True if the model is a Codex model; otherwise, false. + */ public get isCodexModel(): boolean { if (this.model == null) { return false; @@ -117,14 +159,29 @@ export class ModelManager { return this.model.includes("instruct") || this.model.includes("code-"); } + /** + * Checks if the currently selected model is a GPT-3.5 model. + * + * @returns True if the model is a GPT-3.5 model; otherwise, false. + */ public get isGpt35Model(): boolean { return !this.isCodexModel && !this.isClaude && !this.isGemini; } + /** + * Checks if the currently selected model is a Claude model. + * + * @returns True if the model is a Claude model; otherwise, false. + */ public get isClaude(): boolean { return !!this.model?.startsWith("claude-"); } + /** + * Checks if the currently selected model is a Gemini model. + * + * @returns True if the model is a Gemini model; otherwise, false. + */ public get isGemini(): boolean { return !!this.model?.startsWith("gemini-"); } diff --git a/src/sinkLoggers/fileLogger.ts b/src/sinkLoggers/fileLogger.ts new file mode 100644 index 0000000..fc719fa --- /dev/null +++ b/src/sinkLoggers/fileLogger.ts @@ -0,0 +1,33 @@ +// File: src/sinkLoggers/fileLogger.ts + +import * as fs from "fs"; +import { ISinkLogger } from "../interfaces/ISinkLogger"; + +/** + * The `FileLogger` class is responsible for logging messages to a file. + * It appends messages to the specified log file, maintaining a record of events + * or errors that occur during the application's execution. + */ +export class FileLogger implements ISinkLogger { + private logFilePath: string; + + /** + * Constructor for the `FileLogger` class. + * Initializes the logger with the path to the log file. + * + * @param logFilePath - The path to the log file where messages will be written. + */ + constructor(logFilePath: string) { + this.logFilePath = logFilePath; + } + + /** + * Logs a message to the specified log file. + * Appends the message to the file, ensuring each log entry is on a new line. + * + * @param message - The message to log. + */ + public log(message: string) { + fs.appendFileSync(this.logFilePath, message + '\n', { encoding: 'utf8' }); + } +} diff --git a/src/sinkLoggers/outputChannelLogger.ts b/src/sinkLoggers/outputChannelLogger.ts new file mode 100644 index 0000000..8255446 --- /dev/null +++ b/src/sinkLoggers/outputChannelLogger.ts @@ -0,0 +1,43 @@ +// File: src/sinkLoggers/outputChannelLogger.ts + +import * as vscode from "vscode"; +import { ISinkLogger } from "../interfaces/ISinkLogger"; + +/** + * The `OutputChannelLogger` class provides logging capabilities using the + * VS Code output channel. It formats and displays log messages at various + * levels of severity. + */ +export class OutputChannelLogger implements ISinkLogger { + private outputChannel: vscode.OutputChannel; + private channelName: string; + + /** + * Constructor for the `OutputChannelLogger` class. + * Initializes the output channel for logging messages. + * + * @param channelName - The name of the output channel to be created. + */ + constructor(channelName: string) { + this.channelName = channelName; + this.outputChannel = vscode.window.createOutputChannel(channelName); + } + + /** + * Logs a message to the output channel with a timestamp. + * + * @param message - The message to log. + */ + public log(message: string) { + this.outputChannel.appendLine(`${new Date().toISOString()} - ${message}`); + } + + /** + * Retrieves the name of the output channel. + * + * @returns The name of the output channel. + */ + public getChannelName(): string { + return this.channelName; + } +} diff --git a/src/utils/errorLogger.ts b/src/utils/errorLogger.ts deleted file mode 100644 index bedfc43..0000000 --- a/src/utils/errorLogger.ts +++ /dev/null @@ -1,12 +0,0 @@ -// src/utils/errorLogger.ts -import * as vscode from "vscode"; -import { ILogger } from "../interfaces/ILogger"; - -export function logError(logger: ILogger, error: any, context: string, showUserMessage: boolean = false): void { - const message = error instanceof Error ? error.message : String(error); - logger.error(`Error in ${context}: ${message}`); - - if (showUserMessage) { - vscode.window.showErrorMessage(`Error in ${context}: ${message}`); - } -} diff --git a/src/webviewManager.ts b/src/webviewManager.ts index c61a5bd..55829ac 100644 --- a/src/webviewManager.ts +++ b/src/webviewManager.ts @@ -1,19 +1,51 @@ +// File: src/webviewManager.ts + +/** + * This module provides a management system for webviews within a VS Code extension. + * It handles the initialization and configuration of webviews, including setting up + * HTML content and managing communication between the webview and the extension. + * + * The `WebviewManager` class is responsible for creating and managing webviews, + * providing methods to initialize them with specific content, send messages to + * the webview, and generate the necessary HTML and resources for display. + * + * Key Features: + * - Initializes webviews with customizable HTML content. + * - Supports message sending to the webview. + * - Generates resource URIs for scripts and stylesheets. + * - Handles error logging related to webview operations. + */ + import * as fs from "fs"; import * as vscode from "vscode"; -import { LogLevel, Logger } from "./logger"; +import { CoreLogger } from "./coreLogger"; +/** + * The `WebviewManager` class manages the setup and communication of webviews + * within the extension. It provides methods for initializing webviews and + * sending messages to them. + */ export class WebviewManager { - private webviewView?: vscode.WebviewView; - private logger: Logger; + private webviewView?: vscode.WebviewView; // The webview view instance + private logger: CoreLogger; // Logger instance for logging events - constructor(logger: Logger) { + /** + * Constructor for the `WebviewManager` class. + * Initializes the WebviewManager with a logger instance. + * + * @param logger - An instance of `CoreLogger` for logging events. + */ + constructor(logger: CoreLogger) { this.logger = logger; } /** - * Sets up the webview with HTML content and webview options. - * @param webviewView - The webview view to be set up. - */ + * Sets up the webview with HTML content and webview options. + * + * @param webviewView - The webview view to be set up. + * @param extensionUri - The URI of the extension for resource paths. + * @param nonce - A nonce value for security purposes. + */ public initializeWebView(webviewView: vscode.WebviewView, extensionUri: vscode.Uri, nonce: string) { this.webviewView = webviewView; this.logger.info("Webview set"); @@ -36,8 +68,9 @@ export class WebviewManager { } } - /** + /** * Sends a message to the webview and handles cases where the webview is not focused. + * * @param message - The message to be sent to the webview. */ public sendMessage(message: any) { @@ -49,9 +82,10 @@ export class WebviewManager { } /** - * Retrieves the HTML content for the webview based on the specified configuration. - * @param webview - The webview for which the HTML content is generated. - * @returns A string that contains the HTML content for the webview. + * Generates URIs for various resources used in the webview. + * + * @param extensionUri - The URI of the extension for resource paths. + * @returns An object containing URIs for scripts and stylesheets. */ private generateWebviewHtml(extensionUri: vscode.Uri, nonce: string): string { if (!this.webviewView) { @@ -142,6 +176,14 @@ export class WebviewManager { }; } + /** + * Replaces placeholders in the HTML with actual resource URIs and nonce values. + * + * @param html - The HTML content with placeholders. + * @param resourceUris - An object containing resource URIs. + * @param nonce - A nonce value for security purposes. + * @returns The HTML content with placeholders replaced. + */ private replacePlaceholders(html: string, resourceUris: any, nonce: string): string { return html .replace("${stylesMainUri}", resourceUris.stylesMainUri.toString()) diff --git a/src/webviewMessageHandler.ts b/src/webviewMessageHandler.ts index ed2e968..d9e9c7d 100644 --- a/src/webviewMessageHandler.ts +++ b/src/webviewMessageHandler.ts @@ -1,35 +1,63 @@ -// src/webviewMessageHandler.ts +// File: src/webviewMessageHandler.ts + +/** + * This module handles the communication between the webview and the extension within a VS Code environment. + * It manages incoming messages from the webview and allows for sending responses back to the webview. + * + * The `WebviewMessageHandler` class is responsible for processing messages received from the webview, + * executing the appropriate commands based on the message type, and sending responses back to the webview. + * + * Key Features: + * - Listens for messages from the webview and processes them accordingly. + * - Supports sending messages and data back to the webview. + * - Handles different types of messages, enabling extensible command processing. + */ import * as vscode from "vscode"; import { ChatGptViewProvider, CommandType } from "./chatgptViewProvider"; import { CommandHandler } from "./commandHandler"; -import { Logger, LogLevel } from "./logger"; +import { CoreLogger } from "./coreLogger"; +/** + * The `WebviewMessageHandler` class manages the message communication + * between the webview and the VS Code extension. It processes incoming + * messages and performs actions based on the message type. + */ export class WebviewMessageHandler { - private logger: Logger; - private commandHandler: CommandHandler; + private logger: CoreLogger; // Logger instance for logging events + private commandHandler: CommandHandler; // Command handler for executing commands - constructor(logger: Logger, commandHandler: CommandHandler) { + /** + * Constructor for the `WebviewMessageHandler` class. + * Initializes the message handler with a logger instance and command handler. + * + * @param logger - An instance of `CoreLogger` for logging events. + * @param commandHandler - An instance of `CommandHandler` for executing commands. + */ + constructor(logger: CoreLogger, commandHandler: CommandHandler) { this.logger = logger; this.commandHandler = commandHandler; } /** - * Handles incoming messages from the webview, delegating command execution - * and logging the events. + * Sets up the message listener for the webview. + * This method should be called to start listening for messages. + * + * @param webviewView - The webview instance to listen for messages from. + * @param chatGptViewProvider - The ChatGptViewProvider instance for additional context. */ public handleMessages(webviewView: vscode.WebviewView, chatGptViewProvider: ChatGptViewProvider) { webviewView.webview.onDidReceiveMessage(async (data: { - type: CommandType; - value: any; - language?: string; + type: CommandType; // The type of command to execute + value: any; // The value associated with the command + language?: string; // Optional language information }) => { this.logger.info(`Message received of type: ${data.type}`); try { - await this.commandHandler.executeCommand(data.type, data); + await this.commandHandler.executeCommand(data.type, data); // Execute the command } catch (error) { - this.logger.logError(error, `Error handling command ${data.type}`); + this.logger.logError(error, `Error handling command ${data.type}`); // Log any errors } }); } diff --git a/test/chatgptViewProvider.test.ts b/test/chatgptViewProvider.test.ts deleted file mode 100644 index 4915854..0000000 --- a/test/chatgptViewProvider.test.ts +++ /dev/null @@ -1,386 +0,0 @@ -import * as fs from 'fs'; -// import * as path from 'path'; -import * as vscode from 'vscode'; -import { ChatGptViewProvider, getLineCount } from '../src/chatgptViewProvider'; -import { getConfig } from '../src/config/configuration'; - -jest.mock('fs'); - - -jest.mock('@ai-sdk/anthropic', () => ({ - createAnthropic: jest.fn(), -})); - - -let activeTextEditorMock: vscode.TextEditor; -let SnippetStringMock: any; - -// Mock the vscode module -jest.mock('vscode', () => { - const appendLineMock = jest.fn(); - const createOutputChannelMock = jest.fn(() => ({ - appendLine: appendLineMock, - })); - - // Initialize activeTextEditorMock within the jest mock block - const activeTextEditorMock = { - insertSnippet: jest.fn(), - document: { - getText: jest.fn(), - languageId: 'javascript', - }, - selections: [], - selection: {}, - visibleRanges: [], - options: {}, - viewColumn: undefined, - edit: jest.fn(), - setDecorations: jest.fn(), - revealRange: jest.fn(), - show: jest.fn(), - hide: jest.fn(), - } as unknown as vscode.TextEditor; - - class SnippetStringMock { - value: string; - constructor(value: string) { - this.value = value; - } - } - - return { - window: { - createOutputChannel: createOutputChannelMock, - showErrorMessage: jest.fn(), - showInformationMessage: jest.fn(), - activeTextEditor: activeTextEditorMock, // Use the initialized mock here - showTextDocument: jest.fn(), - commands: { - executeCommand: jest.fn(), - }, - }, - workspace: { - getConfiguration: jest.fn(() => ({ - get: jest.fn((key: string) => { - if (key === 'gpt3.apiBaseUrl') return 'https://api.openai.com/v1'; - if (key === 'gpt3.model') return 'gpt-4'; - return undefined; - }), - })), - openTextDocument: jest.fn(() => Promise.resolve({} as vscode.TextDocument)), - }, - extensions: { - getExtension: jest.fn(() => ({ - exports: { - globalState: { - get: jest.fn(() => undefined), - update: jest.fn(), - }, - }, - })), - }, - SnippetString: SnippetStringMock, - }; -}); - - -jest.mock('../src/config/configuration', () => ({ - getConfig: jest.fn((key) => { - if (key === 'fileInclusionRegex') return '.*\\.ts$'; - if (key === 'fileExclusionRegex') return '.*\\.spec.ts$'; - return undefined; - }), - getRequiredConfig: jest.fn((key) => { - if (key === 'gpt3.apiBaseUrl') return 'https://api.openai.com/v1'; // Mock required config - if (key === 'gpt3.model') return 'gpt-4'; // Another required config - throw new Error(`Missing required configuration: ${key}`); - }), - onConfigurationChanged: jest.fn((callback) => { - // Immediately invoke the mock callback function - callback(); - }), -})); - - -describe('ChatGptViewProvider', () => { - let provider: ChatGptViewProvider; - - beforeEach(() => { - const context = { globalState: {}, workspaceState: {} } as vscode.ExtensionContext; - provider = new ChatGptViewProvider(context); - vscode.window.activeTextEditor = (vscode.window as any).activeTextEditor; // Ensure this is set before each test - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('handleAddFreeTextQuestion', () => { - it('should clear chat history if conversationHistoryEnabled is false', async () => { - provider['conversationHistoryEnabled'] = false; - provider['chatHistory'] = [{ role: 'user', content: 'Old message' }]; - await provider['handleAddFreeTextQuestion']('New Question'); - expect(provider['chatHistory']).toHaveLength(0); - }); - - it('should invoke sendApiRequest with the provided question', async () => { - const spy = jest.spyOn(provider, 'sendApiRequest'); - await provider['handleAddFreeTextQuestion']('What is AI?'); - expect(spy).toHaveBeenCalledWith('What is AI?', { command: 'freeText' }); - }); - }); - - describe('handleEditCode', () => { - it('should insert code snippet into the active text editor', async () => { - const codeSample = 'console.log("Hello, world!");'; - - await provider['handleEditCode'](codeSample); - - // Use expect.anything() to match any object - expect(vscode.window.activeTextEditor!.insertSnippet).toHaveBeenCalledWith(expect.anything()); - expect(vscode.window.activeTextEditor!.insertSnippet).toHaveBeenCalledWith(expect.objectContaining({ value: codeSample })); - }); - - it('should not proceed if there is no active editor', async () => { - vscode.window.activeTextEditor = undefined; // Simulate no active editor - - await provider['handleEditCode']('Some code'); - - // Assert that insertSnippet was not called on the mock - if (activeTextEditorMock) { - expect(activeTextEditorMock.insertSnippet).not.toHaveBeenCalled(); - } - }); - }); - - - describe('handleOpenNew', () => { - it('should open a new text document with the specified content and language', async () => { - const spyOpen = jest.spyOn(vscode.workspace, 'openTextDocument').mockResolvedValue({} as vscode.TextDocument); - const spyShow = jest.spyOn(vscode.window, 'showTextDocument'); - - await provider['handleOpenNew']('New Document Content', 'javascript'); - - expect(spyOpen).toHaveBeenCalledWith({ content: 'New Document Content', language: 'javascript' }); - expect(spyShow).toHaveBeenCalled(); // Ensure it was called - }); - }); - - describe('handleClearConversation', () => { - it('should reset conversationId and clear chatHistory', async () => { - provider['conversationId'] = '123'; - provider['chatHistory'] = [{ role: 'user', content: 'Hello' }]; - await provider['handleClearConversation'](); - expect(provider['conversationId']).toBeUndefined(); - expect(provider['chatHistory']).toHaveLength(0); - }); - }); - - describe('handleLogin', () => { - it('should send a success message if preparation is successful', async () => { - const spyPrepare = jest.spyOn(provider, 'prepareConversation').mockResolvedValue(true); - const spySend = jest.spyOn(provider, 'sendMessage'); - await provider['handleLogin'](); - expect(spySend).toHaveBeenCalledWith({ type: "loginSuccessful", showConversations: false }, true); - }); - - it('should not send a success message if preparation fails', async () => { - const spyPrepare = jest.spyOn(provider, 'prepareConversation').mockResolvedValue(false); - const spySend = jest.spyOn(provider, 'sendMessage'); - await provider['handleLogin'](); - expect(spySend).not.toHaveBeenCalled(); - }); - }); - - describe('showFiles', () => { - it('should show an error message if inclusion regex is not set', async () => { - (getConfig as jest.Mock).mockReturnValueOnce(undefined); - await provider.showFiles(); - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Inclusion regex is not set in the configuration."); - }); - - it('should call findMatchingFiles with the correct arguments', async () => { - const inclusionRegex = '.*\\.js$'; - const exclusionRegex = '.*\\.test.js$'; - (getConfig as jest.Mock).mockReturnValueOnce(inclusionRegex).mockReturnValueOnce(exclusionRegex); - const spyFind = jest.spyOn(provider as any, 'findMatchingFiles').mockResolvedValue([]); - - await provider.showFiles(); - - expect(spyFind).toHaveBeenCalledWith(inclusionRegex, exclusionRegex); - }); - }); - - describe('sendApiRequest', () => { - it('should set inProgress to true while processing', async () => { - const prompt = 'What is AI?'; - await provider.sendApiRequest(prompt, { command: 'freeText' }); - expect(provider.inProgress).toBe(false); // It should be false after processing - }); - - it('should handle errors during API requests gracefully', async () => { - const prompt = 'What is AI?'; - jest.spyOn(provider, 'retrieveContextForPrompt').mockImplementationOnce(() => { - throw new Error('Sample error'); - }); - await provider.sendApiRequest(prompt, { command: 'freeText' }); - expect(vscode.window.showErrorMessage).toHaveBeenCalled(); // Check if error message is shown - }); - }); - - describe('edge cases in handleAddFreeTextQuestion', () => { - it('should not modify chatHistory if conversationHistoryEnabled is true', async () => { - provider['conversationHistoryEnabled'] = true; - provider['chatHistory'] = [{ role: 'user', content: 'Old message' }]; - await provider['handleAddFreeTextQuestion']('New Question'); - expect(provider['chatHistory']).toHaveLength(1); - }); - - it('should not throw error with empty question', async () => { - await provider['handleAddFreeTextQuestion'](''); - expect(provider['chatHistory']).toHaveLength(0); - }); - }); - - // TODO: make these tests pass - // describe('Configuration Change Handling', () => { - // it('should update subscribeToResponse on configuration change', async () => { - // // Arrange - // provider.subscribeToResponse = false; // Initial value - // // Simulate a configuration change - // await provider['onConfigurationChanged'](() => { - // provider.subscribeToResponse = true; // New value - // }); - // // Act - // provider['onConfigurationChanged'](); - // // Assert - // expect(provider.subscribeToResponse).toBe(true); - // }); - - // it('should update autoScroll on configuration change', async () => { - // // Arrange - // provider.autoScroll = false; // Initial value - // // Simulate a configuration change - // await provider['onConfigurationChanged'](() => { - // provider.autoScroll = true; // New value - // }); - // // Act - // provider['onConfigurationChanged'](); - // // Assert - // expect(provider.autoScroll).toBe(true); - // }); - // }); - - // TODO: make these tests pass - // describe('API Response Handling', () => { - // it('should correctly handle a successful API response', async () => { - // // Arrange - // const mockResponse = 'This is a response'; - - // // Mocking the getChatResponse method - // // @ts-ignore - // jest.spyOn(provider as any, 'getChatResponse').mockImplementation(async (_, __, ___) => { - // provider.response = mockResponse; // Set the response directly - // }); - - // // Act - // await provider.sendApiRequest('What can you do?', { command: 'freeText' }); - - // // Assert - // expect(provider.response).toEqual(mockResponse); - // }); - - // it('should handle errors during API requests', async () => { - // // Arrange - // jest.spyOn(provider, 'sendApiRequest').mockRejectedValue(new Error('API error')); - // const spyShowErrorMessage = jest.spyOn(vscode.window, 'showErrorMessage'); - - // // Act - // await provider.sendApiRequest('What can you do?', { command: 'freeText' }); - - // // Assert - // expect(spyShowErrorMessage).toHaveBeenCalledWith(expect.stringContaining('API error')); - // }); - // }); - - // TODO: make these tests pass - // describe('File Matching Logic', () => { - // it('should return only files that match the inclusion regex', async () => { - // // Arrange - // const spyFindMatchingFiles = jest.spyOn(provider as any, 'findMatchingFiles').mockImplementation(async (inclusionPattern, exclusionPattern) => { - // // Mocking inside the implementation to return suitable result - // return ['file1.ts', 'file2.spec.ts', 'otherfile.txt']; // Returning mocked files - // }); - - // // Act - // const matchedFiles = await provider.showFiles(); - - // // Debugging log to see the returned files - // console.log('Matched Files:', matchedFiles); // Add this line to debug - - // // Assert - // expect(matchedFiles).toEqual(['file1.ts']); // only file1.ts should match based on inclusion - // expect(spyFindMatchingFiles).toHaveBeenCalled(); - // }); - - // it('should not include files that match the exclusion regex', async () => { - // // Arrange - // const spyFindMatchingFiles = jest.spyOn(provider as any, 'findMatchingFiles').mockImplementation(async (inclusionPattern, exclusionPattern) => { - // return ['file1.ts', 'file1.test.ts', 'file2.spec.ts']; // Include a file to be excluded - // }); - - // // Act - // const matchedFiles = await provider.showFiles(); - - // // Debugging log to see the returned files - // console.log('Matched Files:', matchedFiles); // Add this line to debug - - // // Assert - // expect(matchedFiles).toEqual(['file1.ts']); // only file1.ts should match - // expect(spyFindMatchingFiles).toHaveBeenCalled(); - // }); - // }); - - - - describe('Message Sending', () => { - it('should send message if webView is available', () => { - // Arrange - const mockWebView = { webview: { postMessage: jest.fn() } }; - // @ts-ignore: Accessing private property for testing - provider.webView = mockWebView as unknown as vscode.WebviewView; - const message = { type: 'testMessage' }; - - // Act - provider.sendMessage(message); - - // @ts-ignore: Accessing private property for testing - expect(provider.webView.webview.postMessage).toHaveBeenCalledWith(message); - }); - - it('should store message if webView is not available', () => { - // @ts-ignore: Accessing private property for testing - provider.webView = undefined; - const message = { type: 'testMessage' }; - - // Act - provider.sendMessage(message); - - // @ts-ignore: Accessing private property for testing - expect(provider.leftOverMessage).toEqual(message); - }); - }); - - describe('Line Counting Functionality', () => { - it('should correctly count the number of lines in a file', () => { - // Arrange - const mockFilePath = '/mock/path/to/file.ts'; - const fsMock = jest.spyOn(fs, 'readFileSync').mockReturnValueOnce('line1\nline2\nline3'); - // Act - const lineCount = getLineCount(mockFilePath); - // Assert - expect(lineCount).toBe(3); - fsMock.mockRestore(); // Cleanup - }); - }); -}); diff --git a/test/configuration.test.ts b/test/configuration.test.ts deleted file mode 100644 index ed7b1a2..0000000 --- a/test/configuration.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -// test/configuration.test.ts - -import * as vscode from 'vscode'; -import { getConfig, getRequiredConfig } from "../src/config/configuration"; - -// Mock the vscode module for consistency -jest.mock('vscode', () => ({ - workspace: { - getConfiguration: jest.fn((namespace: string) => ({ - get: jest.fn((key: string) => { - if (key === 'testKey') { - return undefined; - } - return 'fakeValue'; // Default value for demonstration - }), - })), - }, - window: { - showErrorMessage: jest.fn(), - showInformationMessage: jest.fn(), - }, -})); - -describe('getConfig', () => { - afterEach(() => { - jest.clearAllMocks(); // Clear mocks after each test - }); - - it('should return default value if config value is undefined', () => { - const result = getConfig('testKey', 'defaultValue'); - expect(result).toBe('defaultValue'); - }); - - it('should return the config value if it is defined', () => { - (vscode.workspace.getConfiguration as jest.Mock).mockReturnValueOnce({ - get: jest.fn().mockReturnValue('someValue'), - }); - - const result = getConfig('testKey'); - expect(result).toBe('someValue'); - }); - - it('should return typed value from configuration', () => { - (vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({ - get: jest.fn().mockReturnValue(42), // Mock for numeric type - }); - - const result = getConfig('testKey'); - expect(result).toBe(42); - }); - - it('should handle when no default value is provided and config is undefined', () => { - (vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({ - get: jest.fn().mockReturnValue(undefined), - }); - - expect(() => getConfig('nonExistentKey')).not.toThrow(); - }); - - it('should throw an error when required config value is not present', () => { - expect(() => getRequiredConfig('nonExistentKey')).toThrowError(); - }); -}); diff --git a/test/logger.test.ts b/test/logger.test.ts deleted file mode 100644 index 4265cb6..0000000 --- a/test/logger.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import * as fs from 'fs'; -import * as vscode from "vscode"; -import { Logger, LogLevel } from "../src/logger"; - -jest.mock("fs", () => ({ - appendFileSync: jest.fn(), -})); - -jest.mock("vscode", () => { - const appendLineMock = jest.fn(); - const createOutputChannelMock = jest.fn(() => ({ - appendLine: appendLineMock, - })); - - return { - window: { - createOutputChannel: createOutputChannelMock, - }, - }; -}); - -describe('Logger Tests', () => { - let logger: Logger; - let mockOutputChannel: any; - - beforeEach(() => { - jest.clearAllMocks(); - mockOutputChannel = vscode.window.createOutputChannel("TestLogger"); - logger = Logger.getInstance("TestLogger", "test.log"); - }); - - it('should log info messages correctly to output channel', () => { - logger.info("This is an info message"); - - expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining("INFO This is an info message")); - }); - - it('should log debug messages correctly to output channel', () => { - logger.debug("This is a debug message"); - expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining("DEBUG This is a debug message")); - }); - - it('should log error messages correctly to output channel', () => { - logger.error("This is an error message"); - expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining("ERROR This is an error message")); - }); - - it('should attempt to log to file if log file path is defined', () => { - logger.info("Logging to file"); - expect(fs.appendFileSync).toHaveBeenCalled(); - }); - - it('should format messages with additional properties correctly', () => { - logger.info("Test info message", { key: "value" }); - expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining('{"key":"value"}')); - }); - - it('should log messages with timestamps', () => { - const message = "This message should contain a timestamp"; - logger.info(message); - - // Get the current time formatted to match the expected format with milliseconds - const currentTime = new Date().toISOString(); // Includes milliseconds - - // Check if the message contains the expected string - expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining(message)); - expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining(currentTime)); // Now matches with milliseconds - }); - - it('should log without throwing error if log file path is undefined', () => { - const mockLoggerWithoutPath = Logger.getInstance("MockLoggerWithoutPath"); - expect(() => mockLoggerWithoutPath.logToFile("Test message")).not.toThrow(); - }); - - it('should log different log levels correctly when using the same logger instance', () => { - logger.info("First info message"); - logger.error("First error message"); - expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining("INFO")); - expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining("ERROR")); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); -}); diff --git a/yarn.lock b/yarn.lock index 882fed4..eb396b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,15 @@ "@ai-sdk/provider" "0.0.12" "@ai-sdk/provider-utils" "1.0.2" +"@ai-sdk/google-vertex@^0.0.36": + version "0.0.36" + resolved "https://registry.yarnpkg.com/@ai-sdk/google-vertex/-/google-vertex-0.0.36.tgz#2e33dd59cfe336f1e04fa86368bb1d603fd0016a" + integrity sha512-LtjbkurNt5K5IWWVL2ROmsxyMW9XNJV5XxIeL5+xrWNwt+smxFDGnnLDQDT/U3tbapRNvLmXmtVc8emSjbDFSg== + dependencies: + "@ai-sdk/provider" "0.0.23" + "@ai-sdk/provider-utils" "1.0.18" + json-schema "0.4.0" + "@ai-sdk/google@^0.0.27": version "0.0.27" resolved "https://registry.npmjs.org/@ai-sdk/google/-/google-0.0.27.tgz" @@ -45,6 +54,16 @@ nanoid "3.3.6" secure-json-parse "2.7.0" +"@ai-sdk/provider-utils@1.0.18": + version "1.0.18" + resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-1.0.18.tgz#bd30948d6b4ad73a3fbc94f3beeb92f4fa198ded" + integrity sha512-9u/XE/dB1gsIGcxiC5JfGOLzUz+EKRXt66T8KYWwDg4x8d02P+fI/EPOgkf+T4oLBrcQgvs4GPXPKoXGPJxBbg== + dependencies: + "@ai-sdk/provider" "0.0.23" + eventsource-parser "1.1.2" + nanoid "3.3.6" + secure-json-parse "2.7.0" + "@ai-sdk/provider-utils@1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.2.tgz" @@ -69,6 +88,13 @@ dependencies: json-schema "0.4.0" +"@ai-sdk/provider@0.0.23": + version "0.0.23" + resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-0.0.23.tgz#a69a9103854bbfb500dddf0b44a399edf3db4735" + integrity sha512-oAc49O5+xypVrKM7EUU5P/Y4DUL4JZUWVxhejoAVOTOl3WZUEWsMbP3QZR+TrimQIsS0WR/n9UuF6U0jPdp0tQ== + dependencies: + json-schema "0.4.0" + "@ai-sdk/react@0.0.25": version "0.0.25" resolved "https://registry.npmjs.org/@ai-sdk/react/-/react-0.0.25.tgz" From 662ab98a70646f20b6d3fb6f328dd37ca5ec71b7 Mon Sep 17 00:00:00 2001 From: Jean IBARZ Date: Fri, 6 Sep 2024 23:40:06 +0200 Subject: [PATCH 2/3] Fix issues with removed logError. Bump version to 5.0.1 --- package.json | 2 +- src/chatgptViewProvider.ts | 29 ++++++++++++++--------------- src/errorHandler.ts | 3 +-- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 29017c1..f9eab7a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "displayName": "ChatGPT Copilot", "icon": "images/ai-logo.png", "description": "A forked version of the original ChatGPT Copilot Extension by jeanibarz (https://github.com/jeanibarz/chatgpt-copilot), providing additional features for VS Code integration.", - "version": "5.0.1", + "version": "5.1.0", "aiKey": "", "repository": { "url": "https://github.com/jeanibarz/chatgpt-copilot" diff --git a/src/chatgptViewProvider.ts b/src/chatgptViewProvider.ts index 238dc89..2ce6003 100644 --- a/src/chatgptViewProvider.ts +++ b/src/chatgptViewProvider.ts @@ -41,7 +41,6 @@ import { ErrorHandler } from "./errorHandler"; import { ChatModelFactory } from './llm_models/chatModelFactory'; import { IChatModel } from './llm_models/IChatModel'; import { ModelManager } from "./modelManager"; -import { logError } from "./utils/errorLogger"; import { WebviewManager } from "./webviewManager"; import { WebviewMessageHandler } from "./webviewMessageHandler"; @@ -92,7 +91,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { public messageHandler: WebviewMessageHandler; public errorHandler: ErrorHandler; public commandHandler: CommandHandler; // CommandHandler: Responsible for managing command execution. - + public apiCompletion?: OpenAICompletionLanguageModel | LanguageModelV1; public apiChat?: OpenAIChatLanguageModel | LanguageModelV1; public apiGenerativeModel?: GenerativeModel; @@ -145,13 +144,13 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { return vscode.workspace.getConfiguration("chatgpt"); } -/** - * Resolves the webview view with the provided context and sets up necessary event handling. - * - * @param webviewView - The webview view that is being resolved. - * @param _context - Context information related to the webview view. - * @param _token - A cancellation token to signal if the operation is cancelled. - */ + /** + * Resolves the webview view with the provided context and sets up necessary event handling. + * + * @param webviewView - The webview view that is being resolved. + * @param _context - Context information related to the webview view. + * @param _token - A cancellation token to signal if the operation is cancelled. + */ public resolveWebviewView( webviewView: vscode.WebviewView, _context: vscode.WebviewViewResolveContext, @@ -372,7 +371,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { return formattedContext; } catch (error) { - logError(this.logger, error, "retrieveContextForPrompt"); + this.logger.logError(error, "retrieveContextForPrompt"); throw error; // Rethrow the error if necessary } } @@ -462,7 +461,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { return; } } catch (error) { - logError(this.logger, error, "Failed to prepare conversation", true); + this.logger.logError(error, "Failed to prepare conversation", true); return; } @@ -472,7 +471,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { try { additionalContext = await this.retrieveContextForPrompt(); } catch (error) { - logError(this.logger, error, "Failed to retrieve context for prompt", true); + this.logger.logError(error, "Failed to retrieve context for prompt", true); return; } @@ -486,7 +485,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.webView?.show?.(true); } } catch (error) { - logError(this.logger, error, "Failed to focus or show the ChatGPT view", true); + this.logger.logError(error, "Failed to focus or show the ChatGPT view", true); } this.logger.info("Preparing to create chat model..."); @@ -501,7 +500,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { try { chatModel = await ChatModelFactory.createChatModel(this, modelConfig); } catch (error) { - logError(this.logger, error, "Failed to create chat model", true); + this.logger.logError(error, "Failed to create chat model", true); return; } @@ -527,7 +526,7 @@ export class ChatGptViewProvider implements vscode.WebviewViewProvider { this.logger.info('handle chat response...'); await this.handleChatResponse(chatModel, formattedQuestion, additionalContext, options); // Centralized response handling } catch (error: any) { - logError(this.logger, error, "Error in handleChatResponse", true); + this.logger.logError(error, "Error in handleChatResponse", true); this.handleApiError(error, formattedQuestion, options); } finally { this.inProgress = false; diff --git a/src/errorHandler.ts b/src/errorHandler.ts index 7a049e0..7f27755 100644 --- a/src/errorHandler.ts +++ b/src/errorHandler.ts @@ -16,7 +16,6 @@ import { BaseErrorHandler } from "./base/baseErrorHandler"; import { CoreLogger } from "./coreLogger"; import { ErrorHandlerRegistry } from "./errorHandlerRegistry"; import { delay } from "./utils/delay"; -import { logError } from "./utils/errorLogger"; /** * The `ErrorHandler` class extends the `BaseErrorHandler` and provides specific error handling logic. @@ -79,7 +78,7 @@ export class ErrorHandler extends BaseErrorHandler { // Log the error with context const apiMessage = error?.response?.data?.error?.message || error?.toString?.() || error?.message || error?.name; - logError(this.logger, "api-request-failed", `API Request failed: ${apiMessage}`); + this.logger.logError("api-request-failed", `API Request failed: ${apiMessage}`); if (error?.response) { const { status, statusText } = error.response; From 545b5e69d64ead4e0560781e9dc21881264df662 Mon Sep 17 00:00:00 2001 From: Jean IBARZ Date: Fri, 6 Sep 2024 23:41:20 +0200 Subject: [PATCH 3/3] Updated .gitignore, adding .secrets --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0de9963..ba61b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ package-lock.json .vscode-test/ *.vsix .DS_Store -!node_modules/chatgpt/build/index.js \ No newline at end of file +!node_modules/chatgpt/build/index.js +.secrets/ \ No newline at end of file