diff --git a/src/application/constant.ts b/src/application/constant.ts index a8d89e1..25e69dc 100644 --- a/src/application/constant.ts +++ b/src/application/constant.ts @@ -28,7 +28,7 @@ export enum COMMON { } export const GROQ_CONFIG = { temperature: 0.1, - max_tokens: 60000, + max_tokens: 8192, top_p: 1, stream: false, stop: ["thanks"], diff --git a/src/commands/handler.ts b/src/commands/handler.ts index 7a3d7d1..92ccc73 100644 --- a/src/commands/handler.ts +++ b/src/commands/handler.ts @@ -39,7 +39,6 @@ export abstract class CodeCommandHandler implements ICodeCommandHandler { private readonly xGrokApiKey: string; private readonly xGrokModel: string; private readonly logger: Logger; - // Todo Need to refactor. Only one instance of a model can be created at a time. Therefore no need to retrieve all model information, only retrieve the required model within the application constructor( private readonly action: string, _context: vscode.ExtensionContext, diff --git a/src/emitter/interface.ts b/src/emitter/interface.ts index ce60aa0..a285999 100644 --- a/src/emitter/interface.ts +++ b/src/emitter/interface.ts @@ -13,7 +13,8 @@ type AgentEventKeys = | "onFilesRetrieved" | "onStrategizing" | "onModelChange" - | "onModelChangeSuccess"; + | "onModelChangeSuccess" + | "onHistoryUpdated"; export type IAgentEventMap = Record; diff --git a/src/emitter/publisher.ts b/src/emitter/publisher.ts index 140cf91..242e18d 100644 --- a/src/emitter/publisher.ts +++ b/src/emitter/publisher.ts @@ -28,6 +28,8 @@ export class EventEmitter extends BaseEmitter> { onModelChangeSuccess: vscode.Event = this.createEvent( "onModelChangeSuccess", ); + onHistoryUpdated: vscode.Event = + this.createEvent("onHistoryUpdated"); /** * Emits a generic event with specified status, message, and optional data. diff --git a/src/extension.ts b/src/extension.ts index 3c832e7..c15be4b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -72,7 +72,7 @@ async function connectToDatabase(context: vscode.ExtensionContext) { async function createFileDB(context: vscode.ExtensionContext) { try { - const fileUploader = new FileManager(context, "patterns"); + const fileUploader = new FileManager(context, "database"); const files = await fileUploader.getFiles(); if (!files?.find((file) => file.includes("dev.db"))) { await fileUploader.createFile("dev.db"); @@ -96,7 +96,6 @@ export async function activate(context: vscode.ExtensionContext) { logger.info(`Logged into GitHub as ${session?.account.label}`); Memory.getInstance(); - // TODO This is broken. Need to Fix // const index = CodeIndexingService.createInstance(); // Get each of the folders and call the next line for each // const result = await index.buildFunctionStructureMap(); @@ -235,6 +234,7 @@ export async function activate(context: vscode.ExtensionContext) { selectedGenerativeAiModel, apiKey, apiModel, + true, ); } context.subscriptions.push( diff --git a/src/infrastructure/logger/logger.ts b/src/infrastructure/logger/logger.ts index a634c53..7beb879 100644 --- a/src/infrastructure/logger/logger.ts +++ b/src/infrastructure/logger/logger.ts @@ -84,9 +84,7 @@ export class Logger { Logger.config.filePath = path.join(logDir, `codebuddy-${date}.log`); } } - if (!Logger.outputChannel) { - Logger.outputChannel = vscode.window.createOutputChannel("CodeBuddy"); - } + Logger.outputChannel ??= vscode.window.createOutputChannel("CodeBuddy"); Logger.telemetry = telemetry; Logger.sessionId = Logger.generateId(); Logger.setTraceId(Logger.generateId()); diff --git a/src/llms/gemini/gemini.ts b/src/llms/gemini/gemini.ts index 574e3ac..1fcc0a3 100644 --- a/src/llms/gemini/gemini.ts +++ b/src/llms/gemini/gemini.ts @@ -61,8 +61,8 @@ export class GeminiLLM private intializeDisposable(): void { this.disposables.push( vscode.workspace.onDidChangeConfiguration(() => - this.handleConfigurationChange() - ) + this.handleConfigurationChange(), + ), ); } @@ -72,9 +72,7 @@ export class GeminiLLM } static getInstance(config: ILlmConfig) { - if (!GeminiLLM.instance) { - GeminiLLM.instance = new GeminiLLM(config); - } + GeminiLLM.instance ??= new GeminiLLM(config); return GeminiLLM.instance; } @@ -92,7 +90,7 @@ export class GeminiLLM public async generateText( prompt: string, - instruction?: string + instruction?: string, ): Promise { try { const model = this.getModel(); @@ -142,7 +140,7 @@ export class GeminiLLM } async generateContentWithTools( - userInput: string + userInput: string, ): Promise { try { await this.buildChatHistory( @@ -150,7 +148,7 @@ export class GeminiLLM undefined, undefined, undefined, - true + true, ); const prompt = createPrompt(userInput); const contents = Memory.get(COMMON.GEMINI_CHAT_HISTORY) as Content[]; @@ -185,7 +183,7 @@ export class GeminiLLM */ private async processToolCalls( toolCalls: FunctionCall[], - userInput: string + userInput: string, ): Promise { let finalResult: string | undefined = undefined; try { @@ -221,7 +219,7 @@ export class GeminiLLM functionCall.name, functionResult, undefined, - false + false, ); const snapShot = this.createSnapShot({ @@ -239,12 +237,12 @@ export class GeminiLLM const retry = await vscode.window.showErrorMessage( `Function call failed: ${error.message}. Retry or abort?`, "Retry", - "Abort" + "Abort", ); if (retry === "Retry") { finalResult = await this.fallBackToGroq( - `User Input: ${this.userQuery} \n Plans: ${userInput}Write production ready code to demonstrate your solution` + `User Input: ${this.userQuery} \n Plans: ${userInput}Write production ready code to demonstrate your solution`, ); } else { finalResult = `Function call error: ${error.message}. Falling back to last response.`; @@ -256,13 +254,13 @@ export class GeminiLLM } catch (error) { console.error("Error processing tool calls", error); finalResult = await this.fallBackToGroq( - `User Input: ${this.userQuery} \n Plans: ${userInput}Write production ready code to demonstrate your solution` + `User Input: ${this.userQuery} \n Plans: ${userInput}Write production ready code to demonstrate your solution`, ); } } async processUserQuery( - userInput: string + userInput: string, ): Promise { let finalResult: string | GenerateContentResult | undefined; let userQuery = userInput; @@ -287,8 +285,8 @@ export class GeminiLLM const timeoutPromise = new Promise((_, reject) => setTimeout( () => reject(new Error("TImeout Exceeded")), - this.timeOutMs - ) + this.timeOutMs, + ), ); const responsePromise = await this.generateContentWithTools(userQuery); const result = (await Promise.race([ @@ -321,7 +319,7 @@ export class GeminiLLM finalResult = await this.groqLLM.generateText(userInput); if (finalResult) { finalResult = await this.fallBackToGroq( - `User Input: ${this.userQuery} \n Plans: ${userInput} Write production ready code to demonstrate your solution` + `User Input: ${this.userQuery} \n Plans: ${userInput} Write production ready code to demonstrate your solution`, ); return finalResult; } @@ -329,7 +327,7 @@ export class GeminiLLM this.lastFunctionCalls.add(currentCallSignatures); if (this.lastFunctionCalls.size > 10) { this.lastFunctionCalls = new Set( - [...this.lastFunctionCalls].slice(-10) + [...this.lastFunctionCalls].slice(-10), ); } if (toolCalls && toolCalls.length > 0) { @@ -366,7 +364,7 @@ export class GeminiLLM if (snapshot?.length > 0) { Memory.removeItems( COMMON.GEMINI_SNAPSHOT, - Memory.get(COMMON.GEMINI_SNAPSHOT).length + Memory.get(COMMON.GEMINI_SNAPSHOT).length, ); } this.orchestrator.publish("onQuery", String(finalResult)); @@ -378,7 +376,7 @@ export class GeminiLLM // ); console.log("Error processing user query", error); finalResult = await this.fallBackToGroq( - `User Input: ${this.userQuery} \n Plans: ${userInput}Write production ready code to demonstrate your solution` + `User Input: ${this.userQuery} \n Plans: ${userInput}Write production ready code to demonstrate your solution`, ); console.log("Model not responding at this time, please try again", error); } @@ -386,7 +384,7 @@ export class GeminiLLM private async handleSingleFunctionCall( functionCall: FunctionCall, - attempt: number = 0 + attempt: number = 0, ): Promise { const MAX_RETRIES = 3; const args = functionCall.args as Record; @@ -408,7 +406,7 @@ export class GeminiLLM if (attempt < MAX_RETRIES) { console.warn( `Retry attempt ${attempt + 1} for function ${name}`, - JSON.stringify({ error, args }) + JSON.stringify({ error, args }), ); return this.handleSingleFunctionCall(functionCall, attempt + 1); } @@ -431,7 +429,7 @@ export class GeminiLLM functionCall?: any, functionResponse?: any, chat?: ChatSession, - isInitialQuery: boolean = false + isInitialQuery: boolean = false, ): Promise { // Check if it makes sense to kind of seperate agent and Edit Mode memory, when switching. let chatHistory: any = Memory.get(COMMON.GEMINI_CHAT_HISTORY) || []; @@ -457,17 +455,17 @@ export class GeminiLLM Message.of({ role: "model", parts: [{ functionCall }], - }) + }), ); const observationResult = await chat.sendMessage( - `Tool result: ${JSON.stringify(functionResponse)}` + `Tool result: ${JSON.stringify(functionResponse)}`, ); chatHistory.push( Message.of({ role: "user", parts: [{ text: observationResult.response.text() }], - }) + }), ); } if (chatHistory.length > 50) chatHistory = chatHistory.slice(-50); diff --git a/src/services/agent-state.ts b/src/services/agent-state.ts index 6c82535..534add2 100644 --- a/src/services/agent-state.ts +++ b/src/services/agent-state.ts @@ -5,7 +5,7 @@ import { FileStorage, IStorage } from "./file-storage"; export class AgentService { private static instance: AgentService; - private storage: IStorage; + private readonly storage: IStorage; private constructor(storage: IStorage) { this.storage = storage; diff --git a/src/services/file-storage.ts b/src/services/file-storage.ts index 206164d..90f0ef2 100644 --- a/src/services/file-storage.ts +++ b/src/services/file-storage.ts @@ -12,11 +12,11 @@ export interface IStorage { } export class FileStorage implements IStorage { - private storagePath: string; + private readonly storagePath: string; constructor() { this.storagePath = path.join( - vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || "", + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "", ".codebuddy", ); if (!fs.existsSync(this.storagePath)) { diff --git a/src/services/url-reranker.ts b/src/services/url-reranker.ts index df4f3d7..e799ae2 100644 --- a/src/services/url-reranker.ts +++ b/src/services/url-reranker.ts @@ -66,7 +66,7 @@ export class UrlReranker { if (!title) return 0; const matches = UrlReranker.KEYWORDS.filter((keyword) => - title.toLowerCase().includes(keyword.toLowerCase()) + title.toLowerCase().includes(keyword.toLowerCase()), ); return matches.length / UrlReranker.KEYWORDS.length; @@ -97,7 +97,7 @@ export class UrlReranker { const codeBlockCount = this.countCodeBlocks(metadata.content); const hasAdequateExplanation = this.hasAdequateExplanation( - metadata.content + metadata.content, ); return codeBlockCount + (hasAdequateExplanation ? 1 : 0); @@ -130,7 +130,7 @@ export class UrlReranker { */ calculateFinalScore(metadata: IPageMetada): number { const titleRelevanceScore = this.calculateTitleRelevanceScore( - metadata.title ?? "" + metadata.title ?? "", ); const reputationScore = this.calculateSourceReputationScore(metadata); const contentQualityScore = this.calculateContentQualityScore(metadata); diff --git a/src/webview-providers/anthropic.ts b/src/webview-providers/anthropic.ts index 37a230d..6354168 100644 --- a/src/webview-providers/anthropic.ts +++ b/src/webview-providers/anthropic.ts @@ -68,13 +68,23 @@ export class AnthropicWebViewProvider extends BaseWebViewProvider { } } - async generateResponse(message: string): Promise { + async generateResponse( + message: string, + metaData?: any, + ): Promise { try { + let context: string | undefined; + if (metaData?.context.length > 0) { + context = await this.getContext(metaData.context); + } const { max_tokens } = GROQ_CONFIG; if (getGenerativeAiModel() === generativeAiModels.GROK) { this.baseUrl = getXGroKBaseURL(); } - const userMessage = Message.of({ role: "user", content: message }); + const userMessage = Message.of({ + role: "user", + content: `${message} \n context: ${context}`, + }); let chatHistory = Memory.has(COMMON.ANTHROPIC_CHAT_HISTORY) ? Memory.get(COMMON.ANTHROPIC_CHAT_HISTORY) : [userMessage]; diff --git a/src/webview-providers/base.ts b/src/webview-providers/base.ts index d2a32c6..8ee8313 100644 --- a/src/webview-providers/base.ts +++ b/src/webview-providers/base.ts @@ -136,17 +136,10 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { let response: any; switch (message.command) { case "user-input": - if (message.metaData.mode === "Agent") { - response = await this.generateResponse( - message.message, - message.metaData, - ); - } else { - response = await this.generateResponse( - message.message, - message.metaData, - ); - } + response = await this.generateResponse( + message.message, + message.metaData, + ); if (response) { await this.sendResponse(formatText(response), "bot"); } @@ -161,12 +154,9 @@ export abstract class BaseWebViewProvider implements vscode.Disposable { await this.orchestrator.publish("onModelChange", message); break; //Publish an event instead to prevent cyclic dependendency - // case "chat-history-import": - // await this.agentService.saveChatHistory( - // WebViewProviderManager.AgentId, - // JSON.parse(message.message), - // ); - // break; + case "messages-updated": + this.orchestrator.publish("onHistoryUpdated", message); + break; default: throw new Error("Unknown command"); } diff --git a/src/webview-providers/groq.ts b/src/webview-providers/groq.ts index e203bf7..5eb3935 100644 --- a/src/webview-providers/groq.ts +++ b/src/webview-providers/groq.ts @@ -79,10 +79,20 @@ export class GroqWebViewProvider extends BaseWebViewProvider { } } - async generateResponse(message: string): Promise { + async generateResponse( + message: string, + metaData?: any, + ): Promise { try { + let context: string | undefined; + if (metaData?.context.length > 0) { + context = await this.getContext(metaData.context); + } const { temperature, max_tokens, top_p, stop } = GROQ_CONFIG; - const userMessage = Message.of({ role: "user", content: message }); + const userMessage = Message.of({ + role: "user", + content: `${message} \n context: ${context}`, + }); let chatHistory = Memory.has(COMMON.GROQ_CHAT_HISTORY) ? Memory.get(COMMON.GROQ_CHAT_HISTORY) : [userMessage]; diff --git a/src/webview-providers/manager.ts b/src/webview-providers/manager.ts index 6f46eb0..8ce3858 100644 --- a/src/webview-providers/manager.ts +++ b/src/webview-providers/manager.ts @@ -4,7 +4,7 @@ import { generativeAiModels } from "../application/constant"; import { IEventPayload } from "../emitter/interface"; import { AgentService } from "../services/agent-state"; import { Logger } from "../services/telemetry"; -import { getAPIKeyAndModel, getConfigValue } from "../utils/utils"; +import { getAPIKeyAndModel } from "../utils/utils"; import { AnthropicWebViewProvider } from "./anthropic"; import { BaseWebViewProvider } from "./base"; import { DeepseekWebViewProvider } from "./deepseek"; @@ -25,20 +25,24 @@ export class WebViewProviderManager implements vscode.Disposable { > = new Map(); private webviewView: vscode.WebviewView | undefined; private disposables: vscode.Disposable[] = []; - private viewProvider: vscode.WebviewViewProvider | undefined; private webviewViewProvider: vscode.WebviewViewProvider | undefined; protected readonly orchestrator: Orchestrator; private readonly agentService: AgentService; static AgentId = "agentId"; // TODO This is hardcoded for now,in upcoming versions, requests will be tagged to respective agents. private readonly logger = new Logger(WebViewProviderManager.name); - private constructor(private extensionContext: vscode.ExtensionContext) { + private constructor( + private readonly extensionContext: vscode.ExtensionContext, + ) { this.orchestrator = Orchestrator.getInstance(); this.agentService = AgentService.getInstance(); this.registerProviders(); this.disposables.push( this.orchestrator.onModelChange(this.handleModelChange.bind(this)), ); + this.disposables.push( + this.orchestrator.onHistoryUpdated(this.handleHistoryUpdate.bind(this)), + ); } public static getInstance( @@ -90,15 +94,14 @@ export class WebViewProviderManager implements vscode.Disposable { } } - // NOTE. This could be better off as modelName instead of modelType, once we are focusing on specific Models private createProvider( - modelType: string, + modelName: string, apiKey: string, model: string, ): BaseWebViewProvider | undefined { - const providerClass = this.providerRegistry.get(modelType); + const providerClass = this.providerRegistry.get(modelName); if (!providerClass) { - this.logger.warn(`Provider for model type ${modelType} not found`); + this.logger.warn(`Provider for model type ${modelName} not found`); return; } return new providerClass( @@ -110,12 +113,13 @@ export class WebViewProviderManager implements vscode.Disposable { } private async switchProvider( - modelType: string, + modelName: string, apiKey: string, model: string, + onload: boolean, ): Promise { try { - const newProvider = this.createProvider(modelType, apiKey, model); + const newProvider = this.createProvider(modelName, apiKey, model); if (!newProvider) { return; } @@ -127,7 +131,7 @@ export class WebViewProviderManager implements vscode.Disposable { if (this.webviewView) { await this.currentProvider.resolveWebviewView(this.webviewView); } - if (chatHistory?.length > 0) { + if (chatHistory.messages?.length > 0 && onload) { await this.restoreChatHistory(); } const webviewProviderDisposable = this.registerWebViewProvider(); @@ -139,7 +143,7 @@ export class WebViewProviderManager implements vscode.Disposable { "onModelChangeSuccess", JSON.stringify({ success: true, - modelType, + modelName, }), ); } catch (error: any) { @@ -148,7 +152,7 @@ export class WebViewProviderManager implements vscode.Disposable { "onModelChangeSuccess", JSON.stringify({ success: false, - modelType, + modelName, }), ); throw new Error(error); @@ -156,11 +160,12 @@ export class WebViewProviderManager implements vscode.Disposable { } async initializeProvider( - modelType: string, + modelName: string, apiKey: string, model: string, + onload: boolean, ): Promise { - await this.switchProvider(modelType, apiKey, model); + await this.switchProvider(modelName, apiKey, model, onload); } private async handleModelChange(event: IEventPayload): Promise { @@ -179,14 +184,14 @@ export class WebViewProviderManager implements vscode.Disposable { if (!apiKey) { this.logger.warn(`${modelName} APIkey is required`); } - await this.switchProvider(modelName, apiKey, model); + await this.switchProvider(modelName, apiKey, model, false); } catch (error: any) { this.logger.error("Error handling model change", error); throw new Error(error.message); } } - private async getCurrentHistory(): Promise { + private async getCurrentHistory(): Promise { const history = await this.agentService.getChatHistory( WebViewProviderManager.AgentId, ); @@ -195,10 +200,13 @@ export class WebViewProviderManager implements vscode.Disposable { private async restoreChatHistory() { const history = await this.getCurrentHistory(); - await this.webviewView?.webview.postMessage({ - type: "chat-history-export", - message: JSON.stringify(history), - }); + setTimeout(async () => { + const lastTenMessages = history.messages.slice(-10); + await this.webviewView?.webview.postMessage({ + type: "chat-history", + message: JSON.stringify(lastTenMessages), + }); + }, 6000); } async setCurrentHistory(data: any[]): Promise { @@ -208,6 +216,12 @@ export class WebViewProviderManager implements vscode.Disposable { ); } + async handleHistoryUpdate({ type, message }: IEventPayload) { + if (message.command === "messages-updated" && message.messages?.length) { + await this.setCurrentHistory(message); + } + } + getCurrentProvider(): BaseWebViewProvider | undefined { return this.currentProvider; } diff --git a/webviewUi/src/components/webview.tsx b/webviewUi/src/components/webview.tsx index cf9eef0..dbe1a79 100644 --- a/webviewUi/src/components/webview.tsx +++ b/webviewUi/src/components/webview.tsx @@ -12,12 +12,7 @@ export interface ExtensionMessage { payload: any; } -import { - VSCodeTextArea, - VSCodePanels, - VSCodePanelTab, - VSCodePanelView, -} from "@vscode/webview-ui-toolkit/react"; +import { VSCodeTextArea, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react"; import type hljs from "highlight.js"; import { useEffect, useState } from "react"; import { codeBuddyMode, modelOptions } from "../constants/constant"; @@ -64,7 +59,6 @@ export const WebviewUI = () => { const message = event.data; switch (message.type) { case "bot-response": - setIsBotLoading(false); setMessages((prevMessages) => [ ...(prevMessages || []), { @@ -76,6 +70,15 @@ export const WebviewUI = () => { break; case "bootstrap": setFolders(message); + break; + case "chat-history": + try { + setMessages((prevMessages) => [...JSON.parse(message.message), ...(prevMessages || [])]); + } catch (error: any) { + console.log(error); + throw new Error(error.message); + } + break; case "error": console.error("Extension error", message.payload); @@ -89,11 +92,19 @@ export const WebviewUI = () => { }; window.addEventListener("message", messageHandler); highlightCodeBlocks(hljsApi, messages); + setIsBotLoading(false); return () => { window.removeEventListener("message", messageHandler); }; }, [messages]); + useEffect(() => { + vsCode.postMessage({ + command: "messages-updated", + messages, + }); + }, [messages]); + const handleContextChange = (value: string) => { setSelectedContext(value); }; @@ -129,8 +140,6 @@ export const WebviewUI = () => { }; const handleSend = () => { - // TODO Compare the data to be sent to the data recieved. - // TODO Since the folders will come through the parent, you can filter the values with the folders and files if (!userInput.trim()) return; setMessages((previousMessages) => [ @@ -179,10 +188,7 @@ export const WebviewUI = () => { OTHERS - +
@@ -190,11 +196,7 @@ export const WebviewUI = () => { msg.type === "bot" ? ( ) : ( - + ) )} {isBotLoading && } @@ -203,9 +205,9 @@ export const WebviewUI = () => {
- 1 - 2 - 3 + In Dev + In Dev + In Dev
{ Active workspace: - {selectedContext.includes(activeEditor) - ? "" - : `${activeEditor}`} - - - {Array.from( - new Set(selectedContext.split("@").join(", ").split(", ")) - ).join(" ")} + {selectedContext.includes(activeEditor) ? "" : `${activeEditor}`} + {Array.from(new Set(selectedContext.split("@").join(", ").split(", "))).join(" ")}
- + {
- + .codicon { display: flex; gap: 16px; padding: 20px; - background-color: var(--vscode-editor-background); + /* background-color: var(--vscode-editor-background); */ border-radius: 8px; max-width: 800px; margin-bottom: 15px; @@ -555,9 +555,7 @@ hr { } .url-grid-container { - display: grid; - grid-template-columns: repeat(3, 1fr); - /* Exactly 3 cards per row */ + display: inline; gap: 20px; padding: 16px; width: 100%; @@ -755,5 +753,4 @@ hr { .vscodePanels { height: 700px; -} - +} \ No newline at end of file