diff --git a/CHANGELOG.md b/CHANGELOG.md index d4051b3a..0e79be2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ All notable changes to the "fluttergpt" extension will be documented in this fil Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. -## [0.2.1] +## [0.2.2] -- Major improvement in chat user experience. -- Feature to provide workspace as context. -- Minor bug fixes. \ No newline at end of file +- Major improvement in workspace chat user experience. +- Feature to provide workspace as context in chat history. +- Added a clear chat button. \ No newline at end of file diff --git a/README.md b/README.md index 8ac226f0..62149886 100644 --- a/README.md +++ b/README.md @@ -146,10 +146,10 @@ To get a sense of direction of where we're heading, please check out our [Roadma This is the beta version and can be unstable. Please check our [issues board](https://github.com/Welltested-AI/fluttergpt/issues) for any known issues. -## Release Notes: 0.2.1 -- Major improvement in chat user experience. -- Feature to provide workspace as context. -- Minor bug fixes. +## Release Notes: 0.2.2 +- Major improvement in workspace chat user experience. +- Feature to provide workspace as context in chat history. +- Added a clear chat button. ## License diff --git a/media/chat/chat.html b/media/chat/chat.html index be13dd50..27c3ef1a 100644 --- a/media/chat/chat.html +++ b/media/chat/chat.html @@ -7,11 +7,8 @@ - - - + @@ -26,17 +23,30 @@
- +
+
+
+ +
- + + + \ No newline at end of file diff --git a/media/chat/css/chatpage.css b/media/chat/css/chatpage.css index 02e01d02..26ee60fe 100644 --- a/media/chat/css/chatpage.css +++ b/media/chat/css/chatpage.css @@ -45,18 +45,18 @@ body { position: absolute; align-items: center; justify-content: center; - top: 86%; + top: 86%; left: 50%; transform: translateX(-50%); - background-color: #ffcc00; + background-color: #ffcc00; color: #ffffff; text-align: center; padding: 10px; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); - width: calc(100% - 18px); - z-index: 100; - animation: fadeInOut 5s ease; + width: calc(100% - 18px); + z-index: 100; + animation: fadeInOut 5s ease; } #snackbar i { @@ -91,12 +91,12 @@ body { top: 90%; left: 50%; transform: translateX(-50%) translateY(100%); - background-color: #ffcc00; + background-color: #ffcc00; color: #ffffff; padding: 10px; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); - width: calc(100% - 18px); + width: calc(100% - 18px); z-index: 100; animation: slideUp 0.5s ease-in-out forwards; } @@ -109,39 +109,67 @@ body { margin-left: 8px; } -.loader { - width: 8px; - height: 8px; - left: 16px; - border-radius: 50%; - background-color: var(--vscode-progressBar-background); - box-shadow: 32px 0 var(--vscode-progressBar-background), -32px 0 var(--vscode-progressBar-background); - position: relative; - animation: flash 0.5s ease-out infinite alternate; +.file-path { + font-family: 'Courier New', Courier, monospace; + color: var(--vscode-editor-foreground); } -@keyframes flash { - 0% { - background-color: #FFF2; - box-shadow: 16px 0 #FFF2, -16px 0 var(--vscode-progressBar-background); - } +.dot-flashing { + position: relative; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: var(--vscode-progressBar-background); + color: var(--vscode-progressBar-background); + animation: dot-flashing 1s infinite linear alternate; + animation-delay: 0.5s; + display: none; + } - 50% { - background-color: var(--vscode-progressBar-background); - box-shadow: 16px 0 #FFF2, -16px 0 #FFF2; - } + .dot-flashing::before, .dot-flashing::after { + content: ""; + display: inline-block; + position: absolute; + top: 0; + } - 100% { - background-color: #FFF2; - box-shadow: 16px 0 var(--vscode-progressBar-background), -16px 0 #FFF2; + .dot-flashing::before { + left: -15px; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: var(--vscode-progressBar-background); + color: var(--vscode-progressBar-background); + animation: dot-flashing 1s infinite alternate; + animation-delay: 0s; + } + + .dot-flashing::after { + left: 15px; + width: 10px; + height: 10px; + border-radius: 5px; + background-color: var(--vscode-progressBar-background); + color: var(--vscode-progressBar-background); + animation: dot-flashing 1s infinite alternate; + animation-delay: 1s; + } + + @keyframes dot-flashing { + 0% { + background-color: var(--vscode-progressBar-background); } -} + 50%, 100% { + background-color: var(--vscode-editor-foreground); + } + } @keyframes slideUp { from { opacity: 0; transform: translateX(-50%) translateY(100%); } + to { opacity: 1; transform: translateX(-50%) translateY(0); @@ -149,8 +177,16 @@ body { } @keyframes fadeInOut { - 0%, 100% { opacity: 0; } - 10%, 90% { opacity: 1; } + + 0%, + 100% { + opacity: 0; + } + + 10%, + 90% { + opacity: 1; + } } .message { @@ -200,7 +236,13 @@ body { } #dynamic-messages { -word-break: break-word; + word-break: break-word; +} + +#workspace-loader { + background-color: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); + fill: var(--vscode-editor-foreground); } .user-gemini-pro p { @@ -246,7 +288,8 @@ ul li::before { margin-right: 5px; } -ol, ul { +ol, +ul { padding-left: 11px; } @@ -256,4 +299,35 @@ ol { ul { list-style-type: disc; +} + +.typing-loader { + width: 4px; + height: 4px; + border-radius: 50%; + animation: loading 1s linear infinite alternate; + margin-bottom: 4px; +} + +@keyframes loading { + 0% { + background-color: var(--vscode-progressBar-background); + box-shadow: + 8px 0px 0px 0px var(--vscode-editor-foreground), + 16px 0px 0px 0px var(--vscode-editor-foreground); + } + + 25% { + background-color: var(--vscode-editor-foreground); + box-shadow: + 8px 0px 0px 0px var(--vscode-progressBar-background), + 16px 0px 0px 0px var(--vscode-editor-foreground); + } + + 75% { + background-color: var(--vscode-editor-foreground); + box-shadow: + 8px 0px 0px 0px var(--vscode-editor-foreground), + 16px 0px 0px 0px var(--vscode-progressBar-background); + } } \ No newline at end of file diff --git a/media/chat/scripts/main.js b/media/chat/scripts/main.js index 66c5c8dd..b03073d1 100644 --- a/media/chat/scripts/main.js +++ b/media/chat/scripts/main.js @@ -15,6 +15,30 @@ const mergeIcon = ` + + + + + + + + + + + + + + + + + + + + + +`; + const activityBarBackground = getComputedStyle(document.documentElement).getPropertyValue("--vscode-activityBar-background"); const activityBarForeground = getComputedStyle(document.documentElement).getPropertyValue("--vscode-activityBar-foreground"); @@ -242,11 +266,13 @@ class Mentionify { // Define an empty array, which will be loaded through the displayMessages function let conversationHistory = []; let loadingIndicator = document.getElementById('loader'); + let workspaceLoader = document.getElementById('workspace-loader'); + let workspaceLoaderText = document.getElementById('workspace-loader-text'); + let fileNameContainer = document.getElementById("file-names"); // Handle mexssages sent from the extension to the webview window.addEventListener("message", (event) => { const message = event.data; - debugger; switch (message.type) { case "addResponse": { response = message.value; @@ -288,11 +314,51 @@ class Mentionify { }); break; } - case "displaySnackbar": { - response = message.value; - showSnackbar(response); - break; - } + case "displaySnackbar": { + response = message.value; + showSnackbar(response); + break; + } + case 'workspaceLoader': { + workspaceLoader.style.display = message.value ? 'flex' : 'none'; + if (message.value) { + workspaceLoader.classList.remove("animate__slideOutDown"); + workspaceLoader.classList.add("animate__slideInUp"); + } else { + workspaceLoader.classList.remove("animate__slideInUp"); + workspaceLoader.classList.add("animate__slideOutDown"); + } + if (!message.value) { + fileNameContainer.innerHTML = ''; + workspaceLoaderText.textContent = "Finding the most relevant files"; + } + break; + } + case 'stepLoader': { + if (message.value?.fetchingFileLoader) { + workspaceLoaderText.textContent = "Finding most relevant files\n(this may take a while for first time)"; + } else if (message.value?.creatingResultLoader) { + fileNameContainer.style.display = "inline-flex"; + workspaceLoaderText.textContent = "Preparing a result"; + message.value?.filePaths?.forEach((_filePath) => { + const divBlock = document.createElement("div"); + divBlock.classList.add("inline-flex", "flex-row", "items-center", "mt-2"); + divBlock.id = "divBlock"; + const fileNames = document.createElement("span"); + const _dartIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + _dartIcon.innerHTML = dartIcon; + _dartIcon.classList.add("h-3", "w-3", "mx-1"); + _dartIcon.id = "dartIcon"; + fileNames.textContent = _filePath; + fileNames.classList.add("file-path"); + fileNames.id = "fileNames"; + divBlock.appendChild(_dartIcon); + divBlock.appendChild(fileNames); + fileNameContainer.appendChild(divBlock); + }); + } + break; + } } }); @@ -415,7 +481,7 @@ class Mentionify { dynamicMessagesContainer.innerHTML = ""; // Loop through the messages array and create message elements - conversationHistory.slice(2, conversationHistory.length).forEach((message, index) => { + conversationHistory.forEach((message, index) => { const messageElement = document.createElement("div"); if (message.role === "model") { messageElement.classList.add("message", "user-gemini-pro"); // Change class to "user-gemini-pro" @@ -429,9 +495,69 @@ class Mentionify { // Scroll the chat container to the most recent message dynamicMessagesContainer.scrollTop = dynamicMessagesContainer.scrollHeight; + const preCodeBlocks = document.querySelectorAll("pre code"); + preCodeBlocks.forEach((_preCodeBlock) => { + _preCodeBlock.classList.add( + "p-1", + "my-2", + "block", + "language-dart" + ); + }); + + const preBlocks = document.querySelectorAll("pre"); + preBlocks.forEach((_preBlock) => { + _preBlock.classList.add("language-dart", "relative", "my-5"); + const textToHighlight = _preBlock.textContent.trim(); + Prism.highlightElement(textToHighlight); + + const iconContainer = document.createElement("div"); + iconContainer.id = "icon-container"; + iconContainer.classList.add("absolute", "top-2", "right-2", "inline-flex", "flex-row", "h-8", "w-16", "z-10", "justify-center", "items-center", "rounded-md", "opacity-0"); + iconContainer.style.backgroundColor = activityBarBackground; + + const _copyIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + _copyIcon.innerHTML = copyIcon; + _copyIcon.id = "copy-icon"; + _copyIcon.classList.add("h-6", "w-6", "inline-flex", "justify-center", "items-center", "cursor-pointer"); + _copyIcon.style.fill = activityBarForeground; + _copyIcon.setAttribute("alt", "Copy"); + iconContainer.appendChild(_copyIcon); + + _copyIcon.addEventListener("click", () => { + const textToCopy = _preBlock.textContent.trim(); + navigator.clipboard.writeText(textToCopy); + }); + + const _mergeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + _mergeIcon.innerHTML = mergeIcon; + _mergeIcon.id = "merge-icon"; + _mergeIcon.classList.add("h-6", "w-6", "inline-flex", "justify-center", "items-center", "cursor-pointer"); + _mergeIcon.style.fill = activityBarForeground; + _mergeIcon.setAttribute("alt", "Merge"); + iconContainer.appendChild(_mergeIcon); + + _mergeIcon.addEventListener("click", () => { + vscode.postMessage({ + type: "pasteCode", + value: _preBlock.textContent.trim(), + }); + }); + + _preBlock.appendChild(iconContainer); + + _preBlock.addEventListener("mouseenter", () => { + iconContainer.style.opacity = 1; + }); + + _preBlock.addEventListener("mouseleave", () => { + iconContainer.style.opacity = 0; + }); + }); } + function markdownToPlain(input) { const converter = new showdown.Converter({ omitExtraWLInCodeBlocks: true, @@ -496,14 +622,14 @@ class Mentionify { }); document.getElementById("clear-chat-button").addEventListener("click", function () { - const dynamicMessagesContainer = document.getElementById("dynamic-messages"); - dynamicMessagesContainer.innerHTML = ""; - conversationHistory = []; + const dynamicMessagesContainer = document.getElementById("dynamic-messages"); + dynamicMessagesContainer.innerHTML = ""; + conversationHistory = []; - vscode.postMessage({ - type: "clearChat", - }); - }); + vscode.postMessage({ + type: "clearChat", + }); + }); // Function to introduce a delay using a Promise function delay(ms) { @@ -524,16 +650,16 @@ class Mentionify { toastContainer.style.display = 'none'; } - async function showSnackbar(errorMessage) { - const snackbar = document.getElementById("snackbar"); - const errorTextNode = document.createTextNode(errorMessage); - const iconElement = snackbar.querySelector("i"); - snackbar.insertBefore(errorTextNode, iconElement.nextSibling); - snackbar.style.display = "flex"; + async function showSnackbar(errorMessage) { + const snackbar = document.getElementById("snackbar"); + const errorTextNode = document.createTextNode(errorMessage); + const iconElement = snackbar.querySelector("i"); + snackbar.insertBefore(errorTextNode, iconElement.nextSibling); + snackbar.style.display = "flex"; - await delay(5000); + await delay(5000); - snackbar.removeChild(errorTextNode); - snackbar.style.display = "none"; - } + snackbar.removeChild(errorTextNode); + snackbar.style.display = "none"; + } })(); \ No newline at end of file diff --git a/package.json b/package.json index c7a59f16..22db0863 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fluttergpt", "displayName": "FlutterGPT: AI Copilot with Gemini inside Dart", "description": "Gemini capabilities fused inside Dart Analyzer to ship Flutter apps faster.", - "version": "0.2.1", + "version": "0.2.2", "publisher": "WelltestedAI", "icon": "media/icon.png", "engines": { diff --git a/src/providers/chat_view_provider.ts b/src/providers/chat_view_provider.ts index 6eaaf18a..a7b2ea15 100644 --- a/src/providers/chat_view_provider.ts +++ b/src/providers/chat_view_provider.ts @@ -55,7 +55,7 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { break; } - case "pasteCode": + case "pasteCode": { const editor = vscode.window.activeTextEditor; if (editor) { @@ -78,7 +78,7 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { }); vscode.window.onDidChangeActiveColorTheme(() => { - webviewView.webview.postMessage({type: 'updateTheme'}); + webviewView.webview.postMessage({ type: 'updateTheme' }); }); } @@ -109,7 +109,8 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { } - private _conversationHistory: Array<{ role: string, parts: string }> = []; + private _publicConversationHistory: Array<{ role: string, parts: string }> = []; + private _privateConversationHistory: Array<{ role: string, parts: string }> = []; private async getResponse(prompt: string) { if (!this._view) { @@ -120,41 +121,67 @@ export class FlutterGPTViewProvider implements vscode.WebviewViewProvider { } // Initialize conversation history if it's the first time - // debugger; - if (this._conversationHistory.length === 0) { - this._conversationHistory.push( - { role: 'user', parts: "You are a flutter/dart development expert who specializes in providing production-ready well-formatted code.\n\n" }, - { role: 'model', parts: "I am a flutter/dart development expert who specializes in providing production-ready well-formatted code. How can I help you?\n\n" } + if (this._privateConversationHistory.length === 0) { + this._privateConversationHistory.push( + { role: 'user', parts: "You are a flutter/dart development expert who specializes in providing production-ready well-formatted code. Each response from you must be STRICTLY under 8192 characters. DO NOT MENTION EXTRA DETAILS APART FROMT THE QUERY ASKED BY THE USER.\n\n" }, + { role: 'model', parts: "I am a flutter/dart development expert who specializes in providing production-ready well-formatted code. Each response of mine will be STRICTLY under 8192 characters. How can I help you?\n\n" } ); } - console.debug('conversation history', this._conversationHistory); + let workspacePrompt = ""; - // Append the current user prompt to the conversation history - this._conversationHistory.push({ role: 'user', parts: prompt }); - this._view?.webview.postMessage({ type: 'displayMessages', value: this._conversationHistory }); + // Add a simplified version to the public history + this._publicConversationHistory.push({ role: 'user', parts: prompt }); + this._view?.webview.postMessage({ type: 'displayMessages', value: this._publicConversationHistory }); this._view?.webview.postMessage({ type: 'setPrompt', value: '' }); - this._view?.webview.postMessage({ type: 'showLoadingIndicator' }); + // Check if the prompt includes '@workspace' and handle accordingly try { - // Use the stored conversation history for the prompt - const isWorkspacePresent = prompt.includes('@workspace'); - const response = await this.aiRepo.getCompletion(this._conversationHistory, isWorkspacePresent); - this._conversationHistory.push({ role: 'user', parts: prompt }); - this._conversationHistory.push({ role: 'model', parts: response }); - this._view?.webview.postMessage({ type: 'displayMessages', value: this._conversationHistory }); + if (prompt.includes('@workspace')) { + // Add the full prompt to the private history for completion + this._view?.webview.postMessage({ type: 'workspaceLoader', value: true }); + const dartFiles = await this.aiRepo.findClosestDartFiles(prompt, this._view); + workspacePrompt = "You've complete access to the codebase. I'll provide you with top 5 closest files code as context and your job is to read following files code end-to-end and answer the prompt initialised by `@workspace` symbol. If you're unable to find answer for the requested prompt, suggest an alternative solution as a dart expert. Be crisp & crystal clear in your answer. Make sure to provide your thinking process in steps including the file paths, name & code. Here's the code: \n\n" + dartFiles + "\n\n" + prompt; + this._privateConversationHistory.push({ role: 'user', parts: workspacePrompt }); + } else { + // Append the current user prompt to the conversation history + this._privateConversationHistory.push({ role: 'user', parts: prompt }); + this._view?.webview.postMessage({ type: 'showLoadingIndicator' }); + } + } catch (error) { + console.error("Error processing workspace prompt: ", error); + } + // Use the stored conversation history for the prompt + try { + let response = ''; + if (prompt.includes('@workspace')) { + response = await this.aiRepo.getCompletion(this._privateConversationHistory, true, this._view); + this._privateConversationHistory.push({ role: 'user', parts: workspacePrompt }); + } else { + response = await this.aiRepo.getCompletion(this._privateConversationHistory); + this._privateConversationHistory.push({ role: 'user', parts: prompt }); + this._view?.webview.postMessage({ type: 'showLoadingIndicator' }); + } + this._privateConversationHistory.push({ role: 'model', parts: response }); + this._publicConversationHistory.push({ role: 'model', parts: response }); + this._view?.webview.postMessage({ type: 'displayMessages', value: this._publicConversationHistory }); + this._view?.webview.postMessage({ type: 'stepLoader', value: { creatingResultLoader: true } }); this._view?.webview.postMessage({ type: 'addResponse', value: '' }); + } catch (error) { console.error(error); const response = error instanceof Error ? error.message : 'An unexpected error occurred.'; this._view?.webview.postMessage({ type: 'displaySnackbar', value: response }); + this._view?.webview.postMessage({ type: 'addResponse', value: '' }); } finally { this._view?.webview.postMessage({ type: 'hideLoadingIndicator' }); + this._view?.webview.postMessage({ type: 'workspaceLoader', value: false }); } } private clearConversationHistory() { - this._conversationHistory = []; + this._privateConversationHistory = []; + this._publicConversationHistory = []; } } diff --git a/src/repository/gemini-repository.ts b/src/repository/gemini-repository.ts index 1557a389..b58bfe4e 100644 --- a/src/repository/gemini-repository.ts +++ b/src/repository/gemini-repository.ts @@ -5,14 +5,14 @@ import * as crypto from 'crypto'; import path = require("path"); function handleError(error: Error, userFriendlyMessage: string): never { - console.error(error); // Log the detailed error for debugging purposes - // Here you could also include logic to log to an external monitoring service - throw new Error(userFriendlyMessage); // Throw a user-friendly message + console.error(error); + throw new Error(userFriendlyMessage); } export class GeminiRepository { private apiKey?: string; private genAI: GoogleGenerativeAI; + private _view?: vscode.Webview; constructor(apiKey: string) { this.apiKey = apiKey; @@ -29,38 +29,55 @@ export class GeminiRepository { ]; const result = await model.generateContent([prompt, ...imageParts]); - const response = await result.response; + const response = result.response; const text = response.text(); return text; } - public async getCompletion(prompt: { role: string, parts: string }[], isReferenceAdded?: boolean): Promise { - + public async getCompletion(prompt: { role: string, parts: string }[], isReferenceAdded?: boolean, view?: vscode.WebviewView): Promise { if (!this.apiKey) { throw new Error('API token not set, please go to extension settings to set it (read README.md for more info)'); } let lastMessage = prompt.pop(); - if (lastMessage && isReferenceAdded) { - const dartFiles = await this.findClosestDartFiles(lastMessage.parts); - lastMessage.parts = "You've complete access to the codebase. I'll provide you with top 5 closest files code as context and your job is to read following workspace code end-to-end and answer the prompt initialised by `@workspace` symbol. If you're unable to find answer for the requested prompt, suggest an alternative solution as a dart expert. Be crisp & crystal clear in your answer. Make sure to provide your thinking process in steps along with file name, path & code. Here's the code: \n\n" + dartFiles + "\n\n" + lastMessage.parts; + + // Count the tokens in the prompt + const model = this.genAI.getGenerativeModel({ model: "gemini-pro" }); + const { totalTokens } = await model.countTokens(lastMessage?.parts ?? ""); + console.log("Total input tokens: " + totalTokens); + + // Check if the token count exceeds the limit + if (totalTokens > 30720) { + throw Error('Input prompt exceeds the maximum token limit.'); } + const chat = this.genAI.getGenerativeModel({ model: "gemini-pro", generationConfig: { temperature: 0.0, topP: 0.2 } }).startChat( { - history: prompt, + history: prompt, generationConfig: { + maxOutputTokens: 2048, + }, } ); const result = await chat.sendMessage(lastMessage?.parts ?? ""); const response = result.response; const text = response.text(); - - // Creating a result for you return text; } // Cache structure private codehashCache: { [filePath: string]: { codehash: string, embedding: ContentEmbedding } } = {}; + public displayWebViewMessage(view?: vscode.WebviewView, type?: string, value?: any) { + view?.webview.postMessage({ + type, + value + }); + } + + private async sleep(msec: number) { + return new Promise(resolve => setTimeout(resolve, msec)); + } + // Modify the get cacheFilePath getter to point to a more secure location private get cacheFilePath() { @@ -122,7 +139,17 @@ export class GeminiRepository { } // Find 5 closest dart files for query - public async findClosestDartFiles(query: string): Promise { + public async findClosestDartFiles(query: string, view?: vscode.WebviewView): Promise { + //start timer + let operationCompleted = false; + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => { + if (!operationCompleted) { + this.displayWebViewMessage(view, 'stepLoader', { fetchingFileLoader: true }); + } + resolve(); + }, 5000); + }); try { if (!this.apiKey) { throw new Error('API token not set, please go to extension settings to set it (read README.md for more info)'); @@ -192,7 +219,7 @@ export class GeminiRepository { // Save updated cache await this.saveCache(); - //Accessing work structure(it can take a while in first time) + operationCompleted = true; // -> fetching most relevant files // Generate embedding for the query const queryEmbedding = await embeddingModel.embedContent({ @@ -216,11 +243,20 @@ export class GeminiRepository { resultString += fileContent; } + // A list of most relevant file paths + const filePaths = distances.slice(0, 5).map(fileEmbedding => { + return fileEmbedding.file.path.split("/").pop(); + }); + this.displayWebViewMessage(view, 'stepLoader', { creatingResultLoader: true, filePaths }); //-> generating results along with file names + console.log("Most relevant file paths:" + filePaths); + // Fetching most relevant files return resultString.trim(); } catch (error) { console.error("Error finding closest Dart files: ", error); throw error; // Rethrow the error to be handled by the caller + } finally { + await timeoutPromise; } }