├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── esbuild.extension.js ├── eslint.config.ts ├── examples ├── array │ ├── .vscode │ │ └── launch.json │ ├── array.js │ ├── array.test.js │ └── package.json └── numbers │ ├── .vscode │ └── launch.json │ └── median.js ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── res ├── icon.png └── icon.svg ├── src ├── ai │ ├── Chat.ts │ └── prompts.ts ├── context │ └── SourceCodeCollector.ts ├── debug │ ├── DebugAdapterTracker.ts │ ├── DebugConfigurationProvider.ts │ ├── DebugLoopController.ts │ └── DebugState.ts ├── index.ts ├── logger │ └── index.ts ├── types.ts ├── views │ └── SidebarView.ts └── webview │ ├── .parcelrc │ ├── App.tsx │ ├── Markdown.tsx │ ├── Spinner.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── style.css │ └── tsconfig.json ├── test └── index.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | *.vsix 7 | coverage 8 | dist 9 | lib-cov 10 | logs 11 | node_modules 12 | temp 13 | src/generated 14 | package/ 15 | out/ 16 | **/*.tsbuildinfo 17 | .parcel-cache -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | node-linker=hoisted 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["amodio.tsl-problem-matcher", "emeraldwalk.runonsave"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 9 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 10 | "preLaunchTask": "npm: build" 11 | }, 12 | { 13 | "name": "Extension", 14 | "type": "extensionHost", 15 | "request": "launch", 16 | "runtimeExecutable": "${execPath}", 17 | "args": [ 18 | "--extensionDevelopmentPath=${workspaceFolder}", 19 | "${workspaceFolder}/examples/array" 20 | ], 21 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 22 | "preLaunchTask": "npm: dev" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Auto generate metadata 3 | "emeraldwalk.runonsave": { 4 | "commands": [ 5 | { 6 | "match": "package.json", 7 | "isAsync": true, 8 | "cmd": "npm run update" 9 | } 10 | ] 11 | }, 12 | 13 | // Disable the default formatter, use eslint instead 14 | "prettier.enable": false, 15 | "editor.formatOnSave": false, 16 | 17 | // Auto fix 18 | "editor.codeActionsOnSave": { 19 | "source.fixAll.eslint": "explicit", 20 | "source.organizeImports": "never" 21 | }, 22 | 23 | // Silent the stylistic rules in you IDE, but still auto fix them 24 | "eslint.rules.customizations": [ 25 | { "rule": "style/*", "severity": "off" }, 26 | { "rule": "*-indent", "severity": "off" }, 27 | { "rule": "*-spacing", "severity": "off" }, 28 | { "rule": "*-spaces", "severity": "off" }, 29 | { "rule": "*-order", "severity": "off" }, 30 | { "rule": "*-dangle", "severity": "off" }, 31 | { "rule": "*-newline", "severity": "off" }, 32 | { "rule": "*quotes", "severity": "off" }, 33 | { "rule": "*semi", "severity": "off" } 34 | ], 35 | 36 | // Enable eslint for all supported languages 37 | "eslint.validate": [ 38 | "javascript", 39 | "javascriptreact", 40 | "typescript", 41 | "typescriptreact", 42 | "vue", 43 | "html", 44 | "markdown", 45 | "json", 46 | "jsonc", 47 | "yaml" 48 | ], 49 | "files.exclude": { 50 | "node_modules": true, 51 | "package": true, 52 | "dist": true 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "dev", 9 | "isBackground": true, 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "problemMatcher": [], 14 | "group": "build" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LLM Debugger 2 | 3 | LLM Debugger is a VSCode extension that demonstrates the use of large language models (LLMs) for active debugging of programs. This project is a **proof of concept** developed as a research experiment and will not be actively maintained or further developed. 4 | 5 | 6 | 7 | https://github.com/user-attachments/assets/8052f75f-bc3f-4382-97f7-b1e01936df47 8 | 9 | 10 | 11 | ## Overview 12 | 13 | Traditional LLM-based debugging approaches analyze only static source code. With LLM Debugger, the LLM is provided with real-time runtime context including: 14 | - **Runtime Variable Values:** Observe actual variable states as the program executes. 15 | - **Function Behavior:** Track how functions are called, what values they return, and how they interact. 16 | - **Branch Decisions:** Understand which code paths are taken during execution. 17 | 18 | This enriched context allows the LLM to diagnose bugs faster and more accurately. The extension also has the capability to generate synthetic data by running code and capturing execution details beyond the static source, offering unique insights into program behavior. 19 | 20 | 21 | ```mermaid 22 | graph TB 23 | subgraph "VSCode Editor" 24 | User[User] --> Editor[VSCode Editor] 25 | Editor --> DebugSession((Debug Session)) 26 | end 27 | 28 | subgraph "LLM Debugger Extension" 29 | DebugSession --> DebugAdapter[Debug Adapter Tracker]:::extensionComponent 30 | DebugAdapter --> DebugSession 31 | DebugAdapter -- Debug State --> LLMClient[LLM Client]:::extensionComponent 32 | LLMClient -- Function Calls --> DebugAdapter 33 | end 34 | 35 | subgraph "External Services" 36 | LLMClient --- LLM[Large Language Model] 37 | end 38 | DebugSession -- Debug Protocol --> NodeDebugAdapter[Node.js Debug Adapter] 39 | NodeDebugAdapter -- Executes --> NodeApp[Node.js Application] 40 | NodeApp -- Runtime Events --> NodeDebugAdapter 41 | 42 | ``` 43 | 44 | ## Key Features 45 | 46 | - **Active Debugging:** Integrates live debugging information (variables, stack traces, breakpoints) into the LLM’s context. 47 | - **Automated Breakpoint Management:** Automatically sets initial breakpoints based on code analysis and LLM recommendations. 48 | - **Runtime Inspection:** Monitors events like exceptions and thread stops, gathering detailed runtime state to guide debugging. 49 | - **Debug Operations:** Supports common debugging actions such as stepping over (`next`), stepping into (`stepIn`), stepping out (`stepOut`), and continuing execution. 50 | - **Synthetic Data Generation:** Captures interesting execution details to generate data that extends beyond static code analysis. 51 | - **Integrated UI:** Features a sidebar panel within the Run and Debug view that lets you toggle AI debugging and view live LLM suggestions and results. 52 | 53 | ## Commands and Contributions 54 | 55 | - **Start LLM Debug Session:** 56 | - Command: `llm-debugger.startLLMDebug` 57 | - Description: Launches an AI-assisted debugging session. Once activated, the extension configures the debugging environment for Node.js sessions and starts gathering runtime data for LLM analysis. 58 | 59 | - **Sidebar Panel:** 60 | - Location: Run and Debug view 61 | - ID: `llmDebuggerPanel` 62 | - Description: Displays the current state of the AI debugging session. Use the control panel to toggle "Debug with AI" mode. It shows live debugging insights, LLM function calls, and final debug results. 63 | 64 | - **Debug Configuration Provider & Debug Adapter Tracker:** 65 | - Automatically integrated with Node.js debug sessions. 66 | - Injects LLM context into the session by reading the workspace state flag `llmDebuggerEnabled` and automatically setting breakpoints and handling debug events (e.g., exceptions, thread stops). 67 | - Supports LLM-guided commands for common operations like `next`, `stepIn`, `stepOut`, and `continue`. 68 | 69 | ## Configuration 70 | 71 | The extension maintains a single configuration flag (`llmDebuggerEnabled`) stored in the workspace state. This flag determines whether AI-assisted debugging is enabled. You can toggle this option via the sidebar panel. No additional settings are exposed in the Settings UI. 72 | 73 | ## How It Works 74 | 75 | 1. **Session Initialization:** 76 | When you launch a Node.js debug session (or use the command `llm-debugger.startLLMDebug`), the extension activates and attaches its debug adapter tracker to the session. 77 | 78 | 2. **Breakpoint Management:** 79 | The extension automatically sets initial breakpoints based on an analysis of the workspace code. It then monitors runtime events to adjust breakpoints or trigger LLM actions as needed. 80 | 81 | 3. **Runtime Inspection:** 82 | As the debug session progresses, the extension gathers live data including variable values, stack traces, and output (stderr/stdout). This data is sent to the LLM to determine the next debugging steps. 83 | 84 | 4. **LLM Guidance and Action Execution:** 85 | The LLM processes the combined static and runtime context to suggest actions such as stepping through code or modifying breakpoints. These actions are executed automatically, streamlining the debugging process. 86 | 87 | 5. **Session Termination:** 88 | When the debug session ends (either normally or due to an exception), the extension collects final runtime data and generates a summary with a code fix and explanation based on the LLM’s analysis. 89 | 90 | ## Installation 91 | 92 | You can install LLM Debugger in VSCode using the "Install from VSIX" feature: 93 | 94 | 1. **Build the Extension Package:** 95 | - Run the following command in the project root to build the extension: 96 | ```bash 97 | npm run build 98 | ``` 99 | 100 | 2. **Install the Extension in VSCode:** 101 | - Open VSCode. 102 | - Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) to open the Command Palette. 103 | - Type and select **"Extensions: Install from from location..."**. 104 | - Browse to the directory of this repo 105 | - Reload VSCode if prompted. 106 | 107 | Alternatively, if you prefer to load the extension directly from the source for development: 108 | - Open the project folder in VSCode. 109 | - Run the **"Debug: Start Debugging"** command to launch a new Extension Development Host. 110 | 111 | ## Use Cases 112 | 113 | - **Faster Bug Resolution:** 114 | The integration of runtime state with static code provides the LLM with a comprehensive view, enabling quicker identification of the root cause of issues. 115 | 116 | - **Enhanced Debugging Workflow:** 117 | Developers benefit from real-time, AI-driven insights that help navigate complex codebases and manage debugging tasks more efficiently. 118 | 119 | - **Research & Data Generation:** 120 | The tool can be used to generate synthetic runtime data for research purposes, offering new perspectives on program behavior that static analysis cannot capture. 121 | 122 | --- 123 | 124 | LLM Debugger is an experimental project showcasing how combining live debugging data with LLM capabilities can revolutionize traditional debugging practices. 125 | -------------------------------------------------------------------------------- /esbuild.extension.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable @typescript-eslint/no-require-imports */ 3 | const process = require("node:process"); 4 | const esbuild = require("esbuild"); 5 | 6 | const watch = process.argv.includes("--watch"); 7 | 8 | esbuild 9 | .context({ 10 | entryPoints: ["src/index.ts"], // or whatever your extension entry is 11 | bundle: true, 12 | outdir: "out", 13 | // output CJS 14 | format: "cjs", 15 | platform: "node", 16 | target: "es2020", 17 | sourcemap: true, 18 | external: ["vscode", "src/webview/"], 19 | }) 20 | .then(async (ctx) => { 21 | if (watch) { 22 | console.log("Watching for changes"); 23 | await ctx.watch(); 24 | } else { 25 | await ctx.rebuild(); 26 | await ctx.dispose(); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | 4 | export default tseslint.config( 5 | eslint.configs.recommended, 6 | tseslint.configs.recommended, 7 | { 8 | ignores: ["src/generated", "dist"], 9 | }, 10 | ); 11 | -------------------------------------------------------------------------------- /examples/array/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Debug Test", 7 | "program": "${workspaceFolder}/array.test.js", 8 | "env": { 9 | "NODE_ENV": "test" 10 | }, 11 | "internalConsoleOptions": "neverOpen", 12 | "skipFiles": ["/**"] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/array/array.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Appends items to the end of an array. 3 | * @template T 4 | * @param {T} arr 5 | * @param {T[]} other 6 | * @returns {T[]} The modified array 7 | */ 8 | export function appendArray(arr, other) { 9 | arr.push(...other); 10 | return arr; 11 | } 12 | -------------------------------------------------------------------------------- /examples/array/array.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import { appendArray } from "./array.js"; 4 | 5 | console.log("should append the given numbers to the array"); 6 | const arr = [1, 2, 3]; 7 | const largeArray = Array.from({ length: 1_000_000 }, (_, i) => i); 8 | const result = appendArray(arr, largeArray); 9 | console.assert(result.length === 1_000_003, "Result length is incorrect"); 10 | console.log("Test passed"); 11 | -------------------------------------------------------------------------------- /examples/array/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "array-example", 3 | "type": "module", 4 | "scripts": { 5 | "test": "node array.test.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/numbers/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Debug Test", 7 | "program": "${workspaceFolder}/median.js", 8 | "env": { 9 | "NODE_ENV": "test" 10 | }, 11 | "internalConsoleOptions": "neverOpen", 12 | "skipFiles": ["/**"] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/numbers/median.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | function median(arr) { 4 | if (!Array.isArray(arr) || arr.length === 0) { 5 | throw new Error('Invalid input'); 6 | } 7 | const sorted = arr.slice().sort(); 8 | const mid = Math.floor(sorted.length / 2); 9 | if (sorted.length % 2 !== 0) { 10 | return sorted[mid]; 11 | } 12 | return (sorted[mid - 1] + sorted[mid]) / 2; 13 | } 14 | 15 | console.assert(median([3, 1, 2]) === 2, 'Odd length with single-digit numbers'); 16 | console.assert(median([1, 2, 3, 4]) === 2.5, 'Even length with single-digit numbers'); 17 | console.assert(median([1]) === 1, 'Single element'); 18 | console.assert(median([1, 2, 10, 4]) === 3, 'Even length with numbers larger than 10'); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "mohsen1", 3 | "name": "llm-debugger", 4 | "displayName": "LLM Debugger", 5 | "version": "0.0.0", 6 | "private": true, 7 | "volta": { 8 | "node": "20.18.1", 9 | "pnpm": "9.7.1" 10 | }, 11 | "workspaces": [ 12 | "src/webview" 13 | ], 14 | "description": "This is a VSCode extension that allows you to debug your code using an LLM.", 15 | "author": "Mohsen Azimi ", 16 | "license": "MIT", 17 | "keywords": [ 18 | "debug", 19 | "debugger", 20 | "llm", 21 | "ai", 22 | "openai" 23 | ], 24 | "categories": [ 25 | "Debuggers" 26 | ], 27 | "main": "./out/index.js", 28 | "icon": "./res/icon.png", 29 | "files": [ 30 | "out/*", 31 | "LICENSE.md", 32 | "res/*" 33 | ], 34 | "engines": { 35 | "vscode": "^1.96.0" 36 | }, 37 | "activationEvents": [ 38 | "*" 39 | ], 40 | "contributes": { 41 | "debuggers": [ 42 | { 43 | "type": "node" 44 | } 45 | ], 46 | "viewsContainers": { 47 | "debug": [ 48 | { 49 | "id": "llmDebuggerSidebar", 50 | "title": "LLM Debugger" 51 | } 52 | ] 53 | }, 54 | "views": { 55 | "debug": [ 56 | { 57 | "id": "llmDebuggerPanel", 58 | "name": "LLM Debugger", 59 | "type": "webview" 60 | } 61 | ] 62 | }, 63 | "commands": [ 64 | { 65 | "command": "llm-debugger.startLLMDebug", 66 | "title": "Start LLM Debug Session" 67 | } 68 | ], 69 | "configuration": { 70 | "type": "object", 71 | "title": "LLM Debugger", 72 | "properties": {} 73 | } 74 | }, 75 | "scripts": { 76 | "build:webview": "pnpm -F ./src/webview build", 77 | "dev:webview": "pnpm -F ./src/webview dev", 78 | "build:extension": "node esbuild.extension.js", 79 | "build": "npm run build:extension && npm run build:webview", 80 | "dev:extension": "node esbuild.extension.js --watch", 81 | "dev": "rm -rf out && rm -rf src/webview/out && concurrently \"npm run dev:extension\" \"npm run dev:webview\"", 82 | "update": "vscode-ext-gen --output src/generated/meta.ts", 83 | "lint": "eslint .", 84 | "vscode:prepublish": "tsc", 85 | "publish": "vsce publish --no-dependencies", 86 | "pack": "vsce package --no-dependencies", 87 | "test": "vitest", 88 | "typecheck": "tsc --noEmit", 89 | "release": "bumpp && pnpm publish" 90 | }, 91 | "dependencies": { 92 | "@vscode/debugadapter": "^1.68.0", 93 | "cheerio": "^1.0.0", 94 | "fs-extra": "^11.3.0", 95 | "markdown-it": "^14.1.0", 96 | "openai": "^4.82.0", 97 | "react": "^18.2.0", 98 | "react-dom": "^18.2.0" 99 | }, 100 | "devDependencies": { 101 | "@types/fs-extra": "^11.0.4", 102 | "@types/markdown-it": "^14.1.2", 103 | "@types/node": "^22.4.1", 104 | "@types/react": "^19.0.0", 105 | "@types/react-dom": "^19.0.0", 106 | "@types/vscode": "1.96.0", 107 | "@vscode/vsce": "^3.0.0", 108 | "bumpp": "^9.5.1", 109 | "concurrently": "^8.2.0", 110 | "esbuild": "^0.24.2", 111 | "eslint": "^9.19.0", 112 | "process": "^0.11.10", 113 | "reactive-vscode": "^0.2.0", 114 | "typescript": "^5.5.4", 115 | "typescript-eslint": "^8.23.0", 116 | "vite": "^5.4.1", 117 | "vitest": "^2.0.5", 118 | "vscode-ext-gen": "^0.4.1" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - examples/* 4 | - src/webview 5 | -------------------------------------------------------------------------------- /res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohsen1/llm-debugger-vscode-extension/7c5e4eff95bea3b726566e7c8c1255ec49a82d2d/res/icon.png -------------------------------------------------------------------------------- /res/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/ai/Chat.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import os from "node:os"; 3 | import path from "node:path"; 4 | import process from "node:process"; 5 | import { OpenAI } from "openai"; 6 | import type { ChatCompletion } from "openai/resources/chat/completions"; 7 | import type { ChatCompletionMessageParam, ChatCompletionTool } from "../types"; 8 | import { initialBreakPointsSystemMessage } from "./prompts"; 9 | import vscode from "vscode"; 10 | 11 | export class AIChat { 12 | #messageHistory: ChatCompletionMessageParam[] = []; 13 | #output = vscode.window.createOutputChannel("LLM Debugger (AI Chat)", {log: true}); 14 | #functions: ChatCompletionTool[]; 15 | constructor( 16 | systemMessage: ChatCompletionMessageParam, 17 | functions: ChatCompletionTool[], 18 | ) { 19 | this.#messageHistory = [systemMessage]; 20 | this.#functions = functions; 21 | } 22 | 23 | clearHistory() { 24 | this.#messageHistory = [initialBreakPointsSystemMessage]; 25 | } 26 | 27 | async ask(message: string, { withFunctions = true } = {}) { 28 | this.#output.appendLine(''); // Add a new line 29 | this.#output.appendLine('------------------------ USER ----------------------'); 30 | this.#output.info(`User: ${message}`); 31 | 32 | this.#messageHistory.push({ role: "user", content: message }); 33 | try { 34 | const response = await callLlm( 35 | this.#messageHistory, 36 | withFunctions ? this.#functions : [] 37 | ); 38 | const responseMessage = response.choices[0].message; 39 | this.#output.appendLine(''); // Add a new line 40 | this.#output.appendLine('------------------------ AI ----------------------'); 41 | if (responseMessage.content) { 42 | this.#output.info(`AI: ${responseMessage.content}`); 43 | } 44 | if (responseMessage.tool_calls) { 45 | for (const toolCall of responseMessage.tool_calls) { 46 | this.#output.info(`AI FN: ${toolCall.function.name}(${toolCall.function.arguments === '{}' ? '' : toolCall.function.arguments})`); 47 | } 48 | } 49 | return response; 50 | } catch (error) { 51 | throw error; 52 | } 53 | } 54 | } 55 | 56 | export async function callLlm( 57 | promptOrMessages: string | ChatCompletionMessageParam[], 58 | functions?: ChatCompletionTool[], 59 | ): Promise { 60 | const messages: ChatCompletionMessageParam[] = []; 61 | if (Array.isArray(promptOrMessages)) { 62 | if (promptOrMessages?.[0].role !== "system") { 63 | messages.push(initialBreakPointsSystemMessage); 64 | } 65 | messages.push(...promptOrMessages); 66 | } else { 67 | messages.push(initialBreakPointsSystemMessage); 68 | messages.push({ role: "user", content: promptOrMessages }); 69 | } 70 | 71 | const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); 72 | const withTools = functions && functions.length > 0; 73 | const completion = await openai.chat.completions.create({ 74 | model: "gpt-4o", 75 | tools: withTools ? functions : undefined, 76 | messages, 77 | tool_choice: withTools ? "required" : undefined, 78 | max_tokens: 1000, 79 | }); 80 | 81 | const promptCacheFile = path.join( 82 | os.homedir(), 83 | ".llm-debugger-prompt-cache.json", 84 | ); 85 | if (!fs.existsSync(promptCacheFile)) { 86 | fs.writeFileSync(promptCacheFile, JSON.stringify([], null, 2)); 87 | } 88 | 89 | return completion; 90 | } 91 | -------------------------------------------------------------------------------- /src/ai/prompts.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ChatCompletionMessageParam, 3 | ChatCompletionSystemMessageParam, 4 | ChatCompletionTool, 5 | } from "openai/resources"; 6 | import type { StructuredCode } from "../types"; 7 | 8 | interface StackFrame { 9 | name: string; 10 | source: string; 11 | line: number; 12 | column: number; 13 | } 14 | 15 | interface Variable { 16 | name: string; 17 | value: string; 18 | } 19 | 20 | interface Scope { 21 | scopeName: string; 22 | variables: Variable[]; 23 | } 24 | 25 | interface PausedState { 26 | pausedStack: StackFrame[]; 27 | topFrameVariables: Scope[]; 28 | } 29 | 30 | export const systemMessage: ChatCompletionSystemMessageParam = { 31 | role: "system", 32 | content: "You are an AI assistant that decides debugging steps." 33 | }; 34 | 35 | export function getInitialBreakpointsMessage( 36 | structuredCode: StructuredCode[], 37 | ): string { 38 | return [ 39 | "Here is the workspace code in a structured format (filePath -> [lines]):", 40 | serializeStructuredCode(structuredCode), 41 | "", 42 | "Decide on an initial breakpoint by calling setBreakpoint on a line in the code the is most likely to be the root cause of the problem.", 43 | "You may reference lines precisely now.", 44 | ].join("\n"); 45 | } 46 | 47 | export function getPausedMessage( 48 | structuredCode: StructuredCode[], 49 | pausedState: PausedState, 50 | ): string { 51 | const message = ["# Code:", serializeStructuredCode(structuredCode), ""]; 52 | 53 | if (pausedState) { 54 | message.push("# Current Debug State:", JSON.stringify(pausedState)); 55 | } 56 | 57 | message.push( 58 | "# Instructions:", 59 | "Debugger is in paused state", 60 | "Choose next action by calling setBreakpoint, removeBreakpoint, next, stepIn, stepOut, or continue.", 61 | "Always make sure there are breakpoints set before calling continue.", 62 | "Once you understood the problem, instead of calling any tools, respond with a code fix and explain your reasoning.", 63 | ); 64 | 65 | return message.join("\n"); 66 | } 67 | 68 | export function getExceptionMessage( 69 | structuredCode: StructuredCode[], 70 | pausedState: PausedState, 71 | stderr: string, 72 | stdout: string 73 | ): string { 74 | const message = ["# Code:", serializeStructuredCode(structuredCode), ""]; 75 | 76 | if (pausedState) { 77 | const stack = pausedState.pausedStack 78 | .map( 79 | (frame: StackFrame) => 80 | `at ${frame.name} (${frame.source}:${frame.line}:${frame.column})`, 81 | ) 82 | .join("\n"); 83 | 84 | // Find the 'Exception' scope and extract the exception details 85 | const exceptionScope = pausedState.topFrameVariables.find( 86 | (scope) => scope.scopeName === "Exception", 87 | ); 88 | const exceptionValue = exceptionScope?.variables.find( 89 | (v) => v.name === "exception", 90 | )?.value; 91 | 92 | message.push( 93 | "# Exception:", 94 | exceptionValue || "Unknown exception", 95 | "", 96 | "# Stack Trace:", 97 | stack, 98 | '# stderr', 99 | stderr, 100 | '# stdout', 101 | stdout 102 | ); 103 | } 104 | 105 | message.push( 106 | "# Instructions:", 107 | "The program has stopped due to an uncaught exception.", 108 | "Analyze the code, the exception details, and the stack trace to identify the cause.", 109 | "Provide a code fix and explain your reasoning. Do *not* suggest further debugging actions.", 110 | ); 111 | 112 | return message.join("\n"); 113 | } 114 | 115 | export function serializeStructuredCode(structuredCode: StructuredCode[]) { 116 | const serialized = structuredCode 117 | .map( 118 | ({ filePath, lines }) => 119 | `${filePath}\n${lines 120 | .map( 121 | ({ lineNumber, text }) => 122 | `${String(lineNumber).padStart(3, " ")}| ${text}`, 123 | ) 124 | .join("\n")}`, 125 | ) 126 | .join("\n\n"); 127 | 128 | return serialized; 129 | } 130 | 131 | export const initialBreakPointsSystemMessage: ChatCompletionMessageParam = { 132 | role: "system" as const, 133 | content: 134 | "You are an AI assistant that sets initial breakpoints before launch of a debugger.", 135 | }; 136 | 137 | export const debugLoopSystemMessage: ChatCompletionMessageParam = { 138 | role: "system" as const, 139 | content: 140 | "You are an AI assistant that decides debugging steps. suggest next action by calling a function", 141 | }; 142 | 143 | 144 | export const breakpointFunctions: ChatCompletionTool[] = [ 145 | { 146 | type: "function", 147 | function: { 148 | name: "setBreakpoint", 149 | description: "Sets a breakpoint in a specific file and line.", 150 | parameters: { 151 | type: "object", 152 | properties: { 153 | file: { type: "string" }, 154 | line: { type: "number" }, 155 | reason: { type: "string", description: "Reason for taking this action. Maximum 30 words" }, 156 | }, 157 | required: ["file", "line", "reason"], 158 | }, 159 | }, 160 | }, 161 | { 162 | type: "function", 163 | function: { 164 | name: "removeBreakpoint", 165 | description: "Removes a breakpoint from a specific file and line.", 166 | parameters: { 167 | type: "object", 168 | properties: { file: { type: "string" }, line: { type: "number" }, reason: { type: "string", description: "Reason for taking this action. Maximum 30 words" }, }, 169 | required: ["file", "line", "reason"], 170 | }, 171 | }, 172 | }, 173 | ]; 174 | 175 | export const debugFunctions: ChatCompletionTool[] = [ 176 | { 177 | type: "function", 178 | function: { 179 | name: "next", 180 | description: "Step over the current line in the debugger.", 181 | parameters: { 182 | type: "object", 183 | required: ['reason'], 184 | properties: { 185 | reason: { type: "string", description: "Reason for taking this action. Maximum 30 words" }, 186 | } 187 | }, 188 | }, 189 | }, 190 | { 191 | type: "function", 192 | function: { 193 | name: "stepIn", 194 | description: "Step into the current function call in the debugger.", 195 | parameters: { 196 | type: "object", 197 | required: ['reason'], 198 | properties: { 199 | reason: { type: "string", description: "Reason for taking this action. Maximum 30 words" }, 200 | } 201 | }, 202 | }, 203 | }, 204 | { 205 | type: "function", 206 | function: { 207 | name: "stepOut", 208 | description: "Step out of the current function call in the debugger.", 209 | parameters: { 210 | type: "object", 211 | required: ['reason'], 212 | properties: { 213 | reason: { type: "string", description: "Reason for taking this action. Maximum 30 words" }, 214 | } 215 | }, 216 | }, 217 | }, 218 | { 219 | type: "function", 220 | function: { 221 | name: "continue", 222 | description: "Continue execution in the debugger.", 223 | parameters: { 224 | type: "object", 225 | required: ['reason'], 226 | properties: { 227 | reason: { type: "string", description: "Reason for taking this action. Maximum 30 words" }, 228 | } 229 | }, 230 | }, 231 | }, 232 | ]; 233 | 234 | export const allFunctions = [ 235 | ...breakpointFunctions, 236 | ...debugFunctions, 237 | ]; -------------------------------------------------------------------------------- /src/context/SourceCodeCollector.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import fs from "node:fs"; 3 | import fsExtra from "fs-extra"; 4 | import * as vscode from "vscode"; 5 | import { StructuredCode } from "../types"; 6 | import log from "../logger"; 7 | 8 | export class SourceCodeCollector { 9 | private workspaceFolder: vscode.WorkspaceFolder | undefined; 10 | constructor(workspaceFolder?: vscode.WorkspaceFolder) { 11 | this.workspaceFolder = workspaceFolder; 12 | } 13 | 14 | setWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder) { 15 | this.workspaceFolder = workspaceFolder; 16 | } 17 | 18 | /** 19 | * Runs `yek` to retrieve a concatenated string of repo code, then splits it into structured lines per file. 20 | */ 21 | gatherWorkspaceCode(): StructuredCode[] { 22 | if (!this.workspaceFolder) return []; 23 | const wsFolder = this.workspaceFolder?.uri.fsPath; 24 | if (!wsFolder) { 25 | log.error("No workspace folder found"); 26 | return []; 27 | } 28 | 29 | // get list of files in the workspace 30 | // TODO: handle gitignore 31 | return fsExtra.readdirSync(wsFolder, { withFileTypes: true, recursive: true }) 32 | .filter(dirent => dirent.isFile()) 33 | .map((dirent) => { 34 | const fullPath = path.join(dirent.parentPath, dirent.name); 35 | return { 36 | filePath: fullPath, 37 | lines: fs 38 | .readFileSync(fullPath, "utf-8") 39 | .split("\n") 40 | .map((text, idx) => ({ 41 | lineNumber: idx + 1, 42 | text, 43 | })), 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/debug/DebugAdapterTracker.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from "vscode"; 3 | import { DebugLoopController } from "./DebugLoopController"; 4 | import logger from "../logger"; 5 | 6 | const log = logger.createSubLogger("DebugAdapterTracker"); 7 | // log.disable(); 8 | 9 | interface ThreadInfo { 10 | id: number; 11 | name: string; 12 | } 13 | 14 | interface DebugMessage { 15 | type: string; 16 | command?: string; 17 | event?: string; 18 | body?: { 19 | reason?: string; 20 | threadId?: number; 21 | allThreadsStopped?: boolean; 22 | threads?: ThreadInfo[]; 23 | text?: string; // Added to capture output event text 24 | output?: string; // Capture output content 25 | category?: string; 26 | }; 27 | } 28 | 29 | export class DebugAdapterTracker implements vscode.DebugAdapterTracker { 30 | private session: vscode.DebugSession; 31 | private controller: DebugLoopController; 32 | private threadId: number | undefined; 33 | private stderr: string = ""; // Accumulate error output 34 | private stdout: string = ""; // Accumulate standard output 35 | 36 | 37 | constructor(session: vscode.DebugSession, controller: DebugLoopController) { 38 | this.session = session; 39 | this.controller = controller; 40 | } 41 | 42 | // async onWillReceiveMessage(message: DebugMessage) { 43 | // log.debug(`onWillReceiveMessage: ${message.type} - ${JSON.stringify(message)}`); 44 | // } 45 | 46 | async onWillStartSession(): Promise { 47 | log.debug("onWillStartSession"); 48 | await this.controller.clear(); 49 | this.controller.setSession(this.session); 50 | 51 | // check if session has thread 52 | const threadsResponse = await this.session.customRequest('threads'); 53 | const threads = threadsResponse?.threads || []; 54 | 55 | if (threads.length > 0 && !this.threadId) { 56 | this.threadId = threads[0].id; 57 | this.controller.setThreadId(this.threadId); 58 | } else { 59 | log.debug('onWillStartSession: No thread found in session'); 60 | return; 61 | } 62 | 63 | await this.controller.start(); 64 | log.debug('started controller for session', this.session.id) 65 | } 66 | 67 | 68 | 69 | async onDidSendMessage(message: DebugMessage) { 70 | if (message.event !== 'loadedSource') { 71 | log.debug("onDidSendMessage", JSON.stringify(message)); 72 | } 73 | 74 | // Track thread creation 75 | if (message.type === "response" && message.command === "threads") { 76 | const threads = message.body?.threads || []; 77 | 78 | // We are ignoreing other threads that are created since we don't support multi-threaded 79 | // debugging just yet 80 | if (threads.length > 0 && !this.threadId) { 81 | this.threadId = threads[0].id; 82 | this.controller.setThreadId(this.threadId); 83 | } 84 | } 85 | 86 | // Handle stopped events 87 | if (message.type === "event" && message.event === "stopped") { 88 | const threadId = message.body?.threadId || this.threadId; 89 | const allThreadsStopped = message.body?.allThreadsStopped || false; 90 | 91 | if (threadId) { 92 | this.threadId = threadId; 93 | this.controller.setThreadId(threadId); 94 | } 95 | if (message.body?.reason === "exception") { 96 | log.debug('stopped due to exception'); 97 | await this.controller.handleException(this.session, this.stderr, this.stdout); 98 | } 99 | else { 100 | // Emit threadStopped before calling loop 101 | this.controller.emit("threadStopped", { threadId, allThreadsStopped }); 102 | await this.controller.loop(); // not sure if we need to loop manually from here, controller should handle looping state 103 | } 104 | } 105 | 106 | // Handle thread exit 107 | if (message.type === "event" && message.event === "thread" && message.body?.reason === "exited") { 108 | this.threadId = undefined; 109 | this.controller.setThreadId(undefined); 110 | } 111 | 112 | // Accumulate error output 113 | if (message.type === "event" && message.event === "output") { 114 | if (message.body?.category === "stderr") { 115 | this.stderr += message.body.output || ""; 116 | } 117 | if (message.body?.category === 'stdout') { 118 | this.stdout += message.body.output || '' 119 | } 120 | } 121 | 122 | if (message.type === "response" && message.command === "disconnect") { 123 | log.debug("onDidSendMessage: disconnect"); 124 | this.controller.finish([ 125 | '# stdout', 126 | this.stdout, 127 | '# stderr', 128 | this.stderr 129 | ].join('\n\n')); 130 | } 131 | 132 | } 133 | 134 | 135 | onError(error: Error) { 136 | log.error(`Error occurred: ${error.message}`); 137 | this.stderr += error.message; 138 | } 139 | 140 | } -------------------------------------------------------------------------------- /src/debug/DebugConfigurationProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { DebugLoopController } from "./DebugLoopController"; 3 | 4 | export class DebugConfigurationProvider implements vscode.DebugConfigurationProvider { 5 | constructor( 6 | private readonly context: vscode.ExtensionContext, 7 | private readonly debugLoopController: DebugLoopController, 8 | ) {} 9 | 10 | resolveDebugConfiguration( 11 | folder: vscode.WorkspaceFolder | undefined, 12 | config: vscode.DebugConfiguration, 13 | ): vscode.ProviderResult { 14 | // Get the current debug enabled state from workspace state 15 | const debugEnabled = this.context.workspaceState.get( 16 | "llmDebuggerEnabled", 17 | false, 18 | ); 19 | 20 | // LLDB specific 21 | config.stopOnTerminate = false; 22 | 23 | // Store the AI debug state in the config for the debug adapter 24 | config.llmDebuggerEnabled = debugEnabled; 25 | 26 | if (debugEnabled) { 27 | // Configure the debugger to stop on uncaught exceptions 28 | config.breakOnUncaughtExceptions = true; 29 | config.stopOnEntry = true; 30 | } 31 | 32 | return config; 33 | } 34 | } -------------------------------------------------------------------------------- /src/debug/DebugLoopController.ts: -------------------------------------------------------------------------------- 1 | // src/debug/DebugLoopController.ts 2 | import { EventEmitter } from "node:events"; 3 | import * as path from "node:path"; 4 | import type { ChatCompletion } from "openai/resources"; 5 | import * as vscode from "vscode"; 6 | import { AIChat, callLlm } from "../ai/Chat"; 7 | import { 8 | allFunctions, 9 | getExceptionMessage, 10 | getInitialBreakpointsMessage, 11 | getPausedMessage, 12 | systemMessage, 13 | } from "../ai/prompts"; 14 | import { SourceCodeCollector } from "../context/SourceCodeCollector"; 15 | import logger from "../logger"; 16 | import { DebugState, PausedState } from "./DebugState"; 17 | 18 | const log = logger.createSubLogger("DebugLoopController"); 19 | 20 | /** 21 | * This controller waits for "stopped" events to retrieve paused state. 22 | * Instead of forcing "pause", we only call gatherPausedState after the debugger 23 | * actually stops. This avoids the 'Thread is not paused' error. 24 | */ 25 | export class DebugLoopController extends EventEmitter { 26 | private live = false; 27 | private finished = false; // Add a flag to track if finish has been called 28 | private session: vscode.DebugSession | null = null; 29 | private previousBreakpoints: vscode.Breakpoint[] = []; 30 | private threadId: number | undefined; 31 | 32 | constructor(private sourceCodeCollector: SourceCodeCollector) { 33 | super(); 34 | } 35 | 36 | private chat = new AIChat(systemMessage, allFunctions); 37 | 38 | setWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder) { 39 | this.sourceCodeCollector.setWorkspaceFolder(workspaceFolder); 40 | } 41 | 42 | setSession(session: vscode.DebugSession) { 43 | this.session = session; 44 | } 45 | 46 | setThreadId(threadId: number | undefined) { 47 | this.threadId = threadId; 48 | } 49 | 50 | async shouldLoop() { 51 | if (!this.session || !this.live) { 52 | return false; 53 | } 54 | 55 | // If we already have a threadId, use it 56 | if (this.threadId) { 57 | return true; 58 | } 59 | 60 | // Otherwise try to get thread info 61 | try { 62 | const threadsResponse = await this.session.customRequest("threads"); 63 | const threads = threadsResponse?.threads || []; 64 | if (threads.length > 0) { 65 | this.threadId = threads[0].id; 66 | return true; 67 | } 68 | } catch (err) { 69 | log.error("Error getting threads:", String(err)); 70 | } 71 | 72 | log.error("No threads found in session"); 73 | return false; 74 | } 75 | 76 | async handleException(session: vscode.DebugSession, stderr: string, stdout: string) { 77 | log.debug("Handling exception..."); 78 | 79 | if (session !== this.session) return; 80 | log.debug("Gathering paused state"); 81 | const debugState = new DebugState(); 82 | let pausedState: PausedState | undefined; 83 | try { 84 | pausedState = await debugState.gatherPausedState(this.session!); 85 | } catch (error) { 86 | log.error("Error gathering paused state", String(error)); 87 | // If we fail to get the paused state (most probably because program already exited) we make an empty state to continue 88 | pausedState = { 89 | breakpoints: [], 90 | pausedStack: [], 91 | topFrameVariables: [], 92 | } 93 | } 94 | 95 | // checking again since while we were gathering paused state, the live flag could have been set to false 96 | const shouldLoop = await this.shouldLoop(); 97 | if (!shouldLoop) return; 98 | 99 | log.debug("Thinking.."); 100 | this.emit("spinner", { active: true, message: "Handling exception" }); 101 | 102 | const messageToSend = getExceptionMessage(this.sourceCodeCollector.gatherWorkspaceCode(), pausedState, stderr, stdout); 103 | log.debug("Message to LLM:", messageToSend); 104 | 105 | const llmResponse = await this.chat.ask(messageToSend, { withFunctions: false }); 106 | this.emit("spinner", { active: false }); 107 | if (!this.live) return; 108 | 109 | const [choice] = llmResponse.choices; 110 | const content = choice?.message?.content; 111 | if (content) { 112 | log.info(content); 113 | this.emit("debugResults", { results: content }); 114 | } else { 115 | log.info("No content from LLM"); 116 | } 117 | this.emit('isInSession', { isInSession: false }); 118 | this.stop(); 119 | } 120 | 121 | waitForThreadStopped() { 122 | log.debug("Waiting for thread to stop..."); 123 | return new Promise((resolve) => { 124 | this.once("threadStopped", resolve); 125 | }); 126 | } 127 | 128 | async setInitialBreakpoints(removeExisting = true) { 129 | log.debug("Setting initial breakpoints"); 130 | if (removeExisting) { 131 | vscode.debug.removeBreakpoints(vscode.debug.breakpoints); 132 | this.previousBreakpoints = []; 133 | } 134 | this.emit("spinner", { active: true, message: "Setting initial breakpoints" }); 135 | const structuredCode = this.sourceCodeCollector.gatherWorkspaceCode(); 136 | const response = await callLlm( 137 | getInitialBreakpointsMessage(structuredCode), 138 | allFunctions 139 | ); 140 | 141 | await this.handleLlmFunctionCall(response); 142 | this.emit("spinner", { active: false }); 143 | } 144 | 145 | reset() { 146 | this.session = null; 147 | this.threadId = undefined; 148 | this.chat.clearHistory(); 149 | this.live = false; 150 | this.finished = false; // Reset the finished flag 151 | } 152 | 153 | async loop() { 154 | if (!await this.shouldLoop()) return; 155 | 156 | log.debug("Gathering paused state"); 157 | const debugState = new DebugState(); 158 | const pausedState = await debugState.gatherPausedState(this.session!); 159 | 160 | if (!await this.shouldLoop()) return; 161 | 162 | log.debug("Thinking.."); 163 | this.emit("spinner", { active: true, message: "Deciding the next step to take..." }); 164 | const messageToSend = getPausedMessage(this.sourceCodeCollector.gatherWorkspaceCode(), pausedState); 165 | log.trace("Message to LLM:", messageToSend); 166 | 167 | const llmResponse = await this.chat.ask(messageToSend); 168 | 169 | if (!this.live) return; 170 | this.emit("spinner", { active: false }); 171 | 172 | const [choice] = llmResponse.choices; 173 | const content = choice?.message?.content; 174 | if (content) { 175 | log.info(content); 176 | } 177 | 178 | await this.handleLlmFunctionCall(llmResponse, true); 179 | } 180 | 181 | async clear() { 182 | this.emit("spinner", { active: false }); 183 | this.emit("debugResults", { results: null }); 184 | } 185 | 186 | async start() { 187 | log.debug("Starting debug loop controller"); 188 | this.live = true; 189 | this.emit("isInSession", { isInSession: true }); 190 | await this.setInitialBreakpoints(); 191 | log.debug('Initial breakpoints are set') 192 | this.session?.customRequest('continue') 193 | } 194 | 195 | 196 | async finish(exitReason?: string) { 197 | if (this.finished) { 198 | return; // Prevent multiple calls 199 | } 200 | this.stop(); 201 | this.finished = true; 202 | 203 | log.debug("Debug session finished. Providing code fix and explanation"); 204 | this.emit("spinner", { 205 | active: true, 206 | message: "Debug session finished. Providing code fix and explaination", 207 | }); 208 | 209 | try { 210 | let finalPrompt = "Debug session finished. Provide a code fix and explain your reasoning."; 211 | if (exitReason) { 212 | finalPrompt = `${exitReason}\n\n${finalPrompt}`; 213 | } 214 | 215 | const response = await this.chat.ask(finalPrompt, { withFunctions: false }); 216 | const [choice] = response.choices; 217 | const content = choice?.message?.content; 218 | if (content) { 219 | log.info(content); 220 | this.emit("debugResults", { results: content }); 221 | } else { 222 | log.info("No content from LLM"); 223 | } 224 | } catch (error) { 225 | log.error("Error during final LLM call:", String(error)); 226 | this.emit("debugResults", { results: `An error occurred while generating the final report: ${String(error)}` }); // Show error to the user! 227 | } finally { 228 | this.emit("spinner", { active: false }); 229 | this.emit('isInSession', { isInSession: false }); 230 | this.stop(); 231 | } 232 | } 233 | 234 | 235 | stop() { 236 | this.live = false; 237 | } 238 | 239 | async setBreakpoint(functionArgsString: string) { 240 | try { 241 | const { file, line } = JSON.parse(functionArgsString); 242 | let fullPath = file; 243 | if (!path.isAbsolute(file) && vscode.workspace.workspaceFolders?.length) { 244 | const workspaceRoot = vscode.workspace.workspaceFolders[0].uri.fsPath; 245 | fullPath = path.join(workspaceRoot, file); 246 | } 247 | 248 | const uri = vscode.Uri.file(fullPath); 249 | const position = new vscode.Position(line - 1, 0); 250 | const location = new vscode.Location(uri, position); 251 | const breakpoint = new vscode.SourceBreakpoint(location, true); 252 | 253 | // Remove existing breakpoints before adding a new one 254 | vscode.debug.removeBreakpoints(this.previousBreakpoints); 255 | vscode.debug.addBreakpoints([breakpoint]); 256 | this.previousBreakpoints = [breakpoint]; // Store the new breakpoint 257 | 258 | log.debug(`Set breakpoint at ${fullPath}:${line}`); 259 | 260 | } catch (err) { 261 | log.error(`Failed to set breakpoint: ${String(err)}`); 262 | vscode.window.showErrorMessage( 263 | `Failed to set breakpoint: ${String(err)}`, 264 | ); 265 | } 266 | } 267 | 268 | async removeBreakpoint(functionArgsString: string) { 269 | log.debug(`Removing breakpoint: ${functionArgsString}`); 270 | try { 271 | const { file, line } = JSON.parse(functionArgsString); 272 | const allBreakpoints = vscode.debug.breakpoints; 273 | const toRemove: vscode.Breakpoint[] = []; 274 | 275 | for (const bp of allBreakpoints) { 276 | if (bp instanceof vscode.SourceBreakpoint) { 277 | const thisFile = bp.location.uri.fsPath; 278 | const thisLine = bp.location.range.start.line + 1; 279 | if ( 280 | (thisFile === file || thisFile.endsWith(file)) && 281 | thisLine === line 282 | ) { 283 | toRemove.push(bp); 284 | } 285 | } 286 | } 287 | 288 | if (toRemove.length) { 289 | vscode.debug.removeBreakpoints(toRemove); 290 | log.debug( 291 | `Removed ${toRemove.length} breakpoint(s) at ${file}:${line}`, 292 | ); 293 | } 294 | } catch (err) { 295 | log.error(`Failed to remove breakpoint: ${String(err)}`); 296 | vscode.window.showErrorMessage( 297 | `Failed to remove breakpoint: ${String(err)}`, 298 | ); 299 | } 300 | } 301 | 302 | async next() { 303 | const session = vscode.debug.activeDebugSession; 304 | if (!session) { 305 | log.debug("Cannot run command 'next'. No active debug session."); 306 | return; 307 | } 308 | try { 309 | const threads = await session.customRequest("threads"); 310 | const threadId = threads.threads[0]?.id; 311 | if (threadId === undefined) { 312 | log.debug("Cannot run command 'next'. No active thread found."); 313 | return; 314 | } 315 | await session.customRequest("next", { threadId }); 316 | await this.waitForThreadStopped(); 317 | } catch (err) { 318 | log.error(`Failed to run command 'next': ${String(err)}`); 319 | } 320 | } 321 | 322 | async stepIn() { 323 | const session = vscode.debug.activeDebugSession; 324 | if (!session) { 325 | log.debug("Cannot stepIn. No active debug session."); 326 | return; 327 | } 328 | try { 329 | const threads = await session.customRequest("threads"); 330 | const threadId = threads.threads[0]?.id; 331 | if (threadId === undefined) { 332 | log.debug("Cannot stepIn. No active thread found."); 333 | return; 334 | } 335 | await session.customRequest("stepIn", { threadId }); // Await directly 336 | await this.waitForThreadStopped(); // Then wait. 337 | } catch (err) { 338 | log.error(`Failed to step in: ${String(err)}`); 339 | } 340 | } 341 | 342 | async stepOut() { 343 | const session = vscode.debug.activeDebugSession; 344 | if (!session) { 345 | log.debug("Cannot run command 'stepOut'. No active debug session."); 346 | return; 347 | } 348 | try { 349 | const threads = await session.customRequest("threads"); 350 | const threadId = threads.threads[0]?.id; 351 | if (threadId === undefined) { 352 | log.debug("Cannot run command 'stepOut'. No active thread found."); 353 | return; 354 | } 355 | await session.customRequest("stepOut", { threadId }); // Await the request 356 | await this.waitForThreadStopped(); // Then wait for the stopped event 357 | log.info("Stepped out of the current function call."); 358 | } catch (err) { 359 | log.error(`Failed to run command 'stepOut': ${String(err)}`); 360 | } 361 | } 362 | 363 | async continueExecution() { 364 | const session = vscode.debug.activeDebugSession; 365 | if (!session) { 366 | log.debug("Cannot run command 'continue'. No active debug session."); 367 | return; 368 | } 369 | try { 370 | const threads = await session.customRequest("threads"); 371 | const threadId = threads.threads[0]?.id; 372 | if (threadId === undefined) { 373 | log.debug("Cannot run command 'continue'. No active thread found."); 374 | return; 375 | } 376 | await session.customRequest("continue", { threadId }); // Await directly 377 | await this.waitForThreadStopped(); // Then wait. 378 | } catch (err) { 379 | log.error(`Failed to run command 'continue': ${String(err)}`); 380 | } 381 | } 382 | 383 | async handleLlmFunctionCall(completion: ChatCompletion, continueAfterSettingBreakpoint = false) { 384 | const choice = completion?.choices?.[0]; 385 | if (!choice) { 386 | log.debug(`No choice found in completion. ${JSON.stringify(completion)}`); 387 | return; 388 | } 389 | 390 | const hasActiveBreakpoints = vscode.debug.breakpoints.some( 391 | (bp) => bp.enabled, 392 | ); 393 | 394 | log.debug(`Handling LLM function call: ${JSON.stringify({ choice, hasActiveBreakpoints })}`); 395 | 396 | for (const toolCall of choice.message?.tool_calls || []) { 397 | const { name, arguments: argsStr } = toolCall.function; 398 | log.debug(`${name}(${argsStr && argsStr !== '{}' ? argsStr : ""})`); 399 | 400 | const parsedArgs = argsStr ? JSON.parse(argsStr) : {}; 401 | const { reason, ...args } = parsedArgs; 402 | 403 | this.emit('aiFunctionCall', { functionName: name, args, reason }); 404 | 405 | switch (name) { 406 | case "setBreakpoint": 407 | await this.setBreakpoint(argsStr); 408 | if (continueAfterSettingBreakpoint) { 409 | await this.session?.customRequest('continue'); 410 | } 411 | break; 412 | case "removeBreakpoint": 413 | await this.removeBreakpoint(argsStr); 414 | break; 415 | case "next": 416 | await this.next(); 417 | break; 418 | case "stepIn": 419 | await this.stepIn(); 420 | break; 421 | case "stepOut": 422 | await this.stepOut(); 423 | break; 424 | case "continue": { 425 | if (hasActiveBreakpoints) { 426 | await this.continueExecution(); 427 | } else { 428 | log.debug("Cannot continue. No active breakpoints."); 429 | } 430 | break; 431 | } 432 | default: 433 | break; 434 | } 435 | } 436 | } 437 | } -------------------------------------------------------------------------------- /src/debug/DebugState.ts: -------------------------------------------------------------------------------- 1 | import { StackFrame, Variable } from "@vscode/debugadapter"; 2 | import * as vscode from "vscode"; 3 | import log from "../logger"; 4 | 5 | interface SimpleBreakpoint { 6 | type: "source" | "function" | "unknown"; 7 | path?: string; 8 | line?: number; 9 | functionName?: string; 10 | } 11 | 12 | interface SimpleStackFrame { 13 | name: string; 14 | line: number; 15 | column: number; 16 | source: string; 17 | } 18 | 19 | export interface PausedState { 20 | breakpoints: SimpleBreakpoint[]; 21 | pausedStack: SimpleStackFrame[]; 22 | topFrameVariables: { 23 | scopeName: string; 24 | variables: { name: string; value: string }[]; 25 | }[]; 26 | } 27 | 28 | export class DebugState { 29 | /** 30 | * Returns the set of enabled breakpoints in a simplified format: 31 | * - For SourceBreakpoints: file path and line number 32 | * - For FunctionBreakpoints: function name 33 | */ 34 | getEnabledBreakpoints(): SimpleBreakpoint[] { 35 | return vscode.debug.breakpoints 36 | .filter((bp) => bp.enabled) 37 | .map((bp) => { 38 | if (bp instanceof vscode.SourceBreakpoint) { 39 | return { 40 | type: "source", 41 | path: bp.location.uri.fsPath, 42 | line: bp.location.range.start.line, 43 | }; 44 | } else if (bp instanceof vscode.FunctionBreakpoint) { 45 | return { 46 | type: "function", 47 | functionName: bp.functionName, 48 | }; 49 | } 50 | return { type: "unknown" }; 51 | }); 52 | } 53 | 54 | /** 55 | * Gets a list of frames (name, line, column, source) for the currently paused thread. 56 | * To keep it small, we only include the basic location info. 57 | */ 58 | async getPausedStack(session: vscode.DebugSession): Promise { 59 | const threadsResponse = await session.customRequest("threads"); 60 | const allThreads = threadsResponse?.threads || []; 61 | 62 | // Find a 'paused' thread. In many debug adapters, there's only one paused thread anyway. 63 | // For simplicity, just take the first thread here. 64 | const pausedThread = allThreads[0]; 65 | if (!pausedThread) return []; 66 | 67 | const stackTraceResponse = await session.customRequest("stackTrace", { 68 | threadId: pausedThread.id, 69 | startFrame: 0, 70 | levels: 20, 71 | }); 72 | 73 | const frames = stackTraceResponse?.stackFrames || []; 74 | return frames.map((f: StackFrame) => ({ 75 | name: f.name, 76 | line: f.line, 77 | column: f.column, 78 | source: f.source?.path || f.source?.name || "", 79 | })); 80 | } 81 | 82 | /** 83 | * Retrieves variables from the top stack frame (locals only), excluding large/global scopes. 84 | */ 85 | async getTopFrameVariables(session: vscode.DebugSession): Promise<{ 86 | scopeName: string; 87 | variables: { name: string; value: string }[]; 88 | }[]> { 89 | // Hard-code threadId=1 for simplicity; adapt as needed if you track paused thread IDs elsewhere. 90 | const threadId = 1; 91 | 92 | const stackTraceResponse = await session.customRequest("stackTrace", { 93 | threadId, 94 | startFrame: 0, 95 | levels: 1, 96 | }); 97 | const frames = stackTraceResponse.stackFrames || []; 98 | if (!frames.length) return []; 99 | 100 | const frameId = frames[0].id; 101 | const scopesResponse = await session.customRequest("scopes", { frameId }); 102 | const scopes = scopesResponse?.scopes || []; 103 | 104 | const results: { 105 | scopeName: string; 106 | variables: { name: string; value: string }[]; 107 | }[] = []; 108 | 109 | for (const scope of scopes) { 110 | // Skip anything that isn't a local/closure scope to keep data minimal 111 | if (!["Local", "Closure", "Exception"].includes(scope.name)) continue; 112 | 113 | const varsResponse = await session.customRequest("variables", { 114 | variablesReference: scope.variablesReference, 115 | }); 116 | const vars = varsResponse.variables || []; 117 | // Just pick out name and a short value 118 | const simplified = vars.map((v: Variable) => ({ 119 | name: v.name, 120 | value: v.value, 121 | })); 122 | 123 | results.push({ 124 | scopeName: scope.name, 125 | variables: simplified, 126 | }); 127 | } 128 | return results; 129 | } 130 | 131 | /** 132 | * Collects the current paused state: enabled breakpoints, call stack, and top-frame local variables. 133 | */ 134 | async gatherPausedState(session: vscode.DebugSession): Promise { 135 | if (!session) { 136 | log.error("No active debug session"); 137 | return { 138 | breakpoints: [], 139 | pausedStack: [], 140 | topFrameVariables: [], 141 | }; 142 | } 143 | const result: PausedState = { 144 | breakpoints: [], 145 | pausedStack: [], 146 | topFrameVariables: [], 147 | }; 148 | try { 149 | result.breakpoints = this.getEnabledBreakpoints(); 150 | } catch (error) { 151 | log.error("Error getting enabled breakpoints", String(error)); 152 | } 153 | try { 154 | result.pausedStack = await this.getPausedStack(session); 155 | } catch (error) { 156 | log.error("Error getting paused stack", String(error)); 157 | } 158 | try { 159 | result.topFrameVariables = await this.getTopFrameVariables(session); 160 | } catch (error) { 161 | log.error("Error getting top frame variables", String(error)); 162 | } 163 | 164 | return result; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { DebugLoopController } from "./debug/DebugLoopController"; 3 | import { DebugAdapterTracker } from "./debug/DebugAdapterTracker"; 4 | import log from "./logger"; 5 | import { LlmDebuggerSidebarProvider } from "./views/SidebarView"; 6 | import { DebugConfigurationProvider } from "./debug/DebugConfigurationProvider"; 7 | import { SourceCodeCollector } from "./context/SourceCodeCollector"; 8 | 9 | export async function activate(context: vscode.ExtensionContext) { 10 | // Assume that the first workspace folder is the one we want to debug. 11 | const sourceCodeCollector = new SourceCodeCollector(vscode.workspace.workspaceFolders?.[0]); 12 | const debugLoopController = new DebugLoopController(sourceCodeCollector); 13 | 14 | // Register debug adapter tracker for all debug sessions. 15 | context.subscriptions.push( 16 | vscode.debug.registerDebugAdapterTrackerFactory("*", { 17 | createDebugAdapterTracker(session) { 18 | if (session.parentSession) { 19 | log.debug('Not launching a DebugAdapterTracker for a child session') 20 | } 21 | // Use the SINGLE debugLoopController instance. 22 | return new DebugAdapterTracker(session, debugLoopController); 23 | }, 24 | }) 25 | ); 26 | 27 | // Register the debug configuration provider for the llmDebugger 28 | // TODO: Support other debuggers 29 | context.subscriptions.push( 30 | vscode.debug.registerDebugConfigurationProvider("node", new DebugConfigurationProvider(context, debugLoopController)), 31 | ); 32 | 33 | // Set up and register the sidebar (integrated into the Run and Debug panel) 34 | const sidebarProvider = new LlmDebuggerSidebarProvider(context, debugLoopController); 35 | context.subscriptions.push( 36 | vscode.window.registerWebviewViewProvider("llmDebuggerPanel", sidebarProvider, { 37 | webviewOptions: { retainContextWhenHidden: true }, 38 | }), 39 | ); 40 | 41 | // After setting up initializers log that the extension has been activated. 42 | log.clear(); 43 | log.debug("activated"); 44 | } 45 | 46 | export async function deactivate(context: vscode.ExtensionContext) { 47 | context.subscriptions.forEach((disposable) => disposable.dispose()); 48 | } -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export interface LogEntry { 4 | message: string; 5 | type: string; 6 | timestamp: number; 7 | } 8 | 9 | class Logger { 10 | private isEnabled = true; 11 | private logChannel: vscode.LogOutputChannel; 12 | private logEntries: LogEntry[] = []; 13 | private prefix: string = "" 14 | 15 | constructor(logChannel: vscode.LogOutputChannel | null = null, prefix: string = "") { 16 | this.logChannel = logChannel || vscode.window.createOutputChannel("LLM Debugger", { 17 | log: true, 18 | }); 19 | this.prefix = prefix; 20 | } 21 | 22 | enable() { 23 | this.isEnabled = true; 24 | } 25 | 26 | disable() { 27 | this.isEnabled = false; 28 | } 29 | 30 | 31 | createSubLogger(name: string) { 32 | return new Logger(this.logChannel, `${name}: `); 33 | } 34 | 35 | 36 | 37 | loadPersistedLogs(entries: LogEntry[]) { 38 | this.logEntries = entries; 39 | } 40 | 41 | getPersistedLogs(): LogEntry[] { 42 | return this.logEntries; 43 | } 44 | 45 | 46 | 47 | show() { 48 | this.logChannel.show(); 49 | } 50 | 51 | clear() { 52 | this.logChannel.clear(); 53 | } 54 | 55 | private writeToOutput( 56 | msg: string, 57 | level: keyof Pick< 58 | vscode.LogOutputChannel, 59 | "debug" | "error" | "info" | "warn" | "trace" 60 | > = "info", 61 | ) { 62 | if (!this.isEnabled) { 63 | return; 64 | } 65 | this.logChannel[level](`${this.prefix}${msg}`); 66 | } 67 | 68 | 69 | debug(...msgs: string[]) { 70 | const message = msgs.join(" "); 71 | this.writeToOutput(message, "debug"); 72 | // not writing to sidebar because it's too verbose 73 | } 74 | 75 | trace(...msg: string[]) { 76 | this.writeToOutput(msg.join(''), 'trace') 77 | } 78 | 79 | info(...msgs: string[]) { 80 | const message = msgs.join(" "); 81 | this.writeToOutput(message, "info"); 82 | } 83 | 84 | error(...msgs: string[]) { 85 | const message = msgs.join(" "); 86 | this.writeToOutput(message, "error"); 87 | } 88 | 89 | warn(...msgs: string[]) { 90 | const message = msgs.join(" "); 91 | this.writeToOutput(message, "warn"); 92 | } 93 | } 94 | 95 | export default new Logger(); 96 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ChatCompletionMessageParam, 3 | ChatCompletionTool, 4 | } from "openai/resources"; 5 | 6 | export interface StructuredCode { 7 | filePath: string; 8 | lines: Array<{ 9 | lineNumber: number; 10 | text: string; 11 | hasBreakpoint?: boolean; 12 | }>; 13 | } 14 | 15 | export type { ChatCompletionMessageParam, ChatCompletionTool }; 16 | -------------------------------------------------------------------------------- /src/views/SidebarView.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | import * as crypto from "crypto"; 4 | import * as cheerio from "cheerio"; 5 | import { DebugLoopController } from "../debug/DebugLoopController"; 6 | import logger from "../logger"; 7 | 8 | const log = logger.createSubLogger("SidebarView"); 9 | 10 | export class LlmDebuggerSidebarProvider implements vscode.WebviewViewProvider { 11 | private debugLoopController: DebugLoopController; 12 | private _view?: vscode.WebviewView; 13 | private readonly _extensionContext: vscode.ExtensionContext; 14 | private readonly _extensionUri: vscode.Uri; 15 | 16 | constructor(context: vscode.ExtensionContext, debugLoopController: DebugLoopController) { 17 | this.debugLoopController = debugLoopController; 18 | this._extensionContext = context; 19 | this._extensionUri = context.extensionUri; 20 | 21 | for (const command of ["spinner", "setDebugEnabled", "isInSession", "debugResults", "aiFunctionCall"]) { 22 | this.debugLoopController.on(command, (data) => { 23 | log.debug(`command ${JSON.stringify(command)} with data ${JSON.stringify(data)}`); 24 | if (this._view) { 25 | this._view.webview.postMessage({ 26 | command, 27 | ...data, 28 | }); 29 | } 30 | }) 31 | }; 32 | 33 | // Set initial value for debug mode 34 | this.setDebugEnabled(this._extensionContext.workspaceState.get("llmDebuggerEnabled", true)); 35 | } 36 | 37 | public resolveWebviewView(webviewView: vscode.WebviewView) { 38 | this._view = webviewView; 39 | webviewView.webview.options = { 40 | enableScripts: true, 41 | localResourceRoots: [ 42 | vscode.Uri.joinPath(this._extensionUri, "src", "webview", "out"), 43 | ], 44 | }; 45 | 46 | webviewView.webview.html = this.getHtmlForWebview(webviewView.webview); 47 | 48 | // Send current debug mode state to the webview 49 | const debugWithAI = this._extensionContext.workspaceState.get("llmDebuggerEnabled", false); 50 | webviewView.webview.postMessage({ command: "setDebugEnabled", enabled: debugWithAI }); 51 | 52 | // Listen for messages from the webview 53 | webviewView.webview.onDidReceiveMessage((message) => { 54 | switch (message.command) { 55 | case "toggleDebug": 56 | this.setDebugEnabled(message.enabled); 57 | break; 58 | } 59 | }); 60 | } 61 | 62 | // New method to update debug enabled state 63 | public setDebugEnabled(enabled: boolean): void { 64 | // Update the workspace state for persistent storage 65 | this._extensionContext.workspaceState.update("llmDebuggerEnabled", enabled); 66 | 67 | // If the view is available, send the updated debug state 68 | if (this._view) { 69 | this._view.webview.postMessage({ command: "setDebugEnabled", enabled }); 70 | } 71 | } 72 | 73 | private getHtmlForWebview(webview: vscode.Webview): string { 74 | const webviewOutPath = vscode.Uri.joinPath( 75 | this._extensionUri, 76 | "src", 77 | "webview", 78 | "out" 79 | ); 80 | const htmlPath = vscode.Uri.joinPath(webviewOutPath, "index.html"); 81 | const html = fs.readFileSync(htmlPath.fsPath, "utf8"); 82 | const nonce = getNonce(); 83 | const $ = cheerio.load(html); 84 | 85 | // Update resource URIs for CSS 86 | $("link[rel='stylesheet']").each((_, el) => { 87 | const relativeHref = $(el).attr("href"); 88 | if (relativeHref) { 89 | const newHref = webview.asWebviewUri( 90 | vscode.Uri.joinPath(webviewOutPath, relativeHref) 91 | ).toString(); 92 | $(el).attr("href", newHref); 93 | } 94 | }); 95 | 96 | // Update resource URIs for JS and add nonce 97 | $("script").each((_, el) => { 98 | const relativeSrc = $(el).attr("src"); 99 | if (relativeSrc) { 100 | const newSrc = webview.asWebviewUri( 101 | vscode.Uri.joinPath(webviewOutPath, relativeSrc) 102 | ).toString(); 103 | $(el).attr("src", newSrc); 104 | $(el).attr("nonce", nonce); 105 | } 106 | }); 107 | 108 | // Add CSP meta tag 109 | $("head").prepend(``); 114 | 115 | return $.html(); 116 | } 117 | } 118 | 119 | function getNonce() { 120 | return crypto.randomBytes(16).toString("base64"); 121 | } -------------------------------------------------------------------------------- /src/webview/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "namers": ["@parcel/namer-default"], 4 | "resolvers": ["@parcel/resolver-default"] 5 | } -------------------------------------------------------------------------------- /src/webview/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Markdown } from "./Markdown"; 3 | 4 | // Assume the VS Code API has been injected via the preload script 5 | declare const vscodeApi: { 6 | postMessage(message: { command: string; enabled: boolean }): void; 7 | }; 8 | 9 | 10 | interface AiFunctionCall { 11 | functionName: string; 12 | args: string; 13 | reason: string; 14 | } 15 | 16 | export function App() { 17 | const [debugEnabled, setDebugEnabled] = React.useState(false); 18 | const [isInSession, setIsInSession] = React.useState(false); 19 | const [spinner, setSpinner] = React.useState<{ 20 | active: boolean; 21 | message: string 22 | }>({ 23 | active: false, 24 | message: "" 25 | }); 26 | const [debugResuls, setDebugResults] = React.useState(null); 27 | const [currentAiFunctionCall, setCurrentAiFunctionCall] = React.useState(null); 28 | 29 | // Listen to messages from the extension 30 | React.useEffect(() => { 31 | const handleMessage = (event: MessageEvent) => { 32 | const data = event.data; 33 | switch (data?.command) { 34 | case "aiFunctionCall": 35 | setCurrentAiFunctionCall(data); 36 | break; 37 | case "setDebugEnabled": 38 | setDebugEnabled(data.enabled); 39 | break; 40 | case "spinner": 41 | setSpinner(data); 42 | break; 43 | case "isInSession": 44 | setIsInSession(data.isInSession); 45 | break 46 | case "debugResults": 47 | setDebugResults(data.results); 48 | break; 49 | 50 | } 51 | }; 52 | window.addEventListener("message", handleMessage); 53 | return () => window.removeEventListener("message", handleMessage); 54 | }, []); 55 | 56 | const onCheckboxChange = (e: React.ChangeEvent) => { 57 | const enabled = e.target.checked; 58 | setDebugEnabled(enabled); 59 | vscodeApi.postMessage({ command: "toggleDebug", enabled }); 60 | }; 61 | 62 | 63 | return ( 64 |
65 |
66 | 73 | 74 |
75 | {currentAiFunctionCall && !spinner && !debugResuls && } 76 | {spinner.active && } 77 | {!isInSession && !debugResuls && } 78 | {debugResuls && { setDebugResults(null) }} />} 79 |
80 | ); 81 | } 82 | 83 | function AiFunctionCallView({ functionName, args, reason }: AiFunctionCall) { 84 | return ( 85 |
86 |
{functionName}()
87 |
{reason}
88 |
89 | ); 90 | } 91 | 92 | function Thinking({message}: {message: string}) { 93 | return ( 94 |
95 |
96 |
{message}
97 |
98 | ) 99 | } 100 | 101 | function Results({ message, onClear }: { message: string; onClear: () => void }) { 102 | return ( 103 |
104 |
105 |

Results

106 | onClear()}>Clear 107 |
108 | 109 |
110 | ) 111 | } 112 | 113 | function Help() { 114 | return ( 115 |
116 |

117 | Enable "Debug with AI" above. When you start a debug session via VS 118 | Code's Run and Debug panel, the LLM Debugger workflow will run. 119 |

120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /src/webview/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MarkdownIt from "markdown-it"; 3 | 4 | interface MarkdownProps { 5 | message: string; 6 | } 7 | 8 | export function Markdown({ message }: MarkdownProps) { 9 | const md = new MarkdownIt(); 10 | return ( 11 |
15 | ); 16 | } -------------------------------------------------------------------------------- /src/webview/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function Spinner() { 4 | return
; 5 | } 6 | -------------------------------------------------------------------------------- /src/webview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LLM Debugger 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/webview/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | import { App } from "./App"; 4 | import "./style.css"; 5 | 6 | declare function acquireVsCodeApi< 7 | Commands = { command: string }, 8 | State = unknown, 9 | >(): { 10 | postMessage(message: Commands): void; 11 | setState(state: State): void; 12 | getState(): State; 13 | }; 14 | 15 | declare global { 16 | interface Window { 17 | vscodeApi: ReturnType; 18 | } 19 | } 20 | 21 | const container = document.getElementById("root")!; 22 | const errorContainer = document.getElementById("error-root")!; 23 | const root = ReactDOM.createRoot(container); 24 | const errorRoot = ReactDOM.createRoot(errorContainer); 25 | 26 | try { 27 | window.vscodeApi = acquireVsCodeApi(); 28 | root.render(); 29 | } catch (error) { 30 | errorRoot.render(); 31 | } 32 | 33 | window.addEventListener("error", (event) => { 34 | errorRoot.render(); 35 | }); 36 | 37 | 38 | function GlobalError(props: { error: unknown }) { 39 | return ( 40 |
41 |

LLM Debugger encountered an error

42 |
{String(props.error)}
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/webview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llm-debugger-webview", 3 | "version": "0.0.1", 4 | "description": "LLM Debugger Webview", 5 | "scripts": { 6 | "build": "parcel build index.html --dist-dir out --cache-dir .parcel-cache", 7 | "dev": "parcel watch index.html --watch-dir . --dist-dir out --cache-dir .parcel-cache" 8 | }, 9 | "dependencies": { 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0" 12 | }, 13 | "devDependencies": { 14 | "parcel": "^2.13.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/webview/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: var(--vscode-font-family); 3 | } 4 | 5 | .global-error { 6 | max-width: 100%; 7 | } 8 | 9 | .global-error h1 { 10 | font-weight: 400; 11 | margin-bottom: 10px; 12 | } 13 | 14 | .global-error pre { 15 | border: 1px solid var(--vscode-input-border); 16 | padding: 4px; 17 | margin: 0; 18 | max-width: 100%; 19 | white-space: pre-wrap; 20 | word-wrap: break-word; 21 | word-break: break-all; 22 | font-family: var(--vscode-editor-font-family); 23 | } 24 | 25 | .sidebar-container { 26 | margin-top: 0.5rem; 27 | display: grid; 28 | align-items: center; 29 | grid-template-rows: auto 1fr; 30 | height: 100%; 31 | } 32 | 33 | .control-panel { 34 | display: grid; 35 | gap: 4px; 36 | grid-template-columns: auto 1fr; 37 | } 38 | 39 | .control-selector { 40 | height: 24px; 41 | border: 1px solid var(--vscode-input-border); 42 | background-color: var(--vscode-input-background); 43 | color: var(--vscode-input-foreground); 44 | } 45 | 46 | #start-button { 47 | padding: 8px; 48 | height: 24px; 49 | background: var(--vscode-button-background); 50 | color: var(--vscode-button-foreground); 51 | border: none; 52 | cursor: pointer; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | } 57 | 58 | #start-button:hover { 59 | background: var(--vscode-button-hoverBackground); 60 | } 61 | 62 | #log-area { 63 | overflow-y: auto; 64 | padding: 4px 0; 65 | } 66 | 67 | .log-message { 68 | margin-bottom: 2px; 69 | padding: 0 2px; 70 | } 71 | 72 | .log-ai { 73 | color: var(--vscode-debugTokenExpression-name); 74 | } 75 | 76 | @keyframes ellipsis { 77 | 0% { 78 | content: "..."; 79 | } 80 | 81 | 50% { 82 | content: ".."; 83 | } 84 | 85 | 100% { 86 | content: "."; 87 | } 88 | } 89 | 90 | .log-ai.active::after { 91 | content: "..."; 92 | display: inline-block; 93 | /* animate the ellipsis */ 94 | animation: ellipsis 1.5s infinite; 95 | } 96 | 97 | .log-fn { 98 | font-family: var(--vscode-editor-font-family); 99 | background: var(--vscode-input-background); 100 | border: 1px solid var(--vscode-input-border); 101 | border-radius: 4px; 102 | } 103 | 104 | .log-fn .markdown-content p { 105 | display: inline-block; 106 | margin: 2px; 107 | } 108 | 109 | .log-debug { 110 | color: var(--vscode-debugIcon-breakpointCurrentStackframeForeground); 111 | } 112 | 113 | .log-info { 114 | color: var(--vscode-font-family); 115 | } 116 | 117 | .log-error { 118 | color: var(--vscode-debugConsole-errorForeground); 119 | } 120 | 121 | .log-warn { 122 | color: var(--vscode-debugConsole-warningForeground); 123 | } 124 | 125 | .results { 126 | max-width: 100%; 127 | overflow: auto; 128 | } 129 | 130 | .results header { 131 | display: flex; 132 | justify-content: space-between; 133 | align-items: center; 134 | max-height: 1rem; 135 | margin-top: 1rem; 136 | margin-bottom: 0.5rem; 137 | } 138 | 139 | .markdown-content { 140 | max-width: 100%; 141 | overflow: auto; 142 | } 143 | 144 | .markdown-content p:first-child { 145 | margin-top: 0; 146 | } 147 | 148 | .markdown-content code { 149 | font-family: var(--vscode-editor-font-family); 150 | background: var(--vscode-editor-background); 151 | padding: 2px 4px; 152 | border-radius: 3px; 153 | } 154 | 155 | 156 | .help-text { 157 | opacity: 0.8; 158 | } 159 | 160 | .thinking { 161 | margin: 0.5rem 0; 162 | display: grid; 163 | grid-template-columns: auto 1fr; 164 | gap: 4px; 165 | align-items: center; 166 | } 167 | 168 | .thinking .text::after { 169 | display: inline-block; 170 | content: "."; 171 | animation: ellipsis 1s infinite; 172 | } 173 | 174 | 175 | .spinner { 176 | border: 2px solid var(--vscode-button-background); 177 | border-top: 2px solid var(--vscode-button-foreground); 178 | border-radius: 50%; 179 | margin: 2px 4px; 180 | width: 14px; 181 | height: 14px; 182 | animation: spin 1s linear infinite; 183 | } 184 | 185 | @keyframes ellipse { 186 | 0% { 187 | content: "."; 188 | } 189 | 190 | 33% { 191 | content: ".."; 192 | } 193 | 194 | 66% { 195 | content: "..."; 196 | } 197 | 198 | 100% { 199 | content: "."; 200 | } 201 | 202 | } 203 | 204 | @keyframes spin { 205 | 0% { 206 | transform: rotate(0deg); 207 | } 208 | 209 | 100% { 210 | transform: rotate(360deg); 211 | } 212 | } 213 | 214 | 215 | .ai-function-call { 216 | border: 1px solid var(--vscode-input-border); 217 | margin-top: 1rem; 218 | padding: 0.2rem; 219 | } 220 | 221 | .ai-function-call .function-name { 222 | background: var(--vscode-input-background); 223 | border-bottom: 1px solid var(--vscode-input-border); 224 | padding: 4px; 225 | font-family: var(--vscode-editor-font-family); 226 | } -------------------------------------------------------------------------------- /src/webview/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 5 | "jsx": "react", 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "composite": true, 12 | "declaration": true, 13 | "outDir": "../../out/webview", 14 | "rootDir": "." 15 | }, 16 | "include": ["./**/*.ts", "./**/*.tsx"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | describe("should", () => { 4 | it("exported", () => { 5 | expect(1).toEqual(1); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ESNext"], 5 | "rootDir": "src", 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "types": ["vscode", "node"], 9 | "strict": true, 10 | "outDir": "out", 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "composite": true, 14 | "declaration": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": [ 18 | "node_modules", 19 | "out", 20 | "esbuild.extension.js", 21 | "src/webview/esbuild.webview.js", 22 | "src/webview/**/*" 23 | ], 24 | "references": [ 25 | { 26 | "path": "./src/webview" 27 | } 28 | ] 29 | } 30 | --------------------------------------------------------------------------------