diff --git a/package.json b/package.json index 2b84935..e8fab0e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "publisher": "fiatinnovations", "description": "CodeBuddy is a Visual Studio Code extension that enhances developer productivity through AI-powered code assistance. It provides intelligent code review, refactoring suggestions, optimization tips, and interactive chat capabilities powered by multiple AI models including Gemini, Groq, Anthropic, and Deepseek.", - "version": "3.2.6", + "version": "3.2.9", "engines": { "vscode": "^1.78.0" }, @@ -96,6 +96,10 @@ "when": "editorHasSelection", "command": "CodeBuddy.generateMermaidDiagram", "group": "CodeBuddy" + }, + { + "command": "CodeBuddy.reviewPR", + "group": "CodeBuddy" } ] }, @@ -135,6 +139,14 @@ { "command": "CodeBuddy.generateMermaidDiagram", "title": "CodeBuddy. Generate Mermaid diagram." + }, + { + "command": "CodeBuddy.restart", + "title": "CodeBuddy: Restart Extension" + }, + { + "command": "CodeBuddy.reviewPR", + "title": "CodeBuddy: Review Pull Request" } ], "viewsContainers": { diff --git a/src/application/constant.ts b/src/application/constant.ts index a705ac7..6d93518 100644 --- a/src/application/constant.ts +++ b/src/application/constant.ts @@ -23,6 +23,7 @@ export enum COMMON { GEMINI_CHAT_HISTORY = "geminiChatHistory", ANTHROPIC_CHAT_HISTORY = "anthropicChatHistory", DEEPSEEK_CHAT_HISTORY = "deepseekChatHistory", + SHARED_CHAT_HISTORY = "sharedChatHistory", // Unified chat history for all providers USER_INPUT = "user-input", BOT = "bot", GEMINI_SNAPSHOT = "GeminiSnapshot", diff --git a/src/commands/handler.ts b/src/commands/handler.ts index 570bd86..d8bf0a9 100644 --- a/src/commands/handler.ts +++ b/src/commands/handler.ts @@ -3,7 +3,11 @@ import Anthropic from "@anthropic-ai/sdk"; import { GenerativeModel, GoogleGenerativeAI } from "@google/generative-ai"; import Groq from "groq-sdk"; import * as vscode from "vscode"; -import { APP_CONFIG, COMMON, generativeAiModels } from "../application/constant"; +import { + APP_CONFIG, + COMMON, + generativeAiModels, +} from "../application/constant"; import { AnthropicWebViewProvider } from "../webview-providers/anthropic"; import { DeepseekWebViewProvider } from "../webview-providers/deepseek"; import { GeminiWebViewProvider } from "../webview-providers/gemini"; @@ -41,7 +45,7 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { constructor( private readonly action: string, _context: vscode.ExtensionContext, - errorMessage?: string + errorMessage?: string, ) { this.context = _context; this.error = errorMessage; @@ -84,7 +88,10 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { } { const { CODEBUDDY_ACTIONS } = require("../application/constant"); - const commandDescriptions: Record = { + const commandDescriptions: Record< + string, + { action: string; description: string } + > = { [CODEBUDDY_ACTIONS.comment]: { action: "Adding Code Comments", description: @@ -97,7 +104,8 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { }, [CODEBUDDY_ACTIONS.refactor]: { action: "Refactoring Code", - description: "CodeBuddy is applying SOLID principles and design patterns to improve code maintainability...", + description: + "CodeBuddy is applying SOLID principles and design patterns to improve code maintainability...", }, [CODEBUDDY_ACTIONS.optimize]: { action: "Optimizing Performance", @@ -106,7 +114,8 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { }, [CODEBUDDY_ACTIONS.fix]: { action: "Fixing Code Issues", - description: "CodeBuddy is diagnosing the error and implementing defensive programming solutions...", + description: + "CodeBuddy is diagnosing the error and implementing defensive programming solutions...", }, [CODEBUDDY_ACTIONS.explain]: { action: "Explaining Code Logic", @@ -115,19 +124,23 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { }, [CODEBUDDY_ACTIONS.commitMessage]: { action: "Generating Commit Message", - description: "CodeBuddy is analyzing your staged changes and crafting a professional commit message...", + description: + "CodeBuddy is analyzing your staged changes and crafting a professional commit message...", }, [CODEBUDDY_ACTIONS.interviewMe]: { action: "Preparing Interview Questions", - description: "CodeBuddy is creating comprehensive technical interview questions based on your code...", + description: + "CodeBuddy is creating comprehensive technical interview questions based on your code...", }, [CODEBUDDY_ACTIONS.generateUnitTest]: { action: "Generating Unit Tests", - description: "CodeBuddy is creating comprehensive test suites with edge cases and mocking strategies...", + description: + "CodeBuddy is creating comprehensive test suites with edge cases and mocking strategies...", }, [CODEBUDDY_ACTIONS.generateDiagram]: { action: "Creating System Diagram", - description: "CodeBuddy is visualizing your code architecture with professional Mermaid diagrams...", + description: + "CodeBuddy is visualizing your code architecture with professional Mermaid diagrams...", }, [CODEBUDDY_ACTIONS.reviewPR]: { action: "Reviewing Pull Request", @@ -136,14 +149,16 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { }, [CODEBUDDY_ACTIONS.inlineChat]: { action: "Processing Inline Request", - description: "CodeBuddy is analyzing your inline query and generating a contextual response...", + description: + "CodeBuddy is analyzing your inline query and generating a contextual response...", }, }; return ( commandDescriptions[action] || { action: "Processing Request", - description: "CodeBuddy is analyzing your code and generating a response...", + description: + "CodeBuddy is analyzing your code and generating a response...", } ); } @@ -186,13 +201,15 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { } } - protected createModel(): { generativeAi: string; model: any; modelName: string } | undefined { + protected createModel(): + | { generativeAi: string; model: any; modelName: string } + | undefined { try { let model; let modelName = ""; if (!this.generativeAi) { vscodeErrorMessage( - "Configuration not found. Go to settings, search for Your coding buddy. Fill up the model and model name" + "Configuration not found. Go to settings, search for Your coding buddy. Fill up the model and model name", ); } if (this.generativeAi === generativeAiModels.GROQ) { @@ -200,7 +217,7 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { modelName = this.groqModel; if (!apiKey || !modelName) { vscodeErrorMessage( - "Configuration not found. Go to settings, search for Your coding buddy. Fill up the model and model name" + "Configuration not found. Go to settings, search for Your coding buddy. Fill up the model and model name", ); } model = this.createGroqModel(apiKey); @@ -226,7 +243,9 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { return { generativeAi: this.generativeAi, model, modelName }; } catch (error) { console.error("Error creating model:", error); - vscode.window.showErrorMessage("An error occurred while creating the model. Please try again."); + vscode.window.showErrorMessage( + "An error occurred while creating the model. Please try again.", + ); } } @@ -259,7 +278,9 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { return new Groq({ apiKey }); } - protected async generateModelResponse(text: string): Promise { + protected async generateModelResponse( + text: string, + ): Promise { try { if (text?.length > 0) { this.orchestrator.publish("onUserPrompt", text); @@ -299,7 +320,7 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { if (!response) { throw new Error( - "Could not generate response. Check your settings, ensure the API keys and Model Name is added properly." + "Could not generate response. Check your settings, ensure the API keys and Model Name is added properly.", ); } if (this.action.includes("chart")) { @@ -310,7 +331,9 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { return response; } catch (error) { this.logger.error("Error generating response:", error); - vscode.window.showErrorMessage("An error occurred while generating the response. Please try again."); + vscode.window.showErrorMessage( + "An error occurred while generating the response. Please try again.", + ); } } @@ -321,7 +344,10 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { return inputString; } - async generateGeminiResponse(model: any, text: string): Promise { + async generateGeminiResponse( + model: any, + text: string, + ): Promise { const result = await model.generateContent(text); return result ? await result.response.text() : undefined; } @@ -329,7 +355,7 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { private async anthropicResponse( model: Anthropic, generativeAiModel: string, - userPrompt: string + userPrompt: string, ): Promise { try { const response = await model.messages.create({ @@ -353,9 +379,15 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { } } - private async groqResponse(model: Groq, prompt: string, generativeAiModel: string): Promise { + private async groqResponse( + model: Groq, + prompt: string, + generativeAiModel: string, + ): Promise { try { - const chatHistory = Memory.has(COMMON.ANTHROPIC_CHAT_HISTORY) ? Memory.get(COMMON.GROQ_CHAT_HISTORY) : []; + const chatHistory = Memory.has(COMMON.ANTHROPIC_CHAT_HISTORY) + ? Memory.get(COMMON.GROQ_CHAT_HISTORY) + : []; const params = { messages: [ ...chatHistory, @@ -367,7 +399,8 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { model: generativeAiModel, }; - const completion: Groq.Chat.ChatCompletion = await model.chat.completions.create(params); + const completion: Groq.Chat.ChatCompletion = + await model.chat.completions.create(params); return completion.choices[0]?.message?.content ?? undefined; } catch (error) { this.logger.error("Error generating response:", error); @@ -378,7 +411,9 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { abstract createPrompt(text?: string): any; - async generateResponse(message?: string): Promise { + async generateResponse( + message?: string, + ): Promise { this.logger.info(this.action); let prompt; const selectedCode = this.getSelectedWindowArea(); @@ -390,7 +425,9 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { if (message && selectedCode) { prompt = await this.createPrompt(`${message} \n ${selectedCode}`); } else { - message ? (prompt = await this.createPrompt(message)) : (prompt = await this.createPrompt(selectedCode)); + message + ? (prompt = await this.createPrompt(message)) + : (prompt = await this.createPrompt(selectedCode)); } if (!prompt) { @@ -403,10 +440,12 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { if (prompt && response) { let chatHistory; + const MAX_HISTORY_ITEMS = 20; // Limit chat history to prevent memory issues + switch (model) { - case generativeAiModels.GEMINI: + case generativeAiModels.GEMINI: { chatHistory = getLatestChatHistory(COMMON.GEMINI_CHAT_HISTORY); - Memory.set(COMMON.GEMINI_CHAT_HISTORY, [ + const newGeminiHistory = [ ...chatHistory, { role: "user", @@ -416,11 +455,20 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { role: "model", parts: [{ text: response }], }, - ]); + ]; + + // Trim history if too long (keep system messages at start) + const trimmedGeminiHistory = + newGeminiHistory.length > MAX_HISTORY_ITEMS + ? newGeminiHistory.slice(-MAX_HISTORY_ITEMS) + : newGeminiHistory; + + Memory.set(COMMON.GEMINI_CHAT_HISTORY, trimmedGeminiHistory); break; - case generativeAiModels.GROQ: + } + case generativeAiModels.GROQ: { chatHistory = getLatestChatHistory(COMMON.GROQ_CHAT_HISTORY); - Memory.set(COMMON.GROQ_CHAT_HISTORY, [ + const newGroqHistory = [ ...chatHistory, { role: "user", @@ -430,11 +478,19 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { role: "system", content: response, }, - ]); + ]; + + const trimmedGroqHistory = + newGroqHistory.length > MAX_HISTORY_ITEMS + ? newGroqHistory.slice(-MAX_HISTORY_ITEMS) + : newGroqHistory; + + Memory.set(COMMON.GROQ_CHAT_HISTORY, trimmedGroqHistory); break; - case generativeAiModels.ANTHROPIC: + } + case generativeAiModels.ANTHROPIC: { chatHistory = getLatestChatHistory(COMMON.ANTHROPIC_CHAT_HISTORY); - Memory.set(COMMON.ANTHROPIC_CHAT_HISTORY, [ + const newAnthropicHistory = [ ...chatHistory, { role: "user", @@ -444,11 +500,19 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { role: "assistant", content: response, }, - ]); + ]; + + const trimmedAnthropicHistory = + newAnthropicHistory.length > MAX_HISTORY_ITEMS + ? newAnthropicHistory.slice(-MAX_HISTORY_ITEMS) + : newAnthropicHistory; + + Memory.set(COMMON.ANTHROPIC_CHAT_HISTORY, trimmedAnthropicHistory); break; - case generativeAiModels.GROK: + } + case generativeAiModels.GROK: { chatHistory = getLatestChatHistory(COMMON.ANTHROPIC_CHAT_HISTORY); - Memory.set(COMMON.ANTHROPIC_CHAT_HISTORY, [ + const newGrokHistory = [ ...chatHistory, { role: "user", @@ -458,8 +522,16 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { role: "assistant", content: response, }, - ]); + ]; + + const trimmedGrokHistory = + newGrokHistory.length > MAX_HISTORY_ITEMS + ? newGrokHistory.slice(-MAX_HISTORY_ITEMS) + : newGrokHistory; + + Memory.set(COMMON.ANTHROPIC_CHAT_HISTORY, trimmedGrokHistory); break; + } default: throw new Error(`Generative model ${model} not available`); } @@ -473,7 +545,9 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { placeHolder: "Enter instructions for CodeBuddy", ignoreFocusOut: true, validateInput: (text) => { - return text === "" ? "Enter instructions for CodeBuddy or press Escape to close chat box" : null; + return text === "" + ? "Enter instructions for CodeBuddy or press Escape to close chat box" + : null; }, }); return userPrompt; @@ -488,7 +562,9 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { await this.sendCommandFeedback(action || this.action); let prompt: string | undefined; - const response = (await this.generateResponse(prompt ?? message)) as string; + const response = (await this.generateResponse( + prompt ?? message, + )) as string; if (!response) { vscode.window.showErrorMessage("model not reponding, try again later"); return; @@ -529,7 +605,10 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { break; } } catch (error) { - this.logger.error("Error while passing model response to the webview", error); + this.logger.error( + "Error while passing model response to the webview", + error, + ); } } } diff --git a/src/services/agent-state.ts b/src/services/agent-state.ts index 6b0e3e6..4c7fecb 100644 --- a/src/services/agent-state.ts +++ b/src/services/agent-state.ts @@ -1,7 +1,7 @@ import { AgentState } from "../agents/interface"; import { COMMON } from "../application/constant"; import { GeminiLLMSnapShot } from "../llms/interface"; -import { FileStorage, IStorage } from "./file-storage"; +import { OptimizedFileStorage, IStorage } from "./file-storage-optimized"; export class AgentService { private static instance: AgentService; @@ -13,7 +13,7 @@ export class AgentService { public static getInstance(): AgentService { if (!AgentService.instance) { - AgentService.instance = new AgentService(new FileStorage()); + AgentService.instance = new AgentService(new OptimizedFileStorage()); } return AgentService.instance; } diff --git a/src/services/file-storage-backup.ts b/src/services/file-storage-backup.ts new file mode 100644 index 0000000..de44636 --- /dev/null +++ b/src/services/file-storage-backup.ts @@ -0,0 +1,130 @@ +// Create a new file: src/storage/database.ts + +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +export interface IStorage { + get(key: string): Promise; + set(key: string, value: T): Promise; + delete(key: string): Promise; + has(key: string): Promise; +} + +export class FileStorage implements IStorage { + private storagePath = ""; + private initialized = false; + + constructor() { + // No async operations in constructor + } + + private async ensureInitialized(): Promise { + if (!this.initialized) { + await this.createCodeBuddyFolder(); + this.initialized = true; + } + } + + async createCodeBuddyFolder() { + const workSpaceRoot = + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ""; + this.storagePath = path.join(workSpaceRoot, ".codebuddy"); + if (!fs.existsSync(this.storagePath)) { + fs.mkdirSync(this.storagePath, { recursive: true }); + } + await this.updateGitIgnore(workSpaceRoot, ".codebuddy"); + } + + /** + * Updates or creates a .gitignore file with the specified pattern + * @param workspaceRoot The root folder of the current workspace + * @param pattern The pattern to add to .gitignore (e.g., '.codebuddy') + */ + async updateGitIgnore(workspaceRoot: string, pattern: string): Promise { + const gitIgnorePath = path.join(workspaceRoot, ".gitignore"); + + if (fs.existsSync(gitIgnorePath)) { + const gitIgnoreContent = fs.readFileSync(gitIgnorePath, "utf8"); + const lines = gitIgnoreContent.split(/\r?\n/); + + const patternExists = lines.some( + (line) => + line.trim() === pattern || + line.trim() === `/${pattern}` || + line.trim() === `${pattern}/`, + ); + + if (!patternExists) { + const newContent = gitIgnoreContent.endsWith("\n") + ? `${gitIgnoreContent}${pattern}\n` + : `${gitIgnoreContent}\n${pattern}\n`; + + fs.writeFileSync(gitIgnorePath, newContent, "utf8"); + console.log(`Added ${pattern} to .gitignore`); + } + } else { + fs.writeFileSync( + gitIgnorePath, + `# Generated by CodeBuddy Extension\n${pattern}\n`, + "utf8", + ); + console.log(`Created new .gitignore with ${pattern}`); + } + } + private getFilePath(key: string): string { + return path.join(this.storagePath, `${key}.json`); + } + + async get(key: string): Promise { + await this.ensureInitialized(); + + try { + const filePath = this.getFilePath(key); + if (!fs.existsSync(filePath)) { + return undefined; + } + const data = await fs.promises.readFile(filePath, "utf-8"); + return JSON.parse(data) as T; + } catch (error) { + console.error(`Error reading data for key ${key}:`, error); + return undefined; + } + } + + async set(key: string, value: T): Promise { + await this.ensureInitialized(); + + try { + const filePath = this.getFilePath(key); + await fs.promises.writeFile( + filePath, + JSON.stringify(value, null, 2), + "utf-8", + ); + } catch (error) { + console.error(`Error storing data for key ${key}:`, error); + throw new Error(`Failed to store data: ${error}`); + } + } + + async delete(key: string): Promise { + await this.ensureInitialized(); + + try { + const filePath = this.getFilePath(key); + if (fs.existsSync(filePath)) { + await fs.promises.unlink(filePath); + } + } catch (error) { + console.error(`Error deleting data for key ${key}:`, error); + } + } + + async has(key: string): Promise { + await this.ensureInitialized(); + + const filePath = this.getFilePath(key); + return fs.existsSync(filePath); + } +} diff --git a/src/services/file-storage-optimized.ts b/src/services/file-storage-optimized.ts new file mode 100644 index 0000000..e4fca93 --- /dev/null +++ b/src/services/file-storage-optimized.ts @@ -0,0 +1,254 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +export interface IStorage { + get(key: string): Promise; + set(key: string, value: T): Promise; + delete(key: string): Promise; + has(key: string): Promise; +} + +export class OptimizedFileStorage implements IStorage { + private storagePath = ""; + private readonly cache = new Map< + string, + { data: any; timestamp: number; dirty: boolean } + >(); + private readonly CACHE_TTL = 10000; // 10 seconds cache + private readonly pendingWrites = new Map>(); + private initialized = false; + private batchWriteTimer: NodeJS.Timeout | null = null; + private readonly BATCH_WRITE_DELAY = 1000; // 1 second batching + + constructor() { + // Don't call async operations in constructor + } + + private async ensureInitialized(): Promise { + if (!this.initialized) { + await this.createCodeBuddyFolder(); + this.initialized = true; + } + } + + async createCodeBuddyFolder() { + const workSpaceRoot = + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ""; + this.storagePath = path.join(workSpaceRoot, ".codebuddy"); + + if (!fs.existsSync(this.storagePath)) { + await fs.promises.mkdir(this.storagePath, { recursive: true }); + } + + // Update .gitignore asynchronously and non-blocking + this.updateGitIgnoreAsync(workSpaceRoot, ".codebuddy").catch(console.error); + } + + /** + * Non-blocking .gitignore update + */ + private async updateGitIgnoreAsync( + workspaceRoot: string, + pattern: string, + ): Promise { + try { + const gitIgnorePath = path.join(workspaceRoot, ".gitignore"); + + let gitIgnoreContent = ""; + try { + gitIgnoreContent = await fs.promises.readFile(gitIgnorePath, "utf8"); + } catch { + // File doesn't exist, create new one + } + + const lines = gitIgnoreContent.split(/\r?\n/); + const patternExists = lines.some( + (line) => + line.trim() === pattern || + line.trim() === `/${pattern}` || + line.trim() === `${pattern}/`, + ); + + if (!patternExists) { + let newContent: string; + if (gitIgnoreContent) { + newContent = gitIgnoreContent.endsWith("\n") + ? `${gitIgnoreContent}${pattern}\n` + : `${gitIgnoreContent}\n${pattern}\n`; + } else { + newContent = `# Generated by CodeBuddy Extension\n${pattern}\n`; + } + + await fs.promises.writeFile(gitIgnorePath, newContent, "utf8"); + } + } catch (error) { + // Silently fail .gitignore updates to prevent blocking + console.warn("Could not update .gitignore:", error); + } + } + + private getFilePath(key: string): string { + return path.join(this.storagePath, `${key}.json`); + } + + private isValidCache(entry: { data: any; timestamp: number }): boolean { + return Date.now() - entry.timestamp < this.CACHE_TTL; + } + + async get(key: string): Promise { + await this.ensureInitialized(); + + // Check cache first + const cached = this.cache.get(key); + if (cached && this.isValidCache(cached)) { + return cached.data as T; + } + + try { + const filePath = this.getFilePath(key); + + // Check if file exists without throwing + try { + await fs.promises.access(filePath); + } catch { + return undefined; + } + + const data = await fs.promises.readFile(filePath, "utf-8"); + const parsed = JSON.parse(data) as T; + + // Cache the result + this.cache.set(key, { + data: parsed, + timestamp: Date.now(), + dirty: false, + }); + + return parsed; + } catch (error) { + console.error(`Error reading data for key ${key}:`, error); + return undefined; + } + } + + async set(key: string, value: T): Promise { + await this.ensureInitialized(); + + // Update cache immediately + this.cache.set(key, { + data: value, + timestamp: Date.now(), + dirty: true, + }); + + // Debounce writes to prevent excessive I/O + const existingWrite = this.pendingWrites.get(key); + if (existingWrite) { + return existingWrite; + } + + const writePromise = this.debouncedWrite(key, value); + this.pendingWrites.set(key, writePromise); + + try { + await writePromise; + } finally { + this.pendingWrites.delete(key); + } + } + + private async debouncedWrite(key: string, value: T): Promise { + return new Promise((resolve, reject) => { + // Clear existing timer + if (this.batchWriteTimer) { + clearTimeout(this.batchWriteTimer); + } + + // Set new timer + this.batchWriteTimer = setTimeout(async () => { + try { + const filePath = this.getFilePath(key); + await fs.promises.writeFile( + filePath, + JSON.stringify(value, null, 2), + "utf-8", + ); + + // Mark as clean in cache + const cached = this.cache.get(key); + if (cached) { + cached.dirty = false; + } + + resolve(); + } catch (error) { + console.error(`Error storing data for key ${key}:`, error); + reject(new Error(`Failed to store data: ${error}`)); + } + }, this.BATCH_WRITE_DELAY); + }); + } + + async delete(key: string): Promise { + await this.ensureInitialized(); + + // Remove from cache + this.cache.delete(key); + + try { + const filePath = this.getFilePath(key); + await fs.promises.unlink(filePath); + } catch (error) { + // File might not exist, that's fine + if ( + error instanceof Error && + "code" in error && + error.code !== "ENOENT" + ) { + console.error(`Error deleting data for key ${key}:`, error); + } + } + } + + async has(key: string): Promise { + await this.ensureInitialized(); + + // Check cache first + const cached = this.cache.get(key); + if (cached && this.isValidCache(cached)) { + return true; + } + + try { + const filePath = this.getFilePath(key); + await fs.promises.access(filePath); + return true; + } catch { + return false; + } + } + + // Force flush all pending writes + async flushAll(): Promise { + const promises = Array.from(this.pendingWrites.values()); + await Promise.all(promises); + } + + // Clear cache + clearCache(): void { + this.cache.clear(); + } + + // Get cache stats for monitoring + getCacheStats(): { size: number; dirtyCount: number } { + let dirtyCount = 0; + for (const entry of this.cache.values()) { + if (entry.dirty) dirtyCount++; + } + return { + size: this.cache.size, + dirtyCount, + }; + } +} diff --git a/src/services/file-storage.ts b/src/services/file-storage.ts index dce3c4d..de44636 100644 --- a/src/services/file-storage.ts +++ b/src/services/file-storage.ts @@ -13,9 +13,17 @@ export interface IStorage { export class FileStorage implements IStorage { private storagePath = ""; + private initialized = false; constructor() { - this.createCodeBuddyFolder(); + // No async operations in constructor + } + + private async ensureInitialized(): Promise { + if (!this.initialized) { + await this.createCodeBuddyFolder(); + this.initialized = true; + } } async createCodeBuddyFolder() { @@ -69,6 +77,8 @@ export class FileStorage implements IStorage { } async get(key: string): Promise { + await this.ensureInitialized(); + try { const filePath = this.getFilePath(key); if (!fs.existsSync(filePath)) { @@ -83,6 +93,8 @@ export class FileStorage implements IStorage { } async set(key: string, value: T): Promise { + await this.ensureInitialized(); + try { const filePath = this.getFilePath(key); await fs.promises.writeFile( @@ -97,6 +109,8 @@ export class FileStorage implements IStorage { } async delete(key: string): Promise { + await this.ensureInitialized(); + try { const filePath = this.getFilePath(key); if (fs.existsSync(filePath)) { @@ -108,6 +122,8 @@ export class FileStorage implements IStorage { } async has(key: string): Promise { + await this.ensureInitialized(); + const filePath = this.getFilePath(key); return fs.existsSync(filePath); } diff --git a/src/utils/standardized-prompt.ts b/src/utils/standardized-prompt.ts new file mode 100644 index 0000000..b2a9adc --- /dev/null +++ b/src/utils/standardized-prompt.ts @@ -0,0 +1,73 @@ +/** + * Utility for creating standardized, high-quality prompts for LLM interactions + * Ensures consistent, professional prompts across all webview providers + */ + +export class StandardizedPrompt { + /** + * Creates a comprehensive, professional prompt for user input + * @param userMessage The user's original message/request + * @param context Optional project context (file contents, selections, etc.) + * @returns A standardized prompt optimized for LLM performance + */ + static create(userMessage: string, context?: string): string { + const SYSTEM_PROMPT = `You are CodeBuddy, an expert AI programming assistant and code mentor. You excel at understanding developer intent and providing comprehensive, actionable solutions for all coding challenges. + +## Core Capabilities + +### 🎯 **Code Analysis & Understanding** +- **Language Detection**: Automatically identify programming languages and frameworks +- **Intent Recognition**: Understand what the developer is trying to achieve +- **Context Awareness**: Consider surrounding code, project structure, and best practices +- **Error Diagnosis**: Identify bugs, performance issues, and code smells + +### 🚀 **Code Generation & Enhancement** +- **Smart Completion**: Generate code that follows project patterns and conventions +- **Refactoring**: Improve code structure, readability, and maintainability +- **Optimization**: Enhance performance with efficient algorithms and data structures +- **Best Practices**: Apply SOLID principles, design patterns, and industry standards + +### 🔧 **Problem Solving** +- **Debugging**: Step-by-step problem identification and resolution +- **Implementation**: Convert requirements into working code solutions +- **Architecture**: Design scalable and maintainable code structures +- **Testing**: Generate comprehensive test cases and validation strategies + +### 📚 **Education & Mentoring** +- **Explanations**: Clear, educational explanations of concepts and code +- **Examples**: Practical, real-world code samples and use cases +- **Alternatives**: Multiple approaches with pros/cons analysis +- **Learning Path**: Progressive skill development recommendations + +## Response Guidelines + +### 📋 **Format Standards** +- **Code Blocks**: Use proper syntax highlighting and language tags +- **Documentation**: Include clear comments and explanations +- **Structure**: Organize responses with headers, sections, and bullet points +- **Examples**: Provide practical, runnable code examples + +### ⚡ **Quality Principles** +- **Accuracy**: Ensure all code is syntactically correct and functional +- **Completeness**: Address all aspects of the user's request +- **Clarity**: Use clear, professional language accessible to developers +- **Efficiency**: Optimize for both performance and developer productivity + +### 🎨 **Presentation** +- Use emojis strategically for visual organization +- Provide before/after comparisons when applicable +- Include error handling and edge cases +- Suggest testing and validation approaches + +## Context Integration +${context ? `\n**Project Context:**\n${context}\n` : ""} + +**Developer Request:** ${userMessage} + +--- + +**Instructions**: Analyze the request comprehensively and provide a complete, professional solution that addresses all aspects while following modern coding standards and best practices.`; + + return SYSTEM_PROMPT; + } +} diff --git a/src/webview-providers/anthropic.ts b/src/webview-providers/anthropic.ts index 0f46466..16c5b60 100644 --- a/src/webview-providers/anthropic.ts +++ b/src/webview-providers/anthropic.ts @@ -12,6 +12,7 @@ import { getGenerativeAiModel, getXGroKBaseURL, } from "../utils/utils"; +import { StandardizedPrompt } from "../utils/standardized-prompt"; import { BaseWebViewProvider } from "./base"; export class AnthropicWebViewProvider extends BaseWebViewProvider { @@ -39,10 +40,15 @@ export class AnthropicWebViewProvider extends BaseWebViewProvider { "assistant", response, "anthropic", - "agentId", + COMMON.SHARED_CHAT_HISTORY, ); } else { - await this.modelChatHistory("user", response, "anthropic", "agentId"); + await this.modelChatHistory( + "user", + response, + "anthropic", + COMMON.SHARED_CHAT_HISTORY, + ); } return await this.currentWebView?.webview.postMessage({ type, @@ -67,11 +73,14 @@ export class AnthropicWebViewProvider extends BaseWebViewProvider { this.baseUrl = getXGroKBaseURL(); } + // Create standardized prompt for user input + const standardizedPrompt = StandardizedPrompt.create(message, context); + let chatHistory = await this.modelChatHistory( "user", - `${message} \n context: ${context}`, + standardizedPrompt, "anthropic", - "agentId", + COMMON.SHARED_CHAT_HISTORY, ); const chatCompletion = await this.model.messages.create({ @@ -93,7 +102,7 @@ export class AnthropicWebViewProvider extends BaseWebViewProvider { return response; } catch (error) { console.error(error); - Memory.set(COMMON.ANTHROPIC_CHAT_HISTORY, []); + Memory.set(COMMON.SHARED_CHAT_HISTORY, []); vscode.window.showErrorMessage( "Model not responding, please resend your question", ); diff --git a/src/webview-providers/base.ts b/src/webview-providers/base.ts index a32def8..aa7f420 100644 --- a/src/webview-providers/base.ts +++ b/src/webview-providers/base.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import { Orchestrator } from "../agents/orchestrator"; +import { COMMON } from "../application/constant"; import { FolderEntry, IContextInfo, @@ -29,6 +30,7 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { private readonly fileManager: FileManager; private readonly agentService: AgentService; protected readonly chatHistoryManager: ChatHistoryManager; + private workspacePublished = false; // Track if workspace has been published constructor( private readonly _extensionUri: vscode.Uri, @@ -109,6 +111,8 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { this.setupMessageHandler(this.currentWebView); // Get the current workspace files from DB. await this.getFiles(); + // Publish workspace immediately on webview load + await this.publishWorkSpaceOnce(); } private async setWebviewHtml(view: vscode.WebviewView): Promise { @@ -170,6 +174,13 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { } } + private async publishWorkSpaceOnce(): Promise { + if (!this.workspacePublished) { + await this.publishWorkSpace(); + this.workspacePublished = true; + } + } + private UserMessageCounter = 0; private async setupMessageHandler(_view: vscode.WebviewView): Promise { @@ -183,7 +194,9 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { // Check if we should prune history for performance if (this.UserMessageCounter % 10 === 0) { - const stats = await this.getChatHistoryStats("agentId"); + const stats = await this.getChatHistoryStats( + COMMON.SHARED_CHAT_HISTORY, + ); if ( stats.totalMessages > 100 || stats.estimatedTokens > 16000 @@ -192,7 +205,7 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { `High chat history usage detected: ${stats.totalMessages} messages, ${stats.estimatedTokens} tokens`, ); // Optionally trigger manual pruning here - // await this.pruneHistoryManually("agentId", { maxMessages: 50, maxTokens: 8000 }); + // await this.pruneHistoryManually(COMMON.SHARED_CHAT_HISTORY, { maxMessages: 50, maxTokens: 8000 }); } } @@ -200,17 +213,14 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { message.message, message.metaData, ); - if (this.UserMessageCounter === 1) { - await this.publishWorkSpace(); - } if (response) { await this.sendResponse(formatText(response), "bot"); } break; } - // case "webview-ready": - // await this.publishWorkSpace(); - // break; + case "webview-ready": + await this.publishWorkSpaceOnce(); + break; case "upload-file": await this.fileManager.uploadFileHandler(); break; @@ -222,7 +232,9 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { // this.orchestrator.publish("onHistoryUpdated", message); // break; case "clear-history": - await this.chatHistoryManager.clearHistory("agentId"); + await this.chatHistoryManager.clearHistory( + COMMON.SHARED_CHAT_HISTORY, + ); this.orchestrator.publish("onClearHistory", message); break; case "update-user-info": @@ -267,8 +279,19 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { this.logger.debug( `Disposing BaseWebViewProvider with ${this.disposables.length} disposables`, ); - this.disposables.forEach((d) => d.dispose()); - this.disposables.length = 0; // Clear the array + try { + this.disposables.forEach((d) => { + try { + d.dispose(); + } catch (err) { + this.logger.error(`Error disposing of disposable: ${err}`); + } + }); + } catch (error: any) { + this.logger.error(`Error during dispose: ${error.message}`); + } finally { + this.disposables.length = 0; // Clear the array + } } async getContext(files: string[]) { diff --git a/src/webview-providers/deepseek.ts b/src/webview-providers/deepseek.ts index 9609254..c2c2c40 100644 --- a/src/webview-providers/deepseek.ts +++ b/src/webview-providers/deepseek.ts @@ -2,9 +2,10 @@ import * as vscode from "vscode"; import { BaseWebViewProvider } from "./base"; import { COMMON } from "../application/constant"; import { Memory } from "../memory/base"; -import { IMessageInput, Message } from "../llms/message"; +import { IMessageInput } from "../llms/message"; import { DeepseekLLM } from "../llms/deepseek/deepseek"; import { Logger, LogLevel } from "../infrastructure/logger/logger"; +import { StandardizedPrompt } from "../utils/standardized-prompt"; export class DeepseekWebViewProvider extends BaseWebViewProvider { public static readonly viewId = "chatView"; @@ -39,37 +40,27 @@ export class DeepseekWebViewProvider extends BaseWebViewProvider { try { const type = currentChat === "bot" ? "bot-response" : "user-input"; if (currentChat === "bot") { - this.chatHistory.push( - Message.of({ - role: "assistant", - content: response, - }), + await this.modelChatHistory( + "assistant", + response, + "deepseek", + COMMON.SHARED_CHAT_HISTORY, ); } else { - this.chatHistory.push( - Message.of({ - role: "user", - content: response, - }), + await this.modelChatHistory( + "user", + response, + "deepseek", + COMMON.SHARED_CHAT_HISTORY, ); } - - if (this.chatHistory.length === 2) { - const chatHistory = Memory.has(COMMON.DEEPSEEK_CHAT_HISTORY) - ? Memory.get(COMMON.DEEPSEEK_CHAT_HISTORY) - : []; - Memory.set(COMMON.DEEPSEEK_CHAT_HISTORY, [ - ...chatHistory, - ...this.chatHistory, - ]); - } return await this.currentWebView?.webview.postMessage({ type, message: response, }); } catch (error) { this.logger.error("Error sending response", error); - Memory.set(COMMON.DEEPSEEK_CHAT_HISTORY, []); + Memory.set(COMMON.SHARED_CHAT_HISTORY, []); console.error(error); } } @@ -85,23 +76,26 @@ export class DeepseekWebViewProvider extends BaseWebViewProvider { return; } - const userMessage = Message.of({ - role: "user", - content: message, - }); + // Create standardized prompt for user input + const context = + metaData?.context?.length > 0 + ? await this.getContext(metaData.context) + : undefined; + const standardizedPrompt = StandardizedPrompt.create(message, context); - let chatHistory = Memory.has(COMMON.DEEPSEEK_CHAT_HISTORY) - ? Memory.get(COMMON.DEEPSEEK_CHAT_HISTORY) - : [userMessage]; - - chatHistory = [...chatHistory, userMessage]; + // Use shared chat history like other providers + let chatHistory = await this.modelChatHistory( + "user", + standardizedPrompt, + "deepseek", + COMMON.SHARED_CHAT_HISTORY, + ); - Memory.removeItems(COMMON.DEEPSEEK_CHAT_HISTORY); const result = await this.deepseekLLM.generateText(message); return result; } catch (error) { this.logger.error("Error generating response", error); - Memory.set(COMMON.DEEPSEEK_CHAT_HISTORY, []); + Memory.set(COMMON.SHARED_CHAT_HISTORY, []); vscode.window.showErrorMessage( "Model not responding, please resend your question", ); diff --git a/src/webview-providers/gemini.ts b/src/webview-providers/gemini.ts index 2adfa2c..b22bda3 100644 --- a/src/webview-providers/gemini.ts +++ b/src/webview-providers/gemini.ts @@ -4,6 +4,7 @@ import { COMMON } from "../application/constant"; import { GeminiLLM } from "../llms/gemini/gemini"; import { IMessageInput } from "../llms/message"; import { Memory } from "../memory/base"; +import { StandardizedPrompt } from "../utils/standardized-prompt"; import { BaseWebViewProvider } from "./base"; export class GeminiWebViewProvider extends BaseWebViewProvider { @@ -34,16 +35,26 @@ export class GeminiWebViewProvider extends BaseWebViewProvider { try { const type = currentChat === "bot" ? "bot-response" : "user-input"; if (currentChat === "bot") { - await this.modelChatHistory("model", response, "gemini", "agentId"); + await this.modelChatHistory( + "model", + response, + "gemini", + COMMON.SHARED_CHAT_HISTORY, + ); } else { - await this.modelChatHistory("user", response, "gemini", "agentId"); + await this.modelChatHistory( + "user", + response, + "gemini", + COMMON.SHARED_CHAT_HISTORY, + ); } return await this.currentWebView?.webview.postMessage({ type, message: response, }); } catch (error) { - Memory.set(COMMON.GEMINI_CHAT_HISTORY, []); + Memory.set(COMMON.SHARED_CHAT_HISTORY, []); console.error(error); } } @@ -67,21 +78,24 @@ export class GeminiWebViewProvider extends BaseWebViewProvider { return; } + // Create standardized prompt for user input + const enhancedPrompt = StandardizedPrompt.create(message, context); + let chatHistory = await this.modelChatHistory( "user", - `${message} \n context: ${context}`, + enhancedPrompt, "gemini", - "agentId", + COMMON.SHARED_CHAT_HISTORY, ); const chat = this.model.startChat({ history: [...chatHistory], }); - const result = await chat.sendMessage(message); + const result = await chat.sendMessage(enhancedPrompt); const response = result.response; return response.text(); } catch (error) { - Memory.set(COMMON.GEMINI_CHAT_HISTORY, []); + Memory.set(COMMON.SHARED_CHAT_HISTORY, []); vscode.window.showErrorMessage( "Model not responding, please resend your question", ); diff --git a/src/webview-providers/groq.ts b/src/webview-providers/groq.ts index 9dc1f41..87f54ff 100644 --- a/src/webview-providers/groq.ts +++ b/src/webview-providers/groq.ts @@ -4,6 +4,7 @@ import { COMMON, GROQ_CONFIG } from "../application/constant"; import { Memory } from "../memory/base"; import { BaseWebViewProvider } from "./base"; import { IMessageInput, Message } from "../llms/message"; +import { StandardizedPrompt } from "../utils/standardized-prompt"; export class GroqWebViewProvider extends BaseWebViewProvider { chatHistory: IMessageInput[] = []; @@ -45,9 +46,19 @@ export class GroqWebViewProvider extends BaseWebViewProvider { try { const type = participant === "bot" ? "bot-response" : "user-input"; if (participant === "bot") { - await this.modelChatHistory("system", response, "groq", "agentId"); + await this.modelChatHistory( + "system", + response, + "groq", + COMMON.SHARED_CHAT_HISTORY, + ); } else { - await this.modelChatHistory("user", response, "groq", "agentId"); + await this.modelChatHistory( + "user", + response, + "groq", + COMMON.SHARED_CHAT_HISTORY, + ); } return await this.currentWebView?.webview.postMessage({ type, @@ -69,11 +80,14 @@ export class GroqWebViewProvider extends BaseWebViewProvider { } const { temperature, max_tokens, top_p, stop } = GROQ_CONFIG; + // Create standardized prompt for user input + const standardizedPrompt = StandardizedPrompt.create(message, context); + let chatHistory = await this.modelChatHistory( "user", - `${message} \n context: ${context}`, + standardizedPrompt, "groq", - "agentId", + COMMON.SHARED_CHAT_HISTORY, ); const chatCompletion = this.model.chat.completions.create({ @@ -89,7 +103,7 @@ export class GroqWebViewProvider extends BaseWebViewProvider { return response ?? undefined; } catch (error) { console.error(error); - Memory.set(COMMON.GROQ_CHAT_HISTORY, []); + Memory.set(COMMON.SHARED_CHAT_HISTORY, []); vscode.window.showErrorMessage( "Model not responding, please resend your question", ); diff --git a/src/webview/chat_html.ts b/src/webview/chat_html.ts index 12d5d24..26e6f11 100644 --- a/src/webview/chat_html.ts +++ b/src/webview/chat_html.ts @@ -6,7 +6,8 @@ import { Uri, Webview } from "vscode"; // and ensure script integrity when using Content Security Policy (CSP) function getNonce() { let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const possible = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (let i = 0; i < 32; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } @@ -16,8 +17,18 @@ function getNonce() { const nonce = getNonce(); export const chartComponent = (webview: Webview, extensionUri: Uri) => { - const stylesUri = getUri(webview, extensionUri, ["dist", "webview", "assets", "index.css"]); - const scriptUri = getUri(webview, extensionUri, ["dist", "webview", "assets", "index.js"]); + const stylesUri = getUri(webview, extensionUri, [ + "dist", + "webview", + "assets", + "index.css", + ]); + const scriptUri = getUri(webview, extensionUri, [ + "dist", + "webview", + "assets", + "index.js", + ]); return ` diff --git a/webviewUi/src/components/webview.tsx b/webviewUi/src/components/webview.tsx index 6f90dae..7acd30a 100644 --- a/webviewUi/src/components/webview.tsx +++ b/webviewUi/src/components/webview.tsx @@ -64,6 +64,14 @@ export const WebviewUI = () => { const [darkMode, setDarkMode] = useState(false); const nameInputRef = useRef(null); + // Signal webview is ready on mount + useEffect(() => { + vsCode.postMessage({ + command: "webview-ready", + message: "Webview is ready", + }); + }, []); + useEffect(() => { const messageHandler = (event: any) => { const message = event.data; diff --git a/webviewUi/src/index.css b/webviewUi/src/index.css index 2c0d55d..a501424 100644 --- a/webviewUi/src/index.css +++ b/webviewUi/src/index.css @@ -1,5 +1,7 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + :root { - font-family: "JetBrains Mono", SF Mono, "Geist Mono", "Fira Code", "Fira Mono", "Menlo", "Consolas", "DejaVu Sans Mono", monospace; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; @@ -11,6 +13,34 @@ background-color: #16161e; } +/* Improved typography */ +body { + font-feature-settings: "cv02", "cv03", "cv04", "cv11"; + font-variant-ligatures: common-ligatures; +} + +/* Better headings */ +h1, +h2, +h3, +h4, +h5, +h6 { + color: #7aa2f7; + margin-top: 2rem; + margin-bottom: 1rem; + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1.25; +} + +/* Better paragraph text */ +p { + margin-bottom: 1.2rem; + color: #c0caf5; + line-height: 1.65; +} + .container { @@ -43,19 +73,16 @@ a { font-weight: 500; - color: #646cff; - text-decoration: inherit; + color: #7dcfff; + text-decoration: none; + transition: color 0.2s ease; } a:hover { - color: #535bf2; + color: #bb9af7; + text-decoration: underline; } -/* h1 { - font-size: 3.2em; - line-height: 1.1; -} */ - button { border-radius: 8px; border: 1px solid transparent; @@ -135,7 +162,7 @@ vscode-text-area { border: 2px solid var(--vscode-editor-background); border-radius: 6px; padding: 8px 12px; - font-family: "JetBrains Mono", SF Mono, "Geist Mono", "Fira Code", "Fira Mono", "Menlo", "Consolas", "DejaVu Sans Mono", monospace; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; font-size: 14px; line-height: 1.6; min-height: 36px; @@ -331,7 +358,7 @@ code { } .doc-content { - font-family: "JetBrains Mono", SF Mono, "Geist Mono", "Fira Code", "Fira Mono", "Menlo", "Consolas", "DejaVu Sans Mono", monospace; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; color: var(--vscode-editor-foreground); padding: 16px; text-align: left; @@ -413,38 +440,6 @@ code { } -/* Headings */ -h1, -h2, -h3, -h4, -h5, -h6 { - color: #7aa2f7; - margin-top: 2rem; - margin-bottom: 1rem; - font-weight: 400; -} - -/* Paragraphs and text */ -p { - margin-bottom: 1.2rem; - color: #c0caf5; -} - -/* Links */ -a { - color: #7dcfff; - text-decoration: none; - transition: color 0.2s ease; -} - -a:hover { - color: #bb9af7; - text-decoration: underline; -} - - /* Important notes and warnings */ blockquote { border-left: 4px solid #f7768e; @@ -546,7 +541,7 @@ hr { .url-link { color: #007bff; font-size: 16px; - font-family: "JetBrains Mono", SF Mono, "Geist Mono", "Fira Code", "Fira Mono", "Menlo", "Consolas", "DejaVu Sans Mono", monospace; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; line-height: 1.5; text-decoration: none; word-wrap: break-word; @@ -585,7 +580,7 @@ hr { border: 1px solid var(--vscode-panel-border); border-radius: 6px; margin: 16px 0; - font-family: "JetBrains Mono", SF Mono, "Geist Mono", "Fira Code", "Fira Mono", "Menlo", "Consolas", "DejaVu Sans Mono", monospace; + font-family: "JetBrains Mono", "Fira Code", "Fira Mono", "Menlo", "Consolas", "DejaVu Sans Mono", monospace; } .individual-code-header {