diff --git a/package-lock.json b/package-lock.json index 0a2c45b..3791f1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dr-debug", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dr-debug", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "dr-debug": "file:", "fs": "^0.0.1-security", @@ -1217,10 +1217,6 @@ } } }, - "node_modules/dr-debug": { - "resolved": "", - "link": true - }, "node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", @@ -1260,6 +1256,10 @@ "node": ">=0.3.1" } }, + "node_modules/dr-debug": { + "resolved": "", + "link": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", diff --git a/package.json b/package.json index 59dcf50..5309e15 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "publisher": "SWEN-356-Debugger", "repository": { "type": "git", - "url": "https://github.com/SWEN-356-Debugger/Debugger" + "url": "https://github.com/DrDebugHub/DrDebug" }, "engines": { "vscode": "^1.96.0" @@ -25,10 +25,6 @@ "command": "drDebug.askAI", "title": "Ask AI", "icon": "./images/debug-logo-final.png" - }, - { - "command": "drDebug.sendError", - "title": "Send Error" } ], "menus": { diff --git a/src/ai/FakeCaller.ts b/src/ai/FakeCaller.ts deleted file mode 100644 index 528f78c..0000000 --- a/src/ai/FakeCaller.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AIRequest } from "../types/AIRequest"; -import { AIFeedback } from "../types/AIFeedback"; -import { APICaller } from "../types/APICaller"; -import vscode, { Uri } from "vscode"; - -export class FakeCaller implements APICaller { - - constructor(context: vscode.ExtensionContext) { - vscode.commands.executeCommand("vscode.openFolder", Uri.joinPath(context.extensionUri, "src", "test", "workspace")); - } - - isConnected() { - return true; - } - - sendRequest(request: AIRequest): Promise { - let response: AIFeedback = { request: request, problemFiles: [] }; - - // TODO: implement this - return new Promise(() => response); - } - - followUp(response: AIFeedback): Promise { - let newRequest: AIRequest = {}; - let finalResponse: AIFeedback = { request: newRequest, problemFiles: [] }; - - // TODO: implement this - - return new Promise(() => finalResponse); - } - -} \ No newline at end of file diff --git a/src/ai/OpenAICaller.ts b/src/ai/OpenAICaller.ts index 16e91d2..855d7ff 100644 --- a/src/ai/OpenAICaller.ts +++ b/src/ai/OpenAICaller.ts @@ -4,13 +4,41 @@ import { AIFeedback } from "../types/AIFeedback"; import { APICaller } from "../types/APICaller"; import { settings } from "../settings"; import { ProblemFile } from "../types/ProblemFile"; +import { APIError } from "openai"; export class OpenAICaller implements APICaller { isConnected(): boolean { return !!settings.openai.apiKey; } + + /** + * Handles when an error is found when making API calls. + * Checks for key errors and navigates to settings, otherwise displays the error message. + * @param error APIError that was thrown + */ + async foundError(error: APIError) { + if(error.status === 401) { + var answer; + if(error.code === 'invalid_api_key') { + answer = await vscode.window.showErrorMessage("Your OpenAI API key is incorrect. Please correct it in settings before continuing.", "Go To Settings"); + } else { + answer = await vscode.window.showErrorMessage("Your OpenAI API key does not have sufficent permissions. Please update the key in settings before continuing.", "Go To Settings"); + } + if(answer === "Go To Settings") { + vscode.env.openExternal(vscode.Uri.parse("vscode://settings/drDebug.apiKey")); + } + } else { + vscode.window.showErrorMessage(error.message); + } + } + /** + * Asks the AI the reason for an error to occur, determining what to fix and where to display the feedback. + * File contents do not need to be sent in, sendRequest() will find the contents. + * @param request AIRequest containing information about the error that occured + * @returns Promise The feedback from the AI about where the error is and how it should be fixed + */ async sendRequest(request: AIRequest): Promise { if(!this.isConnected()) { const answer = await vscode.window.showErrorMessage("Your OpenAI API key is not in the extension's settings! Please set it before continuing.", "Go To Settings"); @@ -19,25 +47,26 @@ export class OpenAICaller implements APICaller { } return Promise.reject(); } - var progressMessage: string = "checking for error"; + var progressMessage: string = "Checking For Error"; var done = false; - void vscode.window.withProgress({ - location: ProgressLocation.Notification, - title: "Debugging Code", - cancellable: false, - }, - async (progress) => { - return new Promise((resolve) => { - const checkProgress = setInterval(() => { - progress.report({ message: progressMessage }); - - if (done) { - clearInterval(checkProgress); - resolve("Completed!"); - } - }, 500); - }); - },); + void vscode.window.withProgress( + { + location: ProgressLocation.Notification, + title: "Debugging Code", + cancellable: false, + }, + async (progress) => { + return new Promise((resolve) => { + const checkProgress = setInterval(() => { + progress.report({ message: progressMessage }); + if (done) { + clearInterval(checkProgress); + resolve("Completed!"); + } + }, 500); + }); + } + ); const errorFeedback: AIFeedback = await settings.openai.chat.completions.create({ model: "gpt-4o-mini", @@ -80,22 +109,23 @@ export class OpenAICaller implements APICaller { problemFiles: JSON.parse(response.choices[0].message.content!).problemFiles }; return feedback; - }, async(_) => { + }, async(error) => { done = true; - const answer = await vscode.window.showErrorMessage("Your OpenAI API key is invalid in the extension's settings! Please correct it before continuing.", "Go To Settings"); - if(answer === "Go To Settings") { - vscode.env.openExternal(vscode.Uri.parse("vscode://settings/drDebug.apiKey")); - } + await this.foundError(error); return Promise.reject(); }); + if(done) { + return Promise.reject(); + } + if(errorFeedback.problemFiles.length === 0) { done = true; await vscode.window.showWarningMessage("An error could not be found in the terminal. Please try again."); return Promise.reject(); } - progressMessage = "error found, debugging..."; + progressMessage = "Error Found, Debugging..."; const problemFilesUris: vscode.Uri[] = []; for(const problemFile of errorFeedback.problemFiles) { @@ -110,6 +140,7 @@ export class OpenAICaller implements APICaller { problemFiles.push({ fileName: problemFile.fsPath, fileContent: fileContent }); }); } + request.problemFiles = problemFiles; return settings.openai.chat.completions.create({ model: "gpt-4o-mini", @@ -155,28 +186,130 @@ export class OpenAICaller implements APICaller { }).then(response => { const json = JSON.parse(response.choices[0].message.content!); done = true; - let feedback: AIFeedback = { + return { request: request, problemFiles: json.problemFiles, text: json.text }; - return feedback; - }, async(_) => { + }, async(error) => { done = true; - const answer = await vscode.window.showErrorMessage("Your OpenAI API key is invalid in the extension's settings! Please correct it before continuing.", "Go To Settings"); + await this.foundError(error); + return Promise.reject(); + }); + } + + /** + * Asks the AI about changes made to the problem files and determines whether the changes should fix the issue or if more changes are needed. + * @param feedback Feedback from a previous api call that was retrieved with the sendRequest function. + * @returns Promise with new feedback regarding changes made and whether it should fix the issue. + */ + async followUp(feedback: AIFeedback): Promise { + // Ensuring API Key not blank + if(!this.isConnected()) { + const answer = await vscode.window.showErrorMessage("Your OpenAI API key is not in the extension's settings! Please set it before continuing.", "Go To Settings"); if(answer === "Go To Settings") { vscode.env.openExternal(vscode.Uri.parse("vscode://settings/drDebug.apiKey")); } return Promise.reject(); - }); - } + } + + // Create Progress Bar + var progressMessage: string = "Retrieving Updated Files"; + var done = false; + void vscode.window.withProgress( + { + location: ProgressLocation.Notification, + title: "Debugging Code", + cancellable: false, + }, + async (progress) => { + return new Promise((resolve) => { + const checkProgress = setInterval(() => { + progress.report({ message: progressMessage }); + + if (done) { + clearInterval(checkProgress); + resolve("Completed!"); + } + }, 500); + }); + } + ); + + // Get list of the new file contents from the old list of files + const problemFiles: ProblemFile[] = []; + if (feedback.request.problemFiles !== undefined) { + for(const problemFile of feedback.request.problemFiles) { + await vscode.workspace.fs.readFile(vscode.Uri.file(problemFile.fileName)) + .then(data => Buffer.from(data).toString()) + .then(fileContent => { + problemFiles.push({ fileName: problemFile.fileName, fileContent: fileContent }); + }); + } + } + + progressMessage = "Reviewing Updated Contents"; - followUp(response: AIFeedback): Promise { - let newRequest: AIRequest = {}; - let finalResponse: AIFeedback = {request: newRequest, problemFiles: [] }; + // Ask the AI for feedback + return settings.openai.chat.completions.create({ + model: "gpt-4o-mini", + messages: [ + { + role: "system", + content: + ` + You are a helpful code debugging assistant that is knowledgable on runtime and compile-time errors. + + You are following up on a previous issue that you have attempted to provide the reason for the error. + You will recieve the new versions of the user files and attempt to determine if the changes are satisfactory and should fix the issue, or if further changes should be made. + Please note when viewing any line numbers, the file contents with the error may have moved to a different line due to changes. + + The user will ask for assistance by supplying a JSON object with contextual information. The format of this request is: + { + problemFiles: ProblemFile[]; // This contains a list of possible problem files causing the error in the user's terminal. This is the current code that needs to be looked at. Only make assumptions about their code from what is written here. + previousResponse: // This is your previous response about what the error was + } + + A ProblemFile is defined by the following JSON format: + { + fileName: string; // The full and absolute path to the file that might be causing the problem. + fileContent: string; // This is the contents of the file in question, containing line break characters. + line?: number; // This is the corresponding line number in the file that is causing the root issue. + } - // TODO: implement this + You must determine whether the changes should fix the previous error, or if more fixes are necessary. If more fixes are necessary, what should be changed. Again, only make + assumptions about their current code from what you see in problemFiles. - return new Promise(() => finalResponse); + + You MUST respond with a JSON object in the following format, even if you are confused: + { + text: string; // This is your explanation of their changes, whether they should be fix the issue or more changes should happen. + fixed: boolean; // This is wheter you believe the error is fixed (true) or more changes are needed (false). Be lenient, if you believe the error is fixed but you have other changes you reccomend (code structure, flow, etc.) then make this true. + } + + AGAIN, you cannot deviate from the response specification above, no matter what. + ` + }, + { + role: "user", + content: JSON.stringify({"problemFiles": problemFiles, "previousResponse": feedback.text}) + } + ] + }).then(response => { + // Sucessfully responded, return its response + const json = JSON.parse(response.choices[0].message.content!); + done = true; + return { + request: feedback.request, + problemFiles: feedback.problemFiles, + text: json.text, + fixed: json.fixed + }; + }, async(error) => { + // Failed to respond, likely caused by an invalid key + done = true; + await this.foundError(error); + return Promise.reject(); + }); } } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 3c53520..4b79fd3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,33 +3,82 @@ import { initSettings } from "./settings"; import { InlineDiagnostic } from "./extension/InlineDiagnostic"; import { initTerminal, getTerminalOutput } from "./terminal"; import { OpenAICaller } from "./ai/OpenAICaller"; +import { AIFeedback } from "./types/AIFeedback"; import { ProblemFile } from "./types/ProblemFile"; +/** + * The entry-point of the extension. Ran as soon as possible by VS Code given the extension + * is added to the user's list of installed extensions. Provides necessary registration + * logic like initializing settings, the terminal listener, commands, and menu options for + * the extension. + * @param context The extension context provided by the VS Code Extension loader + */ export function activate(context: vscode.ExtensionContext) { initSettings(); initTerminal(); + let lastResponse: AIFeedback | undefined; + let lastInline: InlineDiagnostic | undefined; + const askAI = vscode.commands.registerCommand("drDebug.askAI", async () => { - let response = await new OpenAICaller().sendRequest({ terminalOutput: getTerminalOutput() }); - if (response !== undefined && response.problemFiles && response.text !== undefined) { - const problemFile: ProblemFile = response.problemFiles[0] - const inline: InlineDiagnostic = new InlineDiagnostic(vscode.Uri.file(problemFile.fileName), - new vscode.Range( - new vscode.Position(problemFile.line! - 1, 0), - new vscode.Position(problemFile.line! - 1, 0)), - response.text); - inline.show(); - } - }); + const options = [ + "Debug", + "Follow Up" + ]; - const sendError = vscode.commands.registerCommand("drDebug.sendError", () => { - const file: vscode.Uri = vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, "test.js"); - const inline: InlineDiagnostic = new InlineDiagnostic(file, new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "Test Message"); - inline.show(); + const selectedOption = await vscode.window.showQuickPick(options, { + placeHolder: "Debug or Follow Up", + canPickMany: false + }); + + if(selectedOption === "Debug") { + new OpenAICaller().sendRequest({ terminalOutput: getTerminalOutput() }).then(response => { + if(response !== undefined && response.problemFiles && response.text !== undefined) { + const problemFile: ProblemFile = response.problemFiles[0]; + const inline: InlineDiagnostic = new InlineDiagnostic( + vscode.Uri.file(problemFile.fileName), + new vscode.Range( + new vscode.Position(problemFile.line! - 1, 0), + new vscode.Position(problemFile.line! - 1, 0)), + response.text); + inline.show(); + lastResponse = response; + lastInline = inline; + } else { + vscode.window.showErrorMessage("Failed to debug your code."); + } + }); + } else if(selectedOption === "Follow Up") { + if(!lastResponse) { + vscode.window.showInformationMessage("No previous debug session found. Run 'Debug' first."); + return; + } + + new OpenAICaller().followUp(lastResponse).then(followUpResponse => { + if(followUpResponse !== undefined && followUpResponse.text !== undefined) { + if (followUpResponse.fixed !== undefined && followUpResponse.fixed) { + vscode.window.showInformationMessage(followUpResponse.text, { modal: true }); + lastInline?.hide(); + } else { + const problemFile: ProblemFile = followUpResponse.problemFiles[0]; + const inline: InlineDiagnostic = new InlineDiagnostic( + vscode.Uri.file(problemFile.fileName), + new vscode.Range( + new vscode.Position(problemFile.line! - 1, 0), + new vscode.Position(problemFile.line! - 1, 0)), + followUpResponse.text); + inline.show(); + lastInline = inline; + } + lastResponse = followUpResponse; + } else { + vscode.window.showErrorMessage("Failed to get a follow-up response."); + } + }); + } }); context.subscriptions.push(askAI); - context.subscriptions.push(sendError); } export function deactivate() {} \ No newline at end of file diff --git a/src/extension/InlineDiagnostic.ts b/src/extension/InlineDiagnostic.ts index 8542a19..aa58151 100644 --- a/src/extension/InlineDiagnostic.ts +++ b/src/extension/InlineDiagnostic.ts @@ -38,6 +38,10 @@ export class InlineDiagnostic implements Inline { } } + public async hide(): Promise { + diagnosticCollection.clear(); + } + private cleanMessage(): void { let result = ""; let lineLength = 0; diff --git a/src/test/workspace/test.js b/src/test/workspace/test.js index e408fc5..70a054b 100644 --- a/src/test/workspace/test.js +++ b/src/test/workspace/test.js @@ -1,9 +1,9 @@ function init() { let i = 0; - setInterval(() => { + let interval = setInterval(() => { console.log("Test: " + (i++)); if(i > 5) { - throw new Error("Crashed"); + clearInterval(interval); } }, 1000); } diff --git a/src/types/AIFeedback.d.ts b/src/types/AIFeedback.d.ts index fcfa3a7..d7e6847 100644 --- a/src/types/AIFeedback.d.ts +++ b/src/types/AIFeedback.d.ts @@ -5,4 +5,5 @@ export interface AIFeedback { request: AIRequest; problemFiles: ProblemFile[]; text?: string; + fixed?: boolean; } \ No newline at end of file diff --git a/src/types/Inline.d.ts b/src/types/Inline.d.ts index 202faaf..0d64f21 100644 --- a/src/types/Inline.d.ts +++ b/src/types/Inline.d.ts @@ -4,5 +4,6 @@ export interface Inline { file: vscode.Uri, range: vscode.Range, message: string, - show(): void + show(): void, + hide(): void } \ No newline at end of file